Add persistence and SQLite integration tests
This commit is contained in:
@@ -212,12 +212,14 @@ For internet publishing, proxy only these paths:
|
|||||||
|
|
||||||
Do not expose `/api/state` or the full admin application. Static serving is restricted to `/src` and `/docs`; `data/`, server scripts and bundled binaries are not public static files.
|
Do not expose `/api/state` or the full admin application. Static serving is restricted to `/src` and `/docs`; `data/`, server scripts and bundled binaries are not public static files.
|
||||||
|
|
||||||
Training logic regression tests use Node's built-in test runner and require no extra dependency:
|
Regression tests use Node's built-in test runner and require no extra test-framework dependency. The suite covers training profiles, race generation and timing, judging corrections, frontend/backend state synchronization, and an isolated SQLite API round-trip using a temporary data directory:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm test
|
npm test
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The integration test starts its own backend on a temporary localhost port and never reads or writes the normal `data/rc_timing.sqlite` database.
|
||||||
|
|
||||||
|
|
||||||
## Create Race Wizard
|
## Create Race Wizard
|
||||||
- `Race Setup` now starts with a four-step wizard for new races:
|
- `Race Setup` now starts with a four-step wizard for new races:
|
||||||
|
|||||||
@@ -201,12 +201,14 @@ Vid extern publicering ska bara dessa paths proxas:
|
|||||||
|
|
||||||
Exponera inte `/api/state` eller hela admin-appen. Statisk servering är begränsad till `/src` och `/docs`; `data/`, serverscripts och bundlade binärer publiceras inte som statiska filer.
|
Exponera inte `/api/state` eller hela admin-appen. Statisk servering är begränsad till `/src` och `/docs`; `data/`, serverscripts och bundlade binärer publiceras inte som statiska filer.
|
||||||
|
|
||||||
Regressionstesterna för träningslogiken använder Node:s inbyggda testverktyg och kräver ingen extra dependency:
|
Regressionstesterna använder Node:s inbyggda testverktyg och kräver inget extra testramverk. Sviten täcker träningsprofiler, racegenerering och tidtagning, domarkorrigeringar, frontend/backend-synk samt ett isolerat SQLite API-test med en temporär datamapp:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm test
|
npm test
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Integrationstestet startar en egen backend på en temporär localhost-port och läser eller skriver aldrig den vanliga databasen `data/rc_timing.sqlite`.
|
||||||
|
|
||||||
|
|
||||||
## Create Race Wizard
|
## Create Race Wizard
|
||||||
- `Race Setup` startar nu med en fyrstegs wizard för nya race:
|
- `Race Setup` startar nu med en fyrstegs wizard för nya race:
|
||||||
|
|||||||
@@ -2090,6 +2090,21 @@ npm test
|
|||||||
- safe handling when no counted lap exists
|
- safe handling when no counted lap exists
|
||||||
- competitor and judging-log filters
|
- competitor and judging-log filters
|
||||||
|
|
||||||
|
`tests/runtime_persistence.test.js` covers:
|
||||||
|
- persisted entity/settings normalization during backend hydration
|
||||||
|
- invalid-theme fallback and retention of local setting defaults
|
||||||
|
- hydration success/error behavior and no-write-back handling
|
||||||
|
- full-state POST payloads and synced-version advancement
|
||||||
|
- dirty-state protection when newer local edits occur during a request
|
||||||
|
- failed-sync behavior without false version advancement
|
||||||
|
|
||||||
|
`tests/server_persistence.integration.test.js` starts an isolated backend with a temporary SQLite data directory and covers:
|
||||||
|
- health/database path reporting
|
||||||
|
- empty-state response and full app-state round-trip
|
||||||
|
- passing persistence and session-scoped passing queries
|
||||||
|
- atomic hot-update of `resultsBySession[sessionId]` for overlay hydration
|
||||||
|
- preservation of the rest of the stored state during a passing hot-update
|
||||||
|
|
||||||
The suite uses Node's built-in `node:test` runner and does not add a test-framework dependency. Windows PowerShell/CMD can run the same suite with `npm.cmd test`.
|
The suite uses Node's built-in `node:test` runner and does not add a test-framework dependency. Windows PowerShell/CMD can run the same suite with `npm.cmd test`.
|
||||||
|
|
||||||
## 19. Recommended Future Additions
|
## 19. Recommended Future Additions
|
||||||
@@ -2099,4 +2114,4 @@ If the project grows further, the next useful additions to this document are:
|
|||||||
- exact `settings` schema snapshot
|
- exact `settings` schema snapshot
|
||||||
- SQLite table contract section
|
- SQLite table contract section
|
||||||
- controller-to-view ID map for critical forms/buttons
|
- controller-to-view ID map for critical forms/buttons
|
||||||
- additional test checklists for persistence modules
|
- browser-level smoke tests for critical create/edit workflows
|
||||||
|
|||||||
@@ -2,3 +2,5 @@ import "./training_logic.test.js";
|
|||||||
import "./public_training_profile.test.js";
|
import "./public_training_profile.test.js";
|
||||||
import "./race_timing_logic.test.js";
|
import "./race_timing_logic.test.js";
|
||||||
import "./judging_logic.test.js";
|
import "./judging_logic.test.js";
|
||||||
|
import "./runtime_persistence.test.js";
|
||||||
|
import "./server_persistence.integration.test.js";
|
||||||
|
|||||||
269
tests/runtime_persistence.test.js
Normal file
269
tests/runtime_persistence.test.js
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
import test from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
|
import {
|
||||||
|
applyPersistedStateHelper,
|
||||||
|
hydrateFromBackendHelper,
|
||||||
|
syncStateToBackendHelper,
|
||||||
|
} from "../src/runtime_services.js";
|
||||||
|
|
||||||
|
function withMockFetch(mock, callback) {
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
globalThis.fetch = mock;
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(callback)
|
||||||
|
.finally(() => {
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createState() {
|
||||||
|
return {
|
||||||
|
classes: [],
|
||||||
|
drivers: [],
|
||||||
|
cars: [],
|
||||||
|
events: [],
|
||||||
|
sessions: [],
|
||||||
|
resultsBySession: {},
|
||||||
|
training: {},
|
||||||
|
activeSessionId: null,
|
||||||
|
settings: {
|
||||||
|
wsUrl: "ws://old:9000",
|
||||||
|
backendUrl: "http://old:8081",
|
||||||
|
language: "sv",
|
||||||
|
theme: "dark",
|
||||||
|
passingSoundMode: "beep",
|
||||||
|
clubName: "Old club",
|
||||||
|
clubTagline: "Old tagline",
|
||||||
|
pdfFooter: "Old footer",
|
||||||
|
pdfTheme: "classic",
|
||||||
|
logoDataUrl: "",
|
||||||
|
racePresets: [],
|
||||||
|
obsOverlay: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test("backend hydration normalizes persisted entities and settings", () => {
|
||||||
|
const state = createState();
|
||||||
|
let themeApplied = 0;
|
||||||
|
const normalizeNamed = (value) => ({ ...value, name: String(value?.name || "").trim() });
|
||||||
|
|
||||||
|
applyPersistedStateHelper(
|
||||||
|
{
|
||||||
|
classes: [{ id: "class_1", name: "Modified" }],
|
||||||
|
drivers: [{ id: "driver_1", name: " Driver One " }, { id: "driver_empty", name: " " }],
|
||||||
|
cars: [{ id: "car_1", name: " Car One " }],
|
||||||
|
events: [{ id: "event_1", name: "Race" }],
|
||||||
|
sessions: [{ id: "session_1", name: "Final" }],
|
||||||
|
resultsBySession: { session_1: { passings: [] } },
|
||||||
|
training: { goals: [{ id: "goal_1" }] },
|
||||||
|
activeSessionId: "session_1",
|
||||||
|
settings: {
|
||||||
|
wsUrl: "ws://decoder:9000",
|
||||||
|
backendUrl: "http://race:8081",
|
||||||
|
language: "en",
|
||||||
|
theme: "light",
|
||||||
|
autoReconnect: false,
|
||||||
|
audioEnabled: false,
|
||||||
|
speakerPassingCueEnabled: true,
|
||||||
|
racePresets: [{ id: "preset_1", name: " Club " }, { id: "preset_empty", name: " " }],
|
||||||
|
obsOverlay: { rows: 8 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state,
|
||||||
|
getDefaultBackendUrl: () => "http://default:8081",
|
||||||
|
DEFAULT_LANGUAGE: "sv",
|
||||||
|
DEFAULT_THEME: "dark",
|
||||||
|
AVAILABLE_THEMES: ["dark", "nord", "light"],
|
||||||
|
normalizeDriver: normalizeNamed,
|
||||||
|
normalizeCar: normalizeNamed,
|
||||||
|
normalizeEvent: (event) => ({ ...event, normalized: true }),
|
||||||
|
normalizeSession: (session) => ({ ...session, normalized: true }),
|
||||||
|
normalizeStoredRacePreset: normalizeNamed,
|
||||||
|
normalizeObsOverlaySettings: (settings) => ({ rows: Number(settings?.rows || 10) }),
|
||||||
|
normalizeTrainingState: (training) => ({ ...training, normalized: true }),
|
||||||
|
applyTheme: () => { themeApplied += 1; },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(state.classes, [{ id: "class_1", name: "Modified" }]);
|
||||||
|
assert.deepEqual(state.drivers.map((driver) => driver.name), ["Driver One"]);
|
||||||
|
assert.deepEqual(state.cars.map((car) => car.name), ["Car One"]);
|
||||||
|
assert.equal(state.events[0].normalized, true);
|
||||||
|
assert.equal(state.sessions[0].normalized, true);
|
||||||
|
assert.equal(state.training.normalized, true);
|
||||||
|
assert.equal(state.activeSessionId, "session_1");
|
||||||
|
assert.equal(state.settings.theme, "light");
|
||||||
|
assert.equal(state.settings.autoReconnect, false);
|
||||||
|
assert.equal(state.settings.audioEnabled, false);
|
||||||
|
assert.equal(state.settings.speakerPassingCueEnabled, true);
|
||||||
|
assert.deepEqual(state.settings.racePresets.map((preset) => preset.name), ["Club"]);
|
||||||
|
assert.deepEqual(state.settings.obsOverlay, { rows: 8 });
|
||||||
|
assert.equal(themeApplied, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("invalid persisted theme falls back without discarding local setting defaults", () => {
|
||||||
|
const state = createState();
|
||||||
|
state.settings.theme = "nord";
|
||||||
|
state.settings.clubName = "Local club";
|
||||||
|
|
||||||
|
applyPersistedStateHelper(
|
||||||
|
{ settings: { theme: "unknown" } },
|
||||||
|
{
|
||||||
|
state,
|
||||||
|
getDefaultBackendUrl: () => "http://default:8081",
|
||||||
|
DEFAULT_LANGUAGE: "sv",
|
||||||
|
DEFAULT_THEME: "dark",
|
||||||
|
AVAILABLE_THEMES: ["dark", "nord", "light"],
|
||||||
|
normalizeDriver: (value) => value,
|
||||||
|
normalizeCar: (value) => value,
|
||||||
|
normalizeEvent: (value) => value,
|
||||||
|
normalizeSession: (value) => value,
|
||||||
|
normalizeStoredRacePreset: (value) => value,
|
||||||
|
normalizeObsOverlaySettings: (value) => value || {},
|
||||||
|
normalizeTrainingState: (value) => value || {},
|
||||||
|
applyTheme: () => {},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(state.settings.theme, "dark");
|
||||||
|
assert.equal(state.settings.clubName, "Local club");
|
||||||
|
assert.equal(state.settings.language, "sv");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("successful backend hydration applies state without scheduling a write-back", async () => {
|
||||||
|
const backend = { available: false, lastError: "old", lastSyncAt: null };
|
||||||
|
const applied = [];
|
||||||
|
const saves = [];
|
||||||
|
|
||||||
|
await withMockFetch(
|
||||||
|
async (url, options) => {
|
||||||
|
assert.equal(url, "http://race:8081/api/state");
|
||||||
|
assert.equal(options, undefined);
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({ state: { activeSessionId: "session_1" }, updatedAt: "2026-08-24T10:00:00.000Z" }),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
() => hydrateFromBackendHelper({
|
||||||
|
backend,
|
||||||
|
t: (key, vars) => `${key}:${vars.msg}`,
|
||||||
|
getBackendUrl: () => "http://race:8081",
|
||||||
|
applyPersistedState: (state) => applied.push(state),
|
||||||
|
saveState: (options) => saves.push(options),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(applied, [{ activeSessionId: "session_1" }]);
|
||||||
|
assert.deepEqual(saves, [{ skipBackend: true }]);
|
||||||
|
assert.equal(backend.available, true);
|
||||||
|
assert.equal(backend.lastError, "");
|
||||||
|
assert.equal(backend.lastSyncAt, "2026-08-24T10:00:00.000Z");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("failed hydration preserves local state and reports backend offline", async () => {
|
||||||
|
const backend = { available: true, lastError: "" };
|
||||||
|
let applied = false;
|
||||||
|
let saved = false;
|
||||||
|
|
||||||
|
await withMockFetch(
|
||||||
|
async () => ({ ok: false, status: 503, json: async () => ({}) }),
|
||||||
|
() => hydrateFromBackendHelper({
|
||||||
|
backend,
|
||||||
|
t: (key, vars) => `${key}:${vars.msg}`,
|
||||||
|
getBackendUrl: () => "http://race:8081",
|
||||||
|
applyPersistedState: () => { applied = true; },
|
||||||
|
saveState: () => { saved = true; },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(applied, false);
|
||||||
|
assert.equal(saved, false);
|
||||||
|
assert.equal(backend.available, false);
|
||||||
|
assert.equal(backend.lastError, "error.backend_offline:HTTP 503");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("state sync posts one snapshot and clears dirty only when no newer edit exists", async () => {
|
||||||
|
const backend = { available: false, lastError: "" };
|
||||||
|
const payload = { events: [{ id: "event_1" }] };
|
||||||
|
let syncedVersion = 2;
|
||||||
|
let dirty = true;
|
||||||
|
let posted = null;
|
||||||
|
|
||||||
|
await withMockFetch(
|
||||||
|
async (url, options) => {
|
||||||
|
posted = { url, options };
|
||||||
|
return { ok: true };
|
||||||
|
},
|
||||||
|
() => syncStateToBackendHelper({
|
||||||
|
backend,
|
||||||
|
t: (key, vars) => `${key}:${vars.msg}`,
|
||||||
|
getBackendUrl: () => "http://race:8081",
|
||||||
|
buildPersistableState: () => payload,
|
||||||
|
getLocalStateVersion: () => 4,
|
||||||
|
getSyncedStateVersion: () => syncedVersion,
|
||||||
|
setSyncedStateVersion: (value) => { syncedVersion = value; },
|
||||||
|
setLocalStateDirty: (value) => { dirty = value; },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(posted.url, "http://race:8081/api/state");
|
||||||
|
assert.equal(posted.options.method, "POST");
|
||||||
|
assert.deepEqual(JSON.parse(posted.options.body), payload);
|
||||||
|
assert.equal(syncedVersion, 4);
|
||||||
|
assert.equal(dirty, false);
|
||||||
|
assert.equal(backend.available, true);
|
||||||
|
assert.equal(backend.lastError, "");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("completed state sync keeps dirty when an edit occurs during the request", async () => {
|
||||||
|
const backend = { available: false, lastError: "" };
|
||||||
|
let versionReadCount = 0;
|
||||||
|
let syncedVersion = 0;
|
||||||
|
let dirtyWrites = 0;
|
||||||
|
|
||||||
|
await withMockFetch(
|
||||||
|
async () => ({ ok: true }),
|
||||||
|
() => syncStateToBackendHelper({
|
||||||
|
backend,
|
||||||
|
t: (key, vars) => `${key}:${vars.msg}`,
|
||||||
|
getBackendUrl: () => "http://race:8081",
|
||||||
|
buildPersistableState: () => ({ revision: 4 }),
|
||||||
|
getLocalStateVersion: () => (++versionReadCount === 1 ? 4 : 5),
|
||||||
|
getSyncedStateVersion: () => syncedVersion,
|
||||||
|
setSyncedStateVersion: (value) => { syncedVersion = value; },
|
||||||
|
setLocalStateDirty: () => { dirtyWrites += 1; },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(syncedVersion, 4);
|
||||||
|
assert.equal(dirtyWrites, 0);
|
||||||
|
assert.equal(backend.available, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("failed state sync leaves version and dirty state unchanged", async () => {
|
||||||
|
const backend = { available: true, lastError: "" };
|
||||||
|
let syncedVersion = 3;
|
||||||
|
let dirtyWrites = 0;
|
||||||
|
|
||||||
|
await withMockFetch(
|
||||||
|
async () => { throw new Error("connection refused"); },
|
||||||
|
() => syncStateToBackendHelper({
|
||||||
|
backend,
|
||||||
|
t: (key, vars) => `${key}:${vars.msg}`,
|
||||||
|
getBackendUrl: () => "http://race:8081",
|
||||||
|
buildPersistableState: () => ({ revision: 4 }),
|
||||||
|
getLocalStateVersion: () => 4,
|
||||||
|
getSyncedStateVersion: () => syncedVersion,
|
||||||
|
setSyncedStateVersion: (value) => { syncedVersion = value; },
|
||||||
|
setLocalStateDirty: () => { dirtyWrites += 1; },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(syncedVersion, 3);
|
||||||
|
assert.equal(dirtyWrites, 0);
|
||||||
|
assert.equal(backend.available, false);
|
||||||
|
assert.equal(backend.lastError, "error.sync_failed:connection refused");
|
||||||
|
});
|
||||||
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