mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
* feat(military): resolve ICAO→IATA callsign for Wingbits flights Server resolves ICAO airline prefix (e.g. UAE→EK) at response time using a static ~100-entry lookup table, populating new proto fields callsign_iata and airline_name. Schedule lookup falls back to the IATA callsign inside the same cachedFetchJson callback to recover schedules that ECS only indexes under the commercial callsign. Callsigns are normalized to uppercase before caching to prevent duplicate cache keys from mixed-case ECS responses. Client popup shows the IATA callsign and airline name above the route section. * fix(military): correct airline data errors and add handler tests Remove BOG (airport code, not airline ICAO), remove TVS/NOZ (incorrect Transavia mappings), add TRA→HV as correct Transavia ICAO code. Remove non-standard TKJ duplicate of THY. Add handler integration tests that verify the IATA schedule fallback path via globalThis.fetch mock, plus guards for empty/invalid icao24 inputs. * docs(military): add refresh comment to airline-codes header * fix(military): correct TGW and WIF airline mappings TGW is Scoot (TR), not Oman Air — add OMA for Oman Air (WY). WIF is Wideroe (WF), not Wizz Air Abu Dhabi — add WAD for Wizz Air Abu Dhabi (W4). Both errors caused wrong popup labels and broken schedule fallback lookups. * fix(military): correct AUB and IBK airline mappings Austrian Airlines uses AUA (not AUB), IATA OS. Iberia Express uses IBS (not IBK), IATA I2 (not NT). Both errors caused toIataCallsign() to return null for live traffic, leaving popup header empty and skipping schedule fallback. * fix(military): remove/correct five more bad airline ICAO entries Remove BCS (European Air Transport cargo, not OpenSkies). Remove SHI (Seoul Air International, no commercial IATA). Fix SHY: Sky Airlines Turkey (ZY), not Uzbekistan Airways. Remove WAD (Waddington RAF training unit, not Wizz Air Abu Dhabi). TGW/Scoot kept as-is — Scoot is the current brand name since 2012. * fix(military): fix SWG/WestJet mapping, add ASH/Mesa SWG is Sunwing (WG), not WestJet — add WJA for WestJet (WS). ASH is Mesa Airlines (YV) — was removed as SHI but correct ICAO never added.
194 lines
7.4 KiB
JavaScript
194 lines
7.4 KiB
JavaScript
import { describe, it, afterEach } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { parseCallsign, toIataCallsign, icaoToIata } from '../server/_shared/airline-codes.ts';
|
|
import { getWingbitsLiveFlight } from '../server/worldmonitor/military/v1/get-wingbits-live-flight.ts';
|
|
|
|
const ECS_BASE = 'https://ecs-api.wingbits.com/v1/flights';
|
|
const PHOTOS_BASE = 'https://api.planespotters.net/pub/photos/hex';
|
|
const originalFetch = globalThis.fetch;
|
|
|
|
function jsonResp(data, status = 200) {
|
|
return new Response(JSON.stringify(data), {
|
|
status,
|
|
headers: { 'content-type': 'application/json' },
|
|
});
|
|
}
|
|
|
|
describe('parseCallsign', () => {
|
|
it('parses standard 3-letter ICAO prefix + number', () => {
|
|
assert.deepEqual(parseCallsign('UAE528'), { prefix: 'UAE', number: '528' });
|
|
assert.deepEqual(parseCallsign('BAW61'), { prefix: 'BAW', number: '61' });
|
|
assert.deepEqual(parseCallsign('EZY13BU'), { prefix: 'EZY', number: '13BU' });
|
|
assert.deepEqual(parseCallsign('DAL123'), { prefix: 'DAL', number: '123' });
|
|
});
|
|
|
|
it('parses 2-letter prefixes', () => {
|
|
assert.deepEqual(parseCallsign('LH123'), { prefix: 'LH', number: '123' });
|
|
});
|
|
|
|
it('parses 4-letter prefixes', () => {
|
|
assert.deepEqual(parseCallsign('DUKE41'), { prefix: 'DUKE', number: '41' });
|
|
});
|
|
|
|
it('normalizes whitespace', () => {
|
|
assert.deepEqual(parseCallsign(' UAE528 '), { prefix: 'UAE', number: '528' });
|
|
});
|
|
|
|
it('normalizes to uppercase', () => {
|
|
assert.deepEqual(parseCallsign('uae528'), { prefix: 'UAE', number: '528' });
|
|
assert.deepEqual(parseCallsign('ezy13bu'), { prefix: 'EZY', number: '13BU' });
|
|
});
|
|
|
|
it('returns null for callsigns starting with letter+digit (N-numbers)', () => {
|
|
assert.equal(parseCallsign('N123AB'), null);
|
|
});
|
|
|
|
it('returns null for empty string', () => {
|
|
assert.equal(parseCallsign(''), null);
|
|
});
|
|
|
|
it('returns null for whitespace-only', () => {
|
|
assert.equal(parseCallsign(' '), null);
|
|
});
|
|
|
|
it('returns null for pure alpha (no number suffix)', () => {
|
|
assert.equal(parseCallsign('ABCD'), null);
|
|
});
|
|
});
|
|
|
|
describe('icaoToIata', () => {
|
|
it('returns IATA mapping for known prefixes', () => {
|
|
assert.deepEqual(icaoToIata('UAE'), { iata: 'EK', name: 'Emirates' });
|
|
assert.deepEqual(icaoToIata('BAW'), { iata: 'BA', name: 'British Airways' });
|
|
assert.deepEqual(icaoToIata('EZY'), { iata: 'U2', name: 'easyJet' });
|
|
});
|
|
|
|
it('is case-insensitive', () => {
|
|
assert.deepEqual(icaoToIata('uae'), { iata: 'EK', name: 'Emirates' });
|
|
assert.deepEqual(icaoToIata('Baw'), { iata: 'BA', name: 'British Airways' });
|
|
});
|
|
|
|
it('returns undefined for unknown prefixes', () => {
|
|
assert.equal(icaoToIata('DUKE'), undefined);
|
|
assert.equal(icaoToIata('XYZ'), undefined);
|
|
assert.equal(icaoToIata(''), undefined);
|
|
});
|
|
});
|
|
|
|
describe('toIataCallsign', () => {
|
|
it('converts ICAO callsign to IATA equivalent', () => {
|
|
assert.deepEqual(toIataCallsign('UAE528'), { callsign: 'EK528', name: 'Emirates' });
|
|
assert.deepEqual(toIataCallsign('BAW61'), { callsign: 'BA61', name: 'British Airways' });
|
|
assert.deepEqual(toIataCallsign('EZY13BU'), { callsign: 'U213BU', name: 'easyJet' });
|
|
assert.deepEqual(toIataCallsign('THY123'), { callsign: 'TK123', name: 'Turkish Airlines' });
|
|
});
|
|
|
|
it('handles whitespace and lowercase input', () => {
|
|
assert.deepEqual(toIataCallsign(' uae528 '), { callsign: 'EK528', name: 'Emirates' });
|
|
});
|
|
|
|
it('returns null for unknown ICAO prefix (military/charter)', () => {
|
|
assert.equal(toIataCallsign('DUKE41'), null);
|
|
assert.equal(toIataCallsign('RCH123'), null);
|
|
});
|
|
|
|
it('returns null for non-standard format', () => {
|
|
assert.equal(toIataCallsign('N123AB'), null);
|
|
});
|
|
|
|
it('returns null for empty or whitespace input', () => {
|
|
assert.equal(toIataCallsign(''), null);
|
|
assert.equal(toIataCallsign(' '), null);
|
|
});
|
|
});
|
|
|
|
describe('getWingbitsLiveFlight — IATA schedule fallback', () => {
|
|
afterEach(() => {
|
|
globalThis.fetch = originalFetch;
|
|
});
|
|
|
|
it('falls back to IATA callsign when ICAO schedule returns 404, populates airline fields', async () => {
|
|
const fetchedUrls = [];
|
|
|
|
globalThis.fetch = async (url) => {
|
|
const u = url.toString();
|
|
fetchedUrls.push(u);
|
|
|
|
// Live flight endpoint
|
|
if (u === `${ECS_BASE}/ae1234`) {
|
|
return jsonResp({
|
|
flight: { h: 'ae1234', f: 'UAE528', la: 25.2, lo: 55.3, ab: 35000, gs: 500, tr: 180, rs: 0, og: false, ra: '2024-01-01T12:00:00Z' },
|
|
});
|
|
}
|
|
// ICAO schedule — miss
|
|
if (u === `${ECS_BASE}/schedule/UAE528`) {
|
|
return new Response('Not Found', { status: 404 });
|
|
}
|
|
// IATA schedule fallback — hit
|
|
if (u === `${ECS_BASE}/schedule/EK528`) {
|
|
return jsonResp({
|
|
schedule: { depIata: 'DXB', arrIata: 'LHR', depTimeUtc: '2024-01-01T10:00:00Z', arrTimeUtc: '2024-01-01T18:00:00Z', status: 'en-route', duration: 480 },
|
|
});
|
|
}
|
|
// Photo — skip cleanly
|
|
if (u.startsWith(PHOTOS_BASE)) {
|
|
return new Response('Not Found', { status: 404 });
|
|
}
|
|
return new Response('Unexpected URL', { status: 500 });
|
|
};
|
|
|
|
const result = await getWingbitsLiveFlight({}, { icao24: 'ae1234' });
|
|
|
|
assert.ok(result.flight, 'flight should be present');
|
|
// Airline fields populated from ICAO→IATA lookup
|
|
assert.equal(result.flight.callsignIata, 'EK528');
|
|
assert.equal(result.flight.airlineName, 'Emirates');
|
|
// Schedule populated via IATA fallback
|
|
assert.equal(result.flight.depIata, 'DXB');
|
|
assert.equal(result.flight.arrIata, 'LHR');
|
|
assert.equal(result.flight.flightStatus, 'en-route');
|
|
assert.equal(result.flight.flightDurationMin, 480);
|
|
// IATA fallback URL was fetched (not just ICAO)
|
|
assert.ok(fetchedUrls.some(u => u.includes('/schedule/EK528')), 'should have fetched IATA schedule');
|
|
// Cache key uses ICAO callsign (uppercase), not IATA
|
|
assert.ok(fetchedUrls.some(u => u.includes('/schedule/UAE528')), 'should have tried ICAO schedule first');
|
|
});
|
|
|
|
it('uses ICAO schedule when available, skips IATA fallback', async () => {
|
|
const fetchedUrls = [];
|
|
|
|
globalThis.fetch = async (url) => {
|
|
const u = url.toString();
|
|
fetchedUrls.push(u);
|
|
|
|
if (u === `${ECS_BASE}/ae1234`) {
|
|
return jsonResp({
|
|
flight: { h: 'ae1234', f: 'UAE528', la: 25.2, lo: 55.3, ab: 35000, gs: 500, tr: 180, rs: 0, og: false, ra: '2024-01-01T12:00:00Z' },
|
|
});
|
|
}
|
|
if (u === `${ECS_BASE}/schedule/UAE528`) {
|
|
return jsonResp({ schedule: { depIata: 'DXB', arrIata: 'LHR', status: 'en-route', duration: 480 } });
|
|
}
|
|
if (u.startsWith(PHOTOS_BASE)) return new Response('Not Found', { status: 404 });
|
|
return new Response('Unexpected URL', { status: 500 });
|
|
};
|
|
|
|
const result = await getWingbitsLiveFlight({}, { icao24: 'ae1234' });
|
|
|
|
assert.ok(result.flight);
|
|
assert.equal(result.flight.depIata, 'DXB');
|
|
// IATA fallback should NOT have been fetched
|
|
assert.ok(!fetchedUrls.some(u => u.includes('/schedule/EK528')), 'should not fetch IATA schedule when ICAO succeeds');
|
|
});
|
|
|
|
it('returns no flight for unknown icao24', async () => {
|
|
const result = await getWingbitsLiveFlight({}, { icao24: '' });
|
|
assert.equal(result.flight, undefined);
|
|
});
|
|
|
|
it('returns no flight for invalid icao24 format', async () => {
|
|
const result = await getWingbitsLiveFlight({}, { icao24: 'ZZZZZZ' });
|
|
assert.equal(result.flight, undefined);
|
|
});
|
|
});
|