Merge branch 'main' into feature/catalonia-cctv-cameras

This commit is contained in:
Sameh Khamis committed 2026-10-15 16:05:20 -07:00
commit a800d28ddf
14 files changed
+1296 -3

No files matched your search

+367
View File
@@ -0,0 +1,367 @@
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');
});
+423
View File
@@ -0,0 +1,423 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
parseRescuCameraList,
torontoCameraToSource,
loadTorontoSourcesFromOpenData,
} from '../../server/providers/cctv/sources.js';
import {
TORONTO_RESCU_LIST_URL,
TORONTO_RESCU_IMAGE_ORIGIN,
TORONTO_MAX_CATALOG_BYTES,
DEFAULT_CCTV_MAX_SOURCES,
} from '../../server/providers/cctv/constants.js';
import { CAMERA_CODE_MAX_CHARS } from '../../server/providers/cctv/normalize.js';
import { allocateSourceCap } from '../../server/providers/cctv/cap.js';
import { createCctvCatalog } from '../../server/providers/cctv/catalog.js';
/**
* A response whose body is a live stream, plus a flag that flips when the
* stream is cancelled. A rejection path that returns without cancelling holds
* the transport open, so the flag is what the refusal tests actually assert.
*/
const streamingResponse = (init = {}) => {
const state = { cancelled: false };
const body = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode('j'));
},
cancel() {
state.cancelled = true;
},
});
return { response: new Response(body, init), state };
};
/** One RESCU row, shaped like the live `tmcearthcameras.json` payload. */
const row = (overrides = {}) => ({
Number: '8048',
Name: 'BATHURST ST & BLOOR ST W',
Latitude: '43.665249',
Longitude: '-79.41114',
D1: 'n',
D2: 'e',
D3: 's',
D4: 'w',
Group: 'Arterial',
...overrides,
});
/** The city wraps the list in a JSONP callback; this is that envelope. */
const jsonp = (rows) =>
`jsonTMCEarthCamerasCallback(${JSON.stringify({ Data: rows })});`;
test('a RESCU row maps to a source on the constructed frame URL', () => {
const source = torontoCameraToSource(row());
assert.equal(source.id, 'toronto-8048');
assert.equal(source.name, 'BATHURST ST & BLOOR ST W');
assert.equal(source.city, 'Toronto');
assert.equal(source.cityId, 'toronto');
assert.equal(source.provider, 'City of Toronto');
assert.equal(source.lat, 43.665249);
assert.equal(source.lon, -79.41114);
assert.equal(source.feedType, 'image');
assert.equal(source.sourceKind, 'toronto-rescu-open-data');
assert.equal(
source.license,
'Contains information licensed under the Open Government Licence – Toronto',
);
assert.equal(
source.url,
`${TORONTO_RESCU_IMAGE_ORIGIN}loc8048.jpg`,
'the documented loc####.jpg convention',
);
assert.equal(source.url, source.snapshotUrl);
assert.ok(source.url.startsWith(TORONTO_RESCU_IMAGE_ORIGIN));
});
test('the frame URL is built from the camera number, never read from the payload', () => {
// The list carries no URL field at all, so an added one must stay inert: the
// frame proxy is pinned to the city origin by construction.
const planted = torontoCameraToSource(
row({
url: 'https://evil.example/x.jpg',
snapshotUrl: 'https://evil.example/x.jpg',
Url: 'https://evil.example/x.jpg',
}),
);
assert.equal(planted.url, `${TORONTO_RESCU_IMAGE_ORIGIN}loc8048.jpg`);
assert.equal(planted.snapshotUrl, planted.url);
// A number that is not plain digits cannot reach the URL builder: path
// traversal and absolute URLs are refused outright rather than escaped.
for (const Number_ of [
'../../etc/passwd',
'8048/../../x',
'https://evil.example/1',
'80 48',
'8048.jpg',
'-8048',
'',
' ',
null,
]) {
assert.equal(
torontoCameraToSource(row({ Number: Number_ })),
null,
`refused: ${String(Number_)}`,
);
}
});
test('the comparison directions never become a camera heading', () => {
// D1-D4 are the city's *comparison image* directions (static 2016 reference
// stills served from ComparisonImages/), not a facing for the live frame:
// these cameras pan. The letters read like a compass and map trivially onto
// one (n -> 0, w -> 270), so the temptation to wire them up is real — and a
// heading derived from them would be confidently wrong for every camera in
// the city. Nobody may "fix" this.
const base = torontoCameraToSource(row());
for (const dirs of [
{ D1: 'n', D2: 'e', D3: 's', D4: 'w' },
{ D1: '', D2: 'e', D3: '', D4: 'w' },
{ D1: '', D2: '', D3: '', D4: '' },
{ D1: 's', D2: '', D3: '', D4: '' },
]) {
const source = torontoCameraToSource(row(dirs));
assert.equal(source.headingConfidence, 'low');
assert.ok(Number.isFinite(source.headingDeg));
// Same id, same fallback heading, whatever the directions say.
assert.equal(source.headingDeg, base.headingDeg);
}
});
test('the two cameras at one intersection are kept apart', () => {
// 8047 and 8048 are both "BATHURST ST & BLOOR ST W", five metres apart. They
// are distinct units, so both survive, and the id-hash fallback fans their
// gizmos apart instead of stacking them on one bearing.
const a = torontoCameraToSource(
row({ Number: '8047', Latitude: '43.665223', Longitude: '-79.411083' }),
);
const b = torontoCameraToSource(row());
assert.equal(a.id, 'toronto-8047');
assert.equal(b.id, 'toronto-8048');
assert.notEqual(a.url, b.url);
assert.notEqual(a.headingDeg, b.headingDeg);
});
test('rows outside Toronto and unusable geometry are dropped', () => {
// Calgary: a plausible lat/lon, but not a Toronto camera.
assert.equal(
torontoCameraToSource(row({ Latitude: '51.0453', Longitude: '-114.1413' })),
null,
);
// Null island, and the empty strings that would become it.
assert.equal(
torontoCameraToSource(row({ Latitude: '0', Longitude: '0' })),
null,
);
assert.equal(
torontoCameraToSource(row({ Latitude: '', Longitude: '' })),
null,
);
assert.equal(
torontoCameraToSource(row({ Latitude: 'north', Longitude: 'west' })),
null,
);
assert.equal(torontoCameraToSource({}), null);
assert.equal(torontoCameraToSource(null), null);
});
test('a nameless row still gets a label', () => {
const source = torontoCameraToSource(row({ Name: ' ' }));
assert.equal(source.name, 'Toronto Camera 8048');
});
test('the JSONP envelope is unwrapped, and only that envelope', () => {
assert.deepEqual(parseRescuCameraList(jsonp([row()])), [row()]);
// Whitespace and a missing trailing semicolon are both tolerated.
assert.equal(
parseRescuCameraList(`\n ${jsonp([row()]).replace(/;$/, '')} \n`).length,
1,
);
// Should the city ever drop the wrapper, the bare document still parses.
assert.equal(
parseRescuCameraList(JSON.stringify({ Data: [row()] })).length,
1,
);
// Anything else yields no cameras rather than a thrown loader.
for (const text of [
'otherCallback({"Data":[]})',
'jsonTMCEarthCamerasCallback(',
'jsonTMCEarthCamerasCallback({"Data":',
'{"Data":"not-an-array"}',
'{"nope":[]}',
'<html>503</html>',
'',
null,
]) {
assert.deepEqual(parseRescuCameraList(text), [], `rejected: ${text}`);
}
});
test('the loader reads the keyless list and collapses duplicate numbers', async (t) => {
t.mock.method(console, 'log', () => {});
const requested = [];
t.mock.method(globalThis, 'fetch', async (url) => {
requested.push(String(url));
return new Response(
jsonp([
row(),
// Same camera number twice: one camera, not two.
row({ Name: 'BATHURST ST & BLOOR ST W (duplicate)' }),
row({
Number: '8001',
Name: 'YORK ST & BREMNER BLVD / RAPTORS WAY',
Latitude: '43.643120',
Longitude: '-79.381386',
}),
row({ Number: '../evil' }),
]),
);
});
const cameras = await loadTorontoSourcesFromOpenData();
assert.deepEqual(requested, [TORONTO_RESCU_LIST_URL]);
assert.deepEqual(
cameras.map((camera) => camera.id),
// Nearest downtown first: York & Bremner is at the core, Bathurst & Bloor
// is three kilometres out.
['toronto-8001', 'toronto-8048'],
);
assert.equal(cameras[1].name, 'BATHURST ST & BLOOR ST W');
});
test('an upstream failure yields an empty pack and releases the response', async (t) => {
t.mock.method(console, 'warn', () => {});
// A 503 can still arrive with a streaming body; returning without cancelling
// it would hold the connection until the socket times out.
const failed = streamingResponse({ status: 503 });
t.mock.method(globalThis, 'fetch', async () => failed.response);
assert.deepEqual(await loadTorontoSourcesFromOpenData(), []);
assert.equal(failed.state.cancelled, true, 'the failed body is cancelled');
t.mock.restoreAll();
t.mock.method(console, 'warn', () => {});
t.mock.method(globalThis, 'fetch', async () => {
throw new Error('network down');
});
assert.deepEqual(await loadTorontoSourcesFromOpenData(), []);
});
test('the list fetch refuses redirects and oversized bodies', async (t) => {
t.mock.method(console, 'warn', () => {});
// A redirect is never followed: the list host cannot be steered. Its body is
// a live stream, so the test also proves the refusal releases the transport.
const seen = [];
const redirected = streamingResponse({
status: 302,
headers: { location: 'https://evil.example/cameras.json' },
});
t.mock.method(globalThis, 'fetch', async (url, init) => {
seen.push([String(url), init.redirect]);
return redirected.response;
});
assert.deepEqual(await loadTorontoSourcesFromOpenData(), []);
assert.deepEqual(seen, [[TORONTO_RESCU_LIST_URL, 'manual']]);
assert.equal(
redirected.state.cancelled,
true,
'the redirect body is cancelled',
);
// A body over the cap is refused rather than buffered.
t.mock.restoreAll();
t.mock.method(console, 'warn', () => {});
t.mock.method(globalThis, 'fetch', async () => {
return new Response(jsonp([row()]), {
headers: {
'content-length': String(TORONTO_MAX_CATALOG_BYTES + 1),
},
});
});
assert.deepEqual(await loadTorontoSourcesFromOpenData(), []);
// So is a body that only declares its size once it is already too long.
t.mock.restoreAll();
t.mock.method(console, 'warn', () => {});
t.mock.method(globalThis, 'fetch', async () => {
const oversized = 'x'.repeat(TORONTO_MAX_CATALOG_BYTES + 1024);
return new Response(
new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode(oversized));
controller.close();
},
}),
);
});
assert.deepEqual(await loadTorontoSourcesFromOpenData(), []);
});
test('the unselected label is the intersection, trimmed to the code width', () => {
assert.equal(torontoCameraToSource(row()).code, 'BATHURST ST & BLOOR ST W');
const long = torontoCameraToSource(
row({
Name: 'BAYVIEW AVE & POTTERY RD / PRIVATE ACCESS @ 620m SOUTH OF - 550 BAYVIEW AVE (EVERGREEN)',
}),
);
assert.ok(long.code.length <= CAMERA_CODE_MAX_CHARS);
assert.ok(long.code.endsWith('…'));
assert.ok(long.code.startsWith('BAYVIEW AVE & POTTERY'));
});
test('a reduced catalog cap thins every pack instead of dropping Toronto', () => {
// Toronto merges last in LIVE_PACKS, so a positional slice would delete it
// outright. The round-robin allocation must give it its share.
const lane = (name, count) => ({
name,
sources: Array.from({ length: count }, (_, i) => ({ id: `${name}-${i}` })),
});
const { sources, packs } = allocateSourceCap(
[lane('austin', 250), lane('calgary', 215), lane('toronto', 336)],
30,
);
assert.equal(sources.length, 30);
assert.deepEqual(
packs.map((p) => [p.name, p.kept]),
[
['austin', 10],
['calgary', 10],
['toronto', 10],
],
);
// Toronto contributes its own highest-priority cameras, in its own order.
assert.deepEqual(
sources.filter((s) => s.id.startsWith('toronto-')).map((s) => s.id),
Array.from({ length: 10 }, (_, i) => `toronto-${i}`),
);
});
/**
* Serve the RESCU list to the Toronto endpoint and an empty payload to every
* other pack, so one catalog refresh exercises the registration without
* reaching the network. Returns the URLs that were requested.
*/
const runCatalogWithMockedUpstreams = async (t) => {
const requested = [];
t.mock.method(console, 'log', () => {});
t.mock.method(console, 'warn', () => {});
t.mock.method(globalThis, 'fetch', async (url) => {
const href = String(url);
requested.push(href);
if (href === TORONTO_RESCU_LIST_URL) {
return new Response(
jsonp([
row({
Number: '8001',
Name: 'YORK ST & BREMNER BLVD / RAPTORS WAY',
Latitude: '43.643120',
Longitude: '-79.381386',
}),
row(),
]),
);
}
return Response.json([]);
});
const sources = await createCctvCatalog({ sourceRoot: '/nonexistent' })();
return { requested, sources };
};
test('the pack is registered, so its cameras reach the served catalog', async (t) => {
const saved = { ...process.env };
try {
delete process.env.CCTV_SOURCES_FILE;
delete process.env.CCTV_SOURCES_JSON;
delete process.env.CCTV_TORONTO_ENABLED;
const { requested, sources } = await runCatalogWithMockedUpstreams(t);
assert.ok(
requested.includes(TORONTO_RESCU_LIST_URL),
'the registered lane reaches its upstream',
);
assert.deepEqual(
sources.filter((s) => s.cityId === 'toronto').map((s) => s.id),
['toronto-8001', 'toronto-8048'],
'Toronto cameras reach the served catalog through the registered lane',
);
} finally {
for (const key of Object.keys(process.env)) {
if (!(key in saved)) delete process.env[key];
}
Object.assign(process.env, saved);
}
});
test('CCTV_TORONTO_ENABLED=0 keeps the lane from being loaded at all', async (t) => {
const saved = { ...process.env };
try {
delete process.env.CCTV_SOURCES_FILE;
delete process.env.CCTV_SOURCES_JSON;
process.env.CCTV_TORONTO_ENABLED = '0';
const { requested, sources } = await runCatalogWithMockedUpstreams(t);
assert.equal(
requested.includes(TORONTO_RESCU_LIST_URL),
false,
'the disabled lane never reaches its upstream',
);
assert.deepEqual(
sources.filter((s) => s.cityId === 'toronto'),
[],
);
} finally {
for (const key of Object.keys(process.env)) {
if (!(key in saved)) delete process.env[key];
}
Object.assign(process.env, saved);
}
});
test('the shipped catalog ceiling is not raised to make room for this pack', () => {
assert.equal(DEFAULT_CCTV_MAX_SOURCES, 4000);
});
+15
View File
@@ -262,6 +262,15 @@ export const DATA_CREDITS = [
'(Llicència oberta d’ús d’informació - Catalunya); some cameras are supplied by partners ' +
'(Ajuntament de Barcelona, Ajuntament de Terrassa), each named in the CCTV panel',
},
{
key: 'kingcounty-cctv',
html:
'CCTV cameras &amp; frames (King County, WA): King County Road Services — ' +
'<a href="https://kingcounty.gov/en/dept/local-services/roads" target="_blank" rel="noopener">kingcounty.gov</a>, ' +
'per the ' +
'<a href="https://kingcounty.gov/en/dept/kcit/data-information-services/gis-center/about/terms-conditions-copyrights" target="_blank" rel="noopener">KCGIS Center terms</a>. ' +
'Partner-city cameras hosted on the county image handler (e.g. Newcastle) name their owner in the CCTV panel.',
},
{
key: 'vegvesen-cctv',
html:
@@ -276,6 +285,12 @@ export const DATA_CREDITS = [
'<a href="https://webcams.nyctmc.org/" target="_blank" rel="noopener">NYC DOT Traffic Management Center</a>. ' +
NYC_DOT_DISCLAIMER,
},
{
key: 'toronto-cctv',
html:
'Traffic cameras (Toronto): contains information licensed under the ' +
'<a href="https://www.toronto.ca/city-government/data-research-maps/open-data/open-data-licence/" target="_blank" rel="noopener">Open Government Licence – Toronto</a>',
},
{
key: 'gbfs',
html: