mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
test(zalo): broaden webhook monitor coverage
This commit is contained in:
@@ -21,113 +21,84 @@ async function withServer(handler: RequestListener, fn: (baseUrl: string) => Pro
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_ACCOUNT: ResolvedZaloAccount = {
|
||||
accountId: "default",
|
||||
enabled: true,
|
||||
token: "tok",
|
||||
tokenSource: "config",
|
||||
config: {},
|
||||
};
|
||||
|
||||
const webhookRequestHandler: RequestListener = async (req, res) => {
|
||||
const handled = await handleZaloWebhookRequest(req, res);
|
||||
if (!handled) {
|
||||
res.statusCode = 404;
|
||||
res.end("not found");
|
||||
}
|
||||
};
|
||||
|
||||
function registerTarget(params: {
|
||||
path: string;
|
||||
secret?: string;
|
||||
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
|
||||
}): () => void {
|
||||
return registerZaloWebhookTarget({
|
||||
token: "tok",
|
||||
account: DEFAULT_ACCOUNT,
|
||||
config: {} as OpenClawConfig,
|
||||
runtime: {},
|
||||
core: {} as PluginRuntime,
|
||||
secret: params.secret ?? "secret",
|
||||
path: params.path,
|
||||
mediaMaxMb: 5,
|
||||
statusSink: params.statusSink,
|
||||
});
|
||||
}
|
||||
|
||||
describe("handleZaloWebhookRequest", () => {
|
||||
it("returns 400 for non-object payloads", async () => {
|
||||
const core = {} as PluginRuntime;
|
||||
const account: ResolvedZaloAccount = {
|
||||
accountId: "default",
|
||||
enabled: true,
|
||||
token: "tok",
|
||||
tokenSource: "config",
|
||||
config: {},
|
||||
};
|
||||
const unregister = registerZaloWebhookTarget({
|
||||
token: "tok",
|
||||
account,
|
||||
config: {} as OpenClawConfig,
|
||||
runtime: {},
|
||||
core,
|
||||
secret: "secret",
|
||||
path: "/hook",
|
||||
mediaMaxMb: 5,
|
||||
});
|
||||
const unregister = registerTarget({ path: "/hook" });
|
||||
|
||||
try {
|
||||
await withServer(
|
||||
async (req, res) => {
|
||||
const handled = await handleZaloWebhookRequest(req, res);
|
||||
if (!handled) {
|
||||
res.statusCode = 404;
|
||||
res.end("not found");
|
||||
}
|
||||
},
|
||||
async (baseUrl) => {
|
||||
const response = await fetch(`${baseUrl}/hook`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-bot-api-secret-token": "secret",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: "null",
|
||||
});
|
||||
await withServer(webhookRequestHandler, async (baseUrl) => {
|
||||
const response = await fetch(`${baseUrl}/hook`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-bot-api-secret-token": "secret",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: "null",
|
||||
});
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(await response.text()).toBe("Bad Request");
|
||||
},
|
||||
);
|
||||
expect(response.status).toBe(400);
|
||||
expect(await response.text()).toBe("Bad Request");
|
||||
});
|
||||
} finally {
|
||||
unregister();
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects ambiguous routing when multiple targets match the same secret", async () => {
|
||||
const core = {} as PluginRuntime;
|
||||
const account: ResolvedZaloAccount = {
|
||||
accountId: "default",
|
||||
enabled: true,
|
||||
token: "tok",
|
||||
tokenSource: "config",
|
||||
config: {},
|
||||
};
|
||||
const sinkA = vi.fn();
|
||||
const sinkB = vi.fn();
|
||||
const unregisterA = registerZaloWebhookTarget({
|
||||
token: "tok",
|
||||
account,
|
||||
config: {} as OpenClawConfig,
|
||||
runtime: {},
|
||||
core,
|
||||
secret: "secret",
|
||||
path: "/hook",
|
||||
mediaMaxMb: 5,
|
||||
statusSink: sinkA,
|
||||
});
|
||||
const unregisterB = registerZaloWebhookTarget({
|
||||
token: "tok",
|
||||
account,
|
||||
config: {} as OpenClawConfig,
|
||||
runtime: {},
|
||||
core,
|
||||
secret: "secret",
|
||||
path: "/hook",
|
||||
mediaMaxMb: 5,
|
||||
statusSink: sinkB,
|
||||
});
|
||||
const unregisterA = registerTarget({ path: "/hook", statusSink: sinkA });
|
||||
const unregisterB = registerTarget({ path: "/hook", statusSink: sinkB });
|
||||
|
||||
try {
|
||||
await withServer(
|
||||
async (req, res) => {
|
||||
const handled = await handleZaloWebhookRequest(req, res);
|
||||
if (!handled) {
|
||||
res.statusCode = 404;
|
||||
res.end("not found");
|
||||
}
|
||||
},
|
||||
async (baseUrl) => {
|
||||
const response = await fetch(`${baseUrl}/hook`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-bot-api-secret-token": "secret",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: "{}",
|
||||
});
|
||||
await withServer(webhookRequestHandler, async (baseUrl) => {
|
||||
const response = await fetch(`${baseUrl}/hook`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-bot-api-secret-token": "secret",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: "{}",
|
||||
});
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(sinkA).not.toHaveBeenCalled();
|
||||
expect(sinkB).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
expect(response.status).toBe(401);
|
||||
expect(sinkA).not.toHaveBeenCalled();
|
||||
expect(sinkB).not.toHaveBeenCalled();
|
||||
});
|
||||
} finally {
|
||||
unregisterA();
|
||||
unregisterB();
|
||||
@@ -135,73 +106,29 @@ describe("handleZaloWebhookRequest", () => {
|
||||
});
|
||||
|
||||
it("returns 415 for non-json content-type", async () => {
|
||||
const core = {} as PluginRuntime;
|
||||
const account: ResolvedZaloAccount = {
|
||||
accountId: "default",
|
||||
enabled: true,
|
||||
token: "tok",
|
||||
tokenSource: "config",
|
||||
config: {},
|
||||
};
|
||||
const unregister = registerZaloWebhookTarget({
|
||||
token: "tok",
|
||||
account,
|
||||
config: {} as OpenClawConfig,
|
||||
runtime: {},
|
||||
core,
|
||||
secret: "secret",
|
||||
path: "/hook-content-type",
|
||||
mediaMaxMb: 5,
|
||||
});
|
||||
const unregister = registerTarget({ path: "/hook-content-type" });
|
||||
|
||||
try {
|
||||
await withServer(
|
||||
async (req, res) => {
|
||||
const handled = await handleZaloWebhookRequest(req, res);
|
||||
if (!handled) {
|
||||
res.statusCode = 404;
|
||||
res.end("not found");
|
||||
}
|
||||
},
|
||||
async (baseUrl) => {
|
||||
const response = await fetch(`${baseUrl}/hook-content-type`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-bot-api-secret-token": "secret",
|
||||
"content-type": "text/plain",
|
||||
},
|
||||
body: "{}",
|
||||
});
|
||||
await withServer(webhookRequestHandler, async (baseUrl) => {
|
||||
const response = await fetch(`${baseUrl}/hook-content-type`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-bot-api-secret-token": "secret",
|
||||
"content-type": "text/plain",
|
||||
},
|
||||
body: "{}",
|
||||
});
|
||||
|
||||
expect(response.status).toBe(415);
|
||||
},
|
||||
);
|
||||
expect(response.status).toBe(415);
|
||||
});
|
||||
} finally {
|
||||
unregister();
|
||||
}
|
||||
});
|
||||
|
||||
it("deduplicates webhook replay by event_name + message_id", async () => {
|
||||
const core = {} as PluginRuntime;
|
||||
const account: ResolvedZaloAccount = {
|
||||
accountId: "default",
|
||||
enabled: true,
|
||||
token: "tok",
|
||||
tokenSource: "config",
|
||||
config: {},
|
||||
};
|
||||
const sink = vi.fn();
|
||||
const unregister = registerZaloWebhookTarget({
|
||||
token: "tok",
|
||||
account,
|
||||
config: {} as OpenClawConfig,
|
||||
runtime: {},
|
||||
core,
|
||||
secret: "secret",
|
||||
path: "/hook-replay",
|
||||
mediaMaxMb: 5,
|
||||
statusSink: sink,
|
||||
});
|
||||
const unregister = registerTarget({ path: "/hook-replay", statusSink: sink });
|
||||
|
||||
const payload = {
|
||||
event_name: "message.text.received",
|
||||
@@ -215,91 +142,56 @@ describe("handleZaloWebhookRequest", () => {
|
||||
};
|
||||
|
||||
try {
|
||||
await withServer(
|
||||
async (req, res) => {
|
||||
const handled = await handleZaloWebhookRequest(req, res);
|
||||
if (!handled) {
|
||||
res.statusCode = 404;
|
||||
res.end("not found");
|
||||
}
|
||||
},
|
||||
async (baseUrl) => {
|
||||
const first = await fetch(`${baseUrl}/hook-replay`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-bot-api-secret-token": "secret",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const second = await fetch(`${baseUrl}/hook-replay`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-bot-api-secret-token": "secret",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
await withServer(webhookRequestHandler, async (baseUrl) => {
|
||||
const first = await fetch(`${baseUrl}/hook-replay`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-bot-api-secret-token": "secret",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const second = await fetch(`${baseUrl}/hook-replay`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-bot-api-secret-token": "secret",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
expect(first.status).toBe(200);
|
||||
expect(second.status).toBe(200);
|
||||
expect(sink).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
);
|
||||
expect(first.status).toBe(200);
|
||||
expect(second.status).toBe(200);
|
||||
expect(sink).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
} finally {
|
||||
unregister();
|
||||
}
|
||||
});
|
||||
|
||||
it("returns 429 when per-path request rate exceeds threshold", async () => {
|
||||
const core = {} as PluginRuntime;
|
||||
const account: ResolvedZaloAccount = {
|
||||
accountId: "default",
|
||||
enabled: true,
|
||||
token: "tok",
|
||||
tokenSource: "config",
|
||||
config: {},
|
||||
};
|
||||
const unregister = registerZaloWebhookTarget({
|
||||
token: "tok",
|
||||
account,
|
||||
config: {} as OpenClawConfig,
|
||||
runtime: {},
|
||||
core,
|
||||
secret: "secret",
|
||||
path: "/hook-rate",
|
||||
mediaMaxMb: 5,
|
||||
});
|
||||
const unregister = registerTarget({ path: "/hook-rate" });
|
||||
|
||||
try {
|
||||
await withServer(
|
||||
async (req, res) => {
|
||||
const handled = await handleZaloWebhookRequest(req, res);
|
||||
if (!handled) {
|
||||
res.statusCode = 404;
|
||||
res.end("not found");
|
||||
}
|
||||
},
|
||||
async (baseUrl) => {
|
||||
let saw429 = false;
|
||||
for (let i = 0; i < 130; i += 1) {
|
||||
const response = await fetch(`${baseUrl}/hook-rate`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-bot-api-secret-token": "secret",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: "{}",
|
||||
});
|
||||
if (response.status === 429) {
|
||||
saw429 = true;
|
||||
break;
|
||||
}
|
||||
await withServer(webhookRequestHandler, async (baseUrl) => {
|
||||
let saw429 = false;
|
||||
for (let i = 0; i < 130; i += 1) {
|
||||
const response = await fetch(`${baseUrl}/hook-rate`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-bot-api-secret-token": "secret",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: "{}",
|
||||
});
|
||||
if (response.status === 429) {
|
||||
saw429 = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
expect(saw429).toBe(true);
|
||||
},
|
||||
);
|
||||
expect(saw429).toBe(true);
|
||||
});
|
||||
} finally {
|
||||
unregister();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user