@openclaw/zalo 2026.3.12 → 2026.5.1-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/api.ts +9 -0
- package/channel-plugin-api.ts +1 -0
- package/contract-api.ts +5 -0
- package/index.test.ts +15 -0
- package/index.ts +16 -13
- package/openclaw.plugin.json +514 -1
- package/package.json +31 -5
- package/runtime-api.test.ts +17 -0
- package/runtime-api.ts +75 -0
- package/secret-contract-api.ts +5 -0
- package/setup-api.ts +34 -0
- package/setup-entry.ts +13 -0
- package/src/accounts.test.ts +70 -0
- package/src/accounts.ts +19 -19
- package/src/actions.runtime.ts +5 -0
- package/src/actions.test.ts +32 -0
- package/src/actions.ts +20 -14
- package/src/api.test.ts +108 -22
- package/src/api.ts +29 -2
- package/src/approval-auth.test.ts +17 -0
- package/src/approval-auth.ts +25 -0
- package/src/channel.directory.test.ts +22 -16
- package/src/channel.runtime.ts +93 -0
- package/src/channel.startup.test.ts +36 -35
- package/src/channel.ts +228 -336
- package/src/config-schema.ts +3 -3
- package/src/group-access.ts +4 -3
- package/src/monitor.group-policy.test.ts +0 -12
- package/src/monitor.image.polling.test.ts +110 -0
- package/src/monitor.lifecycle.test.ts +77 -92
- package/src/monitor.pairing.lifecycle.test.ts +141 -0
- package/src/monitor.polling.media-reply.test.ts +425 -0
- package/src/monitor.reply-once.lifecycle.test.ts +171 -0
- package/src/monitor.ts +527 -304
- package/src/monitor.types.ts +4 -0
- package/src/monitor.webhook.test.ts +392 -62
- package/src/monitor.webhook.ts +73 -36
- package/src/outbound-media.test.ts +182 -0
- package/src/outbound-media.ts +241 -0
- package/src/outbound-payload.contract.test.ts +45 -0
- package/src/probe.ts +1 -1
- package/src/proxy.ts +1 -1
- package/src/runtime-api.ts +75 -0
- package/src/runtime-support.ts +91 -0
- package/src/runtime.ts +6 -3
- package/src/secret-contract.ts +109 -0
- package/src/secret-input.ts +1 -9
- package/src/send.test.ts +120 -0
- package/src/send.ts +64 -40
- package/src/session-route.ts +32 -0
- package/src/setup-allow-from.ts +94 -0
- package/src/setup-core.ts +149 -0
- package/src/{onboarding.status.test.ts → setup-status.test.ts} +13 -4
- package/src/setup-surface.test.ts +175 -0
- package/src/{onboarding.ts → setup-surface.ts} +59 -177
- package/src/status-issues.test.ts +17 -0
- package/src/status-issues.ts +11 -27
- package/src/test-support/lifecycle-test-support.ts +413 -0
- package/src/test-support/monitor-mocks-test-support.ts +209 -0
- package/src/token.test.ts +15 -0
- package/src/token.ts +8 -17
- package/src/types.ts +2 -2
- package/test-api.ts +1 -0
- package/tsconfig.json +16 -0
- package/CHANGELOG.md +0 -95
- package/src/channel.sendpayload.test.ts +0 -44
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { createRuntimeEnv } from "openclaw/plugin-sdk/plugin-test-runtime";
|
|
2
|
+
import { afterAll, beforeEach, describe, expect, it } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
createImageLifecycleCore,
|
|
5
|
+
createImageUpdate,
|
|
6
|
+
createLifecycleMonitorSetup,
|
|
7
|
+
expectImageLifecycleDelivery,
|
|
8
|
+
settleAsyncWork,
|
|
9
|
+
} from "./test-support/lifecycle-test-support.js";
|
|
10
|
+
import {
|
|
11
|
+
getUpdatesMock,
|
|
12
|
+
getZaloRuntimeMock,
|
|
13
|
+
loadCachedLifecycleMonitorModule,
|
|
14
|
+
resetLifecycleTestState,
|
|
15
|
+
sendMessageMock,
|
|
16
|
+
} from "./test-support/monitor-mocks-test-support.js";
|
|
17
|
+
|
|
18
|
+
describe("Zalo polling image handling", () => {
|
|
19
|
+
const {
|
|
20
|
+
core,
|
|
21
|
+
finalizeInboundContextMock,
|
|
22
|
+
recordInboundSessionMock,
|
|
23
|
+
fetchRemoteMediaMock,
|
|
24
|
+
saveMediaBufferMock,
|
|
25
|
+
} = createImageLifecycleCore();
|
|
26
|
+
|
|
27
|
+
beforeEach(async () => {
|
|
28
|
+
await resetLifecycleTestState();
|
|
29
|
+
getZaloRuntimeMock.mockReturnValue(core);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterAll(async () => {
|
|
33
|
+
await resetLifecycleTestState();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("downloads inbound image media from photo_url and preserves display_name", async () => {
|
|
37
|
+
getUpdatesMock
|
|
38
|
+
.mockResolvedValueOnce({
|
|
39
|
+
ok: true,
|
|
40
|
+
result: createImageUpdate({ date: 1774084566880 }),
|
|
41
|
+
})
|
|
42
|
+
.mockImplementation(() => new Promise(() => {}));
|
|
43
|
+
|
|
44
|
+
const { monitorZaloProvider } = await loadCachedLifecycleMonitorModule("zalo-image-polling");
|
|
45
|
+
const abort = new AbortController();
|
|
46
|
+
const runtime = createRuntimeEnv();
|
|
47
|
+
const { account, config } = createLifecycleMonitorSetup({
|
|
48
|
+
accountId: "default",
|
|
49
|
+
dmPolicy: "open",
|
|
50
|
+
});
|
|
51
|
+
const run = monitorZaloProvider({
|
|
52
|
+
token: "zalo-token", // pragma: allowlist secret
|
|
53
|
+
account,
|
|
54
|
+
config,
|
|
55
|
+
runtime,
|
|
56
|
+
abortSignal: abort.signal,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
await settleAsyncWork();
|
|
60
|
+
expect(fetchRemoteMediaMock).toHaveBeenCalledTimes(1);
|
|
61
|
+
expectImageLifecycleDelivery({
|
|
62
|
+
fetchRemoteMediaMock,
|
|
63
|
+
saveMediaBufferMock,
|
|
64
|
+
finalizeInboundContextMock,
|
|
65
|
+
recordInboundSessionMock,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
abort.abort();
|
|
69
|
+
await run;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("rejects unauthorized DM images before downloading media", async () => {
|
|
73
|
+
getUpdatesMock
|
|
74
|
+
.mockResolvedValueOnce({
|
|
75
|
+
ok: true,
|
|
76
|
+
result: createImageUpdate({
|
|
77
|
+
messageId: "msg-unauthorized-1",
|
|
78
|
+
userId: "user-unauthorized-1",
|
|
79
|
+
chatId: "chat-unauthorized-1",
|
|
80
|
+
}),
|
|
81
|
+
})
|
|
82
|
+
.mockImplementation(() => new Promise(() => {}));
|
|
83
|
+
|
|
84
|
+
const { monitorZaloProvider } = await loadCachedLifecycleMonitorModule("zalo-image-polling");
|
|
85
|
+
const abort = new AbortController();
|
|
86
|
+
const runtime = createRuntimeEnv();
|
|
87
|
+
const { account, config } = createLifecycleMonitorSetup({
|
|
88
|
+
accountId: "default",
|
|
89
|
+
dmPolicy: "pairing",
|
|
90
|
+
allowFrom: ["allowed-user"],
|
|
91
|
+
});
|
|
92
|
+
const run = monitorZaloProvider({
|
|
93
|
+
token: "zalo-token", // pragma: allowlist secret
|
|
94
|
+
account,
|
|
95
|
+
config,
|
|
96
|
+
runtime,
|
|
97
|
+
abortSignal: abort.signal,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
await settleAsyncWork();
|
|
101
|
+
expect(sendMessageMock).toHaveBeenCalledTimes(1);
|
|
102
|
+
expect(fetchRemoteMediaMock).not.toHaveBeenCalled();
|
|
103
|
+
expect(saveMediaBufferMock).not.toHaveBeenCalled();
|
|
104
|
+
expect(finalizeInboundContextMock).not.toHaveBeenCalled();
|
|
105
|
+
expect(recordInboundSessionMock).not.toHaveBeenCalled();
|
|
106
|
+
|
|
107
|
+
abort.abort();
|
|
108
|
+
await run;
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
createEmptyPluginRegistry,
|
|
3
|
+
createRuntimeEnv,
|
|
4
|
+
setActivePluginRegistry,
|
|
5
|
+
} from "openclaw/plugin-sdk/plugin-test-runtime";
|
|
2
6
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
-
import {
|
|
4
|
-
import { setActivePluginRegistry } from "../../../src/plugins/runtime.js";
|
|
7
|
+
import type { OpenClawConfig } from "../runtime-api.js";
|
|
5
8
|
import type { ResolvedZaloAccount } from "./accounts.js";
|
|
6
9
|
|
|
7
10
|
const getWebhookInfoMock = vi.fn(async () => ({ ok: true, result: { url: "" } }));
|
|
@@ -9,8 +12,8 @@ const deleteWebhookMock = vi.fn(async () => ({ ok: true, result: { url: "" } }))
|
|
|
9
12
|
const getUpdatesMock = vi.fn(() => new Promise(() => {}));
|
|
10
13
|
const setWebhookMock = vi.fn(async () => ({ ok: true, result: { url: "" } }));
|
|
11
14
|
|
|
12
|
-
vi.mock("./api.js", async (
|
|
13
|
-
const actual = await
|
|
15
|
+
vi.mock("./api.js", async () => {
|
|
16
|
+
const actual = await vi.importActual<typeof import("./api.js")>("./api.js");
|
|
14
17
|
return {
|
|
15
18
|
...actual,
|
|
16
19
|
deleteWebhook: deleteWebhookMock,
|
|
@@ -28,8 +31,39 @@ vi.mock("./runtime.js", () => ({
|
|
|
28
31
|
}),
|
|
29
32
|
}));
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
const TEST_ACCOUNT = {
|
|
35
|
+
accountId: "default",
|
|
36
|
+
config: {},
|
|
37
|
+
} as unknown as ResolvedZaloAccount;
|
|
38
|
+
|
|
39
|
+
const TEST_CONFIG = {} as OpenClawConfig;
|
|
40
|
+
|
|
41
|
+
async function settleLifecycleWork(): Promise<void> {
|
|
42
|
+
for (let i = 0; i < 6; i += 1) {
|
|
43
|
+
await Promise.resolve();
|
|
44
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function startLifecycleMonitor(
|
|
49
|
+
options: {
|
|
50
|
+
useWebhook?: boolean;
|
|
51
|
+
webhookSecret?: string;
|
|
52
|
+
webhookUrl?: string;
|
|
53
|
+
} = {},
|
|
54
|
+
) {
|
|
55
|
+
const { monitorZaloProvider } = await import("./monitor.js");
|
|
56
|
+
const abort = new AbortController();
|
|
57
|
+
const runtime = createRuntimeEnv();
|
|
58
|
+
const run = monitorZaloProvider({
|
|
59
|
+
token: "test-token",
|
|
60
|
+
account: TEST_ACCOUNT,
|
|
61
|
+
config: TEST_CONFIG,
|
|
62
|
+
runtime,
|
|
63
|
+
abortSignal: abort.signal,
|
|
64
|
+
...options,
|
|
65
|
+
});
|
|
66
|
+
return { abort, runtime, run };
|
|
33
67
|
}
|
|
34
68
|
|
|
35
69
|
describe("monitorZaloProvider lifecycle", () => {
|
|
@@ -39,30 +73,14 @@ describe("monitorZaloProvider lifecycle", () => {
|
|
|
39
73
|
});
|
|
40
74
|
|
|
41
75
|
it("stays alive in polling mode until abort", async () => {
|
|
42
|
-
const { monitorZaloProvider } = await import("./monitor.js");
|
|
43
|
-
const abort = new AbortController();
|
|
44
|
-
const runtime = {
|
|
45
|
-
log: vi.fn<(message: string) => void>(),
|
|
46
|
-
error: vi.fn<(message: string) => void>(),
|
|
47
|
-
};
|
|
48
|
-
const account = {
|
|
49
|
-
accountId: "default",
|
|
50
|
-
config: {},
|
|
51
|
-
} as unknown as ResolvedZaloAccount;
|
|
52
|
-
const config = {} as OpenClawConfig;
|
|
53
|
-
|
|
54
76
|
let settled = false;
|
|
55
|
-
const run =
|
|
56
|
-
|
|
57
|
-
account,
|
|
58
|
-
config,
|
|
59
|
-
runtime,
|
|
60
|
-
abortSignal: abort.signal,
|
|
61
|
-
}).then(() => {
|
|
77
|
+
const { abort, runtime, run } = await startLifecycleMonitor();
|
|
78
|
+
const monitoredRun = run.then(() => {
|
|
62
79
|
settled = true;
|
|
63
80
|
});
|
|
64
81
|
|
|
65
|
-
await
|
|
82
|
+
await settleLifecycleWork();
|
|
83
|
+
expect(getUpdatesMock).toHaveBeenCalledTimes(1);
|
|
66
84
|
|
|
67
85
|
expect(getWebhookInfoMock).toHaveBeenCalledTimes(1);
|
|
68
86
|
expect(deleteWebhookMock).not.toHaveBeenCalled();
|
|
@@ -70,7 +88,7 @@ describe("monitorZaloProvider lifecycle", () => {
|
|
|
70
88
|
expect(settled).toBe(false);
|
|
71
89
|
|
|
72
90
|
abort.abort();
|
|
73
|
-
await
|
|
91
|
+
await monitoredRun;
|
|
74
92
|
|
|
75
93
|
expect(settled).toBe(true);
|
|
76
94
|
expect(runtime.log).toHaveBeenCalledWith(
|
|
@@ -84,27 +102,10 @@ describe("monitorZaloProvider lifecycle", () => {
|
|
|
84
102
|
result: { url: "https://example.com/hooks/zalo" },
|
|
85
103
|
});
|
|
86
104
|
|
|
87
|
-
const {
|
|
88
|
-
const abort = new AbortController();
|
|
89
|
-
const runtime = {
|
|
90
|
-
log: vi.fn<(message: string) => void>(),
|
|
91
|
-
error: vi.fn<(message: string) => void>(),
|
|
92
|
-
};
|
|
93
|
-
const account = {
|
|
94
|
-
accountId: "default",
|
|
95
|
-
config: {},
|
|
96
|
-
} as unknown as ResolvedZaloAccount;
|
|
97
|
-
const config = {} as OpenClawConfig;
|
|
98
|
-
|
|
99
|
-
const run = monitorZaloProvider({
|
|
100
|
-
token: "test-token",
|
|
101
|
-
account,
|
|
102
|
-
config,
|
|
103
|
-
runtime,
|
|
104
|
-
abortSignal: abort.signal,
|
|
105
|
-
});
|
|
105
|
+
const { abort, runtime, run } = await startLifecycleMonitor();
|
|
106
106
|
|
|
107
|
-
await
|
|
107
|
+
await settleLifecycleWork();
|
|
108
|
+
expect(getUpdatesMock).toHaveBeenCalledTimes(1);
|
|
108
109
|
|
|
109
110
|
expect(getWebhookInfoMock).toHaveBeenCalledTimes(1);
|
|
110
111
|
expect(deleteWebhookMock).toHaveBeenCalledTimes(1);
|
|
@@ -120,27 +121,10 @@ describe("monitorZaloProvider lifecycle", () => {
|
|
|
120
121
|
const { ZaloApiError } = await import("./api.js");
|
|
121
122
|
getWebhookInfoMock.mockRejectedValueOnce(new ZaloApiError("Not Found", 404, "Not Found"));
|
|
122
123
|
|
|
123
|
-
const {
|
|
124
|
-
const abort = new AbortController();
|
|
125
|
-
const runtime = {
|
|
126
|
-
log: vi.fn<(message: string) => void>(),
|
|
127
|
-
error: vi.fn<(message: string) => void>(),
|
|
128
|
-
};
|
|
129
|
-
const account = {
|
|
130
|
-
accountId: "default",
|
|
131
|
-
config: {},
|
|
132
|
-
} as unknown as ResolvedZaloAccount;
|
|
133
|
-
const config = {} as OpenClawConfig;
|
|
134
|
-
|
|
135
|
-
const run = monitorZaloProvider({
|
|
136
|
-
token: "test-token",
|
|
137
|
-
account,
|
|
138
|
-
config,
|
|
139
|
-
runtime,
|
|
140
|
-
abortSignal: abort.signal,
|
|
141
|
-
});
|
|
124
|
+
const { abort, runtime, run } = await startLifecycleMonitor();
|
|
142
125
|
|
|
143
|
-
await
|
|
126
|
+
await settleLifecycleWork();
|
|
127
|
+
expect(getUpdatesMock).toHaveBeenCalledTimes(1);
|
|
144
128
|
|
|
145
129
|
expect(getWebhookInfoMock).toHaveBeenCalledTimes(1);
|
|
146
130
|
expect(deleteWebhookMock).not.toHaveBeenCalled();
|
|
@@ -157,52 +141,53 @@ describe("monitorZaloProvider lifecycle", () => {
|
|
|
157
141
|
const registry = createEmptyPluginRegistry();
|
|
158
142
|
setActivePluginRegistry(registry);
|
|
159
143
|
|
|
144
|
+
let resolveSetWebhookCalled: (() => void) | undefined;
|
|
145
|
+
const setWebhookCalled = new Promise<void>((resolve) => {
|
|
146
|
+
resolveSetWebhookCalled = resolve;
|
|
147
|
+
});
|
|
148
|
+
setWebhookMock.mockImplementationOnce(async () => {
|
|
149
|
+
resolveSetWebhookCalled?.();
|
|
150
|
+
return { ok: true, result: { url: "" } };
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
let resolveDeleteWebhookCalled: (() => void) | undefined;
|
|
154
|
+
const deleteWebhookCalled = new Promise<void>((resolve) => {
|
|
155
|
+
resolveDeleteWebhookCalled = resolve;
|
|
156
|
+
});
|
|
160
157
|
let resolveDeleteWebhook: (() => void) | undefined;
|
|
161
158
|
deleteWebhookMock.mockImplementationOnce(
|
|
162
159
|
() =>
|
|
163
160
|
new Promise((resolve) => {
|
|
161
|
+
resolveDeleteWebhookCalled?.();
|
|
164
162
|
resolveDeleteWebhook = () => resolve({ ok: true, result: { url: "" } });
|
|
165
163
|
}),
|
|
166
164
|
);
|
|
167
165
|
|
|
168
|
-
const { monitorZaloProvider } = await import("./monitor.js");
|
|
169
|
-
const abort = new AbortController();
|
|
170
|
-
const runtime = {
|
|
171
|
-
log: vi.fn<(message: string) => void>(),
|
|
172
|
-
error: vi.fn<(message: string) => void>(),
|
|
173
|
-
};
|
|
174
|
-
const account = {
|
|
175
|
-
accountId: "default",
|
|
176
|
-
config: {},
|
|
177
|
-
} as unknown as ResolvedZaloAccount;
|
|
178
|
-
const config = {} as OpenClawConfig;
|
|
179
|
-
|
|
180
166
|
let settled = false;
|
|
181
|
-
const run =
|
|
182
|
-
token: "test-token",
|
|
183
|
-
account,
|
|
184
|
-
config,
|
|
185
|
-
runtime,
|
|
186
|
-
abortSignal: abort.signal,
|
|
167
|
+
const { abort, runtime, run } = await startLifecycleMonitor({
|
|
187
168
|
useWebhook: true,
|
|
188
169
|
webhookUrl: "https://example.com/hooks/zalo",
|
|
189
170
|
webhookSecret: "supersecret", // pragma: allowlist secret
|
|
190
|
-
})
|
|
171
|
+
});
|
|
172
|
+
const monitoredRun = run.then(() => {
|
|
191
173
|
settled = true;
|
|
192
174
|
});
|
|
193
175
|
|
|
194
|
-
await
|
|
195
|
-
|
|
176
|
+
await setWebhookCalled;
|
|
177
|
+
await settleLifecycleWork();
|
|
178
|
+
expect(setWebhookMock).toHaveBeenCalledTimes(1);
|
|
179
|
+
expect(registry.httpRoutes).toHaveLength(2);
|
|
196
180
|
|
|
197
181
|
abort.abort();
|
|
198
182
|
|
|
199
|
-
await
|
|
183
|
+
await deleteWebhookCalled;
|
|
184
|
+
expect(deleteWebhookMock).toHaveBeenCalledTimes(1);
|
|
200
185
|
expect(deleteWebhookMock).toHaveBeenCalledWith("test-token", undefined, 5000);
|
|
201
186
|
expect(settled).toBe(false);
|
|
202
|
-
expect(registry.httpRoutes).toHaveLength(
|
|
187
|
+
expect(registry.httpRoutes).toHaveLength(2);
|
|
203
188
|
|
|
204
189
|
resolveDeleteWebhook?.();
|
|
205
|
-
await
|
|
190
|
+
await monitoredRun;
|
|
206
191
|
|
|
207
192
|
expect(settled).toBe(true);
|
|
208
193
|
expect(registry.httpRoutes).toHaveLength(0);
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { withServer } from "openclaw/plugin-sdk/test-env";
|
|
2
|
+
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
createLifecycleMonitorSetup,
|
|
5
|
+
createTextUpdate,
|
|
6
|
+
postWebhookReplay,
|
|
7
|
+
settleAsyncWork,
|
|
8
|
+
} from "./test-support/lifecycle-test-support.js";
|
|
9
|
+
import {
|
|
10
|
+
resetLifecycleTestState,
|
|
11
|
+
sendMessageMock,
|
|
12
|
+
setLifecycleRuntimeCore,
|
|
13
|
+
startWebhookLifecycleMonitor,
|
|
14
|
+
} from "./test-support/monitor-mocks-test-support.js";
|
|
15
|
+
|
|
16
|
+
describe("Zalo pairing lifecycle", () => {
|
|
17
|
+
const readAllowFromStoreMock = vi.fn(async () => [] as string[]);
|
|
18
|
+
const upsertPairingRequestMock = vi.fn(async () => ({ code: "PAIRCODE", created: true }));
|
|
19
|
+
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
await resetLifecycleTestState();
|
|
22
|
+
setLifecycleRuntimeCore({
|
|
23
|
+
pairing: {
|
|
24
|
+
readAllowFromStore: readAllowFromStoreMock,
|
|
25
|
+
upsertPairingRequest: upsertPairingRequestMock,
|
|
26
|
+
},
|
|
27
|
+
commands: {
|
|
28
|
+
shouldComputeCommandAuthorized: vi.fn(() => false),
|
|
29
|
+
resolveCommandAuthorizedFromAuthorizers: vi.fn(() => false),
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterAll(async () => {
|
|
35
|
+
await resetLifecycleTestState();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
function createPairingMonitorSetup() {
|
|
39
|
+
return createLifecycleMonitorSetup({
|
|
40
|
+
accountId: "acct-zalo-pairing",
|
|
41
|
+
dmPolicy: "pairing",
|
|
42
|
+
allowFrom: [],
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
it("emits one pairing reply across duplicate webhook replay and scopes reads and writes to accountId", async () => {
|
|
47
|
+
const monitor = await startWebhookLifecycleMonitor({
|
|
48
|
+
...createPairingMonitorSetup(),
|
|
49
|
+
cacheKey: "zalo-pairing-lifecycle",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
await withServer(
|
|
54
|
+
(req, res) => monitor.route.handler(req, res),
|
|
55
|
+
async (baseUrl) => {
|
|
56
|
+
const { first, replay } = await postWebhookReplay({
|
|
57
|
+
baseUrl,
|
|
58
|
+
path: "/hooks/zalo",
|
|
59
|
+
secret: "supersecret",
|
|
60
|
+
payload: createTextUpdate({
|
|
61
|
+
messageId: `zalo-pairing-${Date.now()}`,
|
|
62
|
+
userId: "user-unauthorized",
|
|
63
|
+
userName: "Unauthorized User",
|
|
64
|
+
chatId: "dm-pairing-1",
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(first.status).toBe(200);
|
|
69
|
+
expect(replay.status).toBe(200);
|
|
70
|
+
await settleAsyncWork();
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
expect(readAllowFromStoreMock).toHaveBeenCalledTimes(1);
|
|
75
|
+
expect(readAllowFromStoreMock).toHaveBeenCalledWith(
|
|
76
|
+
expect.objectContaining({
|
|
77
|
+
channel: "zalo",
|
|
78
|
+
accountId: "acct-zalo-pairing",
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
expect(upsertPairingRequestMock).toHaveBeenCalledTimes(1);
|
|
82
|
+
expect(upsertPairingRequestMock).toHaveBeenCalledWith(
|
|
83
|
+
expect.objectContaining({
|
|
84
|
+
channel: "zalo",
|
|
85
|
+
accountId: "acct-zalo-pairing",
|
|
86
|
+
id: "user-unauthorized",
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
expect(sendMessageMock).toHaveBeenCalledTimes(1);
|
|
90
|
+
expect(sendMessageMock).toHaveBeenCalledWith(
|
|
91
|
+
"zalo-token",
|
|
92
|
+
expect.objectContaining({
|
|
93
|
+
chat_id: "dm-pairing-1",
|
|
94
|
+
text: expect.stringContaining("PAIRCODE"),
|
|
95
|
+
}),
|
|
96
|
+
undefined,
|
|
97
|
+
);
|
|
98
|
+
} finally {
|
|
99
|
+
await monitor.stop();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("does not emit a second pairing reply when replay arrives after the first send fails", async () => {
|
|
104
|
+
sendMessageMock.mockRejectedValueOnce(new Error("pairing send failed"));
|
|
105
|
+
|
|
106
|
+
const monitor = await startWebhookLifecycleMonitor({
|
|
107
|
+
...createPairingMonitorSetup(),
|
|
108
|
+
cacheKey: "zalo-pairing-lifecycle",
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
await withServer(
|
|
113
|
+
(req, res) => monitor.route.handler(req, res),
|
|
114
|
+
async (baseUrl) => {
|
|
115
|
+
const { first, replay } = await postWebhookReplay({
|
|
116
|
+
baseUrl,
|
|
117
|
+
path: "/hooks/zalo",
|
|
118
|
+
secret: "supersecret",
|
|
119
|
+
payload: createTextUpdate({
|
|
120
|
+
messageId: `zalo-pairing-retry-${Date.now()}`,
|
|
121
|
+
userId: "user-unauthorized",
|
|
122
|
+
userName: "Unauthorized User",
|
|
123
|
+
chatId: "dm-pairing-1",
|
|
124
|
+
}),
|
|
125
|
+
settleBeforeReplay: true,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(first.status).toBe(200);
|
|
129
|
+
expect(replay.status).toBe(200);
|
|
130
|
+
await settleAsyncWork();
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
expect(upsertPairingRequestMock).toHaveBeenCalledTimes(1);
|
|
135
|
+
expect(sendMessageMock).toHaveBeenCalledTimes(1);
|
|
136
|
+
expect(monitor.runtime.error).not.toHaveBeenCalled();
|
|
137
|
+
} finally {
|
|
138
|
+
await monitor.stop();
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
});
|