mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
The catalog fetch now refuses redirects, cancels rejected bodies and reads through the shared capped JSON reader. Duplicate ids are dropped before prioritization so they cannot use up the per-pack cap. Vendor-generic model strings (PTZ, PTZ IP, BOSCH/PTZ) are treated as no model. Comments and credits say why WSDOT rows are skipped (their frames are WSDOT-hosted, outside the pinned origin) and name Newcastle as the partner city that appears.
368 lines
13 KiB
JavaScript
368 lines
13 KiB
JavaScript
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');
|
|
});
|