mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
430 lines
13 KiB
JavaScript
430 lines
13 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
gnssIntegrityAnchor,
|
|
gnssIntegrityProxy,
|
|
gnssRetryCooldownMs,
|
|
} from '../../server/providers/gnssIntegrity.js';
|
|
|
|
const snapshot = {
|
|
now: 4_000_000_000_000,
|
|
ac: [
|
|
{
|
|
hex: '4ca7b5',
|
|
type: 'adsb_icao',
|
|
version: 2,
|
|
alt_baro: 36000,
|
|
lat: 50.1,
|
|
lon: 19.9,
|
|
nic: 0,
|
|
nac_p: 0,
|
|
seen_pos: 1,
|
|
flight: 'RYR1AB ',
|
|
r: 'EI-ABC',
|
|
},
|
|
{ hex: '000001', type: 'mlat', lat: 50, lon: 20, nic: 0, nac_p: 0 },
|
|
],
|
|
};
|
|
|
|
function install(options = {}, hook = 'configureServer') {
|
|
let handler;
|
|
const plugin = gnssIntegrityProxy({ monotonicNow: options.now, ...options });
|
|
assert.equal(plugin.name, 'gnss-integrity');
|
|
plugin[hook]({
|
|
middlewares: {
|
|
use(path, callback) {
|
|
assert.equal(path, '/api/gnss-integrity');
|
|
handler = callback;
|
|
},
|
|
},
|
|
});
|
|
return async (url = '/?lat=50&lon=20', method = 'GET', peer = 'local') => {
|
|
const res = {
|
|
writeHead(status, headers) {
|
|
this.status = status;
|
|
this.headers = headers;
|
|
},
|
|
end(body) {
|
|
this.body = JSON.parse(body);
|
|
},
|
|
};
|
|
await handler({ url, method, socket: { remoteAddress: peer } }, res);
|
|
return res;
|
|
};
|
|
}
|
|
|
|
test('anchors snap to whole degrees and reject missing or out-of-range values', () => {
|
|
assert.deepEqual(gnssIntegrityAnchor('/?lat=50.44&lon=19.51'), {
|
|
lat: 50,
|
|
lon: 20,
|
|
});
|
|
assert.equal(gnssIntegrityAnchor('/?lat=91&lon=0'), null);
|
|
assert.equal(gnssIntegrityAnchor('/?lat=0&lon=-181'), null);
|
|
assert.equal(gnssIntegrityAnchor('/?lat=0'), null);
|
|
assert.equal(gnssIntegrityAnchor('/?lat=abc&lon=1'), null);
|
|
});
|
|
|
|
for (const hook of ['configureServer', 'configurePreviewServer']) {
|
|
test(`${hook}: fetches the fixed adsb.lol origin and returns only integrity facts`, async () => {
|
|
const calls = [];
|
|
const request = install(
|
|
{
|
|
now: () => 1234,
|
|
fetchImpl: async (url, options) => {
|
|
calls.push(url);
|
|
assert.ok(options.signal instanceof AbortSignal);
|
|
assert.equal(options.redirect, 'error');
|
|
return Response.json(snapshot);
|
|
},
|
|
},
|
|
hook,
|
|
);
|
|
const res = await request(
|
|
'/?lat=50.2&lon=19.8&url=https://invalid.example',
|
|
);
|
|
assert.equal(res.status, 200);
|
|
assert.deepEqual(calls, ['https://api.adsb.lol/v2/lat/50/lon/20/dist/250']);
|
|
assert.deepEqual(res.body, {
|
|
fetchedAt: 1234,
|
|
ageMs: 0,
|
|
anchor: { lat: 50, lon: 20 },
|
|
radiusNm: 250,
|
|
classifier: 'gev-nic-nacp-v2',
|
|
rows: [
|
|
{
|
|
hex: '4ca7b5',
|
|
lat: 50.1,
|
|
lon: 19.9,
|
|
nic: 0,
|
|
nacp: 0,
|
|
gpsLost: false,
|
|
degraded: true,
|
|
},
|
|
],
|
|
});
|
|
assert.equal(res.headers['Cache-Control'], 'no-store');
|
|
});
|
|
}
|
|
|
|
test('snapshots are cached per anchor and concurrent reads share one upstream call', async () => {
|
|
let clock = 0;
|
|
let calls = 0;
|
|
const request = install({
|
|
now: () => clock,
|
|
fetchImpl: async () => {
|
|
calls += 1;
|
|
return Response.json(snapshot);
|
|
},
|
|
});
|
|
await Promise.all([request(), request('/?lat=50.3&lon=20.2')]);
|
|
assert.equal(calls, 1);
|
|
clock = 59_000;
|
|
const cached = await request();
|
|
assert.equal(calls, 1);
|
|
assert.equal(cached.body.fetchedAt, 0);
|
|
assert.equal(
|
|
cached.body.ageMs,
|
|
59_000,
|
|
'a cached answer reports its age on the server clock',
|
|
);
|
|
clock = 61_000;
|
|
await request();
|
|
assert.equal(calls, 2);
|
|
await request('/?lat=10&lon=10');
|
|
assert.equal(calls, 3);
|
|
});
|
|
|
|
test('an upstream failure serves the last snapshot as stale, else 502', async () => {
|
|
let clock = 0;
|
|
let fail = false;
|
|
const request = install({
|
|
now: () => clock,
|
|
fetchImpl: async () =>
|
|
fail ? new Response('down', { status: 503 }) : Response.json(snapshot),
|
|
});
|
|
await request();
|
|
fail = true;
|
|
clock = 120_000;
|
|
const stale = await request();
|
|
assert.equal(stale.status, 200);
|
|
assert.equal(stale.body.stale, true);
|
|
assert.equal(
|
|
stale.body.fetchedAt,
|
|
0,
|
|
'a stale answer keeps the time it was observed',
|
|
);
|
|
assert.equal(stale.body.ageMs, 120_000);
|
|
assert.equal(stale.headers['X-Data-Stale'], 'true');
|
|
const missing = await request('/?lat=-10&lon=-10');
|
|
assert.equal(missing.status, 502);
|
|
assert.deepEqual(missing.body, { error: 'gnss_integrity_unavailable' });
|
|
});
|
|
|
|
test('a stale snapshot older than the layer window is no longer served', async () => {
|
|
let clock = 0;
|
|
let fail = false;
|
|
const request = install({
|
|
now: () => clock,
|
|
fetchImpl: async () =>
|
|
fail ? new Response('down', { status: 503 }) : Response.json(snapshot),
|
|
});
|
|
await request();
|
|
fail = true;
|
|
clock = 30 * 60_000;
|
|
assert.equal((await request()).status, 200);
|
|
clock = 30 * 60_000 + 1;
|
|
const expired = await request();
|
|
assert.equal(expired.status, 502);
|
|
assert.deepEqual(expired.body, { error: 'gnss_integrity_unavailable' });
|
|
});
|
|
|
|
test('an upstream 429 starts a cooldown with no further upstream calls', async () => {
|
|
let clock = 0;
|
|
let calls = 0;
|
|
const request = install({
|
|
now: () => clock,
|
|
fetchImpl: async () => {
|
|
calls += 1;
|
|
return new Response('slow down', {
|
|
status: 429,
|
|
headers: { 'retry-after': '90' },
|
|
});
|
|
},
|
|
});
|
|
const first = await request();
|
|
assert.equal(first.status, 429);
|
|
assert.equal(first.headers['Retry-After'], '90');
|
|
clock = 30_500;
|
|
const cooling = await request('/?lat=1&lon=1');
|
|
assert.equal(cooling.status, 429);
|
|
// The browser is told the remaining shared cooldown, rounded up.
|
|
assert.equal(cooling.headers['Retry-After'], '60');
|
|
assert.equal(calls, 1);
|
|
clock = 91_000;
|
|
await request('/?lat=2&lon=2');
|
|
assert.equal(calls, 2);
|
|
});
|
|
|
|
test('Retry-After accepts sane seconds or HTTP dates up to one hour', () => {
|
|
const nowMs = Date.parse('2026-09-29T12:00:00Z');
|
|
assert.equal(gnssRetryCooldownMs('30', nowMs), 30_000);
|
|
assert.equal(gnssRetryCooldownMs('1', nowMs), 5_000);
|
|
assert.equal(gnssRetryCooldownMs('3600', nowMs), 3_600_000);
|
|
assert.equal(
|
|
gnssRetryCooldownMs('Tue, 29 Sep 2026 13:00:00 GMT', nowMs),
|
|
3_600_000,
|
|
);
|
|
assert.equal(gnssRetryCooldownMs('1e309', nowMs), 60_000);
|
|
assert.equal(
|
|
gnssRetryCooldownMs('Tue, 29 Sep 2026 12:00:45 GMT', nowMs),
|
|
45_000,
|
|
);
|
|
// Garbage, a past date or no header fall back to the default minute.
|
|
assert.equal(gnssRetryCooldownMs('soon', nowMs), 60_000);
|
|
assert.equal(
|
|
gnssRetryCooldownMs('Tue, 29 Sep 2026 11:00:00 GMT', nowMs),
|
|
60_000,
|
|
);
|
|
assert.equal(gnssRetryCooldownMs(null, nowMs), 60_000);
|
|
});
|
|
|
|
test('overlapping 429s preserve the longest shared deadline', async () => {
|
|
let clock = 0,
|
|
calls = 0;
|
|
const pending = [];
|
|
const request = install({
|
|
now: () => clock,
|
|
fetchImpl: () => {
|
|
calls += 1;
|
|
return new Promise((resolve) => pending.push(resolve));
|
|
},
|
|
});
|
|
const long = request('/?lat=1&lon=0'),
|
|
short = request('/?lat=2&lon=0');
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
pending[0](
|
|
new Response('', { status: 429, headers: { 'Retry-After': '3600' } }),
|
|
);
|
|
assert.equal((await long).headers['Retry-After'], '3600');
|
|
clock = 1000;
|
|
pending[1](
|
|
new Response('', { status: 429, headers: { 'Retry-After': '5' } }),
|
|
);
|
|
assert.equal((await short).headers['Retry-After'], '3599');
|
|
clock = 7000;
|
|
assert.equal((await request('/?lat=3&lon=0')).status, 429);
|
|
assert.equal(calls, 2);
|
|
clock = 3_600_001;
|
|
const resumed = request('/?lat=3&lon=0');
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
pending[2](Response.json(snapshot));
|
|
assert.equal((await resumed).status, 200);
|
|
assert.equal(calls, 3);
|
|
});
|
|
|
|
test('a cached anchor keeps serving its last snapshot as stale during a cooldown', async () => {
|
|
let clock = 0;
|
|
let limited = false;
|
|
let calls = 0;
|
|
const request = install({
|
|
now: () => clock,
|
|
fetchImpl: async () => {
|
|
calls += 1;
|
|
return limited
|
|
? new Response('slow down', { status: 429 })
|
|
: Response.json(snapshot);
|
|
},
|
|
});
|
|
await request();
|
|
limited = true;
|
|
clock = 61_000;
|
|
// Another anchor trips the shared cooldown…
|
|
assert.equal((await request('/?lat=5&lon=5')).status, 429);
|
|
clock = 62_000;
|
|
// …and the cached anchor is answered from cache without an upstream call.
|
|
const stale = await request();
|
|
assert.equal(stale.status, 200);
|
|
assert.equal(stale.body.stale, true);
|
|
assert.equal(stale.body.rows.length, 1);
|
|
assert.equal(calls, 2);
|
|
});
|
|
|
|
test('an oversized upstream body is refused as 502 without being parsed', async () => {
|
|
const request = install({
|
|
fetchImpl: async () =>
|
|
new Response('{}', {
|
|
headers: { 'content-length': String(9 * 1024 * 1024) },
|
|
}),
|
|
});
|
|
const res = await request();
|
|
assert.equal(res.status, 502);
|
|
assert.deepEqual(res.body, { error: 'gnss_integrity_unavailable' });
|
|
});
|
|
|
|
test('the route rejects other methods, paths, bad anchors, a malformed feed and floods', async () => {
|
|
const request = install({
|
|
fetchImpl: async () => Response.json({ nope: true }),
|
|
});
|
|
assert.equal((await request('/', 'POST')).status, 405);
|
|
assert.equal((await request('/other?lat=1&lon=1')).status, 404);
|
|
assert.equal((await request('/?lat=1')).status, 400);
|
|
assert.equal((await request('/?lat=1&lon=1')).status, 502);
|
|
const flood = install({ fetchImpl: async () => Response.json(snapshot) });
|
|
let limited = 0;
|
|
for (let i = 0; i < 40; i += 1)
|
|
if ((await flood(`/?lat=${i}&lon=1`, 'GET', 'flood')).status === 429)
|
|
limited += 1;
|
|
assert.ok(limited > 0);
|
|
});
|
|
|
|
test('GERMAN: pathological retry deadlines are bounded', () => {
|
|
const at = Date.parse('2026-10-14T12:00:00Z');
|
|
assert.ok(gnssRetryCooldownMs('1e305', at) <= 3600000);
|
|
assert.equal(gnssRetryCooldownMs('9999999999999999999999', at), 3600000);
|
|
assert.equal(
|
|
gnssRetryCooldownMs('Wed, 14 Oct 2099 12:00:00 GMT', at),
|
|
3600000,
|
|
);
|
|
assert.equal(gnssRetryCooldownMs('0x10', at), 60000);
|
|
});
|
|
|
|
test('GERMAN: 5xx, network timeouts and malformed JSON share a short cooldown', async () => {
|
|
for (const fail of [
|
|
async () => new Response('', { status: 503 }),
|
|
async () => {
|
|
throw new DOMException('timeout', 'TimeoutError');
|
|
},
|
|
async () => new Response('bad JSON'),
|
|
]) {
|
|
let clock = 0,
|
|
calls = 0;
|
|
const request = install({
|
|
now: () => clock,
|
|
fetchImpl: async () => {
|
|
calls++;
|
|
return fail();
|
|
},
|
|
});
|
|
assert.equal((await request()).status, 502);
|
|
clock = 14000;
|
|
assert.equal((await request('/?lat=1&lon=1')).status, 502);
|
|
assert.equal(calls, 1);
|
|
clock = 15001;
|
|
await request('/?lat=2&lon=2');
|
|
assert.equal(calls, 2);
|
|
}
|
|
});
|
|
|
|
test('GERMAN: source timestamp dates evidence; frozen old snapshots are rejected', async () => {
|
|
const now = 1800000000000;
|
|
const request = install({
|
|
now: () => now,
|
|
fetchImpl: async () =>
|
|
Response.json({ ...snapshot, now: (now - 120000) / 1000 }),
|
|
});
|
|
const res = await request();
|
|
assert.equal(res.body.ageMs, 120000);
|
|
assert.equal(res.body.stale, true);
|
|
for (const sourceNow of [(now - 1800001) / 1000, undefined, NaN]) {
|
|
const missing = install({
|
|
now: () => now,
|
|
fetchImpl: async () => Response.json({ ...snapshot, now: sourceNow }),
|
|
});
|
|
assert.equal((await missing()).status, 502);
|
|
}
|
|
});
|
|
|
|
test('GERMAN: cache age and cooldown remain stable across a wall-clock rollback', async () => {
|
|
let wall = 1800000000000,
|
|
mono = 0,
|
|
calls = 0;
|
|
const request = install({
|
|
now: () => wall,
|
|
monotonicNow: () => mono,
|
|
fetchImpl: async () => {
|
|
calls++;
|
|
return calls === 1
|
|
? Response.json({ ...snapshot, now: wall / 1000 })
|
|
: new Response('', { status: 429, headers: { 'Retry-After': '60' } });
|
|
},
|
|
});
|
|
await request();
|
|
wall -= 3600000;
|
|
mono = 61000;
|
|
const stale = await request();
|
|
assert.equal(stale.body.ageMs, 61000);
|
|
assert.equal(stale.body.stale, true);
|
|
mono += 1000;
|
|
assert.equal((await request('/?lat=1&lon=1')).headers['Retry-After'], '59');
|
|
assert.equal(calls, 2);
|
|
});
|
|
|
|
test('GERMAN: cache hits and coalesced reads do not consume upstream admission', async () => {
|
|
let calls = 0;
|
|
const request = install({
|
|
now: () => 1800000000000,
|
|
fetchImpl: async () => {
|
|
calls++;
|
|
return Response.json(snapshot);
|
|
},
|
|
});
|
|
for (let i = 0; i < 100; i++) assert.equal((await request()).status, 200);
|
|
assert.equal((await request('/?lat=1&lon=1')).status, 200);
|
|
assert.equal(calls, 2);
|
|
});
|
|
|
|
test('GERMAN: distinct concurrent upstream reads are bounded', async () => {
|
|
const pending = [];
|
|
const request = install({
|
|
fetchImpl: () => new Promise((resolve) => pending.push(resolve)),
|
|
});
|
|
const jobs = Array.from({ length: 30 }, (_, i) =>
|
|
request(`/?lat=${i}&lon=1`),
|
|
);
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
assert.ok(pending.length <= 8);
|
|
pending.forEach((resolve) => resolve(Response.json(snapshot)));
|
|
const results = await Promise.all(jobs);
|
|
assert.ok(results.some((result) => result.status === 429));
|
|
});
|