import { test } from 'node:test'; import assert from 'node:assert/strict'; import { randomBytes, randomUUID } from 'node:crypto'; import { MongoClient } from 'mongodb'; import { io } from 'socket.io-client'; import { createGameServer } from '../src/server/create-server.js'; import { ManualClock } from './helpers.js'; const uri = process.env.TEST_MONGODB_URI; test('server cleanup releases a closed browser while an authenticated presence connection protects another tab', { skip: !uri }, async () => { const databaseName = 'dragon_ball_test_cleanup_' + randomUUID().replaceAll('-', ''), clock = new ManualClock(); const app = await createGameServer({ mongoUri: uri!, databaseName, clientDirectory: 'dist/client', clock }); const client = await new MongoClient(uri!).connect(); let socket: ReturnType | undefined; try { const url = await app.listen({ host: '127.0.0.1', port: 0 }); const create = async (guestToken?: string) => (await app.inject({ method: 'POST', url: '/api/game-sessions', payload: { createRequestId: randomBytes(32).toString('base64url'), guestToken } })).json().data; const old = await create(), live = await create(old.guestToken), otherBrowser = await create(); assert.equal(old.playerId, live.playerId); assert.notEqual(old.playerId, otherBrowser.playerId); const headers = (s: typeof old) => ({ authorization: 'Bearer ' + s.token, 'x-game-session': s.gameSessionId }); for (const session of [old, live]) { assert.equal((await app.inject({ method: 'POST', url: '/api/game-session/matches', headers: headers(session), payload: { requestId: 'bot', matchId: session.gameSessionId, mode: 'bot', expectedPreviousSeq: 0 } })).statusCode, 200); } assert.equal((await app.inject({ method: 'POST', url: '/api/ranked/queue', headers: headers(otherBrowser), payload: {} })).statusCode, 200); await clock.advance(90000); socket = io(url + '/presence', { autoConnect: false, reconnection: false, auth: { gameSessionId: live.gameSessionId, token: live.token } }); const connected = new Promise((resolve, reject) => { socket!.once('connect', resolve); socket!.once('connect_error', reject); }); socket.connect(); await connected; await clock.advance(60000); const get = async (s: typeof old) => (await app.inject({ url: '/api/game-session', headers: headers(s) })).json().data; assert.equal((await get(old)).activeMatch, undefined); assert.equal((await get(old)).stats.bot.losses, 0); assert.equal((await get(live)).activeMatch.matchId, live.gameSessionId); assert.equal((await app.inject({ method: 'POST', url: '/api/ranked/queue', headers: headers(old), payload: {} })).json().code, 'MATCH_PENDING'); socket.disconnect(); await clock.advance(90000); assert.equal((await get(live)).activeMatch, undefined); assert.equal((await app.inject({ method: 'POST', url: '/api/ranked/queue', headers: headers(old), payload: {} })).statusCode, 200); } finally { socket?.disconnect(); await app.close(); await client.db(databaseName).dropDatabase(); await client.close(); } }); test('HTTP game registration survives server restart without game timers and old gameplay sockets are rejected', { skip: !uri }, async () => { const databaseName = 'dragon_ball_test_server_' + randomUUID().replaceAll('-', ''), clock = new ManualClock(); const options = { mongoUri: uri!, databaseName, clientDirectory: 'dist/client', clock }; let app = await createGameServer(options); const client = await new MongoClient(uri!).connect(); let socket: ReturnType | undefined; try { const url = await app.listen({ host: '127.0.0.1', port: 0 }); assert.equal((await app.inject('/health')).statusCode, 200); assert.equal((await app.inject('/ready')).statusCode, 200); const session = (await app.inject({ method: 'POST', url: '/api/game-sessions', payload: { createRequestId: randomBytes(32).toString('base64url') } })).json().data; const headers = { authorization: 'Bearer ' + session.token, 'x-game-session': session.gameSessionId }; const input = { requestId: 'register', matchId: 'bot-match', mode: 'bot', expectedPreviousSeq: 0 }; const match = (await app.inject({ method: 'POST', url: '/api/game-session/matches', headers, payload: input })).json().data; assert.equal(match.seq, 1); await clock.advance(12000); assert.equal((await app.inject({ url: '/api/game-session', headers })).json().data.activeMatch.matchId, 'bot-match'); assert.equal(await client.db(databaseName).collection('matches').countDocuments(), 0); socket = io(url, { autoConnect: false, reconnection: false, transports: ['websocket'] }); const status = new Promise(resolve => { socket!.once('connect_error', () => resolve('rejected')); socket!.once('connect', () => resolve('accepted')); }); socket.connect(); assert.equal(await status, 'rejected'); socket.disconnect(); await app.close(); app = await createGameServer(options); assert.equal((await app.inject({ url: '/api/game-session', headers })).json().data.activeMatch.matchId, 'bot-match'); const payload = { ...match, outcome: 'loss', reason: 'timeout' }; assert.equal((await app.inject({ method: 'POST', url: '/api/game-session/results', headers, payload })).json().data.stats.bot.losses, 1); assert.equal((await app.inject({ method: 'POST', url: '/api/game-session/results', headers, payload })).json().data.status, 'duplicate'); assert.equal((await app.inject({ method: 'POST', url: '/api/game-session/results', payload })).statusCode, 401); const state = (await app.inject({ url: '/api/game-session', headers })).json().data; assert.equal(state.activeMatch, undefined); assert.equal(state.stats.bot.losses, 1); } finally { socket?.disconnect(); await app.close(); try { await client.db(databaseName).dropDatabase(); } finally { await client.close(); } } }); test('production server enforces session limits before database creation', { skip: !uri }, async () => { const databaseName = 'dragon_ball_test_limits_' + randomUUID().replaceAll('-', ''); const app = await createGameServer({ mongoUri: uri!, databaseName, clientDirectory: 'dist/client', environment: 'test' }); const client = await new MongoClient(uri!).connect(); try { for (let i = 0; i < 30; i++) { assert.equal((await app.inject({ method: 'POST', url: '/api/game-sessions', payload: {} })).statusCode, 400); } const response = await app.inject({ method: 'POST', url: '/api/game-sessions', payload: { createRequestId: randomBytes(32).toString('base64url') } }); assert.equal(response.statusCode, 429); assert.ok(Number(response.headers['retry-after']) > 0); assert.equal(await client.db(databaseName).collection('players').countDocuments(), 0); assert.equal(await client.db(databaseName).collection('game_sessions').countDocuments(), 0); } finally { await app.close(); try { await client.db(databaseName).dropDatabase(); } finally { await client.close(); } } });