import test from 'node:test'; import assert from 'node:assert/strict'; import { EventEmitter } from 'node:events'; import { cycloneProxy, parseCycloneStatus, attachCycloneGeometry, } from '../../server/providers/cyclones.js'; const NOW = Date.UTC(2026, 8, 16, 3, 35); const TIME = '2026-09-16T03:00:00.000Z'; function status(overrides = {}) { return { activeStorms: [ { id: 'ep152026', name: 'Fifteen-E', classification: 'PTC', intensity: '25', pressure: '1006', longitudeNumeric: -125.8, latitudeNumeric: 15.5, movementDir: 255, movementSpeed: 12, lastUpdate: TIME, forecastAdvisory: { advNum: '010', issuance: TIME, url: 'https://www.nhc.noaa.gov/text/MIATCMEP5.shtml', }, ...overrides, }, ], }; } function collections(adv = '10') { const geometry = [ { type: 'Point', coordinates: [-125.8, 15.5] }, { type: 'LineString', coordinates: [ [-125.8, 15.5], [-128, 15], ], }, { type: 'Polygon', coordinates: [ [ [-126, 15], [-128, 15], [-127, 17], [-126, 15], ], ], }, ]; return geometry.map((shape, index) => ({ type: 'FeatureCollection', features: [ { type: 'Feature', properties: { idp_source: `ep152026-${adv.padStart(3, '0')}_5day_${['pts', 'lin', 'pgn'][index]}`, advisnum: adv, tau: 0, maxwind: 25, gust: 9999, }, geometry: shape, }, ], })); } function install(options = {}, preview = false) { let handler; const plugin = cycloneProxy({ now: () => NOW, ...options }); plugin[preview ? 'configurePreviewServer' : 'configureServer']({ middlewares: { use(path, callback) { assert.equal(path, '/api/cyclones'); handler = callback; }, }, }); const begin = (url = '/', method = 'GET') => { const res = new EventEmitter(); res.writeHead = (code, headers) => { res.statusCode = code; res.headers = headers; }; res.end = (body) => { res.body = body; }; return { res, done: handler({ url, method }, res).then(() => res) }; }; return { begin, request: (url, method) => begin(url, method).done }; } const body = (res) => JSON.parse(res.body); const nextTurn = () => new Promise((resolve) => setImmediate(resolve)); function fetcher(calls = [], data = status(), parts = collections()) { return async (url, options) => { calls.push({ url: new URL(url), options }); return Response.json( url.includes('CurrentStorms') ? data : parts[Number(url.match(/\/(\d)\/query/)[1]) - 5], ); }; } test('status retains official clocks, knots, source links and finite current position', () => { const storm = parseCycloneStatus(status(), NOW)[0]; assert.equal(storm.advisoryNumber, '10'); assert.equal(storm.issuedAt, TIME); assert.equal(storm.positionAt, TIME); assert.deepEqual(storm.position, { longitude: -125.8, latitude: 15.5 }); assert.deepEqual(storm.movement, { directionDegrees: 255, speedKt: 12 }); assert.equal(storm.windKt, 25); assert.equal(storm.pressureHpa, 1006); assert.match(storm.advisoryUrl, /^https:\/\/www\.nhc\.noaa\.gov\/text\//); const missing = parseCycloneStatus( status({ intensity: '9999', pressure: null, movementSpeed: '' }), NOW, )[0]; assert.equal(missing.windKt, null); assert.equal(missing.pressureHpa, null); assert.equal(missing.movement.speedKt, null); assert.equal( parseCycloneStatus( status({ forecastAdvisory: { advNum: '10', issuance: TIME, url: 'https://www.nhc.noaa.gov.evil/secret', }, }), NOW, )[0].advisoryUrl, null, ); }); test('malformed, duplicate, oversized, future and expired status is rejected rather than reported empty', () => { for (const payload of [ {}, { activeStorms: null }, { activeStorms: Array(33).fill(status().activeStorms[0]) }, { activeStorms: [...status().activeStorms, ...status().activeStorms] }, status({ id: 'wp012026' }), status({ name: 'a'.repeat(81) }), status({ name: '