CoompanionApp/server/websocket.test.ts

102 lines
3.1 KiB
TypeScript

import { afterEach, expect, test } from "bun:test";
import { createSession, removeSession } from "./store";
import { joinSession } from "./domain";
import {
STALE_SOCKET_TIMEOUT_MS,
handleSocketMessage,
reapStaleSockets,
registerSocket,
resetWebsocketStateForTests,
unregisterSocket,
} from "./websocket";
const OPEN = 1;
const CLOSED = 3;
type FakeSocket = {
readyState: number;
sent: string[];
closes: Array<{ code?: number; reason?: string }>;
send: (payload: string) => void;
close: (code?: number, reason?: string) => void;
};
function createFakeSocket(): FakeSocket {
return {
readyState: OPEN,
sent: [],
closes: [],
send(payload: string) {
this.sent.push(payload);
},
close(code?: number, reason?: string) {
this.closes.push({ code, reason });
this.readyState = CLOSED;
},
};
}
afterEach(() => {
resetWebsocketStateForTests();
});
test("overlapping sockets do not disconnect a player until the last socket closes", () => {
const { session } = createSession("Banker");
const player = joinSession(session, "Jules");
const firstSocket = createFakeSocket();
const secondSocket = createFakeSocket();
registerSocket(firstSocket as unknown as WebSocket, session.id, player.id);
registerSocket(secondSocket as unknown as WebSocket, session.id, player.id);
unregisterSocket(firstSocket as unknown as WebSocket);
expect(session.players.get(player.id)?.connected).toBe(true);
expect(session.players.get(player.id)?.isDummy).toBe(false);
unregisterSocket(secondSocket as unknown as WebSocket);
expect(session.players.get(player.id)?.connected).toBe(false);
expect(session.players.get(player.id)?.isDummy).toBe(true);
removeSession(session.id);
});
test("stale sockets are reaped and disconnect the player", () => {
const { session } = createSession("Banker");
const player = joinSession(session, "Rosa");
const socket = createFakeSocket();
registerSocket(socket as unknown as WebSocket, session.id, player.id);
reapStaleSockets(Date.now() + STALE_SOCKET_TIMEOUT_MS + 1);
expect(socket.closes[0]).toEqual({ code: 4000, reason: "Connection stale" });
expect(session.players.get(player.id)?.connected).toBe(false);
expect(session.players.get(player.id)?.isDummy).toBe(true);
removeSession(session.id);
});
test("invalid session registration closes the socket with a terminal code", () => {
const socket = createFakeSocket();
registerSocket(socket as unknown as WebSocket, "missing-session", "missing-player");
expect(socket.closes[0]).toEqual({ code: 1008, reason: "Session not found" });
});
test("ping messages update liveness and emit a pong", () => {
const { session } = createSession("Banker");
const player = joinSession(session, "Nina");
const socket = createFakeSocket();
registerSocket(socket as unknown as WebSocket, session.id, player.id);
socket.sent = [];
handleSocketMessage(
socket as unknown as WebSocket,
JSON.stringify({ type: "ping", sessionId: session.id, playerId: player.id }),
);
expect(socket.sent.some((entry) => entry.includes('"type":"pong"'))).toBe(true);
removeSession(session.id);
});