| 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' }); |
| assert.deepEqual(icaoToIata('SWR'), { iata: 'LX', name: 'Swiss International Air Lines' }); |
| assert.deepEqual(icaoToIata('TGW'), { iata: 'TR', name: 'Scoot' }); |
| }); |
|
|
| 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('ELK'), 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' }); |
| assert.deepEqual(toIataCallsign('SWR42'), { callsign: 'LX42', name: 'Swiss International Air Lines' }); |
| assert.deepEqual(toIataCallsign('TGW42'), { callsign: 'TR42', name: 'Scoot' }); |
| }); |
|
|
| 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); |
|
|
| |
| 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 new Response('Not Found', { status: 404 }); |
| } |
| |
| 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 }, |
| }); |
| } |
| |
| 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'); |
| |
| assert.equal(result.flight.callsignIata, 'EK528'); |
| assert.equal(result.flight.airlineName, 'Emirates'); |
| |
| 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); |
| |
| assert.ok(fetchedUrls.some(u => u.includes('/schedule/EK528')), 'should have fetched IATA schedule'); |
| |
| 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'); |
| |
| 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); |
| }); |
| }); |
|
|