import { test } from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { DEFAULT_KING_COUNTY_MAX_SOURCES, KING_COUNTY_CAMERAS_URL, KING_COUNTY_MAX_CATALOG_BYTES, } from '../../server/providers/cctv/constants.js'; import { loadKingCountySourcesFromOpenData } from '../../server/providers/cctv/sources.js'; import { normalizeSourceItem } from '../../server/providers/cctv/normalize.js'; import { cctvProxy } from '../../server/providers/cctv.js'; import { createCatalog } from '../layers/cctv/catalog.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', () => {}); } /** A FeatureServer GeoJSON feature carrying the fields the loader reads. */ function kingCountyFeature(id, lat, lon, overrides = {}) { const { properties = {}, geometry } = overrides; return { type: 'Feature', geometry: geometry === undefined ? { type: 'Point', coordinates: [lon, lat] } : geometry, properties: { AssetID: id, Location: `Road ${id} at Test Ave`, ImageURL: `http://info.kingcounty.gov/transportation/kcdot/Roads/TrafficCameras/ImageHandler/Handler.ashx?id=cam${id}.jpg`, Live: 1, Model: '3950', Manufacturer: 'Cohu', Owner: 'King County', CurrentStatus: 'Active', ...properties, }, }; } test('King County loader keeps live county cameras and upgrades frame URLs to HTTPS', async (t) => { quiet(t); withEnv(t, { CCTV_KINGCOUNTY_MAX_SOURCES: undefined }); const requested = []; t.mock.method(globalThis, 'fetch', async (url) => { requested.push(String(url)); return Response.json({ type: 'FeatureCollection', features: [ kingCountyFeature(56, 47.70059, -122.02443, { properties: { Location: 'NE Novelty Hill Road at Trilogy Parkway NE', // The live layer carries stray whitespace in model strings. Model: 'Q6135-LE\n', Manufacturer: 'Axis', }, }), // City-owned camera the county hosts: kept, owner becomes the credit. kingCountyFeature(90, 47.674, -122.119, { properties: { Owner: 'City of Newcastle', Model: null }, }), // WSDOT-owned row: its frames are WSDOT-hosted, so it is skipped. kingCountyFeature(70, 47.6, -122.3, { properties: { Owner: 'WSDOT' } }), // Off-host frame, not live, retired, bad id, bad geometry, out of county. kingCountyFeature(71, 47.6, -122.3, { properties: { ImageURL: 'https://example.com/cam71.jpg' }, }), kingCountyFeature(72, 47.6, -122.3, { properties: { Live: 0 } }), kingCountyFeature(73, 47.6, -122.3, { properties: { CurrentStatus: 'Retired' }, }), kingCountyFeature('74', 47.6, -122.3), kingCountyFeature(75, 47.6, -122.3, { geometry: null }), kingCountyFeature(76, 34.05, -118.24), ], }); }); const cameras = await loadKingCountySourcesFromOpenData(); assert.deepEqual(requested, [KING_COUNTY_CAMERAS_URL]); assert.deepEqual(cameras.map((camera) => camera.id).sort(), [ 'kingcounty-56', 'kingcounty-90', ]); const novelty = cameras.find((camera) => camera.id === 'kingcounty-56'); assert.equal(novelty.name, 'NE Novelty Hill Road at Trilogy Parkway NE'); assert.equal(novelty.provider, 'King County Road Services'); assert.equal( novelty.url, 'https://info.kingcounty.gov/transportation/kcdot/Roads/TrafficCameras/ImageHandler/Handler.ashx?id=cam56.jpg', ); assert.equal(novelty.snapshotUrl, novelty.url); assert.equal(novelty.feedType, 'image'); // Published hardware model rides along, whitespace-collapsed. assert.equal(novelty.model, 'Q6135-LE'); assert.equal(novelty.manufacturer, 'Axis'); assert.equal(novelty.credit, ''); // No facing in the layer: id-hash fallback at low confidence. assert.equal(novelty.headingConfidence, 'low'); assert.equal(novelty.fovDeg, 44); assert.ok(novelty.headingDeg >= 0 && novelty.headingDeg < 360); const newcastle = cameras.find((camera) => camera.id === 'kingcounty-90'); assert.equal(newcastle.credit, 'City of Newcastle'); assert.equal(newcastle.model, undefined); // A manufacturer without a model identifies nothing, so it is dropped too. assert.equal(newcastle.manufacturer, undefined); }); test('normalizeSourceItem passes a hardware model through and omits it otherwise', () => { const withModel = normalizeSourceItem({ id: 'kingcounty-56', lat: 47.7, lon: -122.0, model: ' Q6135-LE ', manufacturer: ' Axis ', }); assert.equal(withModel.model, 'Q6135-LE'); assert.equal(withModel.manufacturer, 'Axis'); const withoutModel = normalizeSourceItem({ id: 'austin-1', lat: 30.3, lon: -97.7, }); assert.equal(withoutModel.model, undefined); assert.equal(withoutModel.manufacturer, undefined); assert.equal(normalizeSourceItem({ id: 'x', model: ' ' }).model, undefined); }); /** Drive the real `/api/cctv/sources` middleware and return its parsed body. */ async function requestSources(plugin) { let handler; plugin.configureServer({ middlewares: { use(_route, fn) { handler = fn; }, }, }); const res = { writeHead(status, headers) { Object.assign(this, { status, headers }); }, end(body) { this.body = body; }, }; await handler({ url: '/sources', method: 'GET' }, res); assert.equal(res.status, 200); return JSON.parse(res.body); } /** The browser catalog with the pose math stubbed out: only field * passthrough is under test here. */ function browserCatalog() { const model = { safeNumber: (value, fallback = NaN) => Number.isFinite(Number(value)) ? Number(value) : fallback, normalizeHeading: (deg) => ((deg % 360) + 360) % 360, clamp: (value, lo, hi) => Math.min(hi, Math.max(lo, value)), headingFromId: () => 0, normalizeFeedType: (type) => type, ensureCameraPose: () => {}, }; return createCatalog({ state: {}, services: { locations: { CITY_POIS: {} } }, parts: { model }, source: {}, }); } test('King County model and manufacturer survive the /api/cctv/sources boundary into the browser catalog', async (t) => { quiet(t); const root = mkdtempSync(path.join(tmpdir(), 'gev-kingcounty-')); t.after(() => rmSync(root, { recursive: true, force: true })); // No file/env catalog, so the live packs load; every pack but King County // sees an upstream failure and contributes nothing. withEnv(t, { CCTV_SOURCES_FILE: undefined, CCTV_SOURCES_JSON: undefined, CCTV_FORCE_AUSTIN: undefined, CCTV_PREFER_AUSTIN: undefined, CCTV_KINGCOUNTY_ENABLED: undefined, CCTV_KINGCOUNTY_MAX_SOURCES: undefined, CCTV_MAX_SOURCES: undefined, }); t.mock.method(globalThis, 'fetch', async (url) => { if (String(url) !== KING_COUNTY_CAMERAS_URL) return new Response('', { status: 503 }); return Response.json({ type: 'FeatureCollection', features: [ kingCountyFeature(56, 47.70059, -122.02443, { properties: { Model: 'Q6135-LE\n', Manufacturer: 'Axis' }, }), kingCountyFeature(90, 47.674, -122.119, { properties: { Model: null, Manufacturer: 'Cohu' }, }), ], }); }); const body = await requestSources(cctvProxy({ sourceRoot: root })); const served = new Map(body.sources.map((source) => [source.id, source])); assert.deepEqual([...served.keys()].sort(), [ 'kingcounty-56', 'kingcounty-90', ]); assert.equal(served.get('kingcounty-56').model, 'Q6135-LE'); assert.equal(served.get('kingcounty-56').manufacturer, 'Axis'); // Absent hardware is an explicit empty string on the wire, like // headingConfidence, so consumers never have to probe for the key. assert.equal(served.get('kingcounty-90').model, ''); assert.equal(served.get('kingcounty-90').manufacturer, ''); const cameras = browserCatalog().buildCatalogFromSources(body.sources); const byId = new Map(cameras.map((camera) => [camera.id, camera])); assert.equal(byId.get('kingcounty-56').model, 'Q6135-LE'); assert.equal(byId.get('kingcounty-56').manufacturer, 'Axis'); assert.equal(byId.get('kingcounty-90').model, ''); assert.equal(byId.get('kingcounty-90').manufacturer, ''); }); test('King County loader honors the max-sources cap', async (t) => { quiet(t); withEnv(t, { CCTV_KINGCOUNTY_MAX_SOURCES: '8' }); const features = []; for (let i = 0; i < 24; i += 1) { features.push( kingCountyFeature(100 + i, 47.2 + i * 0.02, -122.4 + i * 0.02), ); } t.mock.method(globalThis, 'fetch', async () => Response.json({ type: 'FeatureCollection', features }), ); const cameras = await loadKingCountySourcesFromOpenData(); assert.equal(cameras.length, 8); }); test('King County loader defaults its cap and survives upstream failure', async (t) => { quiet(t); withEnv(t, { CCTV_KINGCOUNTY_MAX_SOURCES: undefined }); assert.equal(DEFAULT_KING_COUNTY_MAX_SOURCES, 125); t.mock.method( globalThis, 'fetch', async () => new Response('', { status: 503 }), ); assert.deepEqual(await loadKingCountySourcesFromOpenData(), []); t.mock.method(globalThis, 'fetch', async () => { throw new Error('network down'); }); assert.deepEqual(await loadKingCountySourcesFromOpenData(), []); }); test('King County loader does not follow a redirected catalog', async (t) => { quiet(t); let init; t.mock.method(globalThis, 'fetch', async (_url, options) => { init = options; return new Response( JSON.stringify({ type: 'FeatureCollection', features: [kingCountyFeature(56, 47.70059, -122.02443)], }), { status: 302, headers: { Location: 'https://example.com/cams' } }, ); }); assert.deepEqual(await loadKingCountySourcesFromOpenData(), []); assert.equal(init.redirect, 'manual'); }); test('King County loader rejects an oversized catalog', async (t) => { quiet(t); const features = [kingCountyFeature(56, 47.70059, -122.02443)]; const body = JSON.stringify({ type: 'FeatureCollection', features, padding: 'x'.repeat(KING_COUNTY_MAX_CATALOG_BYTES), }); t.mock.method(globalThis, 'fetch', async () => new Response(body)); assert.deepEqual(await loadKingCountySourcesFromOpenData(), []); }); test('King County loader drops duplicate ids before the per-pack cap', async (t) => { quiet(t); withEnv(t, { CCTV_KINGCOUNTY_MAX_SOURCES: '8' }); // Repeats of the camera nearest the anchor would fill an 8-camera cap // before the second, farther camera if duplicates reached prioritization. const features = []; for (let i = 0; i < 10; i += 1) { features.push(kingCountyFeature(56, 47.4829, -122.2171)); } features.push(kingCountyFeature(57, 47.70059, -122.02443)); t.mock.method(globalThis, 'fetch', async () => Response.json({ type: 'FeatureCollection', features }), ); const cameras = await loadKingCountySourcesFromOpenData(); assert.deepEqual(cameras.map((camera) => camera.id).sort(), [ 'kingcounty-56', 'kingcounty-57', ]); }); test('King County loader omits vendor-generic model strings', async (t) => { quiet(t); withEnv(t, { CCTV_KINGCOUNTY_MAX_SOURCES: undefined }); t.mock.method(globalThis, 'fetch', async () => Response.json({ type: 'FeatureCollection', features: [ kingCountyFeature(61, 47.5, -122.2, { properties: { Model: 'PTZ', Manufacturer: 'Bosch' }, }), kingCountyFeature(62, 47.51, -122.2, { properties: { Model: 'PTZ IP', Manufacturer: 'Cohu' }, }), kingCountyFeature(63, 47.52, -122.2, { properties: { Model: 'BOSCH/PTZ', Manufacturer: 'Bosch' }, }), kingCountyFeature(64, 47.53, -122.2, { properties: { Model: 'AUTODOME IP 5000', Manufacturer: 'Bosch' }, }), ], }), ); const byId = new Map( (await loadKingCountySourcesFromOpenData()).map((camera) => [ camera.id, camera, ]), ); for (const id of ['kingcounty-61', 'kingcounty-62', 'kingcounty-63']) { assert.equal(byId.get(id).model, undefined, id); assert.equal(byId.get(id).manufacturer, undefined, id); } assert.equal(byId.get('kingcounty-64').model, 'AUTODOME IP 5000'); assert.equal(byId.get('kingcounty-64').manufacturer, 'Bosch'); });