81 lines
6.0 KiB
TypeScript
81 lines
6.0 KiB
TypeScript
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<string, string>(), 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<string, string>(); 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<string, 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 localFetch: typeof fetch = async (input, init) => {
|
|
const headers: Record<string, string> = {}; 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(); } }
|
|
});
|