| import { expect, it, type Mock } from "vitest"; |
|
|
| type PayloadLike = { |
| mediaUrl?: string; |
| mediaUrls?: string[]; |
| text?: string; |
| }; |
|
|
| type SendResultLike = { |
| messageId: string; |
| [key: string]: unknown; |
| }; |
|
|
| type ChunkingMode = |
| | { |
| longTextLength: number; |
| maxChunkLength: number; |
| mode: "split"; |
| } |
| | { |
| longTextLength: number; |
| mode: "passthrough"; |
| }; |
|
|
| export function installSendPayloadContractSuite(params: { |
| channel: string; |
| chunking: ChunkingMode; |
| createHarness: (params: { payload: PayloadLike; sendResults?: SendResultLike[] }) => { |
| run: () => Promise<Record<string, unknown>>; |
| sendMock: Mock; |
| to: string; |
| }; |
| }) { |
| it("text-only delegates to sendText", async () => { |
| const { run, sendMock, to } = params.createHarness({ |
| payload: { text: "hello" }, |
| }); |
| const result = await run(); |
|
|
| expect(sendMock).toHaveBeenCalledTimes(1); |
| expect(sendMock).toHaveBeenCalledWith(to, "hello", expect.any(Object)); |
| expect(result).toMatchObject({ channel: params.channel }); |
| }); |
|
|
| it("single media delegates to sendMedia", async () => { |
| const { run, sendMock, to } = params.createHarness({ |
| payload: { text: "cap", mediaUrl: "https://example.com/a.jpg" }, |
| }); |
| const result = await run(); |
|
|
| expect(sendMock).toHaveBeenCalledTimes(1); |
| expect(sendMock).toHaveBeenCalledWith( |
| to, |
| "cap", |
| expect.objectContaining({ mediaUrl: "https://example.com/a.jpg" }), |
| ); |
| expect(result).toMatchObject({ channel: params.channel }); |
| }); |
|
|
| it("multi-media iterates URLs with caption on first", async () => { |
| const { run, sendMock, to } = params.createHarness({ |
| payload: { |
| text: "caption", |
| mediaUrls: ["https://example.com/1.jpg", "https://example.com/2.jpg"], |
| }, |
| sendResults: [{ messageId: "m-1" }, { messageId: "m-2" }], |
| }); |
| const result = await run(); |
|
|
| expect(sendMock).toHaveBeenCalledTimes(2); |
| expect(sendMock).toHaveBeenNthCalledWith( |
| 1, |
| to, |
| "caption", |
| expect.objectContaining({ mediaUrl: "https://example.com/1.jpg" }), |
| ); |
| expect(sendMock).toHaveBeenNthCalledWith( |
| 2, |
| to, |
| "", |
| expect.objectContaining({ mediaUrl: "https://example.com/2.jpg" }), |
| ); |
| expect(result).toMatchObject({ channel: params.channel, messageId: "m-2" }); |
| }); |
|
|
| it("empty payload returns no-op", async () => { |
| const { run, sendMock } = params.createHarness({ payload: {} }); |
| const result = await run(); |
|
|
| expect(sendMock).not.toHaveBeenCalled(); |
| expect(result).toEqual({ channel: params.channel, messageId: "" }); |
| }); |
|
|
| if (params.chunking.mode === "passthrough") { |
| it("text exceeding chunk limit is sent as-is when chunker is null", async () => { |
| const text = "a".repeat(params.chunking.longTextLength); |
| const { run, sendMock, to } = params.createHarness({ payload: { text } }); |
| const result = await run(); |
|
|
| expect(sendMock).toHaveBeenCalledTimes(1); |
| expect(sendMock).toHaveBeenCalledWith(to, text, expect.any(Object)); |
| expect(result).toMatchObject({ channel: params.channel }); |
| }); |
| return; |
| } |
|
|
| const chunking = params.chunking; |
|
|
| it("chunking splits long text", async () => { |
| const text = "a".repeat(chunking.longTextLength); |
| const { run, sendMock } = params.createHarness({ |
| payload: { text }, |
| sendResults: [{ messageId: "c-1" }, { messageId: "c-2" }], |
| }); |
| const result = await run(); |
|
|
| expect(sendMock.mock.calls.length).toBeGreaterThanOrEqual(2); |
| for (const call of sendMock.mock.calls) { |
| expect((call[1] as string).length).toBeLessThanOrEqual(chunking.maxChunkLength); |
| } |
| expect(result).toMatchObject({ channel: params.channel }); |
| }); |
| } |
|
|
| export function primeSendMock( |
| sendMock: Mock, |
| fallbackResult: Record<string, unknown>, |
| sendResults: SendResultLike[] = [], |
| ) { |
| sendMock.mockReset(); |
| if (sendResults.length === 0) { |
| sendMock.mockResolvedValue(fallbackResult); |
| return; |
| } |
| for (const result of sendResults) { |
| sendMock.mockResolvedValueOnce(result); |
| } |
| } |
|
|