import { test } from 'node:test'; import assert from 'node:assert/strict'; import { randomUUID } from 'node:crypto'; import Fastify from 'fastify'; import { MongoClient } from 'mongodb'; import { createGameSessionStore } from '../src/server/game-session-store.js'; import { registerGameApi } from '../src/server/game-api.js'; import { createGameApi } from '../src/client/api.js'; import { createStatsStore } from '../src/server/stats-store.js'; test('room creation reuses its request ID after response loss and a page reload', async () => { const memory = new Map(), ids: string[] = []; const storage = { getItem: (k: string) => memory.get(k) ?? null, setItem: (k: string, v: string) => { memory.set(k, v); }, removeItem: (k: string) => { memory.delete(k); } }; const send: typeof fetch = async (input, init) => { const data = String(input) === '/api/rooms' ? (() => { ids.push(JSON.parse(String(init?.body)).requestId); if (ids.length === 1) throw new Error('response lost'); return { id: 'room', status: 'waiting' }; })() : { gameSessionId: 'session', token: 'token' }; return new Response(JSON.stringify({ ok: true, data })); }; const first = createGameApi(storage, send); await first.ensureSession(); await assert.rejects(first.createRoom(), /response lost/); const restored = createGameApi(storage, send); await restored.ensureSession(); await restored.createRoom(); await restored.createRoom(); assert.equal(ids[0], ids[1]); assert.notEqual(ids[1], ids[2]); }); test('failed room cleanup survives refresh and expired or deleted rooms count as departed', async () => { const memory = new Map(); let status = 503, leaves = 0; const storage = { getItem: (k: string) => memory.get(k) ?? null, setItem: (k: string, v: string) => { memory.set(k, v); }, removeItem: (k: string) => { memory.delete(k); } }; const send: typeof fetch = async input => { if (String(input).endsWith('/leave')) { leaves++; return new Response(JSON.stringify({ ok: false, code: 'ROOM_EXPIRED', message: 'gone' }), { status }); } return new Response(JSON.stringify({ ok: true, data: { gameSessionId: 's', token: 't'.repeat(43) } })); }; const first = createGameApi(storage, send); await first.ensureSession(); await assert.rejects(first.leaveRoom('r')); const second = createGameApi(storage, send); await second.ensureSession(); status = 410; assert.equal(await second.resumeRoomLeave(), true); assert.equal(await second.resumeRoomLeave(), false); status = 404; await second.leaveRoom('deleted'); assert.equal(leaves, 3); }); test('HTTP client restores its session, records once and surfaces conflicts', { skip: !process.env.TEST_MONGODB_URI }, async () => { const client = await new MongoClient(process.env.TEST_MONGODB_URI!, { serverSelectionTimeoutMS: 5000 }).connect(); const db = client.db(`dragon_ball_test_api_${randomUUID().replaceAll('-', '')}`); const app = Fastify(); try { registerGameApi(app, await createGameSessionStore(db)); const memory = new Map(); const storage = { getItem: (k: string) => memory.get(k) ?? null, setItem: (k: string, v: string) => { memory.set(k, v); }, removeItem: (k: string) => { memory.delete(k); } }; const localFetch: typeof fetch = async (input, init) => { const headers: Record = {}; new Headers(init?.headers).forEach((value, key) => { headers[key] = value; }); const response = await app.inject({ method: (init?.method ?? 'GET') as 'GET' | 'POST', url: String(input), headers, payload: init?.body as string | undefined }); return new Response(response.body, { status: response.statusCode, headers: { 'content-type': 'application/json' } }); }; const api = createGameApi(storage, localFetch); const session = await api.ensureSession(); const restored = createGameApi(storage, localFetch); await restored.ensureSession(); assert.equal(restored.session.gameSessionId, session.gameSessionId); const match = await api.registerMatch({ requestId: randomUUID(), matchId: randomUUID(), mode: 'bot', expectedPreviousSeq: 0 }); const win = { ...match, outcome: 'win' as const, reason: 'hit' as const }; await restored.sync(); assert.equal(restored.session.activeMatch?.matchId, match.matchId); assert.equal((await api.record(win)).stats.bot.wins, 1); assert.equal((await restored.sync()).activeMatch, undefined); assert.equal((await restored.record(win)).status, 'duplicate'); await assert.rejects(api.record({ ...win, outcome: 'loss' }), (e: any) => e.status === 409 && e.code === 'RESULT_CONFLICT'); assert.equal((await app.inject({ method: 'GET', url: '/api/game-session' })).statusCode, 401); assert.equal((await api.sync()).lastAppliedSeq, 1); } finally { await app.close(); try { await db.dropDatabase(); } finally { await client.close(); } } }); test('session creation retry preserves linked visitor credentials and rejects invalid keys before creating visitors', { skip: !process.env.TEST_MONGODB_URI }, async () => { const visitors = await createStatsStore({ mongoUri: process.env.TEST_MONGODB_URI!, databaseName: `dragon_ball_test_api_${randomUUID().replaceAll('-', '')}` }); const app = Fastify(); try { registerGameApi(app, await createGameSessionStore(visitors.db), visitors); const createRequestId = 'a'.repeat(43); const first = (await app.inject({ method: 'POST', url: '/api/game-sessions', payload: { createRequestId } })).json().data; const retry = (await app.inject({ method: 'POST', url: '/api/game-sessions', payload: { createRequestId } })).json().data; assert.equal(retry.gameSessionId, first.gameSessionId); assert.equal(retry.guestToken, first.guestToken); assert.equal((await visitors.findGuest(retry.guestToken))?.playerId, retry.playerId); assert.equal((await app.inject({ method: 'POST', url: '/api/game-sessions', payload: { createRequestId: 'bad' } })).statusCode, 400); assert.equal(await visitors.db.collection('players').countDocuments(), 1); } finally { await app.close(); try { await visitors.db.dropDatabase(); } finally { await visitors.close(); } } });