Files
ssessesse-ki-battle/tests/global-settings.test.ts
T

89 lines
7.0 KiB
TypeScript

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';
test('browser caches settings, checks revision and retains valid settings through failures', async () => {
const memory = new Map<string, string>(), 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: '' } });
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.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 });
assert.equal(changed.matchingDelayMs, 4000); assert.equal(changed.matchSuccessDelayMs, 2000);
assert.equal((await store.get()).matchPreparationDelayMs, 5000);
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(); }
});