mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
refactor: separate aircraft and vessel server providers
This commit is contained in:
1 parent
149517da81
commit
0e221441b4
17 files changed
+1892
-1615
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
Reference in new issue
Block a user