import { test } from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { createCctvCatalog } from '../../server/providers/cctv/catalog.js'; import { DEFAULT_WSDOT_MAX_SOURCES, WSDOT_CAMERAS_URL, WSDOT_MAX_CATALOG_BYTES, } from '../../server/providers/cctv/constants.js'; import { loadWsdotSourcesFromOpenData, webMercatorToWgs84, } from '../../server/providers/cctv/sources.js'; /** Set (or, for `undefined`, delete) environment variables for one test. */ function withEnv(t, env) { for (const [name, value] of Object.entries(env)) { const previous = process.env[name]; t.after(() => { if (previous === undefined) delete process.env[name]; else process.env[name] = previous; }); if (value === undefined) delete process.env[name]; else process.env[name] = value; } } /** Silence the loaders' progress and failure logging. */ function quiet(t) { t.mock.method(console, 'log', () => {}); t.mock.method(console, 'warn', () => {}); } /** Web Mercator metres for a WGS84 coordinate (inverse of the loader's math). */ function mercator(lat, lon) { const R = 6378137; return { x: (lon * Math.PI * R) / 180, y: R * Math.log(Math.tan(Math.PI / 4 + (lat * Math.PI) / 360)), }; } /** A `Cameras.json` point feature carrying the fields the loader reads. */ function wsdotFeature(id, lat, lon, overrides = {}) { const { attributes = {}, geometry } = overrides; return { attributes: { CameraID: id, CameraTitle: `Camera ${id}`, CompassDirection: 'N', ImageURL: `https://images.wsdot.wa.gov/nw/${id}.jpg`, ...attributes, }, geometry: geometry === undefined ? mercator(lat, lon) : geometry, }; } /** Serialize a payload for the mocked fetch as raw Windows-1252 bytes. */ function cp1252Response(payload) { const text = JSON.stringify(payload); const bytes = new Uint8Array(text.length); for (let i = 0; i < text.length; i += 1) { const code = text.charCodeAt(i); // U+2013 EN DASH is 0x96 in Windows-1252 — the byte the live feed ships. bytes[i] = code === 0x2013 ? 0x96 : code; } return new Response(bytes, { headers: { 'Content-Type': 'application/json' }, }); } test('webMercatorToWgs84 recovers WGS84 degrees from layer geometry', () => { // Real feature from the live layer: I-5 at the Interstate Bridge. const { lat, lon } = webMercatorToWgs84(-13656007.102, 5719738.545); assert.ok(Math.abs(lat - 45.62) < 0.05, `lat ${lat}`); assert.ok(Math.abs(lon - -122.675) < 0.05, `lon ${lon}`); }); test('WSDOT loader keeps official-host cameras and maps compass directions', async (t) => { quiet(t); withEnv(t, { CCTV_WSDOT_MAX_SOURCES: undefined }); const requested = []; t.mock.method(globalThis, 'fetch', async (url) => { requested.push(String(url)); return cp1252Response({ features: [ wsdotFeature(1001, 47.6097, -122.3331, { attributes: { // The 0x2013 below is serialized as the feed's 0x96 byte. CameraTitle: 'I-5 – Seattle, looking south', CompassDirection: 'S', }, }), wsdotFeature(9059, 47.66, -117.43, { attributes: { CompassDirection: 'B' }, }), // Partner-hosted frame (Oregon DOT): skipped, never proxied. wsdotFeature(1002, 45.62, -122.675, { attributes: { ImageURL: 'https://www.tripcheck.com/RoadCams/cams/bridge.jpg', }, }), // Bad rows: non-integer id, missing geometry, out-of-state point. wsdotFeature('1003', 47.6, -122.3), wsdotFeature(1004, 47.6, -122.3, { geometry: null }), wsdotFeature(1005, 34.05, -118.24), ], }); }); const cameras = await loadWsdotSourcesFromOpenData(); assert.deepEqual(requested, [WSDOT_CAMERAS_URL]); // Exactly the two valid official-host cameras survive; the partner-hosted // frame and the malformed rows are dropped. assert.deepEqual(cameras.map((camera) => camera.id).sort(), [ 'wsdot-1001', 'wsdot-9059', ]); const seattle = cameras.find((camera) => camera.id === 'wsdot-1001'); assert.equal(seattle.name, 'I-5 – Seattle, looking south'); assert.equal(seattle.provider, 'WSDOT'); assert.equal(seattle.url, 'https://images.wsdot.wa.gov/nw/1001.jpg'); assert.equal(seattle.snapshotUrl, seattle.url); assert.equal(seattle.feedType, 'image'); assert.equal(seattle.headingDeg, 180); assert.equal(seattle.headingConfidence, 'high'); assert.equal(seattle.fovDeg, 56); assert.ok(Math.abs(seattle.lat - 47.6097) < 0.001); assert.ok(Math.abs(seattle.lon - -122.3331) < 0.001); // `B` (both directions) carries no single facing: id-hash fallback at low // confidence, with the low-confidence pose personality. const spokane = cameras.find((camera) => camera.id === 'wsdot-9059'); assert.equal(spokane.headingConfidence, 'low'); assert.equal(spokane.fovDeg, 44); assert.ok(spokane.headingDeg >= 0 && spokane.headingDeg < 360); }); test('WSDOT loader maps every CompassDirection code', async (t) => { quiet(t); withEnv(t, { CCTV_WSDOT_MAX_SOURCES: undefined }); const codes = { N: 0, NE: 45, E: 90, SE: 135, S: 180, SW: 225, W: 270, NW: 315, // Lower-case and padded codes are the same code. ' nw ': 315, }; // The live feed's no-facing codes, plus an unknown one. const noFacing = ['B', 'O', null, 'BW']; const entries = [...Object.keys(codes), ...noFacing]; t.mock.method(globalThis, 'fetch', async () => cp1252Response({ features: entries.map((code, i) => wsdotFeature(3000 + i, 47.0 + i * 0.05, -122.9 + i * 0.05, { attributes: { CompassDirection: code }, }), ), }), ); const cameras = await loadWsdotSourcesFromOpenData(); const byId = new Map(cameras.map((camera) => [camera.id, camera])); entries.forEach((code, i) => { const camera = byId.get(`wsdot-${3000 + i}`); if (code in codes) { assert.equal(camera.headingDeg, codes[code], `heading for ${code}`); assert.equal(camera.headingConfidence, 'high', `confidence for ${code}`); assert.equal(camera.fovDeg, 56, `pose personality for ${code}`); } else { assert.equal(camera.headingConfidence, 'low', `confidence for ${code}`); assert.equal(camera.fovDeg, 44, `pose personality for ${code}`); } }); }); test('WSDOT loader honors the max-sources cap', async (t) => { quiet(t); withEnv(t, { CCTV_WSDOT_MAX_SOURCES: '8' }); const features = []; for (let i = 0; i < 24; i += 1) { features.push(wsdotFeature(2000 + i, 47.0 + i * 0.05, -122.9 + i * 0.05)); } t.mock.method(globalThis, 'fetch', async () => cp1252Response({ features })); const cameras = await loadWsdotSourcesFromOpenData(); assert.equal(cameras.length, 8); }); test('WSDOT loader defaults its cap and survives upstream failure', async (t) => { quiet(t); withEnv(t, { CCTV_WSDOT_MAX_SOURCES: undefined }); assert.equal(DEFAULT_WSDOT_MAX_SOURCES, 250); t.mock.method( globalThis, 'fetch', async () => new Response('', { status: 503 }), ); assert.deepEqual(await loadWsdotSourcesFromOpenData(), []); t.mock.method(globalThis, 'fetch', async () => { throw new Error('network down'); }); assert.deepEqual(await loadWsdotSourcesFromOpenData(), []); }); test('the catalog fetch refuses a redirect and cancels its body', async (t) => { quiet(t); // A redirect is never followed, and its still-open body is cancelled. const seen = []; const state = { cancelled: false }; t.mock.method(globalThis, 'fetch', async (url, init) => { seen.push([String(url), init.redirect]); const body = new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode('{')); }, cancel() { state.cancelled = true; }, }); return new Response(body, { status: 302, headers: { location: 'https://evil.example/Cameras.json' }, }); }); assert.deepEqual(await loadWsdotSourcesFromOpenData(), []); assert.deepEqual(seen, [[WSDOT_CAMERAS_URL, 'manual']]); assert.equal(state.cancelled, true, 'the redirect body is cancelled'); }); test('the catalog fetch refuses a body over the byte cap', async (t) => { quiet(t); // A valid catalog padded past the cap is refused rather than buffered. t.mock.method(globalThis, 'fetch', async () => { const text = JSON.stringify({ features: [wsdotFeature(1001, 47.6097, -122.3331)], }); const padded = text + ' '.repeat(WSDOT_MAX_CATALOG_BYTES); return new Response( new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode(padded)); controller.close(); }, }), { headers: { 'Content-Type': 'application/json' } }, ); }); assert.deepEqual(await loadWsdotSourcesFromOpenData(), []); }); test('CCTV_WSDOT_ENABLED=0 skips the WSDOT request', async (t) => { quiet(t); const sourceRoot = fs.mkdtempSync( path.join(os.tmpdir(), 'gev-cctv-catalog-'), ); t.after(() => fs.rmSync(sourceRoot, { recursive: true, force: true })); withEnv(t, { CCTV_SOURCES_FILE: undefined, CCTV_SOURCES_JSON: undefined, CCTV_FORCE_AUSTIN: undefined, CCTV_PREFER_AUSTIN: undefined, CCTV_MAX_SOURCES: undefined, CCTV_WSDOT_ENABLED: undefined, CCTV_WSDOT_MAX_SOURCES: undefined, }); const requested = []; t.mock.method(globalThis, 'fetch', async (url) => { requested.push(String(url)); // Every other live pack is down; only WSDOT answers. return String(url) === WSDOT_CAMERAS_URL ? cp1252Response({ features: [wsdotFeature(1001, 47.6097, -122.3331)] }) : new Response('unavailable', { status: 503 }); }); const sources = await createCctvCatalog({ sourceRoot })(); assert.deepEqual( sources.map((source) => source.id), ['wsdot-1001'], ); process.env.CCTV_WSDOT_ENABLED = '0'; requested.length = 0; assert.deepEqual(await createCctvCatalog({ sourceRoot })(), []); assert.ok(requested.length > 0, 'the other live packs still load'); assert.equal( requested.includes(WSDOT_CAMERAS_URL), false, 'a disabled pack makes no request', ); });