Add persistence and SQLite integration tests
This commit is contained in:
156
tests/server_persistence.integration.test.js
Normal file
156
tests/server_persistence.integration.test.js
Normal file
@@ -0,0 +1,156 @@
|
||||
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";
|
||||
|
||||
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: "",
|
||||
},
|
||||
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 emptyResponse = await fetch(`${baseUrl}/api/state`);
|
||||
assert.deepEqual(await emptyResponse.json(), { state: null, updatedAt: null });
|
||||
|
||||
const initialState = {
|
||||
events: [{ id: "event_1", name: "Club race" }],
|
||||
sessions: [{ id: "session_1", eventId: "event_1", name: "A final" }],
|
||||
resultsBySession: {},
|
||||
};
|
||||
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 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 });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user