import { beforeEach, describe, expect, it, vi } from "vitest"; const mockState = vi.hoisted(() => { const handlers = new Map 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(); const dedupe = new Set(); const creds = new Map(); const keys = new Map(); 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 = {}) { 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); }); });