import test from 'node:test'; import assert from 'node:assert/strict'; import { randomBytes, randomUUID } from 'node:crypto'; import { MongoClient } from 'mongodb'; import { DEFAULT_GLOBAL_SETTINGS, GLOBAL_SETTINGS_CACHE_KEY } from '../constants/global-settings.js'; import { createGlobalSettingsCache } from '../src/client/global-settings.js'; import { createGameServer } from '../src/server/create-server.js'; import { createGlobalSettingsStore } from '../src/server/global-settings.js'; import { createGameSessionStore } from '../src/server/game-session-store.js'; import { createRoomStore } from '../src/server/rooms.js'; import { createMatchEngine } from '../src/shared/match-engine.js'; import { isGlobalSettings } from '../src/shared/global-settings.js'; test('ranked acceptance timeout is a configurable positive duration', () => { assert.equal(DEFAULT_GLOBAL_SETTINGS.rankedAcceptTimeoutMs, 10000); assert.equal(isGlobalSettings({ ...DEFAULT_GLOBAL_SETTINGS, rankedAcceptTimeoutMs: 5000 }), true); for (const rankedAcceptTimeoutMs of [0, -1, 60001, 1500.5]) { assert.equal(isGlobalSettings({ ...DEFAULT_GLOBAL_SETTINGS, rankedAcceptTimeoutMs }), false); } }); test('browser caches settings, checks revision and retains valid settings through failures', async () => { const memory = new Map(), urls: string[] = []; let settings = { ...DEFAULT_GLOBAL_SETTINGS }, offline = false, writes = 0; const storage = { getItem: (key: string) => memory.get(key) ?? null, setItem: (key: string, value: string) => { writes++; memory.set(key, value); } }; const send = async (input: any) => { urls.push(String(input)); if (offline) throw new Error('offline'); const revision = new URL(String(input), 'http://test').searchParams.get('revision'); return Response.json({ ok: true, data: { revision: settings.revision, ...(Number(revision) === settings.revision ? {} : { settings }) } }); }; const cache = createGlobalSettingsCache(storage, send); assert.deepEqual(await cache.refresh(), settings); assert.equal(writes, 1); const reloaded = createGlobalSettingsCache(storage, send); assert.deepEqual(await reloaded.refresh(), settings); assert.equal(urls.at(-1), '/api/global-settings?revision=1'); assert.equal(writes, 1); settings = { ...DEFAULT_GLOBAL_SETTINGS, revision: 2, turnDurationMs: 2000, finishCutsceneSpeed: 2 }; assert.deepEqual(await reloaded.refresh(), settings); assert.equal(writes, 2); offline = true; assert.deepEqual(await reloaded.refresh(), settings); memory.set(GLOBAL_SETTINGS_CACHE_KEY, '{bad json'); assert.deepEqual(await createGlobalSettingsCache(storage, send).refresh(), DEFAULT_GLOBAL_SETTINGS); }); test('match settings survive reload and early resolution still rejects missing choices', () => { let now = 0; const settings = { ...DEFAULT_GLOBAL_SETTINGS, revision: 2, turnDurationMs: 5000, finishCutsceneSpeed: 2 }; const config = { matchId: 'settings', mode: 'friend' as const, selfIndex: 0 as const, clock: { now: () => now } }; const engine = createMatchEngine({ ...config, settings }); engine.start(); now = 3000; engine.beginTurn(); assert.equal(engine.view().deadline, 8000); assert.throws(() => engine.resolve(['charge', null]), /BEFORE_DEADLINE/); engine.resolve(['charge', 'charge']); assert.equal(engine.view().phase, 'revealing'); const restored = createMatchEngine(config); restored.restore(engine.checkpoint()); now = 4200; restored.beginTurn(); assert.equal(restored.view().deadline, 9200); assert.deepEqual(restored.view().settings, settings); }); test('DB settings update without restart and friend matches share an immutable snapshot', { skip: !process.env.TEST_MONGODB_URI }, async () => { const databaseName = 'dragon_ball_test_settings_' + randomUUID().replaceAll('-', ''); const client = await new MongoClient(process.env.TEST_MONGODB_URI!).connect(), db = client.db(databaseName); const app = await createGameServer({ mongoUri: process.env.TEST_MONGODB_URI!, databaseName, clientDirectory: 'dist/client', adminUser: 'test', adminPassword: 'test-password', environment: 'test' }); const headers = { authorization: 'Basic ' + Buffer.from('test:test-password').toString('base64') }; try { const store = createGlobalSettingsStore(db); assert.deepEqual(await store.get(), DEFAULT_GLOBAL_SETTINGS); assert.equal((await app.inject({ method: 'POST', url: '/api/admin/global-settings', payload: DEFAULT_GLOBAL_SETTINGS })).statusCode, 401); const updated = { ...DEFAULT_GLOBAL_SETTINGS, revision: 1, turnDurationMs: 5000, finishCutsceneSpeed: 2 }; const save = await app.inject({ method: 'POST', url: '/api/admin/global-settings', headers, payload: updated }); assert.equal(save.statusCode, 200); assert.equal(save.json().data.revision, 2); assert.equal((await app.inject({ method: 'POST', url: '/api/admin/global-settings', headers, payload: updated })).statusCode, 409); assert.equal((await app.inject({ method: 'POST', url: '/api/admin/global-settings', headers, payload: { ...updated, revision: 2, finishCutsceneSpeed: 0 } })).statusCode, 400); assert.equal((await app.inject('/api/global-settings?revision=1')).json().data.settings.turnDurationMs, 5000); assert.deepEqual((await app.inject('/api/global-settings?revision=2')).json().data, { revision: 2 }); const sessions = await createGameSessionStore(db), rooms = await createRoomStore(db); const a = await sessions.create(randomBytes(32).toString('base64url')), b = await sessions.create(randomBytes(32).toString('base64url')); const room = await rooms.create(a.gameSessionId, 'create'); await rooms.join(b.gameSessionId, room.code); const input = { requestId: 'register', matchId: room.currentMatchId, mode: 'friend' as const, roomId: room.id, expectedPreviousSeq: 0 }; const first = await sessions.register(a, input); await store.update({ ...DEFAULT_GLOBAL_SETTINGS, revision: 2, turnDurationMs: 2000, finishCutsceneSpeed: 3 }); const second = await sessions.register(b, input); assert.deepEqual(first.settings, second.settings); assert.equal(second.settings?.revision, 2); const c = await sessions.create(randomBytes(32).toString('base64url')); const bot = await sessions.register(c, { requestId: 'bot', matchId: 'bot', mode: 'bot', expectedPreviousSeq: 0 }); assert.equal(bot.settings?.revision, 3); assert.equal(bot.settings?.finishCutsceneSpeed, 3); await db.collection('global_settings').updateOne({ _id: 'global' as any }, { $unset: { matchingDelayMs: '', matchSuccessDelayMs: '', matchPreparationDelayMs: '', rankedAcceptTimeoutMs: '' } }); await store.initialize(); const migrated = await store.get(); assert.equal(migrated.matchingDelayMs, 1000); assert.equal(migrated.matchSuccessDelayMs, 1000); assert.equal(migrated.matchPreparationDelayMs, 3000); assert.equal(migrated.rankedAcceptTimeoutMs, 10000); assert.equal(migrated.finishCutsceneSpeed, 3); assert.equal(migrated.turnDurationMs, 2000); assert.ok(migrated.revision > 3); await store.initialize(); assert.deepEqual(await store.get(), migrated); const changed = await store.update({ ...migrated, matchingDelayMs: 4000, matchSuccessDelayMs: 2000, matchPreparationDelayMs: 5000, rankedAcceptTimeoutMs: 7000 }); assert.equal(changed.matchingDelayMs, 4000); assert.equal(changed.matchSuccessDelayMs, 2000); assert.equal((await store.get()).matchPreparationDelayMs, 5000); assert.equal((await store.get()).rankedAcceptTimeoutMs, 7000); await assert.rejects(store.update({ ...changed, matchPreparationDelayMs: -1 }), /INVALID_SETTINGS/); await assert.rejects(store.update({ ...changed, matchingDelayMs: -1 }), /INVALID_SETTINGS/); } finally { await app.close(); await db.dropDatabase(); await client.close(); } });