diff --git a/extensions/feishu/src/bot.broadcast.test.ts b/extensions/feishu/src/bot.broadcast.test.ts index 81fffa6744c..c3ffb0a8b90 100644 --- a/extensions/feishu/src/bot.broadcast.test.ts +++ b/extensions/feishu/src/bot.broadcast.test.ts @@ -60,6 +60,51 @@ describe("broadcast dispatch", () => { contentType: "video/mp4", }); + function createBroadcastConfig(): ClawdbotConfig { + return { + broadcast: { "oc-broadcast-group": ["susan", "main"] }, + agents: { list: [{ id: "main" }, { id: "susan" }] }, + channels: { + feishu: { + groups: { + "oc-broadcast-group": { + requireMention: true, + }, + }, + }, + }, + } as unknown as ClawdbotConfig; + } + + function createBroadcastEvent(options: { + messageId: string; + text: string; + botMentioned?: boolean; + }): FeishuMessageEvent { + return { + sender: { sender_id: { open_id: "ou-sender" } }, + message: { + message_id: options.messageId, + chat_id: "oc-broadcast-group", + chat_type: "group", + message_type: "text", + content: JSON.stringify({ text: options.text }), + ...(options.botMentioned + ? { + mentions: [ + { + key: "@_user_1", + id: { open_id: "bot-open-id" }, + name: "Bot", + tenant_key: "", + }, + ], + } + : {}), + }, + }; + } + beforeEach(() => { vi.clearAllMocks(); mockResolveAgentRoute.mockReturnValue({ @@ -112,33 +157,12 @@ describe("broadcast dispatch", () => { }); it("dispatches to all broadcast agents when bot is mentioned", async () => { - const cfg: ClawdbotConfig = { - broadcast: { "oc-broadcast-group": ["susan", "main"] }, - agents: { list: [{ id: "main" }, { id: "susan" }] }, - channels: { - feishu: { - groups: { - "oc-broadcast-group": { - requireMention: true, - }, - }, - }, - }, - } as unknown as ClawdbotConfig; - - const event: FeishuMessageEvent = { - sender: { sender_id: { open_id: "ou-sender" } }, - message: { - message_id: "msg-broadcast-mentioned", - chat_id: "oc-broadcast-group", - chat_type: "group", - message_type: "text", - content: JSON.stringify({ text: "hello @bot" }), - mentions: [ - { key: "@_user_1", id: { open_id: "bot-open-id" }, name: "Bot", tenant_key: "" }, - ], - }, - }; + const cfg = createBroadcastConfig(); + const event = createBroadcastEvent({ + messageId: "msg-broadcast-mentioned", + text: "hello @bot", + botMentioned: true, + }); await handleFeishuMessage({ cfg, @@ -160,30 +184,11 @@ describe("broadcast dispatch", () => { }); it("skips broadcast dispatch when bot is NOT mentioned (requireMention=true)", async () => { - const cfg: ClawdbotConfig = { - broadcast: { "oc-broadcast-group": ["susan", "main"] }, - agents: { list: [{ id: "main" }, { id: "susan" }] }, - channels: { - feishu: { - groups: { - "oc-broadcast-group": { - requireMention: true, - }, - }, - }, - }, - } as unknown as ClawdbotConfig; - - const event: FeishuMessageEvent = { - sender: { sender_id: { open_id: "ou-sender" } }, - message: { - message_id: "msg-broadcast-not-mentioned", - chat_id: "oc-broadcast-group", - chat_type: "group", - message_type: "text", - content: JSON.stringify({ text: "hello everyone" }), - }, - }; + const cfg = createBroadcastConfig(); + const event = createBroadcastEvent({ + messageId: "msg-broadcast-not-mentioned", + text: "hello everyone", + }); await handleFeishuMessage({ cfg, @@ -197,30 +202,11 @@ describe("broadcast dispatch", () => { }); it("skips broadcast dispatch when bot identity is unknown (requireMention=true)", async () => { - const cfg: ClawdbotConfig = { - broadcast: { "oc-broadcast-group": ["susan", "main"] }, - agents: { list: [{ id: "main" }, { id: "susan" }] }, - channels: { - feishu: { - groups: { - "oc-broadcast-group": { - requireMention: true, - }, - }, - }, - }, - } as unknown as ClawdbotConfig; - - const event: FeishuMessageEvent = { - sender: { sender_id: { open_id: "ou-sender" } }, - message: { - message_id: "msg-broadcast-unknown-bot-id", - chat_id: "oc-broadcast-group", - chat_type: "group", - message_type: "text", - content: JSON.stringify({ text: "hello everyone" }), - }, - }; + const cfg = createBroadcastConfig(); + const event = createBroadcastEvent({ + messageId: "msg-broadcast-unknown-bot-id", + text: "hello everyone", + }); await handleFeishuMessage({ cfg, diff --git a/extensions/feishu/src/channel.test.ts b/extensions/feishu/src/channel.test.ts index f702cac6792..4e4a973a91c 100644 --- a/extensions/feishu/src/channel.test.ts +++ b/extensions/feishu/src/channel.test.ts @@ -61,6 +61,41 @@ function getDescribedActions(cfg: OpenClawConfig): string[] { return [...(feishuPlugin.actions?.describeMessageTool?.({ cfg })?.actions ?? [])]; } +function createLegacyFeishuButtonCard(value: { command?: string; text?: string }) { + return { + schema: "2.0", + body: { + elements: [ + { + tag: "action", + actions: [ + { + tag: "button", + text: { tag: "plain_text", content: "Run /new" }, + value, + }, + ], + }, + ], + }, + }; +} + +async function expectLegacyFeishuCardPayloadRejected(cfg: OpenClawConfig, card: unknown) { + await expect( + feishuPlugin.actions?.handleAction?.({ + action: "send", + params: { to: "chat:oc_group_1", card }, + cfg, + accountId: undefined, + toolContext: {}, + } as never), + ).rejects.toThrow( + "Feishu card buttons that trigger text or commands must use structured interaction envelopes.", + ); + expect(sendCardFeishuMock).not.toHaveBeenCalled(); +} + describe("feishuPlugin.status.probeAccount", () => { it("uses current account credentials for multi-account config", async () => { const cfg = { @@ -248,69 +283,17 @@ describe("feishuPlugin actions", () => { }); it("rejects raw legacy card command payloads", async () => { - const legacyCard = { - schema: "2.0", - body: { - elements: [ - { - tag: "action", - actions: [ - { - tag: "button", - text: { tag: "plain_text", content: "Run /new" }, - value: { command: "/new" }, - }, - ], - }, - ], - }, - }; - - await expect( - feishuPlugin.actions?.handleAction?.({ - action: "send", - params: { to: "chat:oc_group_1", card: legacyCard }, - cfg, - accountId: undefined, - toolContext: {}, - } as never), - ).rejects.toThrow( - "Feishu card buttons that trigger text or commands must use structured interaction envelopes.", + await expectLegacyFeishuCardPayloadRejected( + cfg, + createLegacyFeishuButtonCard({ command: "/new" }), ); - expect(sendCardFeishuMock).not.toHaveBeenCalled(); }); it("rejects raw legacy card text payloads", async () => { - const legacyCard = { - schema: "2.0", - body: { - elements: [ - { - tag: "action", - actions: [ - { - tag: "button", - text: { tag: "plain_text", content: "Run /new" }, - value: { text: "/new" }, - }, - ], - }, - ], - }, - }; - - await expect( - feishuPlugin.actions?.handleAction?.({ - action: "send", - params: { to: "chat:oc_group_1", card: legacyCard }, - cfg, - accountId: undefined, - toolContext: {}, - } as never), - ).rejects.toThrow( - "Feishu card buttons that trigger text or commands must use structured interaction envelopes.", + await expectLegacyFeishuCardPayloadRejected( + cfg, + createLegacyFeishuButtonCard({ text: "/new" }), ); - expect(sendCardFeishuMock).not.toHaveBeenCalled(); }); it("allows non-button controls to carry text metadata values", async () => { diff --git a/extensions/googlechat/src/monitor-access.test.ts b/extensions/googlechat/src/monitor-access.test.ts index 4fab2ef06a5..8dbc9590bf6 100644 --- a/extensions/googlechat/src/monitor-access.test.ts +++ b/extensions/googlechat/src/monitor-access.test.ts @@ -58,6 +58,61 @@ function primeCommonDefaults() { warnMissingProviderGroupPolicyFallbackOnce.mockReturnValue(undefined); } +const baseAccessConfig = { + channels: { googlechat: {} }, + commands: { useAccessGroups: true }, +} as const; + +const defaultSender = { + senderId: "users/alice", + senderName: "Alice", + senderEmail: "alice@example.com", +} as const; + +function allowInboundGroupTraffic(options?: { + effectiveGroupAllowFrom?: string[]; + effectiveWasMentioned?: boolean; +}) { + createChannelPairingController.mockReturnValue({ + readAllowFromStore: vi.fn(async () => []), + issueChallenge: vi.fn(), + }); + resolveDmGroupAccessWithLists.mockReturnValue({ + decision: "allow", + effectiveAllowFrom: [], + effectiveGroupAllowFrom: options?.effectiveGroupAllowFrom ?? ["users/alice"], + }); + resolveMentionGatingWithBypass.mockReturnValue({ + shouldSkip: false, + effectiveWasMentioned: options?.effectiveWasMentioned ?? true, + }); +} + +async function applyInboundAccessPolicy( + overrides: Partial< + Parameters< + Awaited["applyGoogleChatInboundAccessPolicy"] + >[0] + >, +) { + const { applyGoogleChatInboundAccessPolicy } = await import("./monitor-access.js"); + return applyGoogleChatInboundAccessPolicy({ + account: { + accountId: "default", + config: {}, + } as never, + config: baseAccessConfig as never, + core: createCore() as never, + space: { name: "spaces/AAA", displayName: "Team Room" } as never, + message: { annotations: [] } as never, + isGroup: true, + rawBody: "hello team", + logVerbose: vi.fn(), + ...defaultSender, + ...overrides, + } as never); +} + describe("googlechat inbound access policy", () => { it("issues a pairing challenge for unauthorized DMs in pairing mode", async () => { primeCommonDefaults(); @@ -120,27 +175,13 @@ describe("googlechat inbound access policy", () => { it("allows group traffic when sender and mention gates pass", async () => { primeCommonDefaults(); - createChannelPairingController.mockReturnValue({ - readAllowFromStore: vi.fn(async () => []), - issueChallenge: vi.fn(), - }); - resolveDmGroupAccessWithLists.mockReturnValue({ - decision: "allow", - effectiveAllowFrom: [], - effectiveGroupAllowFrom: ["users/alice"], - }); - resolveMentionGatingWithBypass.mockReturnValue({ - shouldSkip: false, - effectiveWasMentioned: true, - }); + allowInboundGroupTraffic(); const core = createCore(); core.channel.commands.shouldComputeCommandAuthorized.mockReturnValue(true); core.channel.commands.resolveCommandAuthorizedFromAuthorizers.mockReturnValue(true); - const { applyGoogleChatInboundAccessPolicy } = await import("./monitor-access.js"); - await expect( - applyGoogleChatInboundAccessPolicy({ + applyInboundAccessPolicy({ account: { accountId: "default", config: { @@ -154,12 +195,7 @@ describe("googlechat inbound access policy", () => { }, }, } as never, - config: { - channels: { googlechat: {} }, - commands: { useAccessGroups: true }, - } as never, core: core as never, - space: { name: "spaces/AAA", displayName: "Team Room" } as never, message: { annotations: [ { @@ -168,12 +204,6 @@ describe("googlechat inbound access policy", () => { }, ], } as never, - isGroup: true, - senderId: "users/alice", - senderName: "Alice", - senderEmail: "alice@example.com", - rawBody: "hello team", - logVerbose: vi.fn(), }), ).resolves.toEqual({ ok: true, @@ -185,17 +215,8 @@ describe("googlechat inbound access policy", () => { it("drops unauthorized group control commands", async () => { primeCommonDefaults(); - createChannelPairingController.mockReturnValue({ - readAllowFromStore: vi.fn(async () => []), - issueChallenge: vi.fn(), - }); - resolveDmGroupAccessWithLists.mockReturnValue({ - decision: "allow", - effectiveAllowFrom: [], + allowInboundGroupTraffic({ effectiveGroupAllowFrom: [], - }); - resolveMentionGatingWithBypass.mockReturnValue({ - shouldSkip: false, effectiveWasMentioned: false, }); const core = createCore(); @@ -204,25 +225,9 @@ describe("googlechat inbound access policy", () => { core.channel.commands.isControlCommandMessage.mockReturnValue(true); const logVerbose = vi.fn(); - const { applyGoogleChatInboundAccessPolicy } = await import("./monitor-access.js"); - await expect( - applyGoogleChatInboundAccessPolicy({ - account: { - accountId: "default", - config: {}, - } as never, - config: { - channels: { googlechat: {} }, - commands: { useAccessGroups: true }, - } as never, + applyInboundAccessPolicy({ core: core as never, - space: { name: "spaces/AAA", displayName: "Team Room" } as never, - message: { annotations: [] } as never, - isGroup: true, - senderId: "users/alice", - senderName: "Alice", - senderEmail: "alice@example.com", rawBody: "/admin", logVerbose, }), @@ -233,25 +238,11 @@ describe("googlechat inbound access policy", () => { it("does not match group policy by mutable space displayName when the stable id differs", async () => { primeCommonDefaults(); - createChannelPairingController.mockReturnValue({ - readAllowFromStore: vi.fn(async () => []), - issueChallenge: vi.fn(), - }); - resolveDmGroupAccessWithLists.mockReturnValue({ - decision: "allow", - effectiveAllowFrom: [], - effectiveGroupAllowFrom: ["users/alice"], - }); - resolveMentionGatingWithBypass.mockReturnValue({ - shouldSkip: false, - effectiveWasMentioned: true, - }); + allowInboundGroupTraffic(); const logVerbose = vi.fn(); - const { applyGoogleChatInboundAccessPolicy } = await import("./monitor-access.js"); - await expect( - applyGoogleChatInboundAccessPolicy({ + applyInboundAccessPolicy({ account: { accountId: "default", config: { @@ -264,10 +255,6 @@ describe("googlechat inbound access policy", () => { }, }, } as never, - config: { - channels: { googlechat: {} }, - commands: { useAccessGroups: true }, - } as never, core: createCore() as never, space: { name: "spaces/BBB", displayName: "Finance Ops" } as never, message: { @@ -278,10 +265,6 @@ describe("googlechat inbound access policy", () => { }, ], } as never, - isGroup: true, - senderId: "users/alice", - senderName: "Alice", - senderEmail: "alice@example.com", rawBody: "show quarter close status", logVerbose, }), @@ -301,25 +284,11 @@ describe("googlechat inbound access policy", () => { groupPolicy: "open", providerMissingFallbackApplied: false, }); - createChannelPairingController.mockReturnValue({ - readAllowFromStore: vi.fn(async () => []), - issueChallenge: vi.fn(), - }); - resolveDmGroupAccessWithLists.mockReturnValue({ - decision: "allow", - effectiveAllowFrom: [], - effectiveGroupAllowFrom: ["users/alice"], - }); - resolveMentionGatingWithBypass.mockReturnValue({ - shouldSkip: false, - effectiveWasMentioned: true, - }); + allowInboundGroupTraffic(); const logVerbose = vi.fn(); - const { applyGoogleChatInboundAccessPolicy } = await import("./monitor-access.js"); - await expect( - applyGoogleChatInboundAccessPolicy({ + applyInboundAccessPolicy({ account: { accountId: "default", config: { @@ -335,17 +304,8 @@ describe("googlechat inbound access policy", () => { }, }, } as never, - config: { - channels: { googlechat: {} }, - commands: { useAccessGroups: true }, - } as never, core: createCore() as never, space: { name: "spaces/BBB", displayName: "Finance Ops" } as never, - message: { annotations: [] } as never, - isGroup: true, - senderId: "users/alice", - senderName: "Alice", - senderEmail: "alice@example.com", rawBody: "show quarter close status", logVerbose, }), diff --git a/extensions/googlechat/src/monitor-webhook.test.ts b/extensions/googlechat/src/monitor-webhook.test.ts index 6cab10382b1..9e94a1ef7db 100644 --- a/extensions/googlechat/src/monitor-webhook.test.ts +++ b/extensions/googlechat/src/monitor-webhook.test.ts @@ -1,5 +1,7 @@ import type { IncomingMessage, ServerResponse } from "node:http"; import { describe, expect, it, vi } from "vitest"; +import type { WebhookTarget } from "./monitor-types.js"; +import type { GoogleChatEvent } from "./types.js"; const readJsonWebhookBodyOrReject = vi.hoisted(() => vi.fn()); const resolveWebhookTargetWithAuthOrReject = vi.hoisted(() => vi.fn()); @@ -16,6 +18,8 @@ vi.mock("./auth.js", () => ({ verifyGoogleChatRequest, })); +type ProcessEventFn = (event: GoogleChatEvent, target: WebhookTarget) => Promise; + function createRequest(authorization?: string): IncomingMessage { return { method: "POST", @@ -66,6 +70,24 @@ function installSimplePipeline(targets: unknown[]) { ); } +async function runWebhookHandler(options?: { + processEvent?: ProcessEventFn; + authorization?: string; +}) { + const processEvent: ProcessEventFn = + options?.processEvent ?? (vi.fn(async () => {}) as ProcessEventFn); + const { createGoogleChatWebhookRequestHandler } = await import("./monitor-webhook.js"); + const handler = createGoogleChatWebhookRequestHandler({ + webhookTargets: new Map(), + webhookInFlightLimiter: {} as never, + processEvent, + }); + const req = createRequest(options?.authorization); + const res = createResponse(); + await expect(handler(req, res)).resolves.toBe(true); + return { processEvent, res }; +} + describe("googlechat monitor webhook", () => { it("accepts add-on payloads that carry systemIdToken in the body", async () => { installSimplePipeline([ @@ -104,18 +126,7 @@ describe("googlechat monitor webhook", () => { return null; }); verifyGoogleChatRequest.mockResolvedValue({ ok: true }); - const processEvent = vi.fn(async () => {}); - - const { createGoogleChatWebhookRequestHandler } = await import("./monitor-webhook.js"); - const handler = createGoogleChatWebhookRequestHandler({ - webhookTargets: new Map(), - webhookInFlightLimiter: {} as never, - processEvent, - }); - - const req = createRequest(); - const res = createResponse(); - await expect(handler(req, res)).resolves.toBe(true); + const { processEvent, res } = await runWebhookHandler(); expect(verifyGoogleChatRequest).toHaveBeenCalledWith( expect.objectContaining({ @@ -156,18 +167,7 @@ describe("googlechat monitor webhook", () => { }, }, }); - const processEvent = vi.fn(async () => {}); - - const { createGoogleChatWebhookRequestHandler } = await import("./monitor-webhook.js"); - const handler = createGoogleChatWebhookRequestHandler({ - webhookTargets: new Map(), - webhookInFlightLimiter: {} as never, - processEvent, - }); - - const req = createRequest(); - const res = createResponse(); - await expect(handler(req, res)).resolves.toBe(true); + const { processEvent, res } = await runWebhookHandler(); expect(processEvent).not.toHaveBeenCalled(); expect(res.statusCode).toBe(401); diff --git a/extensions/synology-chat/src/channel.test.ts b/extensions/synology-chat/src/channel.test.ts index 17ecf2b6e7f..50fbf426c18 100644 --- a/extensions/synology-chat/src/channel.test.ts +++ b/extensions/synology-chat/src/channel.test.ts @@ -378,6 +378,40 @@ describe("createSynologyChatPlugin", () => { }; } + function makeNamedStartAccountCtx( + accountOverrides: Record, + abortController = new AbortController(), + ) { + return { + abortController, + ctx: { + cfg: { + channels: { + "synology-chat": { + enabled: true, + token: "default-token", + incomingUrl: "https://nas/default", + webhookPath: "/webhook/synology-shared", + dmPolicy: "allowlist", + allowedUserIds: ["123"], + accounts: { + alerts: { + enabled: true, + token: "alerts-token", + incomingUrl: "https://nas/alerts", + ...accountOverrides, + }, + }, + }, + }, + }, + accountId: "alerts", + log: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, + abortSignal: abortController.signal, + }, + }; + } + async function expectPendingStartAccountPromise( result: Promise, abortController: AbortController, @@ -428,31 +462,10 @@ describe("createSynologyChatPlugin", () => { it("startAccount refuses named accounts without explicit webhookPath in multi-account setups", async () => { const registerMock = registerPluginHttpRouteMock; const plugin = createSynologyChatPlugin(); - const abortController = new AbortController(); - const ctx = { - cfg: { - channels: { - "synology-chat": { - enabled: true, - token: "shared-token", - incomingUrl: "https://nas/incoming", - webhookPath: "/webhook/synology-shared", - accounts: { - alerts: { - enabled: true, - token: "alerts-token", - incomingUrl: "https://nas/alerts", - dmPolicy: "allowlist", - allowedUserIds: ["123"], - }, - }, - }, - }, - }, - accountId: "alerts", - log: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, - abortSignal: abortController.signal, - }; + const { ctx, abortController } = makeNamedStartAccountCtx({ + dmPolicy: "allowlist", + allowedUserIds: ["123"], + }); const result = plugin.gateway.startAccount(ctx); await expectPendingStartAccountPromise(result, abortController); @@ -465,33 +478,10 @@ describe("createSynologyChatPlugin", () => { it("startAccount refuses duplicate exact webhook paths across accounts", async () => { const registerMock = registerPluginHttpRouteMock; const plugin = createSynologyChatPlugin(); - const abortController = new AbortController(); - const ctx = { - cfg: { - channels: { - "synology-chat": { - enabled: true, - token: "default-token", - incomingUrl: "https://nas/default", - webhookPath: "/webhook/synology-shared", - dmPolicy: "allowlist", - allowedUserIds: ["123"], - accounts: { - alerts: { - enabled: true, - token: "alerts-token", - incomingUrl: "https://nas/alerts", - webhookPath: "/webhook/synology-shared", - dmPolicy: "open", - }, - }, - }, - }, - }, - accountId: "alerts", - log: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, - abortSignal: abortController.signal, - }; + const { ctx, abortController } = makeNamedStartAccountCtx({ + webhookPath: "/webhook/synology-shared", + dmPolicy: "open", + }); const result = plugin.gateway.startAccount(ctx); await expectPendingStartAccountPromise(result, abortController); diff --git a/extensions/synology-chat/src/webhook-handler.test.ts b/extensions/synology-chat/src/webhook-handler.test.ts index 7c78fbdb5e3..cf12cb1a61d 100644 --- a/extensions/synology-chat/src/webhook-handler.test.ts +++ b/extensions/synology-chat/src/webhook-handler.test.ts @@ -43,6 +43,39 @@ const validBody = makeFormBody({ text: "Hello bot", }); +async function runDangerousNameMatchReply( + log: { info: any; warn: any; error: any }, + options: { + resolvedChatUserId?: number; + accountIdSuffix: string; + }, +) { + vi.mocked(resolveLegacyWebhookNameToChatUserId).mockResolvedValueOnce(options.resolvedChatUserId); + const deliver = vi.fn().mockResolvedValue("Bot reply"); + const handler = createWebhookHandler({ + account: makeAccount({ + accountId: `${options.accountIdSuffix}-${Date.now()}`, + dangerouslyAllowNameMatching: true, + }), + deliver, + log, + }); + + const req = makeReq("POST", validBody); + const res = makeRes(); + await handler(req, res); + + expect(res._status).toBe(204); + expect(resolveLegacyWebhookNameToChatUserId).toHaveBeenCalledWith({ + incomingUrl: "https://nas.example.com/incoming", + mutableWebhookUsername: "testuser", + allowInsecureSsl: true, + log, + }); + + return { deliver }; +} + describe("createWebhookHandler", () => { let log: { info: any; warn: any; error: any }; @@ -479,27 +512,9 @@ describe("createWebhookHandler", () => { }); it("only resolves reply recipient by username when break-glass mode is enabled", async () => { - vi.mocked(resolveLegacyWebhookNameToChatUserId).mockResolvedValueOnce(456); - const deliver = vi.fn().mockResolvedValue("Bot reply"); - const handler = createWebhookHandler({ - account: makeAccount({ - accountId: "dangerous-name-match-test-" + Date.now(), - dangerouslyAllowNameMatching: true, - }), - deliver, - log, - }); - - const req = makeReq("POST", validBody); - const res = makeRes(); - await handler(req, res); - - expect(res._status).toBe(204); - expect(resolveLegacyWebhookNameToChatUserId).toHaveBeenCalledWith({ - incomingUrl: "https://nas.example.com/incoming", - mutableWebhookUsername: "testuser", - allowInsecureSsl: true, - log, + const { deliver } = await runDangerousNameMatchReply(log, { + resolvedChatUserId: 456, + accountIdSuffix: "dangerous-name-match-test", }); expect(deliver).toHaveBeenCalledWith( expect.objectContaining({ @@ -516,27 +531,8 @@ describe("createWebhookHandler", () => { }); it("falls back to payload.user_id when break-glass resolution does not find a match", async () => { - vi.mocked(resolveLegacyWebhookNameToChatUserId).mockResolvedValueOnce(undefined); - const deliver = vi.fn().mockResolvedValue("Bot reply"); - const handler = createWebhookHandler({ - account: makeAccount({ - accountId: "dangerous-name-fallback-test-" + Date.now(), - dangerouslyAllowNameMatching: true, - }), - deliver, - log, - }); - - const req = makeReq("POST", validBody); - const res = makeRes(); - await handler(req, res); - - expect(res._status).toBe(204); - expect(resolveLegacyWebhookNameToChatUserId).toHaveBeenCalledWith({ - incomingUrl: "https://nas.example.com/incoming", - mutableWebhookUsername: "testuser", - allowInsecureSsl: true, - log, + const { deliver } = await runDangerousNameMatchReply(log, { + accountIdSuffix: "dangerous-name-fallback-test", }); expect(log.warn).toHaveBeenCalledWith( 'Could not resolve Chat API user_id for "testuser" — falling back to webhook user_id 123. Reply delivery may fail.',