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_AUSTIN_MAX_SOURCES, DEFAULT_CALTRANS_MAX_SOURCES, DEFAULT_CCTV_MAX_SOURCES, CCTV_MAX_SOURCES_CEILING, DEFAULT_DRIVEBC_MAX_SOURCES, DEFAULT_TFL_MAX_SOURCES, DEFAULT_ONTARIO_MAX_SOURCES, DEFAULT_FINTRAFFIC_MAX_SOURCES, DEFAULT_TXDOT_MAX_SOURCES, DEFAULT_TALLINN_MAX_SOURCES, DEFAULT_TARKTEE_MAX_SOURCES, DEFAULT_NSW_MAX_SOURCES, DRIVEBC_WEBCAMS_URL, } from '../../server/providers/cctv/constants.js'; import { driveBcImageCredit, loadDriveBcSourcesFromOpenData, } 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', () => {}); } /** A DriveBC `/api/webcams/` row carrying the fields the loader reads. */ function driveBcRow(id, lat, lon, overrides = {}) { return { id, name: `Camera ${id}`, is_on: true, should_appear: true, region_name: 'Lower Mainland', orientation: 'N', elevation: 12, location: { type: 'Point', coordinates: [lon, lat] }, links: { imageDisplay: `/images/${id}.jpg?t=1` }, ...overrides, }; } test('DriveBC loader keeps published cameras and builds frame URLs from the camera id', async (t) => { quiet(t); withEnv(t, { CCTV_DRIVEBC_MAX_SOURCES: undefined }); const requested = []; t.mock.method(globalThis, 'fetch', async (url) => { requested.push(String(url)); return Response.json([ driveBcRow(13, 49.001268, -122.756658, { name: 'Douglas (Peace Arch) Border Crossing', region_name: 'Border Cams', elevation: 10, // The payload's own link is ignored; frames come from the pinned host. links: { imageDisplay: 'https://example.com/13.jpg' }, }), driveBcRow(682, 49.0579, -123.0569, { orientation: 'se', elevation: null, credit: 'Images courtesy of TransLink', }), driveBcRow(1, 49.2, -123.1, { is_on: false }), driveBcRow(2, 49.2, -123.1, { should_appear: false }), driveBcRow('3', 49.2, -123.1), driveBcRow(4, null, -123.1), driveBcRow(5, 49.2, -123.1, { location: null }), driveBcRow(6, 1000, -123.1), driveBcRow(7, 34.05, -118.24), ]); }); const cameras = await loadDriveBcSourcesFromOpenData(); assert.deepEqual(requested, [DRIVEBC_WEBCAMS_URL]); // Nearest to the Vancouver anchor first: the pack is always distance-sorted // so the catalog-wide cap thins it from the far end. assert.deepEqual( cameras.map((camera) => camera.id), ['drivebc-682', 'drivebc-13'], ); const border = cameras.find((camera) => camera.id === 'drivebc-13'); const deltaport = cameras.find((camera) => camera.id === 'drivebc-682'); assert.equal(border.url, 'https://www.drivebc.ca/images/13.jpg'); assert.equal(border.snapshotUrl, border.url); assert.equal(border.name, 'Douglas (Peace Arch) Border Crossing'); assert.equal(border.city, 'BC Border'); assert.equal(border.provider, 'DriveBC'); assert.equal(border.feedType, 'image'); assert.equal(border.headingDeg, 0); assert.equal(border.headingConfidence, 'high'); assert.equal(border.groundElevationM, 10); assert.match(border.license, /Open Government Licence – British Columbia/); assert.equal( border.credit, '', 'a provincial camera carries no partner credit', ); assert.equal( deltaport.credit, 'Images courtesy of TransLink', 'a partner-supplied camera names its owner, HTML stripped', ); assert.equal(deltaport.city, 'Lower Mainland'); assert.equal( deltaport.headingDeg, 135, 'orientation codes are case-insensitive', ); assert.equal( deltaport.groundElevationM, 0, 'a missing elevation falls back to sea level', ); }); test('DriveBC loader keeps the cameras nearest Vancouver and Victoria under its cap', async (t) => { quiet(t); withEnv(t, { CCTV_DRIVEBC_MAX_SOURCES: '8' }); const rows = [ // Fort Nelson rows come first in payload order, so only distance can drop them. ...[58.8, 58.81, 58.82, 58.83].map((lat, i) => driveBcRow(100 + i, lat, -122.7), ), ...[49.28, 49.27, 49.26, 49.25].map((lat, i) => driveBcRow(200 + i, lat, -123.12), ), ...[48.43, 48.44, 48.45, 48.46].map((lat, i) => driveBcRow(300 + i, lat, -123.37), ), ]; t.mock.method(globalThis, 'fetch', async () => Response.json(rows)); const cameras = await loadDriveBcSourcesFromOpenData(); assert.equal(cameras.length, 8); assert.deepEqual( cameras .map((camera) => camera.id) .filter((id) => /^drivebc-10\d$/.test(id)), [], 'the northern cameras are the ones dropped', ); }); test('DriveBC loader fails soft on HTTP errors, unexpected payloads and network errors', async (t) => { quiet(t); let respond; t.mock.method(globalThis, 'fetch', (...args) => respond(...args)); for (respond of [ async () => new Response('unavailable', { status: 503 }), async () => Response.json({ webcams: [] }), async () => { throw new TypeError('fetch failed'); }, ]) { assert.deepEqual(await loadDriveBcSourcesFromOpenData(), []); } }); test('CCTV catalog merges DriveBC cameras and CCTV_DRIVEBC_ENABLED=0 skips the 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_CALTRANS_DISTRICTS: undefined, CCTV_TFL_ENABLED: undefined, CCTV_DRIVEBC_ENABLED: undefined, CCTV_DRIVEBC_MAX_SOURCES: undefined, }); const requested = []; t.mock.method(globalThis, 'fetch', async (url) => { requested.push(String(url)); // Every other live pack is down; only DriveBC answers. return String(url) === DRIVEBC_WEBCAMS_URL ? Response.json([driveBcRow(13, 49.001268, -122.756658)]) : new Response('unavailable', { status: 503 }); }); const sources = await createCctvCatalog({ sourceRoot })(); assert.deepEqual( sources.map((source) => source.id), ['drivebc-13'], ); assert.equal(sources[0].url, 'https://www.drivebc.ca/images/13.jpg'); process.env.CCTV_DRIVEBC_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(DRIVEBC_WEBCAMS_URL), false, 'a disabled pack makes no request', ); }); test('default per-pack camera caps fit inside the default catalog cap', () => { // Every default pack at its own cap (Warendorf ships 1 curated camera). const packs = DEFAULT_AUSTIN_MAX_SOURCES + DEFAULT_CALTRANS_MAX_SOURCES + DEFAULT_TFL_MAX_SOURCES + DEFAULT_ONTARIO_MAX_SOURCES + DEFAULT_FINTRAFFIC_MAX_SOURCES + DEFAULT_DRIVEBC_MAX_SOURCES + DEFAULT_TXDOT_MAX_SOURCES + DEFAULT_TALLINN_MAX_SOURCES + DEFAULT_TARKTEE_MAX_SOURCES + DEFAULT_NSW_MAX_SOURCES + 1; assert.ok( packs <= DEFAULT_CCTV_MAX_SOURCES, `default packs (${packs}) would be thinned by the catalog cap (${DEFAULT_CCTV_MAX_SOURCES})`, ); assert.ok( DEFAULT_CCTV_MAX_SOURCES <= CCTV_MAX_SOURCES_CEILING, 'the default cap must sit inside the CCTV_MAX_SOURCES ceiling', ); }); test('DriveBC credit keeps partner attribution and drops operational notes', () => { assert.equal( driveBcImageCredit( 'Images courtesy of TransLink', ), 'Images courtesy of TransLink', ); assert.equal( driveBcImageCredit('Camera image provided by City of Vancouver'), 'Camera image provided by City of Vancouver', ); assert.equal( driveBcImageCredit( 'BC HighwayCam presented in cooperation with AtkinsRéalis.', ), 'BC HighwayCam presented in cooperation with AtkinsRéalis.', ); assert.equal( driveBcImageCredit( 'This camera is located in a remote area and relies on solar power. As a result, transmission delays may occur.', ), '', ); assert.equal(driveBcImageCredit(null), ''); });