import test from "node:test"; import assert from "node:assert/strict"; import { spawn } from "node:child_process"; import { mkdtempSync, rmSync } from "node:fs"; import { createServer } from "node:net"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import QRCode from "qrcode"; const projectRoot = dirname(dirname(fileURLToPath(import.meta.url))); function reservePort() { return new Promise((resolve, reject) => { const server = createServer(); server.once("error", reject); server.listen(0, "127.0.0.1", () => { const address = server.address(); const port = typeof address === "object" && address ? address.port : 0; server.close((error) => error ? reject(error) : resolve(port)); }); }); } function waitForServer(child, timeoutMs = 10000) { return new Promise((resolve, reject) => { let output = ""; const timeout = setTimeout(() => { reject(new Error(`Server startup timed out. Output:\n${output}`)); }, timeoutMs); const onData = (chunk) => { output += chunk.toString(); if (output.includes("RC Timing server listening")) { clearTimeout(timeout); child.stdout.off("data", onData); resolve(); } }; child.stdout.on("data", onData); child.stderr.on("data", (chunk) => { output += chunk.toString(); }); child.once("exit", (code, signal) => { clearTimeout(timeout); reject(new Error(`Server exited before startup (code=${code}, signal=${signal}). Output:\n${output}`)); }); }); } function stopServer(child) { if (child.exitCode !== null || child.signalCode !== null) { return Promise.resolve(); } return new Promise((resolve) => { const timeout = setTimeout(() => { child.kill("SIGKILL"); }, 3000); child.once("exit", () => { clearTimeout(timeout); resolve(); }); child.kill("SIGTERM"); }); } test("SQLite API persists state, passings, and overlay session results", { timeout: 20000 }, async () => { const dataDir = mkdtempSync(join(tmpdir(), "live-rc-test-")); const port = await reservePort(); const baseUrl = `http://127.0.0.1:${port}`; const child = spawn(process.execPath, [join(projectRoot, "server.js")], { cwd: projectRoot, env: { ...process.env, HOST: "127.0.0.1", PORT: String(port), RC_TIMING_DATA_DIR: dataDir, RC_TIMING_PID_FILE: "", RC_ALLOWED_ORIGINS: "https://trusted.example", }, stdio: ["ignore", "pipe", "pipe"], }); try { await waitForServer(child); const healthResponse = await fetch(`${baseUrl}/api/health`); const health = await healthResponse.json(); assert.equal(healthResponse.status, 200); assert.equal(health.ok, true); assert.equal(health.dbPath, join(dataDir, "rc_timing.sqlite")); const sameOriginResponse = await fetch(`${baseUrl}/api/health`, { headers: { Origin: baseUrl } }); assert.equal(sameOriginResponse.status, 200); assert.equal(sameOriginResponse.headers.get("access-control-allow-origin"), baseUrl); const allowedOriginResponse = await fetch(`${baseUrl}/api/health`, { headers: { Origin: "https://trusted.example" } }); assert.equal(allowedOriginResponse.status, 200); assert.equal(allowedOriginResponse.headers.get("access-control-allow-origin"), "https://trusted.example"); const rejectedOriginResponse = await fetch(`${baseUrl}/api/ammc/start`, { method: "POST", headers: { Origin: "https://untrusted.example" }, }); assert.equal(rejectedOriginResponse.status, 403); const soundManifestResponse = await fetch(`${baseUrl}/race-sounds/sound_manifest.json`); const soundManifest = await soundManifestResponse.json(); assert.equal(soundManifestResponse.status, 200); assert.equal(soundManifest.events.start_tone.effect, "effects/start_beep.wav"); assert.equal(soundManifest.voice_events.sv.track_call, "Stopp på banan. Stopp på banan."); const ammcConfigResponse = await fetch(`${baseUrl}/api/ammc/config`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ managedEnabled: true, decoderHost: "192.168.1.10", wsPort: 9000, splitEnabled: true, splitDecoderHost: "192.168.1.11", splitWsPort: 9001, pitEnabled: true, pitEntryDecoderHost: "192.168.1.12", pitEntryWsPort: 9002, pitExitDecoderHost: "192.168.1.13", pitExitWsPort: 9003, }), }); const ammcConfigPayload = await ammcConfigResponse.json(); assert.equal(ammcConfigResponse.status, 200); assert.equal(ammcConfigPayload.config.splitWsPort, 9001); assert.equal(ammcConfigPayload.config.pitExitDecoderHost, "192.168.1.13"); assert.equal(ammcConfigPayload.status.role, "main"); assert.equal(ammcConfigPayload.status.split.role, "split"); assert.equal(ammcConfigPayload.status.pitEntry.wsPort, 9002); const duplicatePortResponse = await fetch(`${baseUrl}/api/ammc/config`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ splitEnabled: true, splitWsPort: 9000 }), }); const duplicatePortPayload = await duplicatePortResponse.json(); assert.equal(duplicatePortResponse.status, 400); assert.match(duplicatePortPayload.error, /unique WebSocket port/); const emptyResponse = await fetch(`${baseUrl}/api/state`); assert.deepEqual(await emptyResponse.json(), { state: null, updatedAt: null }); const initialState = { events: [{ id: "event_1", name: "Club race" }], classes: [{ id: "class_1", name: "Buggy" }], drivers: [{ id: "driver_1", name: "Driver One", classId: "class_1" }], sessions: [{ id: "session_1", eventId: "event_1", name: "A final" }], resultsBySession: {}, settings: { publicBaseUrl: "https://racecore.example.org" }, training: { shares: [{ driverId: "driver_1", token: "test-share-token-1234567890", enabled: true }], }, }; const saveResponse = await fetch(`${baseUrl}/api/state`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(initialState), }); const savePayload = await saveResponse.json(); assert.equal(saveResponse.status, 200); assert.equal(savePayload.ok, true); assert.equal(typeof savePayload.updatedAt, "string"); const shareToken = initialState.training.shares[0].token; const publicQrResponse = await fetch( `${baseUrl}/public-training/qr/${shareToken}?lang=en&baseUrl=${encodeURIComponent("https://public.example.test")}`, ); const publicQrSvg = await publicQrResponse.text(); const expectedPublicQrSvg = await QRCode.toString( `https://public.example.test/public-training/${shareToken}?lang=en`, { type: "svg", errorCorrectionLevel: "M", margin: 2, width: 320 }, ); assert.equal(publicQrResponse.status, 200); assert.match(publicQrResponse.headers.get("content-type"), /^image\/svg\+xml/); assert.equal(publicQrSvg, expectedPublicQrSvg); const localQrResponse = await fetch(`${baseUrl}/public-training/qr/${shareToken}?lang=sv&baseUrl=local`); const localQrSvg = await localQrResponse.text(); assert.equal(localQrResponse.status, 200); assert.match(localQrResponse.headers.get("content-type"), /^image\/svg\+xml/); assert.notEqual(localQrSvg, publicQrSvg); const missingQrResponse = await fetch(`${baseUrl}/public-training/qr/unknown-share-token-123456?baseUrl=local`); assert.equal(missingQrResponse.status, 404); const backupResponse = await fetch(`${baseUrl}/api/backups`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ reason: "pre_start", sessionId: "session_1", eventId: "event_1" }), }); const backupPayload = await backupResponse.json(); assert.equal(backupResponse.status, 201); assert.equal(backupPayload.ok, true); assert.equal(backupPayload.backup.reason, "pre_start"); assert.equal(backupPayload.backup.sessionId, "session_1"); assert.ok(backupPayload.backup.sizeBytes > 0); const backupsResponse = await fetch(`${baseUrl}/api/backups`); const backupsPayload = await backupsResponse.json(); assert.equal(backupsResponse.status, 200); assert.equal(backupsPayload.backups.length, 1); assert.equal(backupsPayload.backups[0].fileName, backupPayload.backup.fileName); const storedResponse = await fetch(`${baseUrl}/api/state`); const storedPayload = await storedResponse.json(); assert.deepEqual(storedPayload.state, initialState); const sessionResult = { competitors: { driver_1: { key: "driver_1", driverName: "Driver One", laps: 1, bestLapMs: 16500 }, }, passings: [{ competitorKey: "driver_1", timestamp: 17500, lapMs: 16500, validLap: true }], adjustments: [], }; const passing = { timestamp: 17500, transponder: "1234567", driverId: "driver_1", driverName: "Driver One", carId: "car_1", carName: "Buggy", strength: -52.5, loopId: "loop_1", resend: false, }; const passingResponse = await fetch(`${baseUrl}/api/passings`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionId: "session_1", passing, sessionResult }), }); assert.deepEqual(await passingResponse.json(), { ok: true }); const hotStateResponse = await fetch(`${baseUrl}/api/state`); const hotState = await hotStateResponse.json(); assert.deepEqual(hotState.state.resultsBySession.session_1, sessionResult); assert.deepEqual(hotState.state.events, initialState.events); const rowsResponse = await fetch(`${baseUrl}/api/passings?sessionId=session_1&limit=10`); const rowsPayload = await rowsResponse.json(); assert.equal(rowsPayload.rows.length, 1); assert.equal(rowsPayload.rows[0].session_id, "session_1"); assert.equal(rowsPayload.rows[0].timestamp_ms, 17500); assert.equal(rowsPayload.rows[0].transponder, "1234567"); assert.equal(rowsPayload.rows[0].driver_id, "driver_1"); assert.equal(rowsPayload.rows[0].car_id, "car_1"); assert.equal(rowsPayload.rows[0].strength, -52.5); assert.deepEqual(JSON.parse(rowsPayload.rows[0].raw_json), passing); } finally { await stopServer(child); rmSync(dataDir, { recursive: true, force: true }); } });