import { test } from 'node:test'; import assert from 'node:assert/strict'; import { TARKTEE_IMAGES_URL, TARKTEE_LOCATIONS_URL, TARKTEE_MAX_CATALOG_BYTES, } from '../../server/providers/cctv/constants.js'; import { loadTallinnSourcesFromCatalog, loadTarkteeSourcesFromDatex, parseTarkteeDatexImages, parseTarkteeDatexLocations, } from '../../server/providers/cctv/sources.js'; /** Silence the loaders' progress and failure logging. */ function quiet(t) { t.mock.method(console, 'log', () => {}); t.mock.method(console, 'warn', () => {}); } const SAMPLE_LOCATIONS_XML = ` Test Site 59.4370 24.7530 `; const SAMPLE_IMAGES_XML = ` https://tarktee.transpordiamet.ee/images/42/42_202608251542.jpg image `; test('Tallinn catalog loads curated ristmikud stills only', () => { const cameras = loadTallinnSourcesFromCatalog(); assert.ok( cameras.length >= 200, `expected a full Tallinn pack, got ${cameras.length}`, ); for (const camera of cameras.slice(0, 25)) { assert.match(camera.id, /^tln-/); assert.equal(camera.cityId, 'tallinn'); assert.equal(camera.feedType, 'image'); assert.equal(camera.sourceKind, 'tallinn-ristmikud'); assert.ok(camera.url.startsWith('https://ristmikud.tallinn.ee/')); assert.equal(camera.url, camera.snapshotUrl); assert.ok(Number.isFinite(camera.lat) && Number.isFinite(camera.lon)); } }); test('Tarktee DATEX parsers join location UUIDs to image URLs', () => { const locations = parseTarkteeDatexLocations(SAMPLE_LOCATIONS_XML); const images = parseTarkteeDatexImages(SAMPLE_IMAGES_XML); assert.equal(locations.size, 1); assert.equal(images.size, 1); const id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'; assert.equal(locations.get(id)?.name, 'Test Site'); assert.equal(locations.get(id)?.lat, 59.437); assert.equal(locations.get(id)?.lon, 24.753); assert.equal( images.get(id), 'https://tarktee.transpordiamet.ee/images/42/42_202608251542.jpg', ); }); test('Tarktee DATEX image parser rejects non-official hosts', () => { const xml = SAMPLE_IMAGES_XML.replace( 'https://tarktee.transpordiamet.ee/images/42/42_202608251542.jpg', 'https://evil.example/images/42/42.jpg', ); const images = parseTarkteeDatexImages(xml); assert.equal(images.size, 0); }); test('Tarktee loader deduplicates cameras sharing an image number before the cap', async (t) => { quiet(t); const dupLocationsXml = ` Tallinn Ringtee 59.437024.7530 Tallinn Ringtee duplicate 59.437124.7531 `; const dupImagesXml = ` https://tarktee.transpordiamet.ee/images/42/42_1.jpg https://tarktee.transpordiamet.ee/images/42/42_2.jpg `; const seen = []; t.mock.method(globalThis, 'fetch', async (url, init) => { seen.push([String(url), init?.redirect]); const body = String(url) === TARKTEE_LOCATIONS_URL ? dupLocationsXml : dupImagesXml; return new Response(body, { status: 200, headers: { 'Content-Type': 'application/xml' }, }); }); const cameras = await loadTarkteeSourcesFromDatex(); assert.deepEqual(seen, [ [TARKTEE_LOCATIONS_URL, 'manual'], [TARKTEE_IMAGES_URL, 'manual'], ]); assert.equal(cameras.length, 1); assert.equal(cameras[0].id, 'ee-tarktee-42'); assert.equal(cameras[0].name, 'Tallinn Ringtee'); }); test('Tarktee loader refuses a redirect and cancels both response bodies', async (t) => { quiet(t); const cancelled = { locations: false, images: false }; t.mock.method(globalThis, 'fetch', async (url) => { const isLocations = String(url) === TARKTEE_LOCATIONS_URL; const body = new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode('')); }, cancel() { if (isLocations) cancelled.locations = true; else cancelled.images = true; }, }); return new Response(body, { status: isLocations ? 302 : 200, headers: isLocations ? { location: 'https://evil.example/loc.xml' } : {}, }); }); assert.deepEqual(await loadTarkteeSourcesFromDatex(), []); assert.equal(cancelled.locations, true, 'redirected body is cancelled'); assert.equal(cancelled.images, true, 'sibling 200 body is also cancelled'); }); test('Tarktee loader cancels fulfilled bodies when either fetch rejects', async (t) => { for (const failedUrl of [TARKTEE_LOCATIONS_URL, TARKTEE_IMAGES_URL]) { for (const delayedSibling of [false, true]) { await t.test( `${failedUrl}, delayed sibling: ${delayedSibling}`, async (t) => { quiet(t); let cancelled = false; const response = new Response( new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode('')); }, cancel() { cancelled = true; }, }), ); t.mock.method(globalThis, 'fetch', async (url) => { if (String(url) === failedUrl) throw new TypeError('fetch failed'); if (delayedSibling) await new Promise(setImmediate); return response; }); try { assert.deepEqual(await loadTarkteeSourcesFromDatex(), []); assert.equal( cancelled, true, 'fulfilled sibling body is cancelled', ); } finally { await response.body.cancel(); } }, ); } } }); test('Tarktee loader cancels the sibling body when one feed fails or overflows the byte cap', async (t) => { quiet(t); let siblingCancelled = false; t.mock.method(globalThis, 'fetch', async (url) => { const isLocations = String(url) === TARKTEE_LOCATIONS_URL; if (isLocations) { return new Response('', { status: 503 }); } const body = new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode(SAMPLE_IMAGES_XML)); }, cancel() { siblingCancelled = true; }, }); return new Response(body, { status: 200 }); }); assert.deepEqual(await loadTarkteeSourcesFromDatex(), []); assert.equal( siblingCancelled, true, 'sibling 200 stream is cancelled on 503', ); let overflowSiblingCancelled = false; t.mock.method(globalThis, 'fetch', async (url) => { const isLocations = String(url) === TARKTEE_LOCATIONS_URL; if (isLocations) { const padded = SAMPLE_LOCATIONS_XML + ' '.repeat(TARKTEE_MAX_CATALOG_BYTES); return new Response(padded, { status: 200, headers: { 'Content-Length': String(padded.length) }, }); } const body = new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode(SAMPLE_IMAGES_XML)); }, cancel() { overflowSiblingCancelled = true; }, }); return new Response(body, { status: 200 }); }); assert.deepEqual(await loadTarkteeSourcesFromDatex(), []); assert.equal( overflowSiblingCancelled, true, 'sibling stream is cancelled when the other feed exceeds the byte cap', ); }); test('Tarktee DATEX requests wait 30 s, past the slow answers measured live', async (t) => { const timeouts = []; const timeout = AbortSignal.timeout; t.mock.method(AbortSignal, 'timeout', (ms) => { timeouts.push(ms); return timeout.call(AbortSignal, ms); }); t.mock.method(console, 'log', () => {}); t.mock.method( globalThis, 'fetch', async (url) => new Response( String(url).includes('roadCameraLocations') ? SAMPLE_LOCATIONS_XML : SAMPLE_IMAGES_XML, ), ); const cameras = await loadTarkteeSourcesFromDatex(); assert.deepEqual(timeouts, [30_000, 30_000]); assert.equal(cameras.length, 1); });