From 03c281412480e1cec3bdab52502e191789ef3772 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 13 Mar 2026 18:24:56 +0000 Subject: [PATCH] refactor: share line webhook test helpers --- src/line/bot-handlers.test.ts | 333 ++++++++++++++-------------------- 1 file changed, 136 insertions(+), 197 deletions(-) diff --git a/src/line/bot-handlers.test.ts b/src/line/bot-handlers.test.ts index 4f2ca707c8b..a2d012a32bb 100644 --- a/src/line/bot-handlers.test.ts +++ b/src/line/bot-handlers.test.ts @@ -89,27 +89,73 @@ function createReplayMessageEvent(params: { } as MessageEvent; } -function createOpenGroupReplayContext( - processMessage: LineWebhookContext["processMessage"], - replayCache: ReturnType, -): Parameters[1] { +function createTestMessageEvent(params: { + message: MessageEvent["message"]; + source: MessageEvent["source"]; + webhookEventId: string; + timestamp?: number; + replyToken?: string; + isRedelivery?: boolean; +}) { return { - cfg: { channels: { line: { groupPolicy: "open" } } }, + type: "message", + message: params.message, + replyToken: params.replyToken ?? "reply-token", + timestamp: params.timestamp ?? Date.now(), + source: params.source, + mode: "active", + webhookEventId: params.webhookEventId, + deliveryContext: { isRedelivery: params.isRedelivery ?? false }, + } as MessageEvent; +} + +function createLineWebhookTestContext(params: { + processMessage: LineWebhookContext["processMessage"]; + groupPolicy?: "open"; + dmPolicy?: "open"; + requireMention?: boolean; + groupHistories?: Map; + replayCache?: ReturnType; +}): Parameters[1] { + const lineConfig = { + ...(params.groupPolicy ? { groupPolicy: params.groupPolicy } : {}), + ...(params.dmPolicy ? { dmPolicy: params.dmPolicy } : {}), + }; + return { + cfg: { channels: { line: lineConfig } }, account: { accountId: "default", enabled: true, channelAccessToken: "token", channelSecret: "secret", tokenSource: "config", - config: { groupPolicy: "open", groups: { "*": { requireMention: false } } }, + config: { + ...lineConfig, + ...(params.requireMention === undefined + ? {} + : { groups: { "*": { requireMention: params.requireMention } } }), + }, }, runtime: createRuntime(), mediaMaxBytes: 1, - processMessage, - replayCache, + processMessage: params.processMessage, + ...(params.groupHistories ? { groupHistories: params.groupHistories } : {}), + ...(params.replayCache ? { replayCache: params.replayCache } : {}), }; } +function createOpenGroupReplayContext( + processMessage: LineWebhookContext["processMessage"], + replayCache: ReturnType, +): Parameters[1] { + return createLineWebhookTestContext({ + processMessage, + groupPolicy: "open", + requireMention: false, + replayCache, + }); +} + vi.mock("../pairing/pairing-store.js", () => ({ readChannelAllowFromStore: readAllowFromStoreMock, upsertChannelPairingRequest: upsertPairingRequestMock, @@ -631,32 +677,20 @@ describe("handleLineWebhookEvents", () => { it("skips group messages by default when requireMention is not configured", async () => { const processMessage = vi.fn(); - const event = { - type: "message", + const event = createTestMessageEvent({ message: { id: "m-default-skip", type: "text", text: "hi there" }, - replyToken: "reply-token", - timestamp: Date.now(), source: { type: "group", groupId: "group-default", userId: "user-default" }, - mode: "active", webhookEventId: "evt-default-skip", - deliveryContext: { isRedelivery: false }, - } as MessageEvent; - - await handleLineWebhookEvents([event], { - cfg: { channels: { line: { groupPolicy: "open" } } }, - account: { - accountId: "default", - enabled: true, - channelAccessToken: "token", - channelSecret: "secret", - tokenSource: "config", - config: { groupPolicy: "open" }, - }, - runtime: createRuntime(), - mediaMaxBytes: 1, - processMessage, }); + await handleLineWebhookEvents( + [event], + createLineWebhookTestContext({ + processMessage, + groupPolicy: "open", + }), + ); + expect(processMessage).not.toHaveBeenCalled(); expect(buildLineMessageContextMock).not.toHaveBeenCalled(); }); @@ -667,33 +701,22 @@ describe("handleLineWebhookEvents", () => { string, import("../auto-reply/reply/history.js").HistoryEntry[] >(); - const event = { - type: "message", + const event = createTestMessageEvent({ message: { id: "m-hist-1", type: "text", text: "hello history" }, - replyToken: "reply-token", timestamp: 1700000000000, source: { type: "group", groupId: "group-hist-1", userId: "user-hist" }, - mode: "active", webhookEventId: "evt-hist-1", - deliveryContext: { isRedelivery: false }, - } as MessageEvent; - - await handleLineWebhookEvents([event], { - cfg: { channels: { line: { groupPolicy: "open" } } }, - account: { - accountId: "default", - enabled: true, - channelAccessToken: "token", - channelSecret: "secret", - tokenSource: "config", - config: { groupPolicy: "open" }, - }, - runtime: createRuntime(), - mediaMaxBytes: 1, - processMessage, - groupHistories, }); + await handleLineWebhookEvents( + [event], + createLineWebhookTestContext({ + processMessage, + groupPolicy: "open", + groupHistories, + }), + ); + expect(processMessage).not.toHaveBeenCalled(); const entries = groupHistories.get("group-hist-1"); expect(entries).toHaveLength(1); @@ -706,35 +729,21 @@ describe("handleLineWebhookEvents", () => { it("skips group messages without mention when requireMention is set", async () => { const processMessage = vi.fn(); - const event = { - type: "message", + const event = createTestMessageEvent({ message: { id: "m-mention-1", type: "text", text: "hi there" }, - replyToken: "reply-token", - timestamp: Date.now(), source: { type: "group", groupId: "group-mention", userId: "user-mention" }, - mode: "active", webhookEventId: "evt-mention-1", - deliveryContext: { isRedelivery: false }, - } as MessageEvent; - - await handleLineWebhookEvents([event], { - cfg: { channels: { line: { groupPolicy: "open" } } }, - account: { - accountId: "default", - enabled: true, - channelAccessToken: "token", - channelSecret: "secret", - tokenSource: "config", - config: { - groupPolicy: "open", - groups: { "*": { requireMention: true } }, - }, - }, - runtime: createRuntime(), - mediaMaxBytes: 1, - processMessage, }); + await handleLineWebhookEvents( + [event], + createLineWebhookTestContext({ + processMessage, + groupPolicy: "open", + requireMention: true, + }), + ); + expect(processMessage).not.toHaveBeenCalled(); expect(buildLineMessageContextMock).not.toHaveBeenCalled(); }); @@ -742,8 +751,7 @@ describe("handleLineWebhookEvents", () => { it("processes group messages with bot mention when requireMention is set", async () => { const processMessage = vi.fn(); // Simulate a LINE text message with mention.mentionees containing isSelf=true - const event = { - type: "message", + const event = createTestMessageEvent({ message: { id: "m-mention-2", type: "text", @@ -751,41 +759,27 @@ describe("handleLineWebhookEvents", () => { mention: { mentionees: [{ index: 0, length: 4, type: "user", isSelf: true }], }, - }, - replyToken: "reply-token", - timestamp: Date.now(), + } as unknown as MessageEvent["message"], source: { type: "group", groupId: "group-mention", userId: "user-mention" }, - mode: "active", webhookEventId: "evt-mention-2", - deliveryContext: { isRedelivery: false }, - } as unknown as MessageEvent; - - await handleLineWebhookEvents([event], { - cfg: { channels: { line: { groupPolicy: "open" } } }, - account: { - accountId: "default", - enabled: true, - channelAccessToken: "token", - channelSecret: "secret", - tokenSource: "config", - config: { - groupPolicy: "open", - groups: { "*": { requireMention: true } }, - }, - }, - runtime: createRuntime(), - mediaMaxBytes: 1, - processMessage, }); + await handleLineWebhookEvents( + [event], + createLineWebhookTestContext({ + processMessage, + groupPolicy: "open", + requireMention: true, + }), + ); + expect(buildLineMessageContextMock).toHaveBeenCalledTimes(1); expect(processMessage).toHaveBeenCalledTimes(1); }); it("processes group messages with @all mention when requireMention is set", async () => { const processMessage = vi.fn(); - const event = { - type: "message", + const event = createTestMessageEvent({ message: { id: "m-mention-3", type: "text", @@ -793,68 +787,41 @@ describe("handleLineWebhookEvents", () => { mention: { mentionees: [{ index: 0, length: 4, type: "all" }], }, - }, - replyToken: "reply-token", - timestamp: Date.now(), + } as MessageEvent["message"], source: { type: "group", groupId: "group-mention", userId: "user-mention" }, - mode: "active", webhookEventId: "evt-mention-3", - deliveryContext: { isRedelivery: false }, - } as MessageEvent; - - await handleLineWebhookEvents([event], { - cfg: { channels: { line: { groupPolicy: "open" } } }, - account: { - accountId: "default", - enabled: true, - channelAccessToken: "token", - channelSecret: "secret", - tokenSource: "config", - config: { - groupPolicy: "open", - groups: { "*": { requireMention: true } }, - }, - }, - runtime: createRuntime(), - mediaMaxBytes: 1, - processMessage, }); + await handleLineWebhookEvents( + [event], + createLineWebhookTestContext({ + processMessage, + groupPolicy: "open", + requireMention: true, + }), + ); + expect(buildLineMessageContextMock).toHaveBeenCalledTimes(1); expect(processMessage).toHaveBeenCalledTimes(1); }); it("does not apply requireMention gating to DM messages", async () => { const processMessage = vi.fn(); - const event = { - type: "message", + const event = createTestMessageEvent({ message: { id: "m-mention-dm", type: "text", text: "hi" }, - replyToken: "reply-token", - timestamp: Date.now(), source: { type: "user", userId: "user-dm" }, - mode: "active", webhookEventId: "evt-mention-dm", - deliveryContext: { isRedelivery: false }, - } as MessageEvent; - - await handleLineWebhookEvents([event], { - cfg: { channels: { line: { dmPolicy: "open" } } }, - account: { - accountId: "default", - enabled: true, - channelAccessToken: "token", - channelSecret: "secret", - tokenSource: "config", - config: { - dmPolicy: "open", - groups: { "*": { requireMention: true } }, - }, - }, - runtime: createRuntime(), - mediaMaxBytes: 1, - processMessage, }); + await handleLineWebhookEvents( + [event], + createLineWebhookTestContext({ + processMessage, + dmPolicy: "open", + requireMention: true, + }), + ); + expect(buildLineMessageContextMock).toHaveBeenCalledTimes(1); expect(processMessage).toHaveBeenCalledTimes(1); }); @@ -862,35 +829,21 @@ describe("handleLineWebhookEvents", () => { it("allows non-text group messages through when requireMention is set (cannot detect mention)", async () => { const processMessage = vi.fn(); // Image message -- LINE only carries mention metadata on text messages. - const event = { - type: "message", + const event = createTestMessageEvent({ message: { id: "m-mention-img", type: "image", contentProvider: { type: "line" } }, - replyToken: "reply-token", - timestamp: Date.now(), source: { type: "group", groupId: "group-1", userId: "user-img" }, - mode: "active", webhookEventId: "evt-mention-img", - deliveryContext: { isRedelivery: false }, - } as MessageEvent; - - await handleLineWebhookEvents([event], { - cfg: { channels: { line: { groupPolicy: "open" } } }, - account: { - accountId: "default", - enabled: true, - channelAccessToken: "token", - channelSecret: "secret", - tokenSource: "config", - config: { - groupPolicy: "open", - groups: { "*": { requireMention: true } }, - }, - }, - runtime: createRuntime(), - mediaMaxBytes: 1, - processMessage, }); + await handleLineWebhookEvents( + [event], + createLineWebhookTestContext({ + processMessage, + groupPolicy: "open", + requireMention: true, + }), + ); + expect(buildLineMessageContextMock).toHaveBeenCalledTimes(1); expect(processMessage).toHaveBeenCalledTimes(1); }); @@ -898,40 +851,26 @@ describe("handleLineWebhookEvents", () => { it("does not bypass mention gating when non-bot mention is present with control command", async () => { const processMessage = vi.fn(); // Text message mentions another user (not bot) together with a control command. - const event = { - type: "message", + const event = createTestMessageEvent({ message: { id: "m-mention-other", type: "text", text: "@other !status", mention: { mentionees: [{ index: 0, length: 6, type: "user", isSelf: false }] }, - }, - replyToken: "reply-token", - timestamp: Date.now(), + } as unknown as MessageEvent["message"], source: { type: "group", groupId: "group-1", userId: "user-other" }, - mode: "active", webhookEventId: "evt-mention-other", - deliveryContext: { isRedelivery: false }, - } as unknown as MessageEvent; - - await handleLineWebhookEvents([event], { - cfg: { channels: { line: { groupPolicy: "open" } } }, - account: { - accountId: "default", - enabled: true, - channelAccessToken: "token", - channelSecret: "secret", - tokenSource: "config", - config: { - groupPolicy: "open", - groups: { "*": { requireMention: true } }, - }, - }, - runtime: createRuntime(), - mediaMaxBytes: 1, - processMessage, }); + await handleLineWebhookEvents( + [event], + createLineWebhookTestContext({ + processMessage, + groupPolicy: "open", + requireMention: true, + }), + ); + // Should be skipped because there is a non-bot mention and the bot was not mentioned. expect(processMessage).not.toHaveBeenCalled(); });