import test from 'node:test'; import assert from 'node:assert/strict'; import { createFlightSource, createMilitarySource, createVesselSource, normalizeReadsbAircraft, normalizeVesselObservation, openSkySnapshot, readsbSnapshot, } from './index.js'; const now = 1800000000000; const aircraft = [ 'AbC123', 'TEST', 'Example', now / 1000 - 15, now / 1000 - 2, -97, 30, 1000, false, 100, 250, -2, null, 1040, ]; const response = (payload, headers = {}, status = 200) => Response.json(payload, { headers, status }); test('civil observations retain identity, separate altitude datums and source epochs', () => { const snapshot = openSkySnapshot( { time: now / 1000 - 130, states: [aircraft] }, { now, source: 'Test source', coverage: 'regional' }, ); const row = snapshot.records[0]; assert.equal(row.id, 'abc123'); assert.equal(row.baroAltitudeM, 1000); assert.equal(row.ellipsoidAltitudeM, 1040); assert.equal(row.positionTimeMs, now - 15000); assert.equal(row.contactTimeMs, now - 2000); assert.equal(snapshot.ageMs, 130000); assert.equal(snapshot.freshness, 'stale'); assert.equal(snapshot.coverage, 'regional'); const missing = openSkySnapshot( { states: [[...aircraft.slice(0, 3), null, null, ...aircraft.slice(5)]] }, { now }, ); assert.equal(missing.observedAtMs, null); assert.equal(missing.freshness, 'unknown'); assert.equal(missing.records[0].positionTimeMs, null); }); test('mixed-invalid and duplicate admissions are explicitly incomplete; all-invalid rejects', () => { const invalid = [...aircraft]; invalid[5] = 181; const partial = openSkySnapshot({ states: [aircraft, invalid, aircraft] }); assert.equal(partial.records.length, 1); assert.equal(partial.complete, false); assert.equal(partial.rejectedCount, 2); assert.throws(() => openSkySnapshot({ states: [invalid] }), { code: 'malformed', }); assert.throws(() => openSkySnapshot({}), { code: 'malformed' }); assert.equal(openSkySnapshot({ states: [] }).complete, true); }); test('position-less OpenSky rows are seen, not missing; malformed rows stay incomplete', () => { // OpenSky's spec allows null longitude/latitude; every live /states/all // carries such rows. const unpositioned = ['DEF456', ...aircraft.slice(1)]; unpositioned[5] = null; unpositioned[6] = null; const snapshot = openSkySnapshot({ states: [aircraft, unpositioned] }); assert.equal(snapshot.records.length, 1); assert.equal(snapshot.complete, true); assert.deepEqual([...snapshot.retainIds], ['def456']); assert.equal(snapshot.rejectedCount, 1); const malformed = [...aircraft]; malformed[5] = 181; const garbled = openSkySnapshot({ states: [aircraft, unpositioned, malformed], }); assert.equal(garbled.complete, false); assert.deepEqual([...garbled.retainIds], ['def456']); // No usable id, only half a position, a truncated row, or no row at all. for (const row of [ [' ', ...unpositioned.slice(1)], Object.assign([...unpositioned], { 5: -97 }), ['DEF456'], null, ]) assert.equal(openSkySnapshot({ states: [aircraft, row] }).complete, false); // Repeated position-less rows stay complete. const repeated = openSkySnapshot({ states: [aircraft, unpositioned, unpositioned], }); assert.equal(repeated.complete, true); // A feed with no positions at all still cannot prove absence. assert.throws(() => openSkySnapshot({ states: [unpositioned] }), { code: 'malformed', }); // Military readsb admission keeps its existing semantics. const military = readsbSnapshot( { ac: [{ hex: 'abc123', lat: 30, lon: -97 }, { hex: 'def456' }] }, { observedAtMs: null }, ); assert.equal(military.complete, false); assert.equal(military.retainIds.size, 0); }); test('readsb units and position/contact ages are normalized once against the source epoch', () => { const row = normalizeReadsbAircraft( { hex: 'abc123', lat: '30', lon: '-97', alt_baro: 10000, alt_geom: 10100, gs: 120, baro_rate: -600, seen: 2, seen_pos: 12, }, now, ); assert.equal(row.baroAltitudeM, 3048); assert.equal(row.ellipsoidAltitudeM, 3078.48); assert.equal(row.speedMps, 120 * 0.514444); assert.equal(row.verticalRateMps, -600 * 0.00508); assert.equal(row.positionTimeMs, now - 12000); assert.equal(row.contactTimeMs, now - 2000); const ground = normalizeReadsbAircraft( { hex: 'abc123', lat: 30, lon: -97, alt_baro: 'ground' }, null, ); assert.equal(ground.onGround, true); assert.equal(ground.baroAltitudeM, null); assert.equal(ground.positionTimeMs, null); assert.equal( readsbSnapshot({ ac: [] }, { observedAtMs: null }).freshness, 'unknown', ); }); test('vessel identity, opaque reference and heading/course remain distinct, unknowns stay null', () => { const row = normalizeVesselObservation( { mmsi: '123456789', lat: 30, lon: -97, speed: 10, heading: 45, course: 60, last_position_epoch: now / 1000, extra: 'discard', }, 'opaque-reference', ); assert.equal(row.id, '123456789'); assert.equal(row.reference, 'opaque-reference'); assert.equal(row.headingDeg, 45); assert.equal(row.courseDeg, 60); assert.equal(row.speedMps, 10 * 0.514444); assert.equal(row.altitudeDatum, 'sea-surface'); assert.equal(row.observedAtMs, now); assert.equal('extra' in row, false); assert.equal( normalizeVesselObservation({ mmsi: 'x', lat: null, lon: null }), null, ); }); test('construction is inert; adapters preserve routes, viewport query, cache epoch and history datum', async () => { const requests = []; const fetchImpl = async (url, init) => { requests.push({ url, init }); if (url.startsWith('/api/flights?')) return response( { time: now / 1000, states: [aircraft] }, { 'x-flight-source': 'adsb.lol', 'x-flight-coverage': '250 nm regional', }, ); if (url.startsWith('/api/flights/track')) return response({ path: [[now / 1000 - 30, 30, -97, 1000, 45, false]] }); if (url === '/api/military') return response( { ac: [{ hex: 'abc123', lat: 30, lon: -97, seen_pos: 15 }] }, { 'x-feed-age-ms': '60000', 'x-feed-cache': 'STALE' }, ); return response({ timestamp: now / 1000 - 60, trace: [[10, 30, -97, 1000]], }); }; const civil = createFlightSource({ fetchImpl, now: () => now }); const military = createMilitarySource({ fetchImpl, now: () => now }); assert.equal(requests.length, 0); const signal = new AbortController().signal; const snapshot = await civil.getSnapshot( { latitude: 30.123456, longitude: -97 }, { signal }, ); assert.equal(requests[0].url, '/api/flights?lat=30.1235&lon=-97.0000'); assert.equal(requests[0].init.signal, signal); assert.equal(snapshot.source, 'adsb.lol'); assert.equal(snapshot.coverage, '250 nm regional'); assert.equal((await civil.getTrack('abc123')).records[0].baroAltitudeM, 1000); const mil = await military.getSnapshot(); assert.equal(mil.observedAtMs, now - 60000); assert.equal(mil.records[0].positionTimeMs, now - 75000); assert.equal(mil.stale, true); assert.equal(military.getTrack, undefined); assert.ok( requests.every(({ url }) => !url.startsWith('/api/military/track')), ); }); test('cancellation after slow body parsing rejects even with a transport that ignores abort', async () => { let release; const controller = new AbortController(); const source = createFlightSource({ fetchImpl: async () => ({ ok: true, json: () => new Promise((resolve) => { release = resolve; }), }), }); const pending = source.getSnapshot({}, { signal: controller.signal }); await new Promise((resolve) => setImmediate(resolve)); controller.abort(); release({ states: [aircraft] }); await assert.rejects(pending, { name: 'AbortError' }); }); test('denials and outages never start another source and do not echo arbitrary response bodies', async () => { for (const status of [401, 403, 429, 502]) { let calls = 0; const source = createFlightSource({ fetchImpl: async () => { calls++; return response({ error: 'sensitive upstream detail' }, {}, status); }, }); await assert.rejects(source.getSnapshot(), (error) => { assert.equal(error.status, status); assert.equal(error.message.includes('sensitive'), false); assert.ok(error.retryAfterMs >= 20000); return true; }); assert.equal(calls, 1); } }); test('AIS reports limited received coverage and keeps connection state separate from positions', async () => { const source = createVesselSource({ origin: () => 'http://example.test', fetchImpl: async (url) => { if (url.includes('/track?')) return response({ samples: [{ lat: 30, lon: -97, t: now / 1000 }] }); assert.equal(url, 'http://example.test/api/vessels?maxRows=500'); return response({ status: 'reconnecting', refreshing: true, rows: [ { mmsi: '123456789', lat: 30, lon: -97, last_position_UTC: new Date(now).toISOString(), }, ], newestPositionAt: new Date(now).toISOString(), }); }, }); const snapshot = await source.getSnapshot({ maxRows: 500 }); assert.equal(snapshot.complete, false); assert.equal(snapshot.stale, true); assert.equal(snapshot.observedAtMs, now); assert.equal(snapshot.transportStatus, 'reconnecting'); assert.equal( (await source.getTrack('123456789')).records[0].observedAtMs, now, ); }); test('a malformed vessel row cannot prevent admission of valid positions', async () => { const source = createVesselSource({ fetchImpl: async () => response({ status: 'live', rows: [null, { mmsi: '123456789', lat: 30, lon: -97 }], }), }); const snapshot = await source.getSnapshot(); assert.equal(snapshot.records.length, 1); assert.equal(snapshot.rejectedCount, 1); assert.equal(snapshot.complete, false); }); test('out-of-range source epochs do not reach Date or globe time constructors', () => { const row = [...aircraft]; row[3] = 1e100; row[4] = 1e100; const snapshot = openSkySnapshot({ time: 1e100, states: [row] }); assert.equal(snapshot.observedAtMs, null); assert.equal(snapshot.records[0].positionTimeMs, null); assert.equal(snapshot.records[0].contactTimeMs, null); }); test('classification identities include positionless aircraft without admitting them to rendering', async () => { const source = createMilitarySource({ fetchImpl: async () => response({ ac: [ { hex: ' ABC123 ' }, { hex: 'abc123' }, { hex: 'def456', lat: 30, lon: -97 }, ], }), }); assert.deepEqual(await source.getIdentities(), ['abc123', 'def456']); const snapshot = await source.getSnapshot(); assert.deepEqual( snapshot.records.map((record) => record.id), ['def456'], ); assert.equal(snapshot.complete, false); }); test('identity lookup validates its response and honors body-parse cancellation', async () => { const malformed = createMilitarySource({ fetchImpl: async () => response({ ac: [{}] }), }); await assert.rejects(malformed.getIdentities(), /Malformed/); const abort = new AbortController(); const source = createMilitarySource({ fetchImpl: async () => ({ ok: true, status: 200, headers: new Headers(), async json() { abort.abort(); return { ac: [{ hex: 'abc123' }] }; }, }), }); await assert.rejects(source.getIdentities({}, { signal: abort.signal }), { name: 'AbortError', }); }); test('squawks are validated and omitted OpenSky status differs from an explicit ADS-B clear', () => { const squawking = [...aircraft, '7700']; const civil = openSkySnapshot({ states: [squawking] }, { now }).records[0]; assert.equal(civil.squawk, '7700'); assert.equal(civil.emergency, undefined); assert.equal(openSkySnapshot({ states: [aircraft] }).records[0].squawk, null); const mil = normalizeReadsbAircraft( { hex: 'abc123', lat: 30, lon: -97, squawk: '7600', emergency: 'NORDO' }, now, ); assert.equal(mil.squawk, '7600'); assert.equal(mil.emergency, 'nordo'); const quiet = normalizeReadsbAircraft( { hex: 'abc123', lat: 30, lon: -97 }, now, ); assert.equal(quiet.squawk, null); assert.equal(quiet.emergency, null); });