Spaces:
Configuration error
Configuration error
File size: 7,942 Bytes
3a65265 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 | import { EventEmitter } from "node:events";
import fsSync from "node:fs";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { resetLogger, setLoggerOverride } from "../logging.js";
import { baileys, getLastSocket, resetBaileysMocks, resetLoadConfigMock } from "./test-helpers.js";
const { createWaSocket, formatError, logWebSelfId, waitForWaConnection } =
await import("./session.js");
describe("web session", () => {
beforeEach(() => {
vi.clearAllMocks();
resetBaileysMocks();
resetLoadConfigMock();
});
afterEach(() => {
resetLogger();
setLoggerOverride(null);
vi.useRealTimers();
});
it("creates WA socket with QR handler", async () => {
await createWaSocket(true, false);
const makeWASocket = baileys.makeWASocket as ReturnType<typeof vi.fn>;
expect(makeWASocket).toHaveBeenCalledWith(
expect.objectContaining({ printQRInTerminal: false }),
);
const passed = makeWASocket.mock.calls[0][0];
const passedLogger = (passed as { logger?: { level?: string; trace?: unknown } }).logger;
expect(passedLogger?.level).toBe("silent");
expect(typeof passedLogger?.trace).toBe("function");
const sock = getLastSocket();
const saveCreds = (await baileys.useMultiFileAuthState.mock.results[0].value).saveCreds;
// trigger creds.update listener
sock.ev.emit("creds.update", {});
await new Promise<void>((resolve) => setImmediate(resolve));
expect(saveCreds).toHaveBeenCalled();
});
it("waits for connection open", async () => {
const ev = new EventEmitter();
const promise = waitForWaConnection({ ev } as unknown as ReturnType<
typeof baileys.makeWASocket
>);
ev.emit("connection.update", { connection: "open" });
await expect(promise).resolves.toBeUndefined();
});
it("rejects when connection closes", async () => {
const ev = new EventEmitter();
const promise = waitForWaConnection({ ev } as unknown as ReturnType<
typeof baileys.makeWASocket
>);
ev.emit("connection.update", {
connection: "close",
lastDisconnect: new Error("bye"),
});
await expect(promise).rejects.toBeInstanceOf(Error);
});
it("logWebSelfId prints cached E.164 when creds exist", () => {
const existsSpy = vi.spyOn(fsSync, "existsSync").mockImplementation((p) => {
if (typeof p !== "string") return false;
return p.endsWith("creds.json");
});
const readSpy = vi.spyOn(fsSync, "readFileSync").mockImplementation((p) => {
if (typeof p === "string" && p.endsWith("creds.json")) {
return JSON.stringify({ me: { id: "12345@s.whatsapp.net" } });
}
throw new Error(`unexpected readFileSync path: ${String(p)}`);
});
const runtime = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
};
logWebSelfId("/tmp/wa-creds", runtime as never, true);
expect(runtime.log).toHaveBeenCalledWith(
expect.stringContaining("Web Channel: +12345 (jid 12345@s.whatsapp.net)"),
);
existsSpy.mockRestore();
readSpy.mockRestore();
});
it("formatError prints Boom-like payload message", () => {
const err = {
error: {
isBoom: true,
output: {
statusCode: 408,
payload: {
statusCode: 408,
error: "Request Time-out",
message: "QR refs attempts ended",
},
},
},
};
expect(formatError(err)).toContain("status=408");
expect(formatError(err)).toContain("Request Time-out");
expect(formatError(err)).toContain("QR refs attempts ended");
});
it("does not clobber creds backup when creds.json is corrupted", async () => {
const credsSuffix = path.join(".clawdbot", "credentials", "whatsapp", "default", "creds.json");
const copySpy = vi.spyOn(fsSync, "copyFileSync").mockImplementation(() => {});
const existsSpy = vi.spyOn(fsSync, "existsSync").mockImplementation((p) => {
if (typeof p !== "string") return false;
return p.endsWith(credsSuffix);
});
const statSpy = vi.spyOn(fsSync, "statSync").mockImplementation((p) => {
if (typeof p === "string" && p.endsWith(credsSuffix)) {
return { isFile: () => true, size: 12 } as never;
}
throw new Error(`unexpected statSync path: ${String(p)}`);
});
const readSpy = vi.spyOn(fsSync, "readFileSync").mockImplementation((p) => {
if (typeof p === "string" && p.endsWith(credsSuffix)) {
return "{" as never;
}
throw new Error(`unexpected readFileSync path: ${String(p)}`);
});
await createWaSocket(false, false);
const sock = getLastSocket();
const saveCreds = (await baileys.useMultiFileAuthState.mock.results[0].value).saveCreds;
sock.ev.emit("creds.update", {});
await new Promise<void>((resolve) => setImmediate(resolve));
expect(copySpy).not.toHaveBeenCalled();
expect(saveCreds).toHaveBeenCalled();
copySpy.mockRestore();
existsSpy.mockRestore();
statSpy.mockRestore();
readSpy.mockRestore();
});
it("serializes creds.update saves to avoid overlapping writes", async () => {
let inFlight = 0;
let maxInFlight = 0;
let release: (() => void) | null = null;
const gate = new Promise<void>((resolve) => {
release = resolve;
});
const saveCreds = vi.fn(async () => {
inFlight += 1;
maxInFlight = Math.max(maxInFlight, inFlight);
await gate;
inFlight -= 1;
});
baileys.useMultiFileAuthState.mockResolvedValueOnce({
state: { creds: {}, keys: {} },
saveCreds,
});
await createWaSocket(false, false);
const sock = getLastSocket();
sock.ev.emit("creds.update", {});
sock.ev.emit("creds.update", {});
await new Promise<void>((resolve) => setImmediate(resolve));
expect(inFlight).toBe(1);
release?.();
// let both queued saves complete
await new Promise<void>((resolve) => setImmediate(resolve));
await new Promise<void>((resolve) => setImmediate(resolve));
expect(saveCreds).toHaveBeenCalledTimes(2);
expect(maxInFlight).toBe(1);
expect(inFlight).toBe(0);
});
it("rotates creds backup when creds.json is valid JSON", async () => {
const credsSuffix = path.join(".clawdbot", "credentials", "whatsapp", "default", "creds.json");
const backupSuffix = path.join(
".clawdbot",
"credentials",
"whatsapp",
"default",
"creds.json.bak",
);
const copySpy = vi.spyOn(fsSync, "copyFileSync").mockImplementation(() => {});
const existsSpy = vi.spyOn(fsSync, "existsSync").mockImplementation((p) => {
if (typeof p !== "string") return false;
return p.endsWith(credsSuffix);
});
const statSpy = vi.spyOn(fsSync, "statSync").mockImplementation((p) => {
if (typeof p === "string" && p.endsWith(credsSuffix)) {
return { isFile: () => true, size: 12 } as never;
}
throw new Error(`unexpected statSync path: ${String(p)}`);
});
const readSpy = vi.spyOn(fsSync, "readFileSync").mockImplementation((p) => {
if (typeof p === "string" && p.endsWith(credsSuffix)) {
return "{}" as never;
}
throw new Error(`unexpected readFileSync path: ${String(p)}`);
});
await createWaSocket(false, false);
const sock = getLastSocket();
const saveCreds = (await baileys.useMultiFileAuthState.mock.results[0].value).saveCreds;
sock.ev.emit("creds.update", {});
await new Promise<void>((resolve) => setImmediate(resolve));
expect(copySpy).toHaveBeenCalledTimes(1);
const args = copySpy.mock.calls[0] ?? [];
expect(String(args[0] ?? "")).toContain(credsSuffix);
expect(String(args[1] ?? "")).toContain(backupSuffix);
expect(saveCreds).toHaveBeenCalled();
copySpy.mockRestore();
existsSpy.mockRestore();
statSpy.mockRestore();
readSpy.mockRestore();
});
});
|