import test from 'node:test'; import assert from 'node:assert/strict'; import { createServer } from 'node:http'; import { Server } from 'socket.io'; import { io as client, type Socket } from 'socket.io-client'; import { registerPresence } from '../src/server/presence.js'; import { ApiError, type GameSessionStore } from '../src/server/game-session-store.js'; import { PRESENCE_AUTH_REFRESH_MS } from '../constants/presence.js'; test('presence counts authenticated players once across tabs and removes disconnected players', async () => { const http = createServer(), io = new Server(http); registerPresence(io, { async get(auth: { token: string; gameSessionId: string }) { if (auth.token !== 'valid') throw new ApiError(401, 'INVALID_SESSION'); return { playerId: auth.gameSessionId.startsWith('a') ? 'a' : 'b' }; } } as unknown as GameSessionStore); await new Promise(resolve => http.listen(0, '127.0.0.1', resolve)); const url = `http://127.0.0.1:${(http.address() as { port: number }).port}/presence`; const sockets: Socket[] = []; const open = (id: string, token = 'valid') => { const socket = client(url, { autoConnect: false, forceNew: true, auth: { gameSessionId: id, token }, reconnection: false }); sockets.push(socket); return socket; }; const next = (socket: Socket) => new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new Error('PRESENCE_TIMEOUT')), 3000); socket.once('presence:count', n => { clearTimeout(timer); resolve(n); }); }); try { const a = open('a1'), aFirst = next(a); a.connect(); assert.equal(await aFirst, 1); const secondTab = open('a2'), tabFirst = next(secondTab); secondTab.connect(); assert.equal(await tabFirst, 1); const b = open('b1'), bFirst = next(b), aUpdate = next(a); b.connect(); assert.equal(await bFirst, 2); assert.equal(await aUpdate, 2); const bad = open('bad', 'invalid'); const rejected = new Promise(resolve => bad.once('connect_error', () => resolve())); bad.connect(); await rejected; const lastA = next(secondTab); a.disconnect(); b.disconnect(); assert.equal(await lastA, 1); } finally { for (const socket of sockets) socket.disconnect(); await new Promise(resolve => io.close(() => resolve())); } }); test('temporary revalidation errors allow reconnect, invalid sessions remain disconnected', async t => { let revalidate!: () => void; const interval = globalThis.setInterval; t.mock.method(globalThis, 'setInterval', ((fn: () => void, ms: number) => { if (ms !== PRESENCE_AUTH_REFRESH_MS) return interval(fn, ms); revalidate = fn; return { unref() {} }; }) as typeof setInterval); let failure: 'temporary' | 'invalid' | undefined; const http = createServer(), io = new Server(http); registerPresence(io, { async get() { if (failure === 'invalid') throw new ApiError(401, 'INVALID_SESSION'); if (failure === 'temporary') { failure = undefined; throw new Error('DB_UNAVAILABLE'); } return { playerId: 'one' }; } } as unknown as GameSessionStore); await new Promise(resolve => http.listen(0, '127.0.0.1', resolve)); const socket = client(`http://127.0.0.1:${(http.address() as { port: number }).port}/presence`, { autoConnect: false, reconnectionDelay: 10 }); const connected = () => new Promise(resolve => socket.once('connect', () => resolve())); try { const first = connected(); socket.connect(); await first; const next = connected(); failure = 'temporary'; revalidate(); await next; assert.equal(socket.connected, true); const disconnected = new Promise(resolve => socket.once('disconnect', resolve)); failure = 'invalid'; revalidate(); assert.equal(await disconnected, 'io server disconnect'); assert.equal(socket.active, false); } finally { socket.disconnect(); await new Promise(resolve => io.close(() => resolve())); } });