diff --git a/extensions/whatsapp/src/inbound/send-api.test.ts b/extensions/whatsapp/src/inbound/send-api.test.ts index 00b13a96ddd..2f9f467ed01 100644 --- a/extensions/whatsapp/src/inbound/send-api.test.ts +++ b/extensions/whatsapp/src/inbound/send-api.test.ts @@ -42,18 +42,50 @@ describe("createWebSendApi", () => { }); }); + function requireRecord(value: unknown, label: string): Record { + expect(typeof value).toBe("object"); + expect(value).not.toBeNull(); + if (typeof value !== "object" || value === null) { + throw new Error(`${label} was not an object`); + } + return value as Record; + } + + function expectRecordFields(record: Record, fields: Record) { + for (const [key, value] of Object.entries(fields)) { + expect(record[key]).toEqual(value); + } + } + + function requireSendContent(callIndex = 0): Record { + return requireRecord(sendMessage.mock.calls[callIndex]?.[1], "sent message content"); + } + + function requireSendOptions(callIndex = 0): Record { + return requireRecord(sendMessage.mock.calls[callIndex]?.[2], "sent message options"); + } + + function expectSendContentFields(callIndex: number, fields: Record) { + expectRecordFields(requireSendContent(callIndex), fields); + } + + function expectSendResultFields( + result: Awaited>, + fields: Record, + ) { + expectRecordFields(requireRecord(result, "send result"), fields); + } + it("uses sendOptions fileName for outbound documents", async () => { const payload = Buffer.from("pdf"); await api.sendMessage("+1555", "doc", payload, "application/pdf", { fileName: "invoice.pdf" }); - expect(sendMessage).toHaveBeenCalledWith( - "1555@s.whatsapp.net", - expect.objectContaining({ - document: payload, - fileName: "invoice.pdf", - caption: "doc", - mimetype: "application/pdf", - }), - ); + expect(sendMessage.mock.calls[0]?.[0]).toBe("1555@s.whatsapp.net"); + expectSendContentFields(0, { + document: payload, + fileName: "invoice.pdf", + caption: "doc", + mimetype: "application/pdf", + }); expect(recordChannelActivity).toHaveBeenCalledWith({ channel: "whatsapp", accountId: "main", @@ -64,21 +96,19 @@ describe("createWebSendApi", () => { it("falls back to default document filename when fileName is absent", async () => { const payload = Buffer.from("pdf"); await api.sendMessage("+1555", "doc", payload, "application/pdf"); - expect(sendMessage).toHaveBeenCalledWith( - "1555@s.whatsapp.net", - expect.objectContaining({ - document: payload, - fileName: "file", - caption: "doc", - mimetype: "application/pdf", - }), - ); + expect(sendMessage.mock.calls[0]?.[0]).toBe("1555@s.whatsapp.net"); + expectSendContentFields(0, { + document: payload, + fileName: "file", + caption: "doc", + mimetype: "application/pdf", + }); }); it("sends plain text messages", async () => { const res = await api.sendMessage("+1555", "hello"); expect(sendMessage).toHaveBeenCalledWith("1555@s.whatsapp.net", { text: "hello" }); - expect(res).toMatchObject({ + expectSendResultFields(res, { kind: "text", messageId: "msg-1", providerAccepted: true, @@ -119,14 +149,12 @@ describe("createWebSendApi", () => { it("supports image media with caption", async () => { const payload = Buffer.from("img"); await api.sendMessage("+1555", "cap", payload, "image/jpeg"); - expect(sendMessage).toHaveBeenCalledWith( - "1555@s.whatsapp.net", - expect.objectContaining({ - image: payload, - caption: "cap", - mimetype: "image/jpeg", - }), - ); + expect(sendMessage.mock.calls[0]?.[0]).toBe("1555@s.whatsapp.net"); + expectSendContentFields(0, { + image: payload, + caption: "cap", + mimetype: "image/jpeg", + }); }); it("adds native mention metadata to group media captions", async () => { @@ -144,28 +172,24 @@ describe("createWebSendApi", () => { await api.sendMessage("120363000000000000@g.us", "cap @15551234567", payload, "image/jpeg"); - expect(sendMessage).toHaveBeenCalledWith( - "120363000000000000@g.us", - expect.objectContaining({ - image: payload, - caption: "cap @15551234567", - mimetype: "image/jpeg", - mentions: ["15551234567@s.whatsapp.net"], - }), - ); + expect(sendMessage.mock.calls[0]?.[0]).toBe("120363000000000000@g.us"); + expectSendContentFields(0, { + image: payload, + caption: "cap @15551234567", + mimetype: "image/jpeg", + mentions: ["15551234567@s.whatsapp.net"], + }); }); it("supports audio as push-to-talk voice note", async () => { const payload = Buffer.from("aud"); await api.sendMessage("+1555", "", payload, "audio/ogg", { accountId: "alt" }); - expect(sendMessage).toHaveBeenCalledWith( - "1555@s.whatsapp.net", - expect.objectContaining({ - audio: payload, - ptt: true, - mimetype: "audio/ogg", - }), - ); + expect(sendMessage.mock.calls[0]?.[0]).toBe("1555@s.whatsapp.net"); + expectSendContentFields(0, { + audio: payload, + ptt: true, + mimetype: "audio/ogg", + }); expect(recordChannelActivity).toHaveBeenCalledWith({ channel: "whatsapp", accountId: "alt", @@ -179,19 +203,16 @@ describe("createWebSendApi", () => { .mockResolvedValueOnce({ key: { id: "voice-1" } }) .mockResolvedValueOnce({ key: { id: "voice-text-1" } }); const res = await api.sendMessage("+1555", "voice text", payload, "audio/ogg"); - expect(sendMessage).toHaveBeenNthCalledWith( - 1, - "1555@s.whatsapp.net", - expect.objectContaining({ - audio: payload, - ptt: true, - mimetype: "audio/ogg", - }), - ); + expect(sendMessage.mock.calls[0]?.[0]).toBe("1555@s.whatsapp.net"); + expectSendContentFields(0, { + audio: payload, + ptt: true, + mimetype: "audio/ogg", + }); expect(sendMessage).toHaveBeenNthCalledWith(2, "1555@s.whatsapp.net", { text: "voice text", }); - expect(res).toMatchObject({ + expectSendResultFields(res, { kind: "media", messageId: "voice-1", providerAccepted: true, @@ -205,15 +226,13 @@ describe("createWebSendApi", () => { it("supports video media and gifPlayback option", async () => { const payload = Buffer.from("vid"); await api.sendMessage("+1555", "cap", payload, "video/mp4", { gifPlayback: true }); - expect(sendMessage).toHaveBeenCalledWith( - "1555@s.whatsapp.net", - expect.objectContaining({ - video: payload, - caption: "cap", - mimetype: "video/mp4", - gifPlayback: true, - }), - ); + expect(sendMessage.mock.calls[0]?.[0]).toBe("1555@s.whatsapp.net"); + expectSendContentFields(0, { + video: payload, + caption: "cap", + mimetype: "video/mp4", + gifPlayback: true, + }); }); it("falls back to unknown messageId if Baileys result does not expose key.id", async () => { @@ -228,12 +247,12 @@ describe("createWebSendApi", () => { options: ["a", "b"], maxSelections: 2, }); - expect(sendMessage).toHaveBeenCalledWith( - "1555@s.whatsapp.net", - expect.objectContaining({ - poll: { name: "Q?", values: ["a", "b"], selectableCount: 2 }, - }), - ); + expect(sendMessage.mock.calls[0]?.[0]).toBe("1555@s.whatsapp.net"); + expect(requireSendContent().poll).toEqual({ + name: "Q?", + values: ["a", "b"], + selectableCount: 2, + }); expect(res.messageId).toBe("msg-1"); expect(recordChannelActivity).toHaveBeenCalledWith({ channel: "whatsapp", @@ -244,21 +263,16 @@ describe("createWebSendApi", () => { it("sends reactions with participant JID normalization", async () => { const res = await api.sendReaction("+1555", "msg-2", "👍", false, "+1999"); - expect(sendMessage).toHaveBeenCalledWith( - "1555@s.whatsapp.net", - expect.objectContaining({ - react: { - text: "👍", - key: expect.objectContaining({ - remoteJid: "1555@s.whatsapp.net", - id: "msg-2", - fromMe: false, - participant: "1999@s.whatsapp.net", - }), - }, - }), - ); - expect(res).toMatchObject({ + expect(sendMessage.mock.calls[0]?.[0]).toBe("1555@s.whatsapp.net"); + const react = requireRecord(requireSendContent().react, "reaction content"); + expect(react.text).toBe("👍"); + expectRecordFields(requireRecord(react.key, "reaction key"), { + remoteJid: "1555@s.whatsapp.net", + id: "msg-2", + fromMe: false, + participant: "1999@s.whatsapp.net", + }); + expectSendResultFields(res, { kind: "reaction", messageId: "msg-1", providerAccepted: true, @@ -270,7 +284,7 @@ describe("createWebSendApi", () => { const res = await api.sendMessage("+1555", "hello"); - expect(res).toMatchObject({ + expectSendResultFields(res, { kind: "text", messageId: "unknown", providerAccepted: false, @@ -280,38 +294,28 @@ describe("createWebSendApi", () => { it("keeps direct-chat reactions without a participant key", async () => { await api.sendReaction("+1555", "msg-2", "👍", false); - expect(sendMessage).toHaveBeenCalledWith( - "1555@s.whatsapp.net", - expect.objectContaining({ - react: { - text: "👍", - key: expect.objectContaining({ - remoteJid: "1555@s.whatsapp.net", - id: "msg-2", - fromMe: false, - participant: undefined, - }), - }, - }), - ); + expect(sendMessage.mock.calls[0]?.[0]).toBe("1555@s.whatsapp.net"); + const react = requireRecord(requireSendContent().react, "reaction content"); + expect(react.text).toBe("👍"); + expectRecordFields(requireRecord(react.key, "reaction key"), { + remoteJid: "1555@s.whatsapp.net", + id: "msg-2", + fromMe: false, + participant: undefined, + }); }); it("preserves LID participants in reaction keys", async () => { await api.sendReaction("12345@g.us", "msg-2", "👍", false, "123@lid"); - expect(sendMessage).toHaveBeenCalledWith( - "12345@g.us", - expect.objectContaining({ - react: { - text: "👍", - key: expect.objectContaining({ - remoteJid: "12345@g.us", - id: "msg-2", - fromMe: false, - participant: "123@lid", - }), - }, - }), - ); + expect(sendMessage.mock.calls[0]?.[0]).toBe("12345@g.us"); + const react = requireRecord(requireSendContent().react, "reaction content"); + expect(react.text).toBe("👍"); + expectRecordFields(requireRecord(react.key, "reaction key"), { + remoteJid: "12345@g.us", + id: "msg-2", + fromMe: false, + participant: "123@lid", + }); }); it("sends composing presence updates to the recipient JID", async () => { @@ -336,13 +340,11 @@ describe("createWebSendApi", () => { await api.sendMessage("123", "hello", mediaBuffer, undefined); - expect(sendMessage).toHaveBeenCalledWith( - "123@s.whatsapp.net", - expect.objectContaining({ - document: mediaBuffer, - mimetype: "application/octet-stream", - }), - ); + expect(sendMessage.mock.calls[0]?.[0]).toBe("123@s.whatsapp.net"); + expectSendContentFields(0, { + document: mediaBuffer, + mimetype: "application/octet-stream", + }); }); it("does not set mediaType when mediaBuffer is absent", async () => { @@ -362,18 +364,13 @@ describe("createWebSendApi", () => { }, }); - expect(sendMessage).toHaveBeenCalledWith( - "1555@s.whatsapp.net", - { text: "hello" }, - expect.objectContaining({ - quoted: expect.objectContaining({ - key: expect.objectContaining({ - remoteJid: "277038292303944@lid", - id: "quoted-1", - }), - }), - }), - ); + expect(sendMessage.mock.calls[0]?.[0]).toBe("1555@s.whatsapp.net"); + expect(sendMessage.mock.calls[0]?.[1]).toEqual({ text: "hello" }); + const quoted = requireRecord(requireSendOptions().quoted, "quoted message"); + expectRecordFields(requireRecord(quoted.key, "quoted key"), { + remoteJid: "277038292303944@lid", + id: "quoted-1", + }); }); }); @@ -382,7 +379,13 @@ describe("createWebSendApi", () => { // otherwise messages going to LID-addressed contacts vanish into a // sender-only ghost chat. describe("createWebSendApi LID resolution (issue #67378)", () => { - const sendMessage = vi.fn(async () => ({ key: { id: "msg-1" } })); + const sendMessage = vi.fn( + async ( + _jid: string, + _content: AnyMessageContent, + _options?: MiscMessageGenerationOptions, + ): Promise => ({ key: { id: "msg-1" } }) as WAMessage, + ); const sendPresenceUpdate = vi.fn(async () => {}); let authDir: string; @@ -423,10 +426,10 @@ describe("createWebSendApi LID resolution (issue #67378)", () => { authDir, }); await api.sendPoll("+15555550000", { question: "Q?", options: ["a", "b"] }); - expect(sendMessage).toHaveBeenCalledWith( - "987654@lid", - expect.objectContaining({ poll: expect.any(Object) }), - ); + expect(sendMessage.mock.calls[0]?.[0]).toBe("987654@lid"); + expect(typeof sendMessage.mock.calls[0]?.[1]).toBe("object"); + expect(sendMessage.mock.calls[0]?.[1]).not.toBeNull(); + expect("poll" in (sendMessage.mock.calls[0]?.[1] as Record)).toBe(true); }); it("resolves PN to LID for sendComposingTo presence", async () => {