Files
openclaw/extensions/zalo/src/monitor.pairing.lifecycle.test.ts

136 lines
4.2 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { withServer } from "../../../test/helpers/http-test-server.js";
import {
createLifecycleMonitorSetup,
createTextUpdate,
postWebhookReplay,
settleAsyncWork,
} from "../test-support/lifecycle-test-support.js";
import {
resetLifecycleTestState,
sendMessageMock,
setLifecycleRuntimeCore,
startWebhookLifecycleMonitor,
} from "../test-support/monitor-mocks-test-support.js";
describe("Zalo pairing lifecycle", () => {
const readAllowFromStoreMock = vi.fn(async () => [] as string[]);
const upsertPairingRequestMock = vi.fn(async () => ({ code: "PAIRCODE", created: true }));
beforeEach(async () => {
await resetLifecycleTestState();
setLifecycleRuntimeCore({
pairing: {
readAllowFromStore: readAllowFromStoreMock,
upsertPairingRequest: upsertPairingRequestMock,
},
commands: {
shouldComputeCommandAuthorized: vi.fn(() => false),
resolveCommandAuthorizedFromAuthorizers: vi.fn(() => false),
},
});
});
afterEach(async () => {
await resetLifecycleTestState();
});
function createPairingMonitorSetup() {
return createLifecycleMonitorSetup({
accountId: "acct-zalo-pairing",
dmPolicy: "pairing",
allowFrom: [],
});
}
it("emits one pairing reply across duplicate webhook replay and scopes reads and writes to accountId", async () => {
const monitor = await startWebhookLifecycleMonitor(createPairingMonitorSetup());
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(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 monitor = await startWebhookLifecycleMonitor(createPairingMonitorSetup());
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(upsertPairingRequestMock).toHaveBeenCalledTimes(1);
expect(sendMessageMock).toHaveBeenCalledTimes(1);
expect(monitor.runtime.error).not.toHaveBeenCalled();
} finally {
await monitor.stop();
}
});
});