test(zalo): broaden webhook monitor coverage

This commit is contained in:
Peter Steinberger
2026-02-22 11:28:27 +00:00
parent 081ab9c99d
commit 5c7ab8eae3

View File

@@ -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", () => { describe("handleZaloWebhookRequest", () => {
it("returns 400 for non-object payloads", async () => { it("returns 400 for non-object payloads", async () => {
const core = {} as PluginRuntime; const unregister = registerTarget({ path: "/hook" });
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,
});
try { try {
await withServer( await withServer(webhookRequestHandler, async (baseUrl) => {
async (req, res) => { const response = await fetch(`${baseUrl}/hook`, {
const handled = await handleZaloWebhookRequest(req, res); method: "POST",
if (!handled) { headers: {
res.statusCode = 404; "x-bot-api-secret-token": "secret",
res.end("not found"); "content-type": "application/json",
} },
}, body: "null",
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(response.status).toBe(400);
expect(await response.text()).toBe("Bad Request"); expect(await response.text()).toBe("Bad Request");
}, });
);
} finally { } finally {
unregister(); unregister();
} }
}); });
it("rejects ambiguous routing when multiple targets match the same secret", async () => { 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 sinkA = vi.fn();
const sinkB = vi.fn(); const sinkB = vi.fn();
const unregisterA = registerZaloWebhookTarget({ const unregisterA = registerTarget({ path: "/hook", statusSink: sinkA });
token: "tok", const unregisterB = registerTarget({ path: "/hook", statusSink: sinkB });
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,
});
try { try {
await withServer( await withServer(webhookRequestHandler, async (baseUrl) => {
async (req, res) => { const response = await fetch(`${baseUrl}/hook`, {
const handled = await handleZaloWebhookRequest(req, res); method: "POST",
if (!handled) { headers: {
res.statusCode = 404; "x-bot-api-secret-token": "secret",
res.end("not found"); "content-type": "application/json",
} },
}, body: "{}",
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(response.status).toBe(401);
expect(sinkA).not.toHaveBeenCalled(); expect(sinkA).not.toHaveBeenCalled();
expect(sinkB).not.toHaveBeenCalled(); expect(sinkB).not.toHaveBeenCalled();
}, });
);
} finally { } finally {
unregisterA(); unregisterA();
unregisterB(); unregisterB();
@@ -135,73 +106,29 @@ describe("handleZaloWebhookRequest", () => {
}); });
it("returns 415 for non-json content-type", async () => { it("returns 415 for non-json content-type", async () => {
const core = {} as PluginRuntime; const unregister = registerTarget({ path: "/hook-content-type" });
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,
});
try { try {
await withServer( await withServer(webhookRequestHandler, async (baseUrl) => {
async (req, res) => { const response = await fetch(`${baseUrl}/hook-content-type`, {
const handled = await handleZaloWebhookRequest(req, res); method: "POST",
if (!handled) { headers: {
res.statusCode = 404; "x-bot-api-secret-token": "secret",
res.end("not found"); "content-type": "text/plain",
} },
}, body: "{}",
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 { } finally {
unregister(); unregister();
} }
}); });
it("deduplicates webhook replay by event_name + message_id", async () => { 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 sink = vi.fn();
const unregister = registerZaloWebhookTarget({ const unregister = registerTarget({ path: "/hook-replay", statusSink: sink });
token: "tok",
account,
config: {} as OpenClawConfig,
runtime: {},
core,
secret: "secret",
path: "/hook-replay",
mediaMaxMb: 5,
statusSink: sink,
});
const payload = { const payload = {
event_name: "message.text.received", event_name: "message.text.received",
@@ -215,91 +142,56 @@ describe("handleZaloWebhookRequest", () => {
}; };
try { try {
await withServer( await withServer(webhookRequestHandler, async (baseUrl) => {
async (req, res) => { const first = await fetch(`${baseUrl}/hook-replay`, {
const handled = await handleZaloWebhookRequest(req, res); method: "POST",
if (!handled) { headers: {
res.statusCode = 404; "x-bot-api-secret-token": "secret",
res.end("not found"); "content-type": "application/json",
} },
}, body: JSON.stringify(payload),
async (baseUrl) => { });
const first = await fetch(`${baseUrl}/hook-replay`, { const second = await fetch(`${baseUrl}/hook-replay`, {
method: "POST", method: "POST",
headers: { headers: {
"x-bot-api-secret-token": "secret", "x-bot-api-secret-token": "secret",
"content-type": "application/json", "content-type": "application/json",
}, },
body: JSON.stringify(payload), 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(first.status).toBe(200);
expect(second.status).toBe(200); expect(second.status).toBe(200);
expect(sink).toHaveBeenCalledTimes(1); expect(sink).toHaveBeenCalledTimes(1);
}, });
);
} finally { } finally {
unregister(); unregister();
} }
}); });
it("returns 429 when per-path request rate exceeds threshold", async () => { it("returns 429 when per-path request rate exceeds threshold", async () => {
const core = {} as PluginRuntime; const unregister = registerTarget({ path: "/hook-rate" });
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,
});
try { try {
await withServer( await withServer(webhookRequestHandler, async (baseUrl) => {
async (req, res) => { let saw429 = false;
const handled = await handleZaloWebhookRequest(req, res); for (let i = 0; i < 130; i += 1) {
if (!handled) { const response = await fetch(`${baseUrl}/hook-rate`, {
res.statusCode = 404; method: "POST",
res.end("not found"); headers: {
} "x-bot-api-secret-token": "secret",
}, "content-type": "application/json",
async (baseUrl) => { },
let saw429 = false; body: "{}",
for (let i = 0; i < 130; i += 1) { });
const response = await fetch(`${baseUrl}/hook-rate`, { if (response.status === 429) {
method: "POST", saw429 = true;
headers: { break;
"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 { } finally {
unregister(); unregister();
} }