test(whatsapp): cover inbound context compatibility

This commit is contained in:
Marcus Castro
2026-05-30 13:40:51 -03:00
committed by Shakker
parent 008d785a80
commit 1bea7d8ef3
6 changed files with 168 additions and 100 deletions

View File

@@ -326,7 +326,7 @@ describe("web inbound media saves with extension", () => {
});
const first = await waitForMessage(onMessage);
const mediaPath = requireMediaPath(first.mediaPath);
const mediaPath = requireMediaPath(first.payload.media?.path);
expect(path.extname(mediaPath)).toBe(".jpg");
const stat = await fs.stat(mediaPath);
expect(stat.size).toBeGreaterThan(0);
@@ -345,7 +345,7 @@ describe("web inbound media saves with extension", () => {
});
const second = await waitForMessage(onMessage);
expect(second.mediaFileName).toBe(fileName);
expect(second.payload.media?.fileName).toBe(fileName);
expect(saveMediaStreamSpy).toHaveBeenCalled();
const lastCall = latestSaveMediaStreamCall();
expect(lastCall[4]).toBe(fileName);
@@ -391,8 +391,8 @@ describe("web inbound media saves with extension", () => {
});
const inbound = await waitForMessage(onMessage);
expect(inbound.replyToBody).toBe("<media:image>");
const mediaPath = requireMediaPath(inbound.mediaPath);
expect(inbound.quote?.body).toBe("<media:image>");
const mediaPath = requireMediaPath(inbound.payload.media?.path);
expect(path.extname(mediaPath)).toBe(".jpg");
expect(saveMediaStreamSpy).toHaveBeenCalled();
const lastCall = latestSaveMediaStreamCall();

View File

@@ -8,6 +8,7 @@ import {
setupAccessControlTestHarness,
upsertPairingRequestMock,
} from "./access-control.test-harness.js";
import { createTestWebInboundMessage } from "./test-message.test-helper.js";
setupAccessControlTestHarness();
let checkInboundAccessControl: typeof import("./access-control.js").checkInboundAccessControl;
@@ -48,15 +49,20 @@ async function checkCommandAuthorizedForDm(params: {
}) {
return await resolveWhatsAppCommandAuthorized({
cfg: params.cfg as never,
msg: {
msg: createTestWebInboundMessage({
event: { id: "cmd-dm" },
payload: { body: "/status" },
platform: {
chatJid: params.from ?? "+15550001111",
recipientJid: params.selfE164 ?? "+15550009999",
senderE164: params.senderE164 ?? params.from ?? "+15550001111",
selfE164: params.selfE164 ?? "+15550009999",
},
accountId: params.accountId ?? "work",
chatType: "direct",
from: params.from ?? "+15550001111",
senderE164: params.senderE164 ?? params.from ?? "+15550001111",
selfE164: params.selfE164 ?? "+15550009999",
body: "/status",
to: params.selfE164 ?? "+15550009999",
} as never,
conversationId: params.from ?? "+15550001111",
}) as never,
});
}
@@ -69,17 +75,20 @@ async function checkCommandAuthorizedForGroup(params: {
}) {
return await resolveWhatsAppCommandAuthorized({
cfg: params.cfg as never,
msg: {
msg: createTestWebInboundMessage({
event: { id: "cmd-group" },
payload: { body: "/status" },
platform: {
chatJid: params.from ?? "120363401234567890@g.us",
recipientJid: params.selfE164 ?? "+15550009999",
senderE164: params.senderE164 ?? "+15550001111",
selfE164: params.selfE164 ?? "+15550009999",
},
accountId: params.accountId ?? "work",
chatType: "group",
from: params.from ?? "120363401234567890@g.us",
conversationId: params.from ?? "120363401234567890@g.us",
chatId: params.from ?? "120363401234567890@g.us",
senderE164: params.senderE164 ?? "+15550001111",
selfE164: params.selfE164 ?? "+15550009999",
body: "/status",
to: params.selfE164 ?? "+15550009999",
} as never,
}) as never,
});
}

View File

