Files
fusion/plugins/fusion-plugin-whatsapp-chat/src/__tests__/connection.test.ts
Fusion ef7efd0c96 feat(FN-3713): migrate WhatsApp plugin to Baileys pairing flow
- Add Baileys-based connection/auth modules and pairing route support for QR, pair code, status, and logout
- Refactor plugin entrypoint and split reply/auth/connection logic into focused modules
- Update plugin metadata/docs/settings schema to pairing-era configuration and remove legacy webhook key usage
- Add targeted tests for auth state, connection handling, reply flow, and updated schema expectations

Fusion-Task-Id: FN-3713
2026-05-07 18:26:14 -07:00

146 lines
6.5 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
const mockState = vi.hoisted(() => {
const handlers = new Map<string, (payload: any) => void>();
const sendMessage = vi.fn();
const end = vi.fn();
const logout = vi.fn();
const requestPairingCode = vi.fn().mockResolvedValue("123-456");
const makeWASocket = vi.fn(() => ({
ev: {
on: (name: string, handler: (payload: any) => void) => handlers.set(name, handler),
off: (name: string) => handlers.delete(name),
},
user: { id: "15550001111@s.whatsapp.net" },
sendMessage,
end,
logout,
requestPairingCode,
}));
return { handlers, sendMessage, end, logout, requestPairingCode, makeWASocket };
});
vi.mock("@whiskeysockets/baileys", () => ({
default: mockState.makeWASocket,
makeWASocket: mockState.makeWASocket,
DisconnectReason: { loggedOut: 401 },
BufferJSON: { reviver: undefined, replacer: undefined },
initAuthCreds: () => ({}),
}));
vi.mock("qrcode", () => ({
default: { toDataURL: vi.fn().mockResolvedValue("data:image/png;base64,abc") },
}));
import { WhatsAppConnection } from "../connection.js";
function createInMemoryDb() {
const sessions = new Map<string, string>();
const dedupe = new Set<string>();
const creds = new Map<string, string>();
const keys = new Map<string, string>();
return {
exec() {},
prepare(sql: string) {
return {
get: (...args: unknown[]) => {
if (sql.includes("FROM whatsapp_chat_sessions")) {
const history = sessions.get(args[0] as string);
return history ? { history } : undefined;
}
if (sql.includes("FROM whatsapp_chat_dedupe")) return dedupe.has(args[0] as string) ? { found: 1 } : undefined;
if (sql.includes("FROM whatsapp_auth_creds")) return creds.get("creds") ? { value: creds.get("creds") } : undefined;
if (sql.includes("FROM whatsapp_auth_keys")) return keys.get(`${args[0]}:${args[1]}`) ? { value: keys.get(`${args[0]}:${args[1]}`) } : undefined;
return undefined;
},
run: (...args: unknown[]) => {
if (sql.includes("whatsapp_chat_sessions")) sessions.set(args[0] as string, args[1] as string);
if (sql.includes("whatsapp_chat_dedupe")) dedupe.add(args[0] as string);
if (sql.includes("INSERT INTO whatsapp_auth_creds")) creds.set("creds", args[0] as string);
if (sql.includes("DELETE FROM whatsapp_auth_creds")) creds.clear();
if (sql.includes("INSERT INTO whatsapp_auth_keys")) keys.set(`${args[0]}:${args[1]}`, args[2] as string);
if (sql.includes("DELETE FROM whatsapp_auth_keys WHERE category")) keys.delete(`${args[0]}:${args[1]}`);
if (sql.includes("DELETE FROM whatsapp_auth_keys")) keys.clear();
},
};
},
};
}
function makeCtx(settings: Record<string, unknown> = {}) {
return {
pluginId: "fusion-plugin-whatsapp-chat",
settings: { allowedSenders: ["15550001111"], ...settings },
taskStore: { getRootDir: () => "/tmp", getPluginStore: () => ({}) },
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
emitEvent: vi.fn(),
} as any;
}
describe("WhatsAppConnection", () => {
beforeEach(() => {
mockState.handlers.clear();
mockState.makeWASocket.mockClear();
mockState.sendMessage.mockClear();
mockState.end.mockClear();
});
it("starts and stops idempotently", async () => {
const connection = new WhatsAppConnection(makeCtx(), "0.1.0", vi.fn().mockResolvedValue("reply"), createInMemoryDb() as any);
await connection.start();
await connection.stop();
await connection.stop();
expect(mockState.makeWASocket).toHaveBeenCalledTimes(1);
expect(mockState.end).toHaveBeenCalledTimes(1);
});
it("exposes qr updates", async () => {
const connection = new WhatsAppConnection(makeCtx(), "0.1.0", vi.fn().mockResolvedValue("reply"), createInMemoryDb() as any);
await connection.start();
await mockState.handlers.get("connection.update")?.({ qr: "abc" });
expect(connection.getStatus()).toMatchObject({ state: "awaiting-qr", qr: "abc" });
});
it("reconnects on close unless logged out", async () => {
vi.useFakeTimers();
const connection = new WhatsAppConnection(makeCtx(), "0.1.0", vi.fn().mockResolvedValue("reply"), createInMemoryDb() as any);
await connection.start();
await mockState.handlers.get("connection.update")?.({ connection: "close", lastDisconnect: { error: new Error("boom") } });
vi.advanceTimersByTime(1000);
expect(mockState.makeWASocket).toHaveBeenCalledTimes(2);
await mockState.handlers.get("connection.update")?.({ connection: "close", lastDisconnect: { error: { output: { statusCode: 401 } } } });
vi.advanceTimersByTime(1000);
expect(mockState.makeWASocket).toHaveBeenCalledTimes(2);
vi.useRealTimers();
});
it("drops unsupported inbound traffic", async () => {
const reply = vi.fn().mockResolvedValue("hello");
const connection = new WhatsAppConnection(makeCtx(), "0.1.0", reply, createInMemoryDb() as any);
await connection.start();
const upsert = mockState.handlers.get("messages.upsert")!;
await upsert({ type: "notify", messages: [{ key: { remoteJid: "abc@g.us", id: "1", fromMe: false }, message: { conversation: "hi" } }] });
await upsert({ type: "notify", messages: [{ key: { remoteJid: "15550001111@s.whatsapp.net", id: "2", fromMe: true }, message: { conversation: "hi" } }] });
await upsert({ type: "notify", messages: [{ key: { remoteJid: "15550001111@s.whatsapp.net", id: "3", fromMe: false }, message: {} }] });
expect(reply).not.toHaveBeenCalled();
});
it("dedupes and handles reply failure with fallback", async () => {
const reply = vi.fn().mockRejectedValue(new Error("nope"));
const connection = new WhatsAppConnection(makeCtx(), "0.1.0", reply, createInMemoryDb() as any);
await connection.start();
const payload = { type: "notify", messages: [{ key: { remoteJid: "15550001111@s.whatsapp.net", id: "m-1", fromMe: false }, message: { conversation: "hi" } }] };
await mockState.handlers.get("messages.upsert")?.(payload);
await mockState.handlers.get("messages.upsert")?.(payload);
expect(reply).toHaveBeenCalledTimes(1);
expect(mockState.sendMessage).toHaveBeenCalledWith("15550001111@s.whatsapp.net", { text: "Sorry, I hit an internal error while processing that message." });
});
it("splits oversized messages", () => {
const chunks = WhatsAppConnection.splitMessageForWhatsapp("x".repeat(9000));
expect(chunks.length).toBeGreaterThan(2);
expect(chunks[0].length).toBeLessThanOrEqual(4096);
});
});