- 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
146 lines
6.5 KiB
TypeScript
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);
|
|
});
|
|
});
|