Files
ridebus-status/test/board-scenario.test.mjs
2026-10-13 02:24:39 +03:00

100 lines
5.5 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { loadConfig } from '../server/config.mjs';
import { createScenarioRunner } from '../server/scenarios.mjs';
const cookie = 'ridebus_board_device=test-device-secret';
const publicSettings = { cityId: 1, defaultStopId: 1, configured: true };
const managedSettings = { cityId: 2, defaultStopId: 28, configured: true };
function fixture({ device = { enabled: true, state: 'paired', config: managedSettings }, deviceStatus = 200, env = { STATUS_BOARD_TEST_COOKIE: cookie } } = {}) {
const config = loadConfig(env);
const service = config.services.find(item => item.id === 'board');
const requests = [], renders = [];
const runner = createScenarioRunner(config, {
fetchImpl: async (url, options) => {
assert.equal(options.method || 'GET', 'GET');
const { pathname, search } = new URL(url);
requests.push({ path: pathname + search, cookie: options.headers.Cookie });
if (pathname === '/api/config') return Response.json(publicSettings);
if (pathname === '/api/device/status') return Response.json(device, { status: deviceStatus });
if (pathname === '/api/stops') return Response.json([{ id: 1, name: 'Default stop' }, { id: 28, name: 'Assigned stop' }, { id: 99, name: 'Override stop' }]);
if (pathname === '/api/board') {
const stopId = Number(new URL(url).searchParams.get('stopId'));
if (device.enabled && env.STATUS_BOARD_TEST_COOKIE && stopId !== managedSettings.defaultStopId) return Response.json({ error: 'settings_changed' }, { status: 409 });
return Response.json({ mode: 'live', stop: { id: stopId }, departures: [], partial: false });
}
throw new Error('Unexpected request');
},
browserChecks: { run: async (_spec, target) => { renders.push({ ...target }); return { ok: true, latencyMs: 20, httpCode: 200 }; }, close: async () => {} },
});
return { runner, service, requests, renders };
}
test('paired board uses its assigned city and stop instead of public defaults or env override', async () => {
const item = fixture({ env: { STATUS_BOARD_TEST_COOKIE: cookie, STATUS_TEST_CITY_ID: '1', STATUS_BOARD_TEST_STOP_ID: '99' } });
try {
const result = await item.runner.run(item.service);
assert.equal(result.ok, true);
assert.deepEqual(item.renders, [{ cityId: 2, stopId: 28 }]);
assert.deepEqual(item.requests.map(value => value.path), ['/api/config', '/api/device/status', '/api/stops', '/api/board?stopId=28']);
assert.ok(item.requests.every(value => value.cookie === cookie));
assert.deepEqual(result.steps.map(value => value.id), ['config', 'device', 'stops', 'departures', 'render']);
assert.doesNotMatch(JSON.stringify(result), /test-device-secret|Assigned stop/);
} finally { await item.runner.close(); }
});
test('standalone board keeps its explicit test stop and needs no device endpoint', async () => {
const item = fixture({ env: { STATUS_BOARD_TEST_STOP_ID: '99' } });
try {
assert.equal((await item.runner.run(item.service)).ok, true);
assert.deepEqual(item.renders, [{ cityId: 1, stopId: 99 }]);
assert.ok(item.requests.every(value => value.path !== '/api/device/status' && value.cookie === undefined));
} finally { await item.runner.close(); }
});
test('cookie on a board with remote settings disabled retains standalone configuration', async () => {
const item = fixture({ device: { enabled: false } });
try {
assert.equal((await item.runner.run(item.service)).ok, true);
assert.deepEqual(item.renders, [{ cityId: 1, stopId: 1 }]);
} finally { await item.runner.close(); }
});
test('unpaired, pending and expired test sessions are missing access rather than outages', async () => {
for (const state of ['unpaired', 'pending', 'expired']) {
const item = fixture({ device: { enabled: true, state } });
try {
const result = await item.runner.run(item.service);
assert.equal(result.ok, null);
assert.equal(result.reason, 'auth_required');
assert.equal(result.steps.at(-1).id, 'device');
assert.equal(result.steps.at(-1).status, 'failed');
assert.equal(item.renders.length, 0);
assert.equal(item.requests.length, 2);
} finally { await item.runner.close(); }
}
});
test('revoked device access is unknown while malformed paired configuration remains a failure', async () => {
for (const options of [{ device: { error: 'device_revoked' }, deviceStatus: 410 }, { device: { enabled: true, state: 'paired', config: { ...managedSettings, defaultStopId: null } } }]) {
const item = fixture(options);
try {
const result = await item.runner.run(item.service);
assert.equal(result.ok, options.deviceStatus === 410 ? null : false);
assert.equal(result.reason, options.deviceStatus === 410 ? 'auth_required' : 'content');
assert.equal(item.renders.length, 0);
} finally { await item.runner.close(); }
}
});
test('only board scenario history is separated from the old public-default probe', () => {
for (const service of loadConfig({ STATUS_BOARD_TEST_COOKIE: cookie }).services.filter(item => item.scenario)) {
const { signature: _signature, ...scenario } = service.scenario;
const previousSignature = createHash('sha256').update(JSON.stringify({ ...scenario, apiKey: Boolean(scenario.apiKey), cookie: Boolean(scenario.cookie) })).digest('hex').slice(0, 24);
if (service.id === 'board') assert.notEqual(service.scenario.signature, previousSignature);
else assert.equal(service.scenario.signature, previousSignature);
}
});