@@ -117,9 +117,13 @@ describe("web monitor inbox", () => {
// Should call onMessage for authorized senders
expect(onMessage).toHaveBeenCalledWith(
expect.objectContaining({
body: "authorized message",
from: "+999",
senderE164: "+999",
payload: expect.objectContaining({
body: "authorized message",
}),
platform: expect.objectContaining({
senderE164: "+999",
}),
}),
);
@@ -146,7 +150,12 @@ describe("web monitor inbox", () => {
// Should allow self-messages even if not in allowFrom
expect(onMessage).toHaveBeenCalledWith(
expect.objectContaining({ body: "self message", from: "+123" }),
expect.objectContaining({
from: "+123",
payload: expect.objectContaining({
body: "self message",
}),
}),
);
await listener.close();
@@ -205,9 +214,13 @@ describe("web monitor inbox", () => {
expect(onMessage).toHaveBeenCalledTimes(1);
expect(onMessage).toHaveBeenCalledWith(
expect.objectContaining({
body: "self ping",
from: "+123",
to: "+123",
payload: expect.objectContaining({
body: "self ping",
}),
platform: expect.objectContaining({
recipientJid: "+123",
}),
}),
);
@@ -290,11 +303,15 @@ describe("web monitor inbox", () => {
expect(onMessage).toHaveBeenCalledWith(
expect.objectContaining({
body: "/status",
chatType: "group",
from: "120363@g.us",
fromMe: true,
senderE164: "+123",
payload: expect.objectContaining({
body: "/status",
}),
platform: expect.objectContaining({
fromMe: true,
senderE164: "+123",
}),
}),
);

View File

@@ -1,7 +1,7 @@
// Whatsapp plugin module implements monitor inbox.blocks messages from unauthorized senders not allowfrom support behavior.
import "./monitor-inbox.test-harness.js";
import { describe, expect, it, vi } from "vitest";
import type { WebInboundMessageWithDeprecatedAliases } from "./inbound/types.js";
import type { WebInboundMessage } from "./inbound/types.js";
import {
DEFAULT_ACCOUNT_ID,
expectPairingPromptSent,
@@ -89,7 +89,7 @@ function firstInboundPayload(onMessage: ReturnType<typeof vi.fn>) {
if (!payload || typeof payload !== "object") {
throw new Error("expected first inbound payload");
}
return payload as WebInboundMessageWithDeprecatedAliases;
return payload as WebInboundMessage;
}
describe("web monitor inbox", () => {
@@ -210,9 +210,13 @@ describe("web monitor inbox", () => {
expect(onMessage).toHaveBeenCalledWith(
expect.objectContaining({
from: "+123",
to: "+123",
body: "self ping",
accessControlPassed: true,
payload: expect.objectContaining({
body: "self ping",
}),
platform: expect.objectContaining({
recipientJid: "+123",
}),
}),
);
expect(sock.readMessages).not.toHaveBeenCalled();
@@ -264,7 +268,7 @@ describe("web monitor inbox", () => {
expect(onMessage).toHaveBeenCalledTimes(1);
const payload = firstInboundPayload(onMessage);
expect(payload.chatType).toBe("group");
expect(payload.senderE164).toBe("+999");
expect(payload.platform.senderE164).toBe("+999");
await listener.close();
});
@@ -352,7 +356,7 @@ describe("web monitor inbox", () => {
expect(onMessage).toHaveBeenCalledTimes(1);
const payload = firstInboundPayload(onMessage);
expect(payload.chatType).toBe("group");
expect(payload.senderE164).toBe("+15551234567");
expect(payload.platform.senderE164).toBe("+15551234567");
await listener.close();
});

View File

@@ -84,7 +84,7 @@ describe("web monitor inbox", () => {
});
expect(onMessage).toHaveBeenCalledTimes(1);
expect(onMessage.mock.calls[0]?.[0]?.body).toBe("<media:image>");
expect(onMessage.mock.calls[0]?.[0]?.payload.body).toBe("<media:image>");
expect(sock.readMessages).toHaveBeenCalledWith([
{
remoteJid: "888@s.whatsapp.net",
@@ -224,8 +224,14 @@ describe("web monitor inbox", () => {
expectSingleGroupMessage(onMessage, {
chatType: "group",
conversationId: "99999@g.us",
senderE164: "+777",
mentionedJids: ["123@s.whatsapp.net"],
group: expect.objectContaining({
mentions: expect.objectContaining({
jids: ["123@s.whatsapp.net"],
}),
}),
platform: expect.objectContaining({
senderE164: "+777",
}),
});
await listener.close();
});
@@ -257,9 +263,17 @@ describe("web monitor inbox", () => {
expectSingleGroupMessage(onMessage, {
chatType: "group",
conversationId: "424242@g.us",
body: "oh hey @Clawd UK !",
mentionedJids: ["123@s.whatsapp.net"],
senderE164: "+888",
group: expect.objectContaining({
mentions: expect.objectContaining({
jids: ["123@s.whatsapp.net"],
}),
}),
payload: expect.objectContaining({
body: "oh hey @Clawd UK !",
}),
platform: expect.objectContaining({
senderE164: "+888",
}),
});
await listener.close();
});
@@ -301,11 +315,17 @@ describe("web monitor inbox", () => {
expectSingleGroupMessage(onMessage, {
chatType: "group",
from: "55555@g.us",
senderE164: "+777",
senderJid: "777@s.whatsapp.net",
mentionedJids: ["123@s.whatsapp.net"],
selfE164: "+123",
selfJid: "123@s.whatsapp.net",
group: expect.objectContaining({
mentions: expect.objectContaining({
jids: ["123@s.whatsapp.net"],
}),
}),
platform: expect.objectContaining({
senderE164: "+777",
senderJid: "777@s.whatsapp.net",
selfE164: "+123",
selfJid: "123@s.whatsapp.net",
}),
});
await listener.close();
});

View File

@@ -9,7 +9,7 @@ import {
} from "./connection-controller-registry.js";
import { WhatsAppRetryableInboundError } from "./inbound/dedupe.js";
import { WHATSAPP_GROUP_METADATA_CACHE_MAX_ENTRIES } from "./inbound/monitor.js";
import type { WebInboundMessageWithDeprecatedAliases } from "./inbound/types.js";
import type { WebInboundMessage } from "./inbound/types.js";
import {
type InboxMonitorOptions,
buildNotifyMessageUpsert,
@@ -63,13 +63,10 @@ function createSocketRef(): NonNullable<InboxMonitorOptions["socketRef"]> {
return { current: null };
}
function inboundMessage(
onMessage: ReturnType<typeof vi.fn>,
index = 0,
): WebInboundMessageWithDeprecatedAliases {
function inboundMessage(onMessage: ReturnType<typeof vi.fn>, index = 0): WebInboundMessage {
const msg = onMessage.mock.calls[index]?.[0];
expect(msg).toBeDefined();
return msg as WebInboundMessageWithDeprecatedAliases;
return msg as WebInboundMessage;
}
async function primeInboundReplyHandle(params: {
@@ -97,9 +94,7 @@ async function primeInboundReplyHandle(params: {
);
await waitForMessageCalls(params.onMessage, 1);
const inbound = inboundMessage(params.onMessage) as {
reply: (text: string) => Promise<void>;
};
const inbound = inboundMessage(params.onMessage);
return { listener, sock, inbound };
}
@@ -118,7 +113,7 @@ describe("web monitor inbox", () => {
async function expectQuotedReplyContext(quotedMessage: unknown) {
const onMessage = vi.fn(async (msg) => {
await msg.reply("pong");
await msg.platform.reply("pong");
});
const { listener, sock } = await startInboxMonitor(onMessage as InboxOnMessage);
@@ -151,13 +146,13 @@ describe("web monitor inbox", () => {
await waitForMessageCalls(onMessage, 1);
const inbound = inboundMessage(onMessage);
expect(inbound.replyToId).toBe("q1");
expect(inbound.replyToBody).toBe("original");
expect(inbound.replyToSender).toBe("+111");
const sender = inbound.sender as { e164?: string; name?: string };
expect(inbound.quote?.id).toBe("q1");
expect(inbound.quote?.body).toBe("original");
expect(inbound.quote?.sender?.displayName).toBe("+111");
const sender = inbound.platform.sender as { e164?: string; name?: string };
expect(sender.e164).toBe("+999");
expect(sender.name).toBe("Tester");
const replyTo = inbound.replyTo as {
const replyTo = inbound.quote?.context as {
body?: string;
id?: string;
sender?: { e164?: string; jid?: string; label?: string };
@@ -167,7 +162,7 @@ describe("web monitor inbox", () => {
expect(replyTo.sender?.jid).toBe("111@s.whatsapp.net");
expect(replyTo.sender?.e164).toBe("+111");
expect(replyTo.sender?.label).toBe("+111");
const self = inbound.self as { e164?: string; jid?: string };
const self = inbound.platform.self as { e164?: string; jid?: string };
expect(self.jid).toBe("123@s.whatsapp.net");
expect(self.e164).toBe("+123");
expect(sock.sendMessage).toHaveBeenCalledWith("999@s.whatsapp.net", {
@@ -180,7 +175,8 @@ describe("web monitor inbox", () => {
it("streams inbound messages", async () => {
const onMessage = vi.fn(async (msg) => {
await msg.sendComposing();
await msg.reply("pong");
await msg.reply("flat reply works");
await msg.sendMedia({ text: "flat media works" });
});
const { listener, sock } = await startInboxMonitor(onMessage as InboxOnMessage);
@@ -198,9 +194,9 @@ describe("web monitor inbox", () => {
await waitForMessageCalls(onMessage, 1);
const inbound = inboundMessage(onMessage);
expect(inbound.body).toBe("ping");
expect(inbound.payload.body).toBe("ping");
expect(inbound.from).toBe("+999");
expect(inbound.to).toBe("+123");
expect(inbound.platform.recipientJid).toBe("+123");
expect(sock.readMessages).toHaveBeenCalledWith([
{
remoteJid: "999@s.whatsapp.net",
@@ -211,8 +207,11 @@ describe("web monitor inbox", () => {
]);
expect(sock.sendPresenceUpdate).toHaveBeenCalledWith("available");
expect(sock.sendPresenceUpdate).toHaveBeenCalledWith("composing", "999@s.whatsapp.net");
expect(sock.sendMessage).toHaveBeenCalledWith("999@s.whatsapp.net", {
text: "pong",
expect(sock.sendMessage).toHaveBeenNthCalledWith(1, "999@s.whatsapp.net", {
text: "flat reply works",
});
expect(sock.sendMessage).toHaveBeenNthCalledWith(2, "999@s.whatsapp.net", {
text: "flat media works",
});
await listener.close();
@@ -277,7 +276,7 @@ describe("web monitor inbox", () => {
);
await waitForMessageCalls(onMessage, 1);
expect(inboundMessage(onMessage).body).toBe("ping");
expect(inboundMessage(onMessage).payload.body).toBe("ping");
await vi.waitFor(() => {
expect(sock.readMessages).toHaveBeenCalledWith([
{
@@ -353,6 +352,32 @@ describe("web monitor inbox", () => {
await listener.close();
});
it("omits group context when a group message has no group facts", async () => {
const sock = getSock();
sock.groupFetchAllParticipating.mockRejectedValueOnce(new Error("no groups"));
const onMessage = vi.fn(async () => {});
const { listener } = await startInboxMonitor(onMessage as InboxOnMessage);
sock.groupMetadata.mockRejectedValueOnce(new Error("group metadata unavailable"));
sock.ev.emit(
"messages.upsert",
buildNotifyMessageUpsert({
id: nextMessageId("group-no-facts"),
remoteJid: "123@g.us",
participant: "444@s.whatsapp.net",
text: "ping",
timestamp: 1_700_000_000,
}),
);
await waitForMessageCalls(onMessage, 1);
const inbound = inboundMessage(onMessage);
expect(inbound.chatType).toBe("group");
expect(inbound.group).toBeUndefined();
await listener.close();
});
it("keeps group inbound alive with cached metadata after reconnect-time metadata fetch failures", async () => {
const groupMetadataCache: NonNullable<InboxMonitorOptions["groupMetadataCache"]> = new Map();
const onMessage = vi.fn(async (_msg: Parameters<InboxOnMessage>[0]) => {});
@@ -394,12 +419,12 @@ describe("web monitor inbox", () => {
await waitForMessageCalls(onMessage, 1);
const inbound = inboundMessage(onMessage);
expect(inbound.body).toBe("ping");
expect(inbound.payload.body).toBe("ping");
expect(inbound.from).toBe("123@g.us");
expect(inbound.groupSubject).toBe("Recovered Group");
expect(inbound.senderE164).toBe("+444");
expect(inbound.group?.subject).toBe("Recovered Group");
expect(inbound.platform.senderE164).toBe("+444");
expect(inbound.chatType).toBe("group");
expect(inbound.groupParticipants).toBeUndefined();
expect(inbound.group?.participants).toBeUndefined();
await second.listener.close();
});
@@ -507,11 +532,7 @@ describe("web monitor inbox", () => {
);
await waitForMessageCalls(onMessage, 1);
const inbound = inboundMessage(onMessage) as {
reply: (text: string) => Promise<void>;
sendMedia: (payload: Record<string, unknown>) => Promise<void>;
sendComposing: () => Promise<void>;
};
const inbound = inboundMessage(onMessage);
const replacementSock = {
sendMessage: vi.fn(async () => undefined),
@@ -521,9 +542,9 @@ describe("web monitor inbox", () => {
InboxMonitorOptions["socketRef"]
>["current"];
await inbound.reply("pong");
await inbound.sendMedia({ text: "after-reconnect" });
await inbound.sendComposing();
await inbound.platform.reply("pong");
await inbound.platform.sendMedia({ text: "after-reconnect" });
await inbound.platform.sendComposing();
expect(replacementSock.sendMessage).toHaveBeenNthCalledWith(1, "999@s.whatsapp.net", {
text: "pong",
@@ -555,15 +576,13 @@ describe("web monitor inbox", () => {
);
await waitForMessageCalls(onMessage, 1);
const inbound = inboundMessage(onMessage) as {
sendMedia: (payload: Record<string, unknown>) => Promise<void>;
};
const inbound = inboundMessage(onMessage);
const image = Buffer.from("img");
const thumbnail = Buffer.from("thumb");
imageOps.getImageMetadata.mockResolvedValueOnce({ width: 640, height: 480 });
imageOps.resizeToJpeg.mockResolvedValueOnce(thumbnail);
await inbound.sendMedia({ image, caption: "cap", mimetype: "image/png" });
await inbound.platform.sendMedia({ image, caption: "cap", mimetype: "image/png" });
expect(imageOps.resizeToJpeg).toHaveBeenCalledWith({
buffer: image,
@@ -610,7 +629,7 @@ describe("web monitor inbox", () => {
>["current"];
});
await inbound?.reply("pong");
await inbound?.platform.reply("pong");
expect(sleepWithAbortMock).toHaveBeenCalledWith(10, undefined);
expect(replacementSock.sendMessage).toHaveBeenCalledWith("999@s.whatsapp.net", {
@@ -652,7 +671,9 @@ describe("web monitor inbox", () => {
await listener.close();
await vi.advanceTimersByTimeAsync(50);
await waitForMessageCalls(onMessage, 1);
expect(inboundMessage(onMessage).body).toBe("first\nsecond");
const inbound = inboundMessage(onMessage);
expect(inbound.payload.body).toBe("first\nsecond");
expect(inbound.chatType).toBe("direct");
} finally {
vi.useRealTimers();
}
@@ -662,8 +683,8 @@ describe("web monitor inbox", () => {
vi.useFakeTimers();
try {
const onMessage = vi.fn(async (msg) => {
await msg.reply("pong");
await msg.sendMedia({ text: "media" });
await msg.platform.reply("pong");
await msg.platform.sendMedia({ text: "media" });
});
const { listener, sock } = await startInboxMonitor(onMessage as InboxOnMessage, {
debounceMs: 50,
@@ -692,7 +713,7 @@ describe("web monitor inbox", () => {
await listener.close();
expect(onMessage).toHaveBeenCalledTimes(1);
expect(inboundMessage(onMessage).body).toBe("first\nsecond");
expect(inboundMessage(onMessage).payload.body).toBe("first\nsecond");
expect(sock.sendMessage).toHaveBeenNthCalledWith(1, "999@s.whatsapp.net", {
text: "pong",
});
@@ -718,7 +739,7 @@ describe("web monitor inbox", () => {
markHandlerStarted = resolve;
});
const onMessage = vi.fn(async (msg) => {
await msg.reply("pong");
await msg.platform.reply("pong");
markHandlerStarted?.();
await handlerGate;
});
@@ -777,7 +798,7 @@ describe("web monitor inbox", () => {
.mockRejectedValueOnce(new Error("operation timed out"))
.mockResolvedValueOnce({ key: { id: "after-timeout" } });
await inbound?.reply("pong");
await inbound?.platform.reply("pong");
expect(sock.sendMessage).toHaveBeenNthCalledWith(1, "999@s.whatsapp.net", {
text: "pong",
@@ -810,7 +831,7 @@ describe("web monitor inbox", () => {
socketRef.current = null;
await expect(inbound?.reply("pong")).rejects.toThrow(
await expect(inbound?.platform.reply("pong")).rejects.toThrow(
"no active socket - reconnection in progress",
);
expect(sleepWithAbortMock).toHaveBeenCalledTimes(11);
@@ -856,10 +877,7 @@ describe("web monitor inbox", () => {
}),
);
await waitForMessageCalls(onMessage, 1);
const inbound = inboundMessage(onMessage) as {
reply: (text: string) => Promise<void>;
sendMedia: (payload: Record<string, unknown>) => Promise<void>;
};
const inbound = inboundMessage(onMessage);
// The mock harness socket exposes user.id = "123@s.whatsapp.net"; the
// successor handle must report a self identity that overlaps that JID
@@ -941,7 +959,7 @@ describe("web monitor inbox", () => {
}),
);
await waitForMessageCalls(onMessage, 1);
const inbound = inboundMessage(onMessage) as { reply: (text: string) => Promise<void> };
const inbound = inboundMessage(onMessage);
socketRefA.current = null;
aShouldRetryDisconnect = false;
@@ -1006,7 +1024,7 @@ describe("web monitor inbox", () => {
}),
);
await waitForMessageCalls(onMessage, 1);
const inbound = inboundMessage(onMessage) as { reply: (text: string) => Promise<void> };
const inbound = inboundMessage(onMessage);
// A's shutdown sequence.
socketRefA.current = null;
@@ -1107,9 +1125,9 @@ describe("web monitor inbox", () => {
expect(getPNForLID).toHaveBeenCalledWith("999@lid");
const inbound = inboundMessage(onMessage);
expect(inbound.body).toBe("ping");
expect(inbound.payload.body).toBe("ping");
expect(inbound.from).toBe("+999");
expect(inbound.to).toBe("+123");
expect(inbound.platform.recipientJid).toBe("+123");
await listener.close();
});
@@ -1135,9 +1153,9 @@ describe("web monitor inbox", () => {
await waitForMessageCalls(onMessage, 1);
const inbound = inboundMessage(onMessage);
expect(inbound.body).toBe("ping");
expect(inbound.payload.body).toBe("ping");
expect(inbound.from).toBe("+1555");
expect(inbound.to).toBe("+123");
expect(inbound.platform.recipientJid).toBe("+123");
expect(getPNForLID).not.toHaveBeenCalled();
await listener.close();
@@ -1162,9 +1180,9 @@ describe("web monitor inbox", () => {
expect(getPNForLID).toHaveBeenCalledWith("444@lid");
const inbound = inboundMessage(onMessage);
expect(inbound.body).toBe("ping");
expect(inbound.payload.body).toBe("ping");
expect(inbound.from).toBe("123@g.us");
expect(inbound.senderE164).toBe("+444");
expect(inbound.platform.senderE164).toBe("+444");
expect(inbound.chatType).toBe("group");
await listener.close();