Files
ridebus-board/test/api.test.mjs
2026-10-08 22:56:41 +03:00

45 lines
3.2 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { createApp } from '../server/index.mjs';
import { ApiError } from '../server/board.mjs';
test('board HTTP API validates inputs, rejects writes, and hides upstream errors', async t => {
const client = { config: () => ({ configured: true, cityId: 1 }), stops: async () => [{ id: 1, name: 'Швейная фабрика' }],
timetable: async id => ({ stop: { id }, routes: [] }), coordinates: async () => ({ latitude: 55, longitude: 28 }),
board: async id => { if (id === 2) throw new Error('secret-api-key and upstream response'); if (id === 3) throw new ApiError(404, 'stop_not_found'); return { stop: { id }, departures: [] }; } };
const weatherClient = { weather: async (cityId, stopId) => ({ cityId, stopId, temperature: 12 }) };
const server = createApp(client, weatherClient).listen(0, '127.0.0.1');
await new Promise((resolve, reject) => { server.once('listening', resolve); server.once('error', reject); });
t.after(() => new Promise(resolve => server.close(resolve)));
const base = `http://127.0.0.1:${server.address().port}`;
await t.test('successful GET returns no-store and server time', async () => {
const response = await fetch(base + '/api/board?stopId=1');
assert.equal(response.status, 200); assert.equal(response.headers.get('cache-control'), 'no-store');
const body = await response.json(); assert.equal(body.stop.id, 1); assert.ok(Number.isFinite(Date.parse(body.serverTime)));
});
await t.test('invalid and missing IDs return 400', async () => {
for (const endpoint of ['board', 'timetable', 'weather']) for (const query of ['', '?stopId=0', '?stopId=-1', '?stopId=abc', '?stopId=1&stopId=2']) assert.equal((await fetch(base + '/api/' + endpoint + query)).status, 400);
});
await t.test('full timetable and weather use only the selected platform and no-store', async () => {
const timetable = await fetch(base + '/api/timetable?stopId=2');
assert.equal(timetable.headers.get('cache-control'), 'no-store'); assert.equal((await timetable.json()).stop.id, 2);
assert.deepEqual(await (await fetch(base + '/api/weather?stopId=2')).json(), { cityId: 1, stopId: 2, temperature: 12 });
});
await t.test('writes are never forwarded', async () => {
const response = await fetch(base + '/api/board?stopId=1', { method: 'POST' });
assert.equal(response.status, 405); assert.equal(response.headers.get('allow'), 'GET');
});
await t.test('raw upstream errors and credentials never reach the browser', async () => {
const response = await fetch(base + '/api/board?stopId=2');
assert.equal(response.status, 502); assert.deepEqual(await response.json(), { error: 'data_unavailable' });
});
await t.test('not-found and unknown endpoints keep their status', async () => {
assert.equal((await fetch(base + '/api/board?stopId=3')).status, 404);
assert.equal((await fetch(base + '/api/unrestricted-proxy')).status, 404);
});
await t.test('config contains only public settings', async () => {
assert.deepEqual(await (await fetch(base + '/api/config')).json(), { configured: true, cityId: 1 });
assert.equal((await fetch(base + '/healthz')).status, 200);
});
});