refactor: separate aircraft and vessel server providers

This commit is contained in:
Sameh Khamis committed 2026-09-11 19:06:09 -07:00
1 parent 149517da81
commit 0e221441b4
17 files changed
+1892 -1615

No files matched your search

+3 -1
View File
@@ -3,7 +3,9 @@ import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import path from 'node:path';
const source = readFileSync(new URL('../server/providers/local.js', import.meta.url), 'utf8');
const source = ['local.js', 'common/http.js', 'aircraft/enrichment.js']
.map(file => readFileSync(new URL(`../server/providers/${file}`, import.meta.url), 'utf8'))
.join('\n');
const detail = 'fixture-secret-token /internal/example <html>';
// Execute the production middleware with isolated upstreams and cache storage.
+163
View File
@@ -0,0 +1,163 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { once } from 'node:events';
import { WebSocketServer } from 'ws';
import { setTimeout as delay } from 'node:timers/promises';
import * as providers from '../../server/providers/live.js';
import * as portable from '../../src/data/adsbLolFallback.js';
function install(plugin, preview = false) {
const routes = new Map();
plugin[preview ? 'configurePreviewServer' : 'configureServer']({
middlewares: { use(route, handler) { routes.set(route, handler); } },
});
return async (route, url = '/', method = 'GET') => {
assert.ok(routes.has(route), `registered route: ${route}`);
const response = {
statusCode: 200, headers: {},
setHeader(key, value) { this.headers[key.toLowerCase()] = value; },
writeHead(status, headers = {}) {
this.statusCode = status;
for (const [key, value] of Object.entries(headers)) this.setHeader(key, value);
},
end(body) { this.body = body; },
};
await routes.get(route)({ url, method }, response);
return response;
};
}
function environment(t, values) {
for (const [key, value] of Object.entries(values)) {
const original = process.env[key];
if (value === undefined) delete process.env[key]; else process.env[key] = value;
t.after(() => { if (original === undefined) delete process.env[key]; else process.env[key] = original; });
}
}
test('live entry resolves in Node and aircraft normalization stays independently portable', async () => {
const entry = await import('gods-eye-view/server/providers/live');
assert.equal(entry.openSkyProxy, providers.openSkyProxy);
assert.equal(entry.aisLiveProxy, providers.aisLiveProxy);
const normalizer = await import('gods-eye-view/sources/adsb-lol');
assert.equal(normalizer.normalizeAdsbLolAircraftState, portable.normalizeAdsbLolAircraftState);
});
test('OpenSky state and track routes share tokens, retain cache and use regional fallback', async t => {
environment(t, {
OPENSKY_CLIENT_ID: 'fixture-client', OPENSKY_CLIENT_SECRET: 'fixture-secret',
OPENSKY_AUTH_MODE: 'oauth', OPENSKY_USERNAME: undefined, OPENSKY_PASSWORD: undefined,
});
let now = Date.now();
t.mock.method(Date, 'now', () => now);
t.mock.method(console, 'log', () => {});
const calls = [];
t.mock.method(globalThis, 'fetch', async (url, options) => {
calls.push({ url, options });
if (url.includes('/token')) return Response.json({ access_token: 'fixture-token', expires_in: 1800 });
if (url.includes('/states/')) {
assert.equal(options.headers.Authorization, 'Bearer fixture-token');
return Response.json({ time: Math.floor(now / 1000), states: [] });
}
if (url.includes('/tracks/')) {
assert.equal(options.headers.Authorization, 'Bearer fixture-token');
return Response.json({ path: [] });
}
if (url.includes('/lat/')) return Response.json({ now: now / 1000, ac: [{ hex: 'abc123', lat: 30, lon: -97, alt_baro: 10000 }] });
throw Error(`Unexpected URL: ${url}`);
});
const states = install(providers.openSkyProxy());
assert.equal((await states('/api/opensky', '?lat=30&lon=-97')).statusCode, 200);
assert.equal((await states('/api/opensky', '?lat=30&lon=-97')).headers['x-opensky-cache'], 'HIT');
assert.equal(calls.length, 2);
const tracks = install(providers.trackBackfillProxies(), true);
assert.equal((await tracks('/api/opensky-track', '?icao24=ABC123')).statusCode, 200);
await tracks('/api/opensky-track', '?icao24=abc123');
assert.equal(calls.filter(call => call.url.includes('/token')).length, 1);
assert.equal(calls.filter(call => call.url.includes('/tracks/')).length, 1);
assert.equal((await tracks('/api/adsblol/trace', '?hex=invalid')).statusCode, 400);
now += 130_000;
t.mock.method(globalThis, 'fetch', async url => {
if (url.includes('/states/')) return new Response('', { status: 503 });
if (url.includes('/lat/')) return Response.json({ now: now / 1000, ac: [{ hex: 'abc123', lat: 30, lon: -97, alt_baro: 10000 }] });
throw Error(`Unexpected URL: ${url}`);
});
// A fresh request without a usable cached worldwide frame should use the regional feed.
const fresh = await import(`../../server/providers/aircraft/opensky.js?fallback=${now}`);
environment(t, { OPENSKY_AUTH_MODE: 'anon' });
const fallback = await install(fresh.openSkyProxy())('/api/opensky', '?lat=30&lon=-97');
assert.equal(fallback.statusCode, 200);
assert.equal(fallback.headers['x-flight-source'], 'adsb.lol');
assert.equal(JSON.parse(fallback.body).states[0][0], 'abc123');
});
test('military aircraft route preserves fresh cache and stale response after upstream failure', async t => {
let now = Date.now();
let calls = 0;
t.mock.method(Date, 'now', () => now);
t.mock.method(console, 'error', () => {});
t.mock.method(globalThis, 'fetch', async () => {
if (++calls > 1) throw Error('offline');
return Response.json({ ac: [{ hex: 'abc123' }] });
});
const request = install(providers.adsbLolProxy());
const first = await request('/api/adsblol/mil');
assert.equal((await request('/api/adsblol/mil')).body, first.body);
assert.equal(calls, 1);
now += 13_000;
assert.equal((await request('/api/adsblol/mil')).body, first.body);
assert.equal(calls, 2);
});
test('AIS preview route ingests through the socket, returns tracks and disposes before restart', async t => {
const upstream = new WebSocketServer({ host: '127.0.0.1', port: 0 });
await once(upstream, 'listening');
const sockets = [];
t.after(async () => {
for (const socket of sockets) socket.terminate();
await new Promise(resolve => upstream.close(resolve));
});
environment(t, {
AISSTREAM_API_KEY: 'fixture-key',
AISSTREAM_URL: `ws://127.0.0.1:${upstream.address().port}`,
AISSTREAM_BOUNDING_BOXES: undefined, AISSTREAM_MESSAGE_TYPES: undefined,
AISSTREAM_SILENCE_TIMEOUT_MS: '0',
});
upstream.on('connection', socket => {
sockets.push(socket);
socket.on('message', raw => {
assert.equal(JSON.parse(raw).APIKey, 'fixture-key');
for (const [lat, epoch] of [[30, 1_800_000_000], [30.01, 1_800_000_060]]) {
socket.send(JSON.stringify({
MessageType: 'PositionReport',
MetaData: { MMSI: 123456789, latitude: lat, longitude: -97, time_utc: new Date(epoch * 1000).toISOString() },
Message: { PositionReport: { UserID: 123456789, Sog: 10, Cog: 90, TrueHeading: 511 } },
}));
}
});
});
const plugin = providers.aisLiveProxy();
t.after(() => plugin.closeBundle());
const request = install(plugin, true);
let res;
for (let i = 0; i < 100; i++) {
res = await request('/api/ais-live');
if (JSON.parse(res.body).rows.length) break;
await delay(10);
}
const data = JSON.parse(res.body);
assert.equal(data.status, 'live');
assert.equal(data.rows[0].mmsi, '123456789');
assert.equal(data.rows[0].heading, null);
const history = await request('/api/ais-live', '/track?mmsi=123456789');
assert.equal(JSON.parse(history.body).samples.length, 2);
assert.equal((await request('/api/ais-live', '/track?mmsi=bad')).statusCode, 400);
assert.equal(sockets.length, 1);
plugin.closeBundle();
for (let i = 0; i < 100 && sockets[0].readyState !== 3; i++) await delay(10);
assert.equal(sockets[0].readyState, 3);
const restarted = install(plugin);
await restarted('/api/ais-live');
for (let i = 0; i < 100 && sockets.length < 2; i++) await delay(10);
assert.equal(sockets.length, 2);
});