Initial Commit
This commit is contained in:
@@ -0,0 +1,315 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user