refactor(line): dedupe replay webhook test fixtures

This commit is contained in:
Peter Steinberger
2026-03-07 16:42:49 +00:00
parent 4575bbbb69
commit 5d37139ee5

View File

@@ -68,6 +68,46 @@ let createLineWebhookReplayCache: typeof import("./bot-handlers.js").createLineW
const createRuntime = () => ({ log: vi.fn(), error: vi.fn(), exit: vi.fn() }); const createRuntime = () => ({ log: vi.fn(), error: vi.fn(), exit: vi.fn() });
function createReplayMessageEvent(params: {
messageId: string;
groupId: string;
userId: string;
webhookEventId: string;
isRedelivery: boolean;
}) {
return {
type: "message",
message: { id: params.messageId, type: "text", text: "hello" },
replyToken: "reply-token",
timestamp: Date.now(),
source: { type: "group", groupId: params.groupId, userId: params.userId },
mode: "active",
webhookEventId: params.webhookEventId,
deliveryContext: { isRedelivery: params.isRedelivery },
} as MessageEvent;
}
function createOpenGroupReplayContext(
processMessage: ReturnType<typeof vi.fn>,
replayCache: ReturnType<typeof createLineWebhookReplayCache>,
): Parameters<typeof handleLineWebhookEvents>[1] {
return {
cfg: { channels: { line: { groupPolicy: "open" } } },
account: {
accountId: "default",
enabled: true,
channelAccessToken: "token",
channelSecret: "secret",
tokenSource: "config",
config: { groupPolicy: "open" },
},
runtime: createRuntime(),
mediaMaxBytes: 1,
processMessage,
replayCache,
};
}
vi.mock("../pairing/pairing-store.js", () => ({ vi.mock("../pairing/pairing-store.js", () => ({
readChannelAllowFromStore: readAllowFromStoreMock, readChannelAllowFromStore: readAllowFromStoreMock,
upsertChannelPairingRequest: upsertPairingRequestMock, upsertChannelPairingRequest: upsertPairingRequestMock,
@@ -377,32 +417,14 @@ describe("handleLineWebhookEvents", () => {
it("deduplicates replayed webhook events by webhookEventId before processing", async () => { it("deduplicates replayed webhook events by webhookEventId before processing", async () => {
const processMessage = vi.fn(); const processMessage = vi.fn();
const event = { const event = createReplayMessageEvent({
type: "message", messageId: "m-replay",
message: { id: "m-replay", type: "text", text: "hello" }, groupId: "group-replay",
replyToken: "reply-token", userId: "user-replay",
timestamp: Date.now(),
source: { type: "group", groupId: "group-replay", userId: "user-replay" },
mode: "active",
webhookEventId: "evt-replay-1", webhookEventId: "evt-replay-1",
deliveryContext: { isRedelivery: true }, isRedelivery: true,
} as MessageEvent; });
const context = createOpenGroupReplayContext(processMessage, createLineWebhookReplayCache());
const context: Parameters<typeof handleLineWebhookEvents>[1] = {
cfg: { channels: { line: { groupPolicy: "open" } } },
account: {
accountId: "default",
enabled: true,
channelAccessToken: "token",
channelSecret: "secret",
tokenSource: "config",
config: { groupPolicy: "open" },
},
runtime: createRuntime(),
mediaMaxBytes: 1,
processMessage,
replayCache: createLineWebhookReplayCache(),
};
await handleLineWebhookEvents([event], context); await handleLineWebhookEvents([event], context);
await handleLineWebhookEvents([event], context); await handleLineWebhookEvents([event], context);
@@ -419,32 +441,14 @@ describe("handleLineWebhookEvents", () => {
const processMessage = vi.fn(async () => { const processMessage = vi.fn(async () => {
await firstDone; await firstDone;
}); });
const event = { const event = createReplayMessageEvent({
type: "message", messageId: "m-inflight",
message: { id: "m-inflight", type: "text", text: "hello" }, groupId: "group-inflight",
replyToken: "reply-token", userId: "user-inflight",
timestamp: Date.now(),
source: { type: "group", groupId: "group-inflight", userId: "user-inflight" },
mode: "active",
webhookEventId: "evt-inflight-1", webhookEventId: "evt-inflight-1",
deliveryContext: { isRedelivery: true }, isRedelivery: true,
} as MessageEvent; });
const context = createOpenGroupReplayContext(processMessage, createLineWebhookReplayCache());
const context: Parameters<typeof handleLineWebhookEvents>[1] = {
cfg: { channels: { line: { groupPolicy: "open" } } },
account: {
accountId: "default",
enabled: true,
channelAccessToken: "token",
channelSecret: "secret",
tokenSource: "config",
config: { groupPolicy: "open" },
},
runtime: createRuntime(),
mediaMaxBytes: 1,
processMessage,
replayCache: createLineWebhookReplayCache(),
};
const firstRun = handleLineWebhookEvents([event], context); const firstRun = handleLineWebhookEvents([event], context);
await Promise.resolve(); await Promise.resolve();
@@ -464,32 +468,14 @@ describe("handleLineWebhookEvents", () => {
const processMessage = vi.fn(async () => { const processMessage = vi.fn(async () => {
await firstDone; await firstDone;
}); });
const event = { const event = createReplayMessageEvent({
type: "message", messageId: "m-inflight-fail",
message: { id: "m-inflight-fail", type: "text", text: "hello" }, groupId: "group-inflight",
replyToken: "reply-token", userId: "user-inflight",
timestamp: Date.now(),
source: { type: "group", groupId: "group-inflight", userId: "user-inflight" },
mode: "active",
webhookEventId: "evt-inflight-fail-1", webhookEventId: "evt-inflight-fail-1",
deliveryContext: { isRedelivery: true }, isRedelivery: true,
} as MessageEvent; });
const context = createOpenGroupReplayContext(processMessage, createLineWebhookReplayCache());
const context: Parameters<typeof handleLineWebhookEvents>[1] = {
cfg: { channels: { line: { groupPolicy: "open" } } },
account: {
accountId: "default",
enabled: true,
channelAccessToken: "token",
channelSecret: "secret",
tokenSource: "config",
config: { groupPolicy: "open" },
},
runtime: createRuntime(),
mediaMaxBytes: 1,
processMessage,
replayCache: createLineWebhookReplayCache(),
};
const firstRun = handleLineWebhookEvents([event], context); const firstRun = handleLineWebhookEvents([event], context);
await Promise.resolve(); await Promise.resolve();
@@ -604,32 +590,14 @@ describe("handleLineWebhookEvents", () => {
.fn() .fn()
.mockRejectedValueOnce(new Error("transient failure")) .mockRejectedValueOnce(new Error("transient failure"))
.mockResolvedValueOnce(undefined); .mockResolvedValueOnce(undefined);
const event = { const event = createReplayMessageEvent({
type: "message", messageId: "m-fail-then-retry",
message: { id: "m-fail-then-retry", type: "text", text: "hello" }, groupId: "group-retry",
replyToken: "reply-token", userId: "user-retry",
timestamp: Date.now(),
source: { type: "group", groupId: "group-retry", userId: "user-retry" },
mode: "active",
webhookEventId: "evt-fail-then-retry", webhookEventId: "evt-fail-then-retry",
deliveryContext: { isRedelivery: false }, isRedelivery: false,
} as MessageEvent; });
const context = createOpenGroupReplayContext(processMessage, createLineWebhookReplayCache());
const context: Parameters<typeof handleLineWebhookEvents>[1] = {
cfg: { channels: { line: { groupPolicy: "open" } } },
account: {
accountId: "default",
enabled: true,
channelAccessToken: "token",
channelSecret: "secret",
tokenSource: "config",
config: { groupPolicy: "open" },
},
runtime: createRuntime(),
mediaMaxBytes: 1,
processMessage,
replayCache: createLineWebhookReplayCache(),
};
await expect(handleLineWebhookEvents([event], context)).rejects.toThrow("transient failure"); await expect(handleLineWebhookEvents([event], context)).rejects.toThrow("transient failure");
await handleLineWebhookEvents([event], context); await handleLineWebhookEvents([event], context);