102 lines
3.1 KiB
TypeScript
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);
|
|
});
|