mirror of https://github.com/openclaw/openclaw.git
test: harden zalo webhook lifecycle tests
This commit is contained in:
parent
9e1b524a00
commit
5ebccf5e30
|
|
@ -42,92 +42,92 @@ describe("Zalo pairing lifecycle", () => {
|
|||
}
|
||||
|
||||
it("emits one pairing reply across duplicate webhook replay and scopes reads and writes to accountId", async () => {
|
||||
const { abort, route, run } = await startWebhookLifecycleMonitor(createPairingMonitorSetup());
|
||||
const monitor = await startWebhookLifecycleMonitor(createPairingMonitorSetup());
|
||||
|
||||
await withServer(
|
||||
(req, res) => route.handler(req, res),
|
||||
async (baseUrl) => {
|
||||
const { first, replay } = await postWebhookReplay({
|
||||
baseUrl,
|
||||
path: "/hooks/zalo",
|
||||
secret: "supersecret",
|
||||
payload: createTextUpdate({
|
||||
messageId: `zalo-pairing-${Date.now()}`,
|
||||
userId: "user-unauthorized",
|
||||
userName: "Unauthorized User",
|
||||
chatId: "dm-pairing-1",
|
||||
}),
|
||||
});
|
||||
try {
|
||||
await withServer(
|
||||
(req, res) => monitor.route.handler(req, res),
|
||||
async (baseUrl) => {
|
||||
const { first, replay } = await postWebhookReplay({
|
||||
baseUrl,
|
||||
path: "/hooks/zalo",
|
||||
secret: "supersecret",
|
||||
payload: createTextUpdate({
|
||||
messageId: `zalo-pairing-${Date.now()}`,
|
||||
userId: "user-unauthorized",
|
||||
userName: "Unauthorized User",
|
||||
chatId: "dm-pairing-1",
|
||||
}),
|
||||
});
|
||||
|
||||
expect(first.status).toBe(200);
|
||||
expect(replay.status).toBe(200);
|
||||
await settleAsyncWork();
|
||||
},
|
||||
);
|
||||
expect(first.status).toBe(200);
|
||||
expect(replay.status).toBe(200);
|
||||
await settleAsyncWork();
|
||||
},
|
||||
);
|
||||
|
||||
expect(readAllowFromStoreMock).toHaveBeenCalledTimes(1);
|
||||
expect(readAllowFromStoreMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channel: "zalo",
|
||||
accountId: "acct-zalo-pairing",
|
||||
}),
|
||||
);
|
||||
expect(upsertPairingRequestMock).toHaveBeenCalledTimes(1);
|
||||
expect(upsertPairingRequestMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channel: "zalo",
|
||||
accountId: "acct-zalo-pairing",
|
||||
id: "user-unauthorized",
|
||||
}),
|
||||
);
|
||||
expect(sendMessageMock).toHaveBeenCalledTimes(1);
|
||||
expect(sendMessageMock).toHaveBeenCalledWith(
|
||||
"zalo-token",
|
||||
expect.objectContaining({
|
||||
chat_id: "dm-pairing-1",
|
||||
text: expect.stringContaining("PAIRCODE"),
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
|
||||
abort.abort();
|
||||
await run;
|
||||
expect(readAllowFromStoreMock).toHaveBeenCalledTimes(1);
|
||||
expect(readAllowFromStoreMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channel: "zalo",
|
||||
accountId: "acct-zalo-pairing",
|
||||
}),
|
||||
);
|
||||
expect(upsertPairingRequestMock).toHaveBeenCalledTimes(1);
|
||||
expect(upsertPairingRequestMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channel: "zalo",
|
||||
accountId: "acct-zalo-pairing",
|
||||
id: "user-unauthorized",
|
||||
}),
|
||||
);
|
||||
expect(sendMessageMock).toHaveBeenCalledTimes(1);
|
||||
expect(sendMessageMock).toHaveBeenCalledWith(
|
||||
"zalo-token",
|
||||
expect.objectContaining({
|
||||
chat_id: "dm-pairing-1",
|
||||
text: expect.stringContaining("PAIRCODE"),
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
} finally {
|
||||
await monitor.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not emit a second pairing reply when replay arrives after the first send fails", async () => {
|
||||
sendMessageMock.mockRejectedValueOnce(new Error("pairing send failed"));
|
||||
|
||||
const { abort, route, run, runtime } = await startWebhookLifecycleMonitor(
|
||||
createPairingMonitorSetup(),
|
||||
);
|
||||
const monitor = await startWebhookLifecycleMonitor(createPairingMonitorSetup());
|
||||
|
||||
await withServer(
|
||||
(req, res) => route.handler(req, res),
|
||||
async (baseUrl) => {
|
||||
const { first, replay } = await postWebhookReplay({
|
||||
baseUrl,
|
||||
path: "/hooks/zalo",
|
||||
secret: "supersecret",
|
||||
payload: createTextUpdate({
|
||||
messageId: `zalo-pairing-retry-${Date.now()}`,
|
||||
userId: "user-unauthorized",
|
||||
userName: "Unauthorized User",
|
||||
chatId: "dm-pairing-1",
|
||||
}),
|
||||
settleBeforeReplay: true,
|
||||
});
|
||||
try {
|
||||
await withServer(
|
||||
(req, res) => monitor.route.handler(req, res),
|
||||
async (baseUrl) => {
|
||||
const { first, replay } = await postWebhookReplay({
|
||||
baseUrl,
|
||||
path: "/hooks/zalo",
|
||||
secret: "supersecret",
|
||||
payload: createTextUpdate({
|
||||
messageId: `zalo-pairing-retry-${Date.now()}`,
|
||||
userId: "user-unauthorized",
|
||||
userName: "Unauthorized User",
|
||||
chatId: "dm-pairing-1",
|
||||
}),
|
||||
settleBeforeReplay: true,
|
||||
});
|
||||
|
||||
expect(first.status).toBe(200);
|
||||
expect(replay.status).toBe(200);
|
||||
await settleAsyncWork();
|
||||
},
|
||||
);
|
||||
expect(first.status).toBe(200);
|
||||
expect(replay.status).toBe(200);
|
||||
await settleAsyncWork();
|
||||
},
|
||||
);
|
||||
|
||||
expect(upsertPairingRequestMock).toHaveBeenCalledTimes(1);
|
||||
expect(sendMessageMock).toHaveBeenCalledTimes(1);
|
||||
expect(runtime.error).not.toHaveBeenCalled();
|
||||
|
||||
abort.abort();
|
||||
await run;
|
||||
expect(upsertPairingRequestMock).toHaveBeenCalledTimes(1);
|
||||
expect(sendMessageMock).toHaveBeenCalledTimes(1);
|
||||
expect(monitor.runtime.error).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
await monitor.stop();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -63,57 +63,58 @@ describe("Zalo reply-once lifecycle", () => {
|
|||
},
|
||||
);
|
||||
|
||||
const { abort, route, run } = await startWebhookLifecycleMonitor(createReplyOnceMonitorSetup());
|
||||
const monitor = await startWebhookLifecycleMonitor(createReplyOnceMonitorSetup());
|
||||
|
||||
await withServer(
|
||||
(req, res) => route.handler(req, res),
|
||||
async (baseUrl) => {
|
||||
const { first, replay } = await postWebhookReplay({
|
||||
baseUrl,
|
||||
path: "/hooks/zalo",
|
||||
secret: "supersecret",
|
||||
payload: createTextUpdate({
|
||||
messageId: `zalo-replay-${Date.now()}`,
|
||||
userId: "user-1",
|
||||
userName: "User One",
|
||||
chatId: "dm-chat-1",
|
||||
}),
|
||||
});
|
||||
try {
|
||||
await withServer(
|
||||
(req, res) => monitor.route.handler(req, res),
|
||||
async (baseUrl) => {
|
||||
const { first, replay } = await postWebhookReplay({
|
||||
baseUrl,
|
||||
path: "/hooks/zalo",
|
||||
secret: "supersecret",
|
||||
payload: createTextUpdate({
|
||||
messageId: `zalo-replay-${Date.now()}`,
|
||||
userId: "user-1",
|
||||
userName: "User One",
|
||||
chatId: "dm-chat-1",
|
||||
}),
|
||||
});
|
||||
|
||||
expect(first.status).toBe(200);
|
||||
expect(replay.status).toBe(200);
|
||||
await settleAsyncWork();
|
||||
},
|
||||
);
|
||||
expect(first.status).toBe(200);
|
||||
expect(replay.status).toBe(200);
|
||||
await settleAsyncWork();
|
||||
},
|
||||
);
|
||||
|
||||
expect(finalizeInboundContextMock).toHaveBeenCalledTimes(1);
|
||||
expect(finalizeInboundContextMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
AccountId: "acct-zalo-lifecycle",
|
||||
SessionKey: "agent:main:zalo:direct:dm-chat-1",
|
||||
MessageSid: expect.stringContaining("zalo-replay-"),
|
||||
From: "zalo:user-1",
|
||||
To: "zalo:dm-chat-1",
|
||||
}),
|
||||
);
|
||||
expect(recordInboundSessionMock).toHaveBeenCalledTimes(1);
|
||||
expect(recordInboundSessionMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sessionKey: "agent:main:zalo:direct:dm-chat-1",
|
||||
}),
|
||||
);
|
||||
expect(sendMessageMock).toHaveBeenCalledTimes(1);
|
||||
expect(sendMessageMock).toHaveBeenCalledWith(
|
||||
"zalo-token",
|
||||
expect.objectContaining({
|
||||
chat_id: "dm-chat-1",
|
||||
text: "zalo reply once",
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
|
||||
abort.abort();
|
||||
await run;
|
||||
expect(finalizeInboundContextMock).toHaveBeenCalledTimes(1);
|
||||
expect(finalizeInboundContextMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
AccountId: "acct-zalo-lifecycle",
|
||||
SessionKey: "agent:main:zalo:direct:dm-chat-1",
|
||||
MessageSid: expect.stringContaining("zalo-replay-"),
|
||||
From: "zalo:user-1",
|
||||
To: "zalo:dm-chat-1",
|
||||
}),
|
||||
);
|
||||
expect(recordInboundSessionMock).toHaveBeenCalledTimes(1);
|
||||
expect(recordInboundSessionMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sessionKey: "agent:main:zalo:direct:dm-chat-1",
|
||||
}),
|
||||
);
|
||||
expect(sendMessageMock).toHaveBeenCalledTimes(1);
|
||||
expect(sendMessageMock).toHaveBeenCalledWith(
|
||||
"zalo-token",
|
||||
expect.objectContaining({
|
||||
chat_id: "dm-chat-1",
|
||||
text: "zalo reply once",
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
} finally {
|
||||
await monitor.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not emit a second visible reply when replay arrives after a post-send failure", async () => {
|
||||
|
|
@ -128,39 +129,38 @@ describe("Zalo reply-once lifecycle", () => {
|
|||
},
|
||||
);
|
||||
|
||||
const { abort, route, run, runtime } = await startWebhookLifecycleMonitor(
|
||||
createReplyOnceMonitorSetup(),
|
||||
);
|
||||
const monitor = await startWebhookLifecycleMonitor(createReplyOnceMonitorSetup());
|
||||
|
||||
await withServer(
|
||||
(req, res) => route.handler(req, res),
|
||||
async (baseUrl) => {
|
||||
const { first, replay } = await postWebhookReplay({
|
||||
baseUrl,
|
||||
path: "/hooks/zalo",
|
||||
secret: "supersecret",
|
||||
payload: createTextUpdate({
|
||||
messageId: `zalo-retry-${Date.now()}`,
|
||||
userId: "user-1",
|
||||
userName: "User One",
|
||||
chatId: "dm-chat-1",
|
||||
}),
|
||||
settleBeforeReplay: true,
|
||||
});
|
||||
try {
|
||||
await withServer(
|
||||
(req, res) => monitor.route.handler(req, res),
|
||||
async (baseUrl) => {
|
||||
const { first, replay } = await postWebhookReplay({
|
||||
baseUrl,
|
||||
path: "/hooks/zalo",
|
||||
secret: "supersecret",
|
||||
payload: createTextUpdate({
|
||||
messageId: `zalo-retry-${Date.now()}`,
|
||||
userId: "user-1",
|
||||
userName: "User One",
|
||||
chatId: "dm-chat-1",
|
||||
}),
|
||||
settleBeforeReplay: true,
|
||||
});
|
||||
|
||||
expect(first.status).toBe(200);
|
||||
expect(replay.status).toBe(200);
|
||||
await settleAsyncWork();
|
||||
},
|
||||
);
|
||||
expect(first.status).toBe(200);
|
||||
expect(replay.status).toBe(200);
|
||||
await settleAsyncWork();
|
||||
},
|
||||
);
|
||||
|
||||
expect(dispatchReplyWithBufferedBlockDispatcherMock).toHaveBeenCalledTimes(1);
|
||||
expect(sendMessageMock).toHaveBeenCalledTimes(1);
|
||||
expect(runtime.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Zalo webhook failed: Error: post-send failure"),
|
||||
);
|
||||
|
||||
abort.abort();
|
||||
await run;
|
||||
expect(dispatchReplyWithBufferedBlockDispatcherMock).toHaveBeenCalledTimes(1);
|
||||
expect(sendMessageMock).toHaveBeenCalledTimes(1);
|
||||
expect(monitor.runtime.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Zalo webhook failed: Error: post-send failure"),
|
||||
);
|
||||
} finally {
|
||||
await monitor.stop();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { request as httpRequest } from "node:http";
|
||||
import { expect, vi } from "vitest";
|
||||
import type { ResolvedZaloAccount } from "../../../extensions/zalo/src/accounts.js";
|
||||
import {
|
||||
|
|
@ -282,13 +283,35 @@ export async function postWebhookUpdate(params: {
|
|||
secret: string;
|
||||
payload: Record<string, unknown>;
|
||||
}) {
|
||||
return await fetch(`${params.baseUrl}${params.path}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-bot-api-secret-token": params.secret,
|
||||
},
|
||||
body: JSON.stringify(params.payload),
|
||||
const url = new URL(params.path, params.baseUrl);
|
||||
const body = JSON.stringify(params.payload);
|
||||
return await new Promise<{ status: number; body: string }>((resolve, reject) => {
|
||||
const req = httpRequest(
|
||||
url,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"content-length": Buffer.byteLength(body),
|
||||
"x-bot-api-secret-token": params.secret,
|
||||
},
|
||||
},
|
||||
(res) => {
|
||||
const chunks: Buffer[] = [];
|
||||
res.on("data", (chunk) => {
|
||||
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
||||
});
|
||||
res.on("end", () => {
|
||||
resolve({
|
||||
status: res.statusCode ?? 0,
|
||||
body: Buffer.concat(chunks).toString("utf8"),
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
req.on("error", reject);
|
||||
req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -349,5 +372,9 @@ export async function startWebhookLifecycleMonitor(params: {
|
|||
route,
|
||||
run,
|
||||
runtime,
|
||||
stop: async () => {
|
||||
abort.abort();
|
||||
await run;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue