@openclaw/zalo 2026.5.2 → 2026.5.3-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/dist/accounts-9NLDDlZ8.js +118 -0
- package/dist/actions.runtime-kJ65ZxW7.js +5 -0
- package/dist/api.js +5 -0
- package/dist/channel-VPbtV3Oq.js +343 -0
- package/dist/channel-plugin-api.js +2 -0
- package/dist/channel.runtime-BnTAWQx5.js +106 -0
- package/dist/contract-api.js +3 -0
- package/dist/group-access-DZR43lOR.js +30 -0
- package/dist/index.js +22 -0
- package/dist/monitor-DMysJBWa.js +823 -0
- package/dist/monitor.webhook-DqnuvgjV.js +175 -0
- package/dist/proxy-CY8VuC6H.js +135 -0
- package/dist/runtime-BRFxnYQx.js +8 -0
- package/dist/runtime-api-MOTmRW4F.js +19 -0
- package/dist/runtime-api.js +3 -0
- package/dist/secret-contract-Dw93tGo2.js +87 -0
- package/dist/secret-contract-api.js +2 -0
- package/dist/send-Gv3l5EGI.js +101 -0
- package/dist/setup-api.js +30 -0
- package/dist/setup-core-DigRD3j1.js +166 -0
- package/dist/setup-entry.js +15 -0
- package/dist/setup-surface-2Up3yWov.js +216 -0
- package/dist/test-api.js +2 -0
- package/package.json +15 -6
- package/api.ts +0 -9
- package/channel-plugin-api.ts +0 -1
- package/contract-api.ts +0 -5
- package/index.test.ts +0 -15
- package/index.ts +0 -20
- package/runtime-api.test.ts +0 -17
- package/runtime-api.ts +0 -75
- package/secret-contract-api.ts +0 -5
- package/setup-api.ts +0 -34
- package/setup-entry.ts +0 -13
- package/src/accounts.test.ts +0 -70
- package/src/accounts.ts +0 -60
- package/src/actions.runtime.ts +0 -5
- package/src/actions.test.ts +0 -32
- package/src/actions.ts +0 -62
- package/src/api.test.ts +0 -149
- package/src/api.ts +0 -265
- package/src/approval-auth.test.ts +0 -17
- package/src/approval-auth.ts +0 -25
- package/src/channel.directory.test.ts +0 -59
- package/src/channel.runtime.ts +0 -93
- package/src/channel.startup.test.ts +0 -101
- package/src/channel.ts +0 -275
- package/src/config-schema.test.ts +0 -30
- package/src/config-schema.ts +0 -29
- package/src/group-access.ts +0 -49
- package/src/monitor.group-policy.test.ts +0 -94
- package/src/monitor.image.polling.test.ts +0 -110
- package/src/monitor.lifecycle.test.ts +0 -198
- package/src/monitor.pairing.lifecycle.test.ts +0 -141
- package/src/monitor.polling.media-reply.test.ts +0 -425
- package/src/monitor.reply-once.lifecycle.test.ts +0 -171
- package/src/monitor.ts +0 -1028
- package/src/monitor.types.ts +0 -4
- package/src/monitor.webhook.test.ts +0 -806
- package/src/monitor.webhook.ts +0 -278
- package/src/outbound-media.test.ts +0 -182
- package/src/outbound-media.ts +0 -241
- package/src/outbound-payload.contract.test.ts +0 -45
- package/src/probe.ts +0 -45
- package/src/proxy.ts +0 -24
- package/src/runtime-api.ts +0 -75
- package/src/runtime-support.ts +0 -91
- package/src/runtime.ts +0 -9
- package/src/secret-contract.ts +0 -109
- package/src/secret-input.ts +0 -5
- package/src/send.test.ts +0 -120
- package/src/send.ts +0 -153
- package/src/session-route.ts +0 -32
- package/src/setup-allow-from.ts +0 -94
- package/src/setup-core.ts +0 -149
- package/src/setup-status.test.ts +0 -33
- package/src/setup-surface.test.ts +0 -175
- package/src/setup-surface.ts +0 -291
- package/src/status-issues.test.ts +0 -17
- package/src/status-issues.ts +0 -37
- package/src/test-support/lifecycle-test-support.ts +0 -413
- package/src/test-support/monitor-mocks-test-support.ts +0 -209
- package/src/token.test.ts +0 -92
- package/src/token.ts +0 -79
- package/src/types.ts +0 -50
- package/test-api.ts +0 -1
- package/tsconfig.json +0 -16
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { __testing } from "./monitor.js";
|
|
3
|
-
|
|
4
|
-
describe("zalo group policy access", () => {
|
|
5
|
-
it("blocks all group messages when policy is disabled", () => {
|
|
6
|
-
const decision = __testing.evaluateZaloGroupAccess({
|
|
7
|
-
providerConfigPresent: true,
|
|
8
|
-
configuredGroupPolicy: "disabled",
|
|
9
|
-
defaultGroupPolicy: "open",
|
|
10
|
-
groupAllowFrom: ["zalo:123"],
|
|
11
|
-
senderId: "123",
|
|
12
|
-
});
|
|
13
|
-
expect(decision).toMatchObject({
|
|
14
|
-
allowed: false,
|
|
15
|
-
groupPolicy: "disabled",
|
|
16
|
-
reason: "disabled",
|
|
17
|
-
});
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it("blocks group messages on allowlist policy with empty allowlist", () => {
|
|
21
|
-
const decision = __testing.evaluateZaloGroupAccess({
|
|
22
|
-
providerConfigPresent: true,
|
|
23
|
-
configuredGroupPolicy: "allowlist",
|
|
24
|
-
defaultGroupPolicy: "open",
|
|
25
|
-
groupAllowFrom: [],
|
|
26
|
-
senderId: "attacker",
|
|
27
|
-
});
|
|
28
|
-
expect(decision).toMatchObject({
|
|
29
|
-
allowed: false,
|
|
30
|
-
groupPolicy: "allowlist",
|
|
31
|
-
reason: "empty_allowlist",
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("blocks sender not in group allowlist", () => {
|
|
36
|
-
const decision = __testing.evaluateZaloGroupAccess({
|
|
37
|
-
providerConfigPresent: true,
|
|
38
|
-
configuredGroupPolicy: "allowlist",
|
|
39
|
-
defaultGroupPolicy: "open",
|
|
40
|
-
groupAllowFrom: ["zalo:victim-user-001"],
|
|
41
|
-
senderId: "attacker-user-999",
|
|
42
|
-
});
|
|
43
|
-
expect(decision).toMatchObject({
|
|
44
|
-
allowed: false,
|
|
45
|
-
groupPolicy: "allowlist",
|
|
46
|
-
reason: "sender_not_allowlisted",
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("allows sender in group allowlist", () => {
|
|
51
|
-
const decision = __testing.evaluateZaloGroupAccess({
|
|
52
|
-
providerConfigPresent: true,
|
|
53
|
-
configuredGroupPolicy: "allowlist",
|
|
54
|
-
defaultGroupPolicy: "open",
|
|
55
|
-
groupAllowFrom: ["zl:12345"],
|
|
56
|
-
senderId: "12345",
|
|
57
|
-
});
|
|
58
|
-
expect(decision).toMatchObject({
|
|
59
|
-
allowed: true,
|
|
60
|
-
groupPolicy: "allowlist",
|
|
61
|
-
reason: "allowed",
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("allows any sender with wildcard allowlist", () => {
|
|
66
|
-
const decision = __testing.evaluateZaloGroupAccess({
|
|
67
|
-
providerConfigPresent: true,
|
|
68
|
-
configuredGroupPolicy: "allowlist",
|
|
69
|
-
defaultGroupPolicy: "open",
|
|
70
|
-
groupAllowFrom: ["*"],
|
|
71
|
-
senderId: "random-user",
|
|
72
|
-
});
|
|
73
|
-
expect(decision).toMatchObject({
|
|
74
|
-
allowed: true,
|
|
75
|
-
groupPolicy: "allowlist",
|
|
76
|
-
reason: "allowed",
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("allows all group senders on open policy", () => {
|
|
81
|
-
const decision = __testing.evaluateZaloGroupAccess({
|
|
82
|
-
providerConfigPresent: true,
|
|
83
|
-
configuredGroupPolicy: "open",
|
|
84
|
-
defaultGroupPolicy: "allowlist",
|
|
85
|
-
groupAllowFrom: [],
|
|
86
|
-
senderId: "attacker-user-999",
|
|
87
|
-
});
|
|
88
|
-
expect(decision).toMatchObject({
|
|
89
|
-
allowed: true,
|
|
90
|
-
groupPolicy: "open",
|
|
91
|
-
reason: "allowed",
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
});
|
|
@@ -1,110 +0,0 @@
|
|
|
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,198 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createEmptyPluginRegistry,
|
|
3
|
-
createRuntimeEnv,
|
|
4
|
-
setActivePluginRegistry,
|
|
5
|
-
} from "openclaw/plugin-sdk/plugin-test-runtime";
|
|
6
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
7
|
-
import type { OpenClawConfig } from "../runtime-api.js";
|
|
8
|
-
import type { ResolvedZaloAccount } from "./accounts.js";
|
|
9
|
-
|
|
10
|
-
const getWebhookInfoMock = vi.fn(async () => ({ ok: true, result: { url: "" } }));
|
|
11
|
-
const deleteWebhookMock = vi.fn(async () => ({ ok: true, result: { url: "" } }));
|
|
12
|
-
const getUpdatesMock = vi.fn(() => new Promise(() => {}));
|
|
13
|
-
const setWebhookMock = vi.fn(async () => ({ ok: true, result: { url: "" } }));
|
|
14
|
-
|
|
15
|
-
vi.mock("./api.js", async () => {
|
|
16
|
-
const actual = await vi.importActual<typeof import("./api.js")>("./api.js");
|
|
17
|
-
return {
|
|
18
|
-
...actual,
|
|
19
|
-
deleteWebhook: deleteWebhookMock,
|
|
20
|
-
getWebhookInfo: getWebhookInfoMock,
|
|
21
|
-
getUpdates: getUpdatesMock,
|
|
22
|
-
setWebhook: setWebhookMock,
|
|
23
|
-
};
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
vi.mock("./runtime.js", () => ({
|
|
27
|
-
getZaloRuntime: () => ({
|
|
28
|
-
logging: {
|
|
29
|
-
shouldLogVerbose: () => false,
|
|
30
|
-
},
|
|
31
|
-
}),
|
|
32
|
-
}));
|
|
33
|
-
|
|
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 };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
describe("monitorZaloProvider lifecycle", () => {
|
|
70
|
-
afterEach(() => {
|
|
71
|
-
vi.clearAllMocks();
|
|
72
|
-
setActivePluginRegistry(createEmptyPluginRegistry());
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("stays alive in polling mode until abort", async () => {
|
|
76
|
-
let settled = false;
|
|
77
|
-
const { abort, runtime, run } = await startLifecycleMonitor();
|
|
78
|
-
const monitoredRun = run.then(() => {
|
|
79
|
-
settled = true;
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
await settleLifecycleWork();
|
|
83
|
-
expect(getUpdatesMock).toHaveBeenCalledTimes(1);
|
|
84
|
-
|
|
85
|
-
expect(getWebhookInfoMock).toHaveBeenCalledTimes(1);
|
|
86
|
-
expect(deleteWebhookMock).not.toHaveBeenCalled();
|
|
87
|
-
expect(getUpdatesMock).toHaveBeenCalledTimes(1);
|
|
88
|
-
expect(settled).toBe(false);
|
|
89
|
-
|
|
90
|
-
abort.abort();
|
|
91
|
-
await monitoredRun;
|
|
92
|
-
|
|
93
|
-
expect(settled).toBe(true);
|
|
94
|
-
expect(runtime.log).toHaveBeenCalledWith(
|
|
95
|
-
expect.stringContaining("Zalo provider stopped mode=polling"),
|
|
96
|
-
);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it("deletes an existing webhook before polling", async () => {
|
|
100
|
-
getWebhookInfoMock.mockResolvedValueOnce({
|
|
101
|
-
ok: true,
|
|
102
|
-
result: { url: "https://example.com/hooks/zalo" },
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
const { abort, runtime, run } = await startLifecycleMonitor();
|
|
106
|
-
|
|
107
|
-
await settleLifecycleWork();
|
|
108
|
-
expect(getUpdatesMock).toHaveBeenCalledTimes(1);
|
|
109
|
-
|
|
110
|
-
expect(getWebhookInfoMock).toHaveBeenCalledTimes(1);
|
|
111
|
-
expect(deleteWebhookMock).toHaveBeenCalledTimes(1);
|
|
112
|
-
expect(runtime.log).toHaveBeenCalledWith(
|
|
113
|
-
expect.stringContaining("Zalo polling mode ready (webhook disabled)"),
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
abort.abort();
|
|
117
|
-
await run;
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("continues polling when webhook inspection returns 404", async () => {
|
|
121
|
-
const { ZaloApiError } = await import("./api.js");
|
|
122
|
-
getWebhookInfoMock.mockRejectedValueOnce(new ZaloApiError("Not Found", 404, "Not Found"));
|
|
123
|
-
|
|
124
|
-
const { abort, runtime, run } = await startLifecycleMonitor();
|
|
125
|
-
|
|
126
|
-
await settleLifecycleWork();
|
|
127
|
-
expect(getUpdatesMock).toHaveBeenCalledTimes(1);
|
|
128
|
-
|
|
129
|
-
expect(getWebhookInfoMock).toHaveBeenCalledTimes(1);
|
|
130
|
-
expect(deleteWebhookMock).not.toHaveBeenCalled();
|
|
131
|
-
expect(runtime.log).toHaveBeenCalledWith(
|
|
132
|
-
expect.stringContaining("webhook inspection unavailable; continuing without webhook cleanup"),
|
|
133
|
-
);
|
|
134
|
-
expect(runtime.error).not.toHaveBeenCalled();
|
|
135
|
-
|
|
136
|
-
abort.abort();
|
|
137
|
-
await run;
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it("waits for webhook deletion before finishing webhook shutdown", async () => {
|
|
141
|
-
const registry = createEmptyPluginRegistry();
|
|
142
|
-
setActivePluginRegistry(registry);
|
|
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
|
-
});
|
|
157
|
-
let resolveDeleteWebhook: (() => void) | undefined;
|
|
158
|
-
deleteWebhookMock.mockImplementationOnce(
|
|
159
|
-
() =>
|
|
160
|
-
new Promise((resolve) => {
|
|
161
|
-
resolveDeleteWebhookCalled?.();
|
|
162
|
-
resolveDeleteWebhook = () => resolve({ ok: true, result: { url: "" } });
|
|
163
|
-
}),
|
|
164
|
-
);
|
|
165
|
-
|
|
166
|
-
let settled = false;
|
|
167
|
-
const { abort, runtime, run } = await startLifecycleMonitor({
|
|
168
|
-
useWebhook: true,
|
|
169
|
-
webhookUrl: "https://example.com/hooks/zalo",
|
|
170
|
-
webhookSecret: "supersecret", // pragma: allowlist secret
|
|
171
|
-
});
|
|
172
|
-
const monitoredRun = run.then(() => {
|
|
173
|
-
settled = true;
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
await setWebhookCalled;
|
|
177
|
-
await settleLifecycleWork();
|
|
178
|
-
expect(setWebhookMock).toHaveBeenCalledTimes(1);
|
|
179
|
-
expect(registry.httpRoutes).toHaveLength(2);
|
|
180
|
-
|
|
181
|
-
abort.abort();
|
|
182
|
-
|
|
183
|
-
await deleteWebhookCalled;
|
|
184
|
-
expect(deleteWebhookMock).toHaveBeenCalledTimes(1);
|
|
185
|
-
expect(deleteWebhookMock).toHaveBeenCalledWith("test-token", undefined, 5000);
|
|
186
|
-
expect(settled).toBe(false);
|
|
187
|
-
expect(registry.httpRoutes).toHaveLength(2);
|
|
188
|
-
|
|
189
|
-
resolveDeleteWebhook?.();
|
|
190
|
-
await monitoredRun;
|
|
191
|
-
|
|
192
|
-
expect(settled).toBe(true);
|
|
193
|
-
expect(registry.httpRoutes).toHaveLength(0);
|
|
194
|
-
expect(runtime.log).toHaveBeenCalledWith(
|
|
195
|
-
expect.stringContaining("Zalo provider stopped mode=webhook"),
|
|
196
|
-
);
|
|
197
|
-
});
|
|
198
|
-
});
|
|
@@ -1,141 +0,0 @@
|
|
|
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
|
-
});
|