import { afterEach, beforeEach, test } from 'node:test'; import assert from 'node:assert/strict'; import { loadTaiwanSources, parseTaiwanCctvXml, resetTaiwanSourcesCache, taiwanCameraToSource, taiwanFrame, TAIWAN_FREEWAY_CCTV_URL, TAIWAN_MAX_CATALOG_BYTES, TAIWAN_RETRY_AFTER_FAILURE_MS, } from '../../server/providers/cctv/taiwan.js'; import { createCctvCatalog } from '../../server/providers/cctv/catalog.js'; // Settings that would steer the loader or the catalog away from the mocks. const ISOLATED_ENV = [ 'CCTV_SOURCES_FILE', 'CCTV_SOURCES_JSON', 'CCTV_PREFER_AUSTIN', 'CCTV_FORCE_AUSTIN', 'CCTV_TAIWAN_ENABLED', 'CCTV_TAIWAN_URL', 'CCTV_TAIWAN_MAX_SOURCES', ]; let savedEnv = {}; beforeEach(() => { resetTaiwanSourcesCache(); savedEnv = Object.fromEntries( ISOLATED_ENV.map((key) => [key, process.env[key]]), ); for (const key of ISOLATED_ENV) delete process.env[key]; }); afterEach(() => { for (const [key, value] of Object.entries(savedEnv)) { if (value === undefined) delete process.env[key]; else process.env[key] = value; } }); function cctvXml(...blocks) { return ` NFB ${blocks.join('')} `; } function block({ id = 'CCTV-N1-S-0.000-M', stream = 'https://cctvn.freeway.gov.tw/abs2mjpg/bmjpg?camera=10000', lat = '25.1229931', lon = '121.735695', } = {}) { return ` ${id} ${stream} ${lon} ${lat} 國道1號 S 基隆端基隆交流道 0K+000 `; } test('parses the MOTC 2.0 table and registers the same camera still', () => { const [row] = parseTaiwanCctvXml(cctvXml(block())); const camera = taiwanCameraToSource(row); assert.equal(camera.id, 'tw-freeway-cctvn-10000'); assert.equal(camera.lat, 25.1229931); assert.equal(camera.lon, 121.735695); assert.equal(camera.name, '國道1號 S 0K+000 基隆端→基隆交流道'); assert.equal(camera.cityId, 'taiwan'); assert.equal(camera.feedType, 'image'); assert.equal(camera.headingConfidence, 'low'); assert.equal( camera.url, 'https://cctvn.freeway.gov.tw/abs2mjpg/jpg?camera=10000', ); assert.equal(camera.snapshotUrl, camera.url); assert.match(camera.license, /Open Government Data License, Taiwan/); }); test('each camera server maps its stream path to its own still path only', () => { assert.equal( taiwanFrame( 'https://cctvs.freeway.gov.tw/live-view/mjpg/video.cgi?camera=373', ).frameUrl, 'https://cctvs.freeway.gov.tw/live-view/jpg/image.cgi?camera=373', ); assert.equal( taiwanFrame( 'https://cctvn5.freeway.gov.tw/abs2mjpg/bmjpg?camera=c3a580c9-0232-46a1-bfc6-927590758ab4', ).frameUrl, 'https://cctvn5.freeway.gov.tw/abs2mjpg/jpg?camera=c3a580c9-0232-46a1-bfc6-927590758ab4', ); assert.equal( taiwanFrame( 'https://cctvc.freeway.gov.tw/abs2mjpg/bmjpg?camera=495&extra=1', ).frameUrl, 'https://cctvc.freeway.gov.tw/abs2mjpg/jpg?camera=495', ); }); test('off-host, downgraded or malformed stream URLs never enter the proxy registry', () => { for (const stream of [ 'http://cctvn.freeway.gov.tw/abs2mjpg/bmjpg?camera=10000', 'https://cctvn.freeway.gov.tw.evil.test/abs2mjpg/bmjpg?camera=10000', 'https://user:pass@cctvn.freeway.gov.tw/abs2mjpg/bmjpg?camera=10000', 'https://cctvn.freeway.gov.tw:8443/abs2mjpg/bmjpg?camera=10000', 'https://cctvn.freeway.gov.tw/live-view/mjpg/video.cgi?camera=10000', 'https://cctvs.freeway.gov.tw/abs2mjpg/bmjpg?camera=373', 'https://cctvn.freeway.gov.tw/abs2mjpg/bmjpg?camera=../../admin', 'https://cctvs.freeway.gov.tw/live-view/mjpg/video.cgi?cacamera=373', 'http://127.0.0.1/private', '', ]) { assert.equal(taiwanFrame(stream), null, stream); const [row] = parseTaiwanCctvXml(cctvXml(block({ stream }))); assert.equal(taiwanCameraToSource(row), null, stream); } }); test('coordinates outside Taiwan, swapped or missing are rejected', () => { for (const [lat, lon] of [ ['121.735695', '25.1229931'], ['35.68', '139.76'], ['', '121.7'], ['NaN', '121.7'], ]) { const [row] = parseTaiwanCctvXml(cctvXml(block({ lat, lon }))); assert.equal(taiwanCameraToSource(row), null, `${lat},${lon}`); } assert.equal(taiwanCameraToSource(null), null); }); test('Chinese place names in CCTVID stay in the record without shaping the key', () => { const [row] = parseTaiwanCctvXml( cctvXml(block({ id: 'CCTV-N1-N-55-R-中壢服務區&(北)' })), ); const camera = taiwanCameraToSource(row); assert.equal(camera.id, 'tw-freeway-cctvn-10000'); assert.equal(row.cctvId, 'CCTV-N1-N-55-R-中壢服務區&(北)'); }); function mockCatalog(t, xml) { const requested = []; t.mock.method(console, 'log', () => {}); t.mock.method(console, 'warn', () => {}); t.mock.method(globalThis, 'fetch', async (url, options) => { requested.push(String(url)); if (String(url) === TAIWAN_FREEWAY_CCTV_URL) { assert.equal(options.redirect, 'manual'); return new Response(xml, { headers: { 'content-type': 'application/xml' }, }); } return Response.json([]); }); return requested; } test('loader deduplicates cameras that share a camera number', async (t) => { mockCatalog( t, cctvXml( block(), block({ id: 'CCTV-N1-S-0.000-M-dup' }), block({ stream: 'https://cctvc.freeway.gov.tw/abs2mjpg/bmjpg?camera=495', lat: '24.1', lon: '120.6', }), ), ); assert.deepEqual((await loadTaiwanSources()).map((c) => c.id).sort(), [ 'tw-freeway-cctvc-495', 'tw-freeway-cctvn-10000', ]); }); test('a redirected or failed catalog contributes an empty pack', async (t) => { t.mock.method(console, 'warn', () => {}); for (const status of [302, 503]) { t.mock.method( globalThis, 'fetch', async () => new Response(cctvXml(block()), { status, headers: status === 302 ? { location: 'https://evil.test/' } : {}, }), ); resetTaiwanSourcesCache(); assert.deepEqual(await loadTaiwanSources(), []); } }); async function runCatalogWithTaiwanFlag(t, flag) { const requested = mockCatalog(t, cctvXml(block())); if (flag !== undefined) process.env.CCTV_TAIWAN_ENABLED = flag; const sources = await createCctvCatalog({ sourceRoot: '/nonexistent' })(); return { requested, sources }; } test('the Taiwan pack is in the catalog by default', async (t) => { const { sources } = await runCatalogWithTaiwanFlag(t, undefined); assert.equal( sources.find((c) => c.id === 'tw-freeway-cctvn-10000')?.cityId, 'taiwan', ); }); test('CCTV_TAIWAN_ENABLED=0 makes no Freeway Bureau request', async (t) => { const { requested, sources } = await runCatalogWithTaiwanFlag(t, '0'); assert.ok(requested.length > 0, 'the other packs still refreshed'); assert.equal( requested.some((href) => href.includes('freeway.gov.tw')), false, ); assert.ok(!sources.some((c) => c.cityId === 'taiwan')); }); test('a catalog over the byte cap or a failed request contributes an empty pack', async (t) => { t.mock.method(console, 'warn', () => {}); const padding = ``; t.mock.method( globalThis, 'fetch', async () => new Response(cctvXml(block()) + padding), ); assert.deepEqual(await loadTaiwanSources(), []); resetTaiwanSourcesCache(); t.mock.method(globalThis, 'fetch', async () => { throw new TypeError('fetch failed'); }); assert.deepEqual(await loadTaiwanSources(), []); }); test('a failed download is not retried until the backoff ends', async (t) => { t.mock.method(console, 'warn', () => {}); let calls = 0; t.mock.method(globalThis, 'fetch', async () => { calls += 1; throw new TypeError('fetch failed'); }); t.mock.method(Date, 'now', () => 1_000_000); assert.deepEqual(await loadTaiwanSources(), []); assert.deepEqual(await loadTaiwanSources(), []); assert.equal(calls, 1, 'the second refresh made no request'); Date.now.mock.mockImplementation( () => 1_000_000 + TAIWAN_RETRY_AFTER_FAILURE_MS + 1, ); await loadTaiwanSources(); assert.equal(calls, 2, 'the backoff ends after an hour'); }); test('a failed download serves the last good table', async (t) => { t.mock.method(console, 'log', () => {}); t.mock.method(console, 'warn', () => {}); const fetchMock = t.mock.method( globalThis, 'fetch', async () => new Response(cctvXml(block())), ); const first = await loadTaiwanSources(); assert.deepEqual( first.map((c) => c.id), ['tw-freeway-cctvn-10000'], ); fetchMock.mock.mockImplementation( async () => new Response('', { status: 503 }), ); assert.deepEqual( (await loadTaiwanSources()).map((c) => c.id), ['tw-freeway-cctvn-10000'], ); }); test('CCTV_TAIWAN_MAX_SOURCES is clamped to 1-2000', async (t) => { t.mock.method(console, 'log', () => {}); const rows = Array.from({ length: 3 }, (_, i) => block({ stream: `https://cctvn.freeway.gov.tw/abs2mjpg/bmjpg?camera=${i + 1}`, }), ); t.mock.method( globalThis, 'fetch', async () => new Response(cctvXml(...rows)), ); for (const [value, expected] of [ ['0', 1], ['2', 2], ['5000', 3], ['abc', 3], ]) { resetTaiwanSourcesCache(); process.env.CCTV_TAIWAN_MAX_SOURCES = value; const sources = await loadTaiwanSources(); assert.equal(sources.length, expected, value); assert.equal(sources.available, 3, value); } }); test('names fall back to the published ID when the row has no road details', () => { const xml = cctvXml(block({ id: 'CCTV-N3-N-100-M' })) .replace('國道1號', '') .replace('0K+000', '') .replace('基隆端基隆交流道', ''); const camera = taiwanCameraToSource(parseTaiwanCctvXml(xml)[0]); assert.equal(camera.name, 'CCTV-N3-N-100-M'); }); test('numeric character references in the table are decoded', () => { const xml = cctvXml(block()).replace( '國道1號', '國道1號', ); assert.equal(parseTaiwanCctvXml(xml)[0].roadName, '國道1號'); }); test('camera numbers that are UUIDs keep their case in the frame URL only', () => { const camera = taiwanCameraToSource( parseTaiwanCctvXml( cctvXml( block({ stream: 'https://cctvn5.freeway.gov.tw/abs2mjpg/bmjpg?camera=C3A580C9-0232-46A1-BFC6-927590758AB4', }), ), )[0], ); assert.equal( camera.id, 'tw-freeway-cctvn5-c3a580c9-0232-46a1-bfc6-927590758ab4', ); assert.match(camera.url, /camera=C3A580C9-0232-46A1-BFC6-927590758AB4$/); });