import { describe, expect, it } from "@suaveplan/testing/runner"; import { FakeWsServer, makeFakeWsServerFactory, } from "../../tests/setup/fake-ws-gateway.js"; import { PresenceTracker } from "../presence/presence.js"; import { ChannelManager } from "./channel-manager.js"; import { connectClient, disconnectClient } from "./client-lifecycle.js"; import { WebSocketGateway } from "./gateway.js"; import { buildEmit, buildFirePresence, buildRequireAuthCtx, } from "./gateway-deps.js"; import { createHeartbeat } from "./heartbeat.js"; import { runAuthHandshake } from "./lifecycle.js"; import { firePresence } from "./presence-fire.js"; import { deliverPresenceWebhook, type PresenceWebhookEvent, } from "./presence-webhook.js"; describe("ChannelManager direct surface", () => { it("size + getOrCreate idempotence + getChannel + publish on unknown + size", { timeout: 5000, }, () => { const mgr = new ChannelManager(); expect(mgr.size).toBe(0); const c1 = mgr.getOrCreateChannel("evt"); const c2 = mgr.getOrCreateChannel("evt"); expect(c1).toBe(c2); expect(mgr.getChannel("evt")).toBe(c1); expect(mgr.getChannel("nope")).toBeUndefined(); expect(mgr.size).toBe(1); expect(mgr.publish("missing", "alice", "topic", null, 1)).toBeNull(); mgr.removeClient("ghost"); mgr.clear(); expect(mgr.size).toBe(0); }); }); describe("heartbeat sweep", () => { it("sweeps timed-out clients", { timeout: 5000 }, async () => { let now = 0; const presence = new PresenceTracker(10, () => now); presence.track("a"); const out: string[] = []; const hb = createHeartbeat( presence, 5, (id) => out.push(id), () => true ); hb.start(); hb.start(); // idempotent now = 1_000; await new Promise((r) => setTimeout(r, 30)); hb.stop(); hb.stop(); // idempotent expect(out).toContain("a"); }); it("skips sweep when disabled", { timeout: 5000 }, async () => { let now = 0; const presence = new PresenceTracker(10, () => now); presence.track("a"); const out: string[] = []; const hb = createHeartbeat( presence, 5, (id) => out.push(id), () => false ); hb.start(); now = 1_000; await new Promise((r) => setTimeout(r, 30)); hb.stop(); expect(out).toEqual([]); }); }); describe("firePresence + presence-webhook branch coverage", () => { it("firePresence with no bus and no webhook is a no-op", { timeout: 5000, }, async () => { await firePresence( { clock: () => 0 }, "join", "a", undefined, "online" ); }); it("firePresence with bus that throws is swallowed", { timeout: 5000, }, async () => { const bus = { emit: () => { throw new Error("bus down"); }, }; await firePresence( { bus: bus as never, clock: () => 0 }, "leave", "a", undefined, "offline" ); }); it("deliverPresenceWebhook filters by events allowlist", { timeout: 5000, }, async () => { let called = false; const originalFetch = globalThis.fetch; (globalThis as { fetch: typeof fetch }).fetch = (async () => { called = true; return new Response(null, { status: 200 }); }) as typeof fetch; try { const ev: PresenceWebhookEvent = { kind: "presence:update", clientId: "a", roomId: undefined, status: "away", at: 0, }; await deliverPresenceWebhook( { url: "https://hooks.example.com", events: ["presence:join"], }, ev ); expect(called).toBe(false); } finally { (globalThis as { fetch: typeof fetch }).fetch = originalFetch; } }); it("deliverPresenceWebhook routes errors through onError", { timeout: 30000, }, async () => { const errors: unknown[] = []; const originalFetch = globalThis.fetch; (globalThis as { fetch: typeof fetch }).fetch = (async () => { throw new Error("net"); }) as typeof fetch; try { await deliverPresenceWebhook( { url: "https://hooks.example.com" }, { kind: "presence:join", clientId: "a", roomId: undefined, status: "online", at: 0, }, (e) => errors.push(e) ); expect(errors.length).toBeGreaterThan(0); } finally { (globalThis as { fetch: typeof fetch }).fetch = originalFetch; } }); }); describe("client-lifecycle branch coverage", () => { it("connectClient with enablePresence:false skips presence tracker", { timeout: 5000, }, () => { const presence = new PresenceTracker(100); const deps = { clients: new Map(), authContexts: new Map(), rooms: new Map(), channelMgr: new ChannelManager(), presence, enablePresence: false, clock: () => 0, emit: () => {}, firePresence: async () => {}, }; connectClient(deps, "a"); expect(presence.size).toBe(0); expect(disconnectClient(deps, "a")).toBe(true); expect(disconnectClient(deps, "a")).toBe(false); }); }); describe("gateway-deps + buildRequireAuthCtx fallback", () => { it("synthetic auth ctx is created on miss", { timeout: 5000 }, () => { const requireAuthCtx = buildRequireAuthCtx({ state: { clients: new Map(), rooms: new Map(), channelMgr: new ChannelManager(), presence: new PresenceTracker(100), wsServer: undefined, started: false, disposed: false, startedAt: undefined, boundAddress: undefined, boundPort: undefined, }, resolved: {} as never, clock: () => 0, authContexts: new Map(), idempotency: {} as never, }); const ctx = requireAuthCtx("ghost"); expect(ctx.clientId).toBe("ghost"); expect(ctx.principal).toBeUndefined(); // Second call retrieves the synthetic expect(requireAuthCtx("ghost")).toBe(ctx); }); }); describe("runAuthHandshake fallback ctx.clientId", () => { it("synthesises clientId when verifier returns empty", { timeout: 5000, }, async () => { const ctx = await runAuthHandshake( { authVerifier: async () => ({ clientId: "", principal: undefined, tenantId: undefined, scopes: [], metadata: {}, }) as never, auditLogger: () => {}, } as never, "sock-x", { headers: {}, remoteAddress: undefined, url: "/", query: {} }, 0 ); expect(ctx.clientId).toBe("sock-x"); }); }); describe("buildEmit no-op without bus + FakeWsServer extras", () => { it("emit is a no-op when bus is undefined", { timeout: 5000 }, () => { const emit = buildEmit({ state: {} as never, resolved: { bus: undefined } as never, clock: () => 0, authContexts: new Map(), idempotency: {} as never, }); // Should not throw, no observable side effect. emit("ws_gateway:stopped", { at: 0 }); }); it("FakeWsServer socket.close, message, getSocket", { timeout: 5000, }, () => { const server = new FakeWsServer(); server.callbacks = { onConnection: () => {}, onMessage: (id, payload) => { expect(id).toBe("a"); expect(payload).toBe("p"); }, onClose: () => {}, onError: () => {}, }; const s = server.connect("a"); s.send("hi"); s.close(); s.close(); // idempotent server.message("a", "p"); expect(server.getSocket("a")).toBeUndefined(); expect(server.socketCount()).toBe(0); }); }); describe("buildFirePresence branch coverage", () => { it("fire with no webhook short-circuits cleanly", { timeout: 5000, }, async () => { const fire = buildFirePresence({ state: {} as never, resolved: { bus: undefined, presenceWebhook: undefined } as never, clock: () => 0, authContexts: new Map(), idempotency: {} as never, }); await fire("update", "a", undefined, "online"); }); }); describe("WebSocketGateway disabled-presence path", () => { it("connectClient does not track presence when disabled", { timeout: 5000, }, () => { const { factory } = makeFakeWsServerFactory(); const gw = new WebSocketGateway({ port: 0, wsServerFactory: factory, enablePresence: false, }); gw.connectClient("a"); expect(gw.getPresenceTracker().size).toBe(0); gw.disconnectClient("a"); }); it("config getter returns the resolved config", { timeout: 5000 }, () => { const { factory } = makeFakeWsServerFactory(); const gw = new WebSocketGateway({ port: 0, wsServerFactory: factory }); expect(gw.config.maxRooms).toBe(100); }); });