import { describe, expect, it } from 'vitest'; import { parseCsv, filterEuAirports, toGeoJson, airportsHandler } from './airports'; const HEADER = '"id","ident","type","name","latitude_deg","longitude_deg","elevation_ft","continent","iso_country","iso_region","municipality","scheduled_service","icao_code","iata_code","gps_code","local_code","home_link","wikipedia_link","keywords"'; function row(over: Record = {}): string { const base: Record = { id: '1', ident: 'XXXX', type: 'small_airport', name: 'Test Field', latitude_deg: '50.0', longitude_deg: '8.0', elevation_ft: '', continent: 'EU', iso_country: 'DE', iso_region: 'DE-BE', municipality: 'Nowhere', scheduled_service: 'no', icao_code: 'XXXX', iata_code: '', gps_code: '', local_code: '', home_link: '', wikipedia_link: '', keywords: '', }; return Object.entries({ ...base, ...over }) .map(([, v]) => `"${v.replace(/"/g, '""')}"`) .join(','); } describe('parseCsv', () => { it('parses simple rows into objects keyed by header', () => { const text = [HEADER, row()].join('\n'); const rows = parseCsv(text); expect(rows).toHaveLength(1); expect(rows[0].ident).toBe('XXXX'); expect(rows[0].continent).toBe('EU'); }); it('handles quoted fields containing commas and escaped quotes', () => { const text = [HEADER, row({ name: 'A, B "South" Field' })].join('\n'); const rows = parseCsv(text); expect(rows[0].name).toBe('A, B "South" Field'); }); it('handles CRLF line endings and trailing newline', () => { const text = [HEADER, row(), row({ ident: 'YYYY' })].join('\r\n') + '\r\n'; const rows = parseCsv(text); expect(rows.map((r) => r.ident)).toEqual(['XXXX', 'YYYY']); }); }); describe('filterEuAirports', () => { it('keeps EU fixed-wing airports and seaplane bases, drops the rest', () => { const rows = parseCsv( [ HEADER, row({ ident: 'EU-SMALL', type: 'small_airport' }), row({ ident: 'EU-MED', type: 'medium_airport' }), row({ ident: 'EU-LRG', type: 'large_airport' }), row({ ident: 'EU-SEA', type: 'seaplane_base' }), row({ ident: 'EU-LEGACY', type: 'airport' }), row({ ident: 'EU-Helio', type: 'heliport' }), row({ ident: 'EU-CLOSED', type: 'closed' }), row({ ident: 'EU-BALLOON', type: 'balloonport' }), row({ ident: 'NA-SMALL', continent: 'NA' }), row({ ident: 'AS-LRG', continent: 'AS', type: 'large_airport' }), ].join('\n'), ); const kept = filterEuAirports(rows).map((r) => r.ident); expect(kept.sort()).toEqual(['EU-LEGACY', 'EU-LRG', 'EU-MED', 'EU-SEA', 'EU-SMALL']); }); }); describe('toGeoJson', () => { it('builds a FeatureCollection with [lon, lat] coordinates and expected properties', () => { const fc = toGeoJson(parseCsv([HEADER, row({ ident: 'EDDF', icao_code: 'EDDF', name: 'Frankfurt Main Airport' })].join('\n'))); expect(fc.type).toBe('FeatureCollection'); expect(fc.features).toHaveLength(1); const f = fc.features[0]; expect(f.geometry).toEqual({ type: 'Point', coordinates: [8.0, 50.0] }); expect(f.properties).toMatchObject({ name: 'Frankfurt Main Airport', icao: 'EDDF', lat: 50.0, lon: 8.0 }); }); it('falls back to ident when icao_code is empty and skips rows without valid coordinates', () => { const fc = toGeoJson( parseCsv( [ HEADER, row({ ident: 'NOICAO', icao_code: '' }), row({ ident: 'BADLAT', latitude_deg: 'not-a-number' }), row({ ident: 'NONAME', name: '' }), ].join('\n'), ), ); expect(fc.features).toHaveLength(1); expect(fc.features[0].properties.icao).toBe('NOICAO'); }); }); describe('/api/airports handler', () => { it('returns a valid GeoJSON FeatureCollection of EU airports', async () => { const res = await airportsHandler(); expect(res.status).toBe(200); expect(res.headers.get('content-type')).toContain('application/json'); const fc = (await res.json()) as { type: string; features: Array<{ properties: Record }> }; expect(fc.type).toBe('FeatureCollection'); expect(fc.features.length).toBeGreaterThan(0); for (const f of fc.features.slice(0, 50)) { expect(f.properties.name).toBeTruthy(); expect(f.properties.icao).toBeTruthy(); expect(typeof f.properties.lat).toBe('number'); expect(typeof f.properties.lon).toBe('number'); } }); it('caches the parsed dataset in memory', async () => { const a = await airportsHandler(); const b = await airportsHandler(); expect(await a.json()).toEqual(await b.json()); }); });