mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-04 21:10:25 +00:00
refactor: localize zalo test support
This commit is contained in:
@@ -12,22 +12,7 @@ import {
|
||||
sendPayloadWithChunkedTextAndMedia as sendZaloPayloadWithChunkedTextAndMedia,
|
||||
} from "../../../src/plugin-sdk/zalo.js";
|
||||
import { sendPayloadWithChunkedTextAndMedia as sendZalouserPayloadWithChunkedTextAndMedia } from "../../../src/plugin-sdk/zalouser.js";
|
||||
import {
|
||||
loadBundledPluginTestApiSync,
|
||||
resolveRelativeBundledPluginPublicModuleId,
|
||||
} from "../../../src/test-utils/bundled-plugin-public-surface.js";
|
||||
|
||||
type ChannelSendResponse = { ok?: boolean; messageId?: string };
|
||||
type SendMessageZalo = (
|
||||
to: string,
|
||||
text: string,
|
||||
options: Record<string, unknown>,
|
||||
) => Promise<ChannelSendResponse>;
|
||||
type SendMessageZalouser = (
|
||||
threadId: string,
|
||||
text: string,
|
||||
options: Record<string, unknown>,
|
||||
) => Promise<ChannelSendResponse>;
|
||||
import { loadBundledPluginTestApiSync } from "../../../src/test-utils/bundled-plugin-public-surface.js";
|
||||
type ParseZalouserOutboundTarget = (raw: string) => { threadId: string; isGroup: boolean };
|
||||
|
||||
const { discordOutbound } = loadBundledPluginTestApiSync<{
|
||||
@@ -36,76 +21,10 @@ const { discordOutbound } = loadBundledPluginTestApiSync<{
|
||||
const { whatsappOutbound } = loadBundledPluginTestApiSync<{
|
||||
whatsappOutbound: ChannelOutboundAdapter;
|
||||
}>("whatsapp");
|
||||
const { sendMessageZalo } = loadBundledPluginTestApiSync<{
|
||||
sendMessageZalo: SendMessageZalo;
|
||||
}>("zalo");
|
||||
const { sendMessageZalouser, parseZalouserOutboundTarget } = loadBundledPluginTestApiSync<{
|
||||
sendMessageZalouser: SendMessageZalouser;
|
||||
const { parseZalouserOutboundTarget } = loadBundledPluginTestApiSync<{
|
||||
parseZalouserOutboundTarget: ParseZalouserOutboundTarget;
|
||||
}>("zalouser");
|
||||
|
||||
const { zaloTestApiModuleId, zalouserTestApiModuleId } = vi.hoisted(() => ({
|
||||
zaloTestApiModuleId: resolveRelativeBundledPluginPublicModuleId({
|
||||
fromModuleUrl: import.meta.url,
|
||||
pluginId: "zalo",
|
||||
artifactBasename: "test-api.js",
|
||||
}),
|
||||
zalouserTestApiModuleId: resolveRelativeBundledPluginPublicModuleId({
|
||||
fromModuleUrl: import.meta.url,
|
||||
pluginId: "zalouser",
|
||||
artifactBasename: "test-api.js",
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock(zaloTestApiModuleId, async (importOriginal) => {
|
||||
const actual = await importOriginal<object>();
|
||||
return {
|
||||
...actual,
|
||||
sendMessageZalo: vi.fn().mockResolvedValue({ ok: true, messageId: "zl-1" }),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock(zalouserTestApiModuleId, async (importOriginal) => {
|
||||
const actual = await importOriginal<object>();
|
||||
return {
|
||||
...actual,
|
||||
listZalouserAccountIds: vi.fn(() => ["default"]),
|
||||
resolveDefaultZalouserAccountId: vi.fn(() => "default"),
|
||||
resolveZalouserAccountSync: vi.fn(() => ({
|
||||
accountId: "default",
|
||||
profile: "default",
|
||||
name: "test",
|
||||
enabled: true,
|
||||
authenticated: true,
|
||||
config: {},
|
||||
})),
|
||||
getZcaUserInfo: vi.fn(async () => null),
|
||||
checkZcaAuthenticated: vi.fn(async () => false),
|
||||
checkZaloAuthenticated: vi.fn(async () => false),
|
||||
getZaloUserInfo: vi.fn(async () => null),
|
||||
listZaloFriendsMatching: vi.fn(async () => []),
|
||||
listZaloGroupMembers: vi.fn(async () => []),
|
||||
listZaloGroupsMatching: vi.fn(async () => []),
|
||||
logoutZaloProfile: vi.fn(async () => {}),
|
||||
resolveZaloAllowFromEntries: vi.fn(async ({ entries }: { entries: string[] }) =>
|
||||
entries.map((entry) => ({ input: entry, resolved: true, id: entry, note: undefined })),
|
||||
),
|
||||
resolveZaloGroupsByEntries: vi.fn(async ({ entries }: { entries: string[] }) =>
|
||||
entries.map((entry) => ({ input: entry, resolved: true, id: entry, note: undefined })),
|
||||
),
|
||||
startZaloQrLogin: vi.fn(async () => ({
|
||||
message: "qr pending",
|
||||
qrDataUrl: undefined,
|
||||
})),
|
||||
waitForZaloQrLogin: vi.fn(async () => ({
|
||||
connected: false,
|
||||
message: "login pending",
|
||||
})),
|
||||
sendMessageZalouser: vi.fn().mockResolvedValue({ ok: true, messageId: "zlu-1" }),
|
||||
sendReactionZalouser: vi.fn().mockResolvedValue({ ok: true }),
|
||||
};
|
||||
});
|
||||
|
||||
type PayloadHarnessParams = {
|
||||
payload: ReplyPayload;
|
||||
sendResults?: Array<{ messageId: string }>;
|
||||
@@ -184,8 +103,8 @@ function createDirectTextMediaHarness(params: PayloadHarnessParams) {
|
||||
}
|
||||
|
||||
function createZaloHarness(params: PayloadHarnessParams) {
|
||||
const mockedSendZalo = vi.mocked(sendMessageZalo);
|
||||
primeChannelOutboundSendMock(mockedSendZalo, { ok: true, messageId: "zl-1" }, params.sendResults);
|
||||
const sendZalo = vi.fn();
|
||||
primeChannelOutboundSendMock(sendZalo, { ok: true, messageId: "zl-1" }, params.sendResults);
|
||||
const ctx = {
|
||||
cfg: {},
|
||||
to: "123456789",
|
||||
@@ -201,7 +120,7 @@ function createZaloHarness(params: PayloadHarnessParams) {
|
||||
sendText: async (nextCtx) =>
|
||||
buildChannelSendResult(
|
||||
"zalo",
|
||||
await mockedSendZalo(nextCtx.to, nextCtx.text, {
|
||||
await sendZalo(nextCtx.to, nextCtx.text, {
|
||||
accountId: undefined,
|
||||
cfg: nextCtx.cfg,
|
||||
}),
|
||||
@@ -209,7 +128,7 @@ function createZaloHarness(params: PayloadHarnessParams) {
|
||||
sendMedia: async (nextCtx) =>
|
||||
buildChannelSendResult(
|
||||
"zalo",
|
||||
await mockedSendZalo(nextCtx.to, nextCtx.text, {
|
||||
await sendZalo(nextCtx.to, nextCtx.text, {
|
||||
accountId: undefined,
|
||||
cfg: nextCtx.cfg,
|
||||
mediaUrl: nextCtx.mediaUrl,
|
||||
@@ -217,18 +136,14 @@ function createZaloHarness(params: PayloadHarnessParams) {
|
||||
),
|
||||
emptyResult: { channel: "zalo", messageId: "" },
|
||||
}),
|
||||
sendMock: mockedSendZalo,
|
||||
sendMock: sendZalo,
|
||||
to: ctx.to,
|
||||
};
|
||||
}
|
||||
|
||||
function createZalouserHarness(params: PayloadHarnessParams) {
|
||||
const mockedSendZalouser = vi.mocked(sendMessageZalouser);
|
||||
primeChannelOutboundSendMock(
|
||||
mockedSendZalouser,
|
||||
{ ok: true, messageId: "zlu-1" },
|
||||
params.sendResults,
|
||||
);
|
||||
const sendZalouser = vi.fn();
|
||||
primeChannelOutboundSendMock(sendZalouser, { ok: true, messageId: "zlu-1" }, params.sendResults);
|
||||
const ctx = {
|
||||
cfg: {},
|
||||
to: "user:987654321",
|
||||
@@ -243,7 +158,7 @@ function createZalouserHarness(params: PayloadHarnessParams) {
|
||||
const target = parseZalouserOutboundTarget(nextCtx.to);
|
||||
return buildChannelSendResult(
|
||||
"zalouser",
|
||||
await mockedSendZalouser(target.threadId, nextCtx.text, {
|
||||
await sendZalouser(target.threadId, nextCtx.text, {
|
||||
profile: "default",
|
||||
isGroup: target.isGroup,
|
||||
textMode: "markdown",
|
||||
@@ -256,7 +171,7 @@ function createZalouserHarness(params: PayloadHarnessParams) {
|
||||
const target = parseZalouserOutboundTarget(nextCtx.to);
|
||||
return buildChannelSendResult(
|
||||
"zalouser",
|
||||
await mockedSendZalouser(target.threadId, nextCtx.text, {
|
||||
await sendZalouser(target.threadId, nextCtx.text, {
|
||||
profile: "default",
|
||||
isGroup: target.isGroup,
|
||||
mediaUrl: nextCtx.mediaUrl,
|
||||
@@ -268,7 +183,7 @@ function createZalouserHarness(params: PayloadHarnessParams) {
|
||||
},
|
||||
emptyResult: { channel: "zalouser", messageId: "" },
|
||||
}),
|
||||
sendMock: mockedSendZalouser,
|
||||
sendMock: sendZalouser,
|
||||
to: "987654321",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ function collectPluginSdkAliases(params: {
|
||||
const realSpecifiers = new Set<string>();
|
||||
const stubSpecifiers = new Set<string>();
|
||||
const visitedFiles = new Set<string>();
|
||||
const stubPath = path.join(params.root, "test", "helpers", "extensions", "plugin-sdk-stub.cjs");
|
||||
const stubPath = path.join(params.root, "test", "helpers", "plugins", "plugin-sdk-stub.cjs");
|
||||
const explicitRealSpecifiers = new Set(params.realPluginSdkSpecifiers ?? []);
|
||||
|
||||
function visitModule(filePath: string, rootModule: boolean): void {
|
||||
|
||||
@@ -1,424 +0,0 @@
|
||||
import { request as httpRequest } from "node:http";
|
||||
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/zalo";
|
||||
import { expect, vi } from "vitest";
|
||||
import { createEmptyPluginRegistry } from "../../../src/plugins/registry.js";
|
||||
import { setActivePluginRegistry } from "../../../src/plugins/runtime.js";
|
||||
import {
|
||||
loadBundledPluginPublicSurfaceSync,
|
||||
resolveRelativeBundledPluginPublicModuleId,
|
||||
} from "../../../src/test-utils/bundled-plugin-public-surface.js";
|
||||
import { withServer } from "../http-test-server.js";
|
||||
import { createPluginRuntimeMock } from "./plugin-runtime-mock.js";
|
||||
import { createRuntimeEnv } from "./runtime-env.js";
|
||||
|
||||
export { withServer };
|
||||
|
||||
type ResolvedZaloAccount = {
|
||||
accountId: string;
|
||||
enabled: boolean;
|
||||
token: string;
|
||||
tokenSource: "env" | "config" | "configFile" | "none";
|
||||
config: Record<string, unknown>;
|
||||
};
|
||||
|
||||
const { clearZaloWebhookSecurityStateForTest, monitorZaloProvider } =
|
||||
loadBundledPluginPublicSurfaceSync<{
|
||||
clearZaloWebhookSecurityStateForTest: () => void;
|
||||
monitorZaloProvider: (params: {
|
||||
token: string;
|
||||
account: ResolvedZaloAccount;
|
||||
config: OpenClawConfig;
|
||||
runtime: ReturnType<typeof createRuntimeEnv>;
|
||||
abortSignal: AbortSignal;
|
||||
useWebhook?: boolean;
|
||||
webhookUrl?: string;
|
||||
webhookSecret?: string;
|
||||
}) => Promise<unknown>;
|
||||
}>({
|
||||
pluginId: "zalo",
|
||||
artifactBasename: "src/monitor.js",
|
||||
});
|
||||
const { normalizeSecretInputString } = loadBundledPluginPublicSurfaceSync<{
|
||||
normalizeSecretInputString: (value: unknown) => string | undefined;
|
||||
}>({
|
||||
pluginId: "zalo",
|
||||
artifactBasename: "src/secret-input.js",
|
||||
});
|
||||
|
||||
const zaloApiModuleId = resolveRelativeBundledPluginPublicModuleId({
|
||||
fromModuleUrl: import.meta.url,
|
||||
pluginId: "zalo",
|
||||
artifactBasename: "src/api.js",
|
||||
});
|
||||
const zaloRuntimeModuleId = resolveRelativeBundledPluginPublicModuleId({
|
||||
fromModuleUrl: import.meta.url,
|
||||
pluginId: "zalo",
|
||||
artifactBasename: "src/runtime.js",
|
||||
});
|
||||
|
||||
const lifecycleMocks = vi.hoisted(() => ({
|
||||
setWebhookMock: vi.fn(async () => ({ ok: true, result: { url: "" } })),
|
||||
deleteWebhookMock: vi.fn(async () => ({ ok: true, result: { url: "" } })),
|
||||
getWebhookInfoMock: vi.fn(async () => ({ ok: true, result: { url: "" } })),
|
||||
getUpdatesMock: vi.fn(() => new Promise(() => {})),
|
||||
sendChatActionMock: vi.fn(async () => ({ ok: true })),
|
||||
sendMessageMock: vi.fn(async () => ({
|
||||
ok: true,
|
||||
result: { message_id: "zalo-test-reply-1" },
|
||||
})),
|
||||
sendPhotoMock: vi.fn(async () => ({ ok: true })),
|
||||
getZaloRuntimeMock: vi.fn(),
|
||||
}));
|
||||
|
||||
export const setWebhookMock = lifecycleMocks.setWebhookMock;
|
||||
export const deleteWebhookMock = lifecycleMocks.deleteWebhookMock;
|
||||
export const getWebhookInfoMock = lifecycleMocks.getWebhookInfoMock;
|
||||
export const getUpdatesMock = lifecycleMocks.getUpdatesMock;
|
||||
export const sendChatActionMock = lifecycleMocks.sendChatActionMock;
|
||||
export const sendMessageMock = lifecycleMocks.sendMessageMock;
|
||||
export const sendPhotoMock = lifecycleMocks.sendPhotoMock;
|
||||
export const getZaloRuntimeMock = lifecycleMocks.getZaloRuntimeMock;
|
||||
|
||||
vi.mock(zaloApiModuleId, async (importOriginal) => {
|
||||
const actual = await importOriginal<object>();
|
||||
return {
|
||||
...actual,
|
||||
deleteWebhook: lifecycleMocks.deleteWebhookMock,
|
||||
getUpdates: lifecycleMocks.getUpdatesMock,
|
||||
getWebhookInfo: lifecycleMocks.getWebhookInfoMock,
|
||||
sendChatAction: lifecycleMocks.sendChatActionMock,
|
||||
sendMessage: lifecycleMocks.sendMessageMock,
|
||||
sendPhoto: lifecycleMocks.sendPhotoMock,
|
||||
setWebhook: lifecycleMocks.setWebhookMock,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock(zaloRuntimeModuleId, () => ({
|
||||
getZaloRuntime: lifecycleMocks.getZaloRuntimeMock,
|
||||
}));
|
||||
|
||||
export function resetLifecycleTestState() {
|
||||
vi.clearAllMocks();
|
||||
clearZaloWebhookSecurityStateForTest();
|
||||
setActivePluginRegistry(createEmptyPluginRegistry());
|
||||
}
|
||||
|
||||
export function createLifecycleConfig(params: {
|
||||
accountId: string;
|
||||
dmPolicy: "open" | "pairing";
|
||||
allowFrom?: string[];
|
||||
webhookUrl?: string;
|
||||
webhookSecret?: string;
|
||||
}): OpenClawConfig {
|
||||
const webhookUrl = params.webhookUrl ?? "https://example.com/hooks/zalo";
|
||||
const webhookSecret = params.webhookSecret ?? "supersecret";
|
||||
return {
|
||||
channels: {
|
||||
zalo: {
|
||||
enabled: true,
|
||||
accounts: {
|
||||
[params.accountId]: {
|
||||
enabled: true,
|
||||
webhookUrl,
|
||||
webhookSecret, // pragma: allowlist secret
|
||||
dmPolicy: params.dmPolicy,
|
||||
...(params.allowFrom ? { allowFrom: params.allowFrom } : {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
}
|
||||
|
||||
export function createLifecycleAccount(params: {
|
||||
accountId: string;
|
||||
dmPolicy: "open" | "pairing";
|
||||
allowFrom?: string[];
|
||||
webhookUrl?: string;
|
||||
webhookSecret?: string;
|
||||
}): ResolvedZaloAccount {
|
||||
const webhookUrl = params.webhookUrl ?? "https://example.com/hooks/zalo";
|
||||
const webhookSecret = params.webhookSecret ?? "supersecret";
|
||||
return {
|
||||
accountId: params.accountId,
|
||||
enabled: true,
|
||||
token: "zalo-token",
|
||||
tokenSource: "config",
|
||||
config: {
|
||||
webhookUrl,
|
||||
webhookSecret, // pragma: allowlist secret
|
||||
dmPolicy: params.dmPolicy,
|
||||
...(params.allowFrom ? { allowFrom: params.allowFrom } : {}),
|
||||
},
|
||||
} as ResolvedZaloAccount;
|
||||
}
|
||||
|
||||
export function createLifecycleMonitorSetup(params: {
|
||||
accountId: string;
|
||||
dmPolicy: "open" | "pairing";
|
||||
allowFrom?: string[];
|
||||
webhookUrl?: string;
|
||||
webhookSecret?: string;
|
||||
}) {
|
||||
return {
|
||||
account: createLifecycleAccount(params),
|
||||
config: createLifecycleConfig(params),
|
||||
};
|
||||
}
|
||||
|
||||
export function createTextUpdate(params: {
|
||||
messageId: string;
|
||||
userId: string;
|
||||
userName: string;
|
||||
chatId: string;
|
||||
text?: string;
|
||||
}) {
|
||||
return {
|
||||
event_name: "message.text.received",
|
||||
message: {
|
||||
from: { id: params.userId, name: params.userName },
|
||||
chat: { id: params.chatId, chat_type: "PRIVATE" as const },
|
||||
message_id: params.messageId,
|
||||
date: Math.floor(Date.now() / 1000),
|
||||
text: params.text ?? "hello from zalo",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createImageUpdate(params?: {
|
||||
messageId?: string;
|
||||
userId?: string;
|
||||
displayName?: string;
|
||||
chatId?: string;
|
||||
photoUrl?: string;
|
||||
date?: number;
|
||||
}) {
|
||||
return {
|
||||
event_name: "message.image.received",
|
||||
message: {
|
||||
date: params?.date ?? 1774086023728,
|
||||
chat: { chat_type: "PRIVATE" as const, id: params?.chatId ?? "chat-123" },
|
||||
caption: "",
|
||||
message_id: params?.messageId ?? "msg-123",
|
||||
message_type: "CHAT_PHOTO",
|
||||
from: {
|
||||
id: params?.userId ?? "user-123",
|
||||
is_bot: false,
|
||||
display_name: params?.displayName ?? "Test User",
|
||||
},
|
||||
photo_url: params?.photoUrl ?? "https://example.com/test-image.jpg",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function setLifecycleRuntimeCore(
|
||||
channel: NonNullable<NonNullable<Parameters<typeof createPluginRuntimeMock>[0]>["channel"]>,
|
||||
) {
|
||||
getZaloRuntimeMock.mockReturnValue(
|
||||
createPluginRuntimeMock({
|
||||
channel,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function createImageLifecycleCore() {
|
||||
const finalizeInboundContextMock = vi.fn((ctx: Record<string, unknown>) => ctx);
|
||||
const recordInboundSessionMock = vi.fn(async () => undefined);
|
||||
const fetchRemoteMediaMock = vi.fn(async () => ({
|
||||
buffer: Buffer.from("image-bytes"),
|
||||
contentType: "image/jpeg",
|
||||
}));
|
||||
const saveMediaBufferMock = vi.fn(async () => ({
|
||||
path: "/tmp/zalo-photo.jpg",
|
||||
contentType: "image/jpeg",
|
||||
}));
|
||||
const core = createPluginRuntimeMock({
|
||||
channel: {
|
||||
media: {
|
||||
fetchRemoteMedia:
|
||||
fetchRemoteMediaMock as unknown as PluginRuntime["channel"]["media"]["fetchRemoteMedia"],
|
||||
saveMediaBuffer:
|
||||
saveMediaBufferMock as unknown as PluginRuntime["channel"]["media"]["saveMediaBuffer"],
|
||||
},
|
||||
reply: {
|
||||
finalizeInboundContext:
|
||||
finalizeInboundContextMock as unknown as PluginRuntime["channel"]["reply"]["finalizeInboundContext"],
|
||||
dispatchReplyWithBufferedBlockDispatcher: vi.fn(
|
||||
async () => undefined,
|
||||
) as unknown as PluginRuntime["channel"]["reply"]["dispatchReplyWithBufferedBlockDispatcher"],
|
||||
},
|
||||
session: {
|
||||
recordInboundSession:
|
||||
recordInboundSessionMock as unknown as PluginRuntime["channel"]["session"]["recordInboundSession"],
|
||||
},
|
||||
commands: {
|
||||
shouldComputeCommandAuthorized: vi.fn(
|
||||
() => false,
|
||||
) as unknown as PluginRuntime["channel"]["commands"]["shouldComputeCommandAuthorized"],
|
||||
resolveCommandAuthorizedFromAuthorizers: vi.fn(
|
||||
() => false,
|
||||
) as unknown as PluginRuntime["channel"]["commands"]["resolveCommandAuthorizedFromAuthorizers"],
|
||||
isControlCommandMessage: vi.fn(
|
||||
() => false,
|
||||
) as unknown as PluginRuntime["channel"]["commands"]["isControlCommandMessage"],
|
||||
},
|
||||
},
|
||||
});
|
||||
return {
|
||||
core,
|
||||
finalizeInboundContextMock,
|
||||
recordInboundSessionMock,
|
||||
fetchRemoteMediaMock,
|
||||
saveMediaBufferMock,
|
||||
};
|
||||
}
|
||||
|
||||
export function expectImageLifecycleDelivery(params: {
|
||||
fetchRemoteMediaMock: ReturnType<typeof vi.fn>;
|
||||
saveMediaBufferMock: ReturnType<typeof vi.fn>;
|
||||
finalizeInboundContextMock: ReturnType<typeof vi.fn>;
|
||||
recordInboundSessionMock: ReturnType<typeof vi.fn>;
|
||||
photoUrl?: string;
|
||||
senderName?: string;
|
||||
mediaPath?: string;
|
||||
mediaType?: string;
|
||||
}) {
|
||||
const photoUrl = params.photoUrl ?? "https://example.com/test-image.jpg";
|
||||
const senderName = params.senderName ?? "Test User";
|
||||
const mediaPath = params.mediaPath ?? "/tmp/zalo-photo.jpg";
|
||||
const mediaType = params.mediaType ?? "image/jpeg";
|
||||
expect(params.fetchRemoteMediaMock).toHaveBeenCalledWith({
|
||||
url: photoUrl,
|
||||
maxBytes: 5 * 1024 * 1024,
|
||||
});
|
||||
expect(params.saveMediaBufferMock).toHaveBeenCalledTimes(1);
|
||||
expect(params.finalizeInboundContextMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
SenderName: senderName,
|
||||
MediaPath: mediaPath,
|
||||
MediaType: mediaType,
|
||||
}),
|
||||
);
|
||||
expect(params.recordInboundSessionMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
ctx: expect.objectContaining({
|
||||
SenderName: senderName,
|
||||
MediaPath: mediaPath,
|
||||
MediaType: mediaType,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export async function settleAsyncWork(): Promise<void> {
|
||||
for (let i = 0; i < 6; i += 1) {
|
||||
await Promise.resolve();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
}
|
||||
|
||||
export async function postWebhookUpdate(params: {
|
||||
baseUrl: string;
|
||||
path: string;
|
||||
secret: string;
|
||||
payload: Record<string, unknown>;
|
||||
}) {
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
export async function postWebhookReplay(params: {
|
||||
baseUrl: string;
|
||||
path: string;
|
||||
secret: string;
|
||||
payload: Record<string, unknown>;
|
||||
settleBeforeReplay?: boolean;
|
||||
}) {
|
||||
const first = await postWebhookUpdate(params);
|
||||
if (params.settleBeforeReplay) {
|
||||
await settleAsyncWork();
|
||||
}
|
||||
const replay = await postWebhookUpdate(params);
|
||||
return { first, replay };
|
||||
}
|
||||
|
||||
export async function startWebhookLifecycleMonitor(params: {
|
||||
account: ResolvedZaloAccount;
|
||||
config: OpenClawConfig;
|
||||
token?: string;
|
||||
webhookUrl?: string;
|
||||
webhookSecret?: string;
|
||||
}) {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
setActivePluginRegistry(registry);
|
||||
const abort = new AbortController();
|
||||
const runtime = createRuntimeEnv();
|
||||
const accountWebhookUrl =
|
||||
typeof params.account.config?.webhookUrl === "string"
|
||||
? params.account.config.webhookUrl
|
||||
: undefined;
|
||||
const webhookUrl = params.webhookUrl ?? accountWebhookUrl;
|
||||
const webhookSecret =
|
||||
params.webhookSecret ?? normalizeSecretInputString(params.account.config?.webhookSecret);
|
||||
const run = monitorZaloProvider({
|
||||
token: params.token ?? "zalo-token",
|
||||
account: params.account,
|
||||
config: params.config,
|
||||
runtime,
|
||||
abortSignal: abort.signal,
|
||||
useWebhook: true,
|
||||
webhookUrl,
|
||||
webhookSecret,
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
if (setWebhookMock.mock.calls.length !== 1 || registry.httpRoutes.length !== 1) {
|
||||
throw new Error("waiting for webhook registration");
|
||||
}
|
||||
});
|
||||
|
||||
const route = registry.httpRoutes[0];
|
||||
if (!route) {
|
||||
throw new Error("missing plugin HTTP route");
|
||||
}
|
||||
|
||||
return {
|
||||
abort,
|
||||
registry,
|
||||
route,
|
||||
run,
|
||||
runtime,
|
||||
stop: async () => {
|
||||
abort.abort();
|
||||
await run;
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user