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 reader before the windows-1252 decode. The feed sends cardinal codes plus B (both), O and null, not BW or diagonals; tests, comments and docs now say so, with unchanged id-hash fallbacks. Documents the 8-1,200 per-pack range, drops the unverified low-volume compliance claim for facts and a link to WSDOT's terms, and stops claiming the per-pack defaults fit under the 4,000 cap: it is filled round-robin.
303 lines
10 KiB
JavaScript
303 lines
10 KiB
JavaScript
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_WSDOT_MAX_SOURCES,
|
||
WSDOT_CAMERAS_URL,
|
||
WSDOT_MAX_CATALOG_BYTES,
|
||
} from '../../server/providers/cctv/constants.js';
|
||
import {
|
||
loadWsdotSourcesFromOpenData,
|
||
webMercatorToWgs84,
|
||
} 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', () => {});
|
||
}
|
||
|
||
/** Web Mercator metres for a WGS84 coordinate (inverse of the loader's math). */
|
||
function mercator(lat, lon) {
|
||
const R = 6378137;
|
||
return {
|
||
x: (lon * Math.PI * R) / 180,
|
||
y: R * Math.log(Math.tan(Math.PI / 4 + (lat * Math.PI) / 360)),
|
||
};
|
||
}
|
||
|
||
/** A `Cameras.json` point feature carrying the fields the loader reads. */
|
||
function wsdotFeature(id, lat, lon, overrides = {}) {
|
||
const { attributes = {}, geometry } = overrides;
|
||
return {
|
||
attributes: {
|
||
CameraID: id,
|
||
CameraTitle: `Camera ${id}`,
|
||
CompassDirection: 'N',
|
||
ImageURL: `https://images.wsdot.wa.gov/nw/${id}.jpg`,
|
||
...attributes,
|
||
},
|
||
geometry: geometry === undefined ? mercator(lat, lon) : geometry,
|
||
};
|
||
}
|
||
|
||
/** Serialize a payload for the mocked fetch as raw Windows-1252 bytes. */
|
||
function cp1252Response(payload) {
|
||
const text = JSON.stringify(payload);
|
||
const bytes = new Uint8Array(text.length);
|
||
for (let i = 0; i < text.length; i += 1) {
|
||
const code = text.charCodeAt(i);
|
||
// U+2013 EN DASH is 0x96 in Windows-1252 — the byte the live feed ships.
|
||
bytes[i] = code === 0x2013 ? 0x96 : code;
|
||
}
|
||
return new Response(bytes, {
|
||
headers: { 'Content-Type': 'application/json' },
|
||
});
|
||
}
|
||
|
||
test('webMercatorToWgs84 recovers WGS84 degrees from layer geometry', () => {
|
||
// Real feature from the live layer: I-5 at the Interstate Bridge.
|
||
const { lat, lon } = webMercatorToWgs84(-13656007.102, 5719738.545);
|
||
assert.ok(Math.abs(lat - 45.62) < 0.05, `lat ${lat}`);
|
||
assert.ok(Math.abs(lon - -122.675) < 0.05, `lon ${lon}`);
|
||
});
|
||
|
||
test('WSDOT loader keeps official-host cameras and maps compass directions', async (t) => {
|
||
quiet(t);
|
||
withEnv(t, { CCTV_WSDOT_MAX_SOURCES: undefined });
|
||
const requested = [];
|
||
t.mock.method(globalThis, 'fetch', async (url) => {
|
||
requested.push(String(url));
|
||
return cp1252Response({
|
||
features: [
|
||
wsdotFeature(1001, 47.6097, -122.3331, {
|
||
attributes: {
|
||
// The 0x2013 below is serialized as the feed's 0x96 byte.
|
||
CameraTitle: 'I-5 – Seattle, looking south',
|
||
CompassDirection: 'S',
|
||
},
|
||
}),
|
||
wsdotFeature(9059, 47.66, -117.43, {
|
||
attributes: { CompassDirection: 'B' },
|
||
}),
|
||
// Partner-hosted frame (Oregon DOT): skipped, never proxied.
|
||
wsdotFeature(1002, 45.62, -122.675, {
|
||
attributes: {
|
||
ImageURL: 'https://www.tripcheck.com/RoadCams/cams/bridge.jpg',
|
||
},
|
||
}),
|
||
// Bad rows: non-integer id, missing geometry, out-of-state point.
|
||
wsdotFeature('1003', 47.6, -122.3),
|
||
wsdotFeature(1004, 47.6, -122.3, { geometry: null }),
|
||
wsdotFeature(1005, 34.05, -118.24),
|
||
],
|
||
});
|
||
});
|
||
|
||
const cameras = await loadWsdotSourcesFromOpenData();
|
||
|
||
assert.deepEqual(requested, [WSDOT_CAMERAS_URL]);
|
||
// Exactly the two valid official-host cameras survive; the partner-hosted
|
||
// frame and the malformed rows are dropped.
|
||
assert.deepEqual(cameras.map((camera) => camera.id).sort(), [
|
||
'wsdot-1001',
|
||
'wsdot-9059',
|
||
]);
|
||
|
||
const seattle = cameras.find((camera) => camera.id === 'wsdot-1001');
|
||
assert.equal(seattle.name, 'I-5 – Seattle, looking south');
|
||
assert.equal(seattle.provider, 'WSDOT');
|
||
assert.equal(seattle.url, 'https://images.wsdot.wa.gov/nw/1001.jpg');
|
||
assert.equal(seattle.snapshotUrl, seattle.url);
|
||
assert.equal(seattle.feedType, 'image');
|
||
assert.equal(seattle.headingDeg, 180);
|
||
assert.equal(seattle.headingConfidence, 'high');
|
||
assert.equal(seattle.fovDeg, 56);
|
||
assert.ok(Math.abs(seattle.lat - 47.6097) < 0.001);
|
||
assert.ok(Math.abs(seattle.lon - -122.3331) < 0.001);
|
||
|
||
// `B` (both directions) carries no single facing: id-hash fallback at low
|
||
// confidence, with the low-confidence pose personality.
|
||
const spokane = cameras.find((camera) => camera.id === 'wsdot-9059');
|
||
assert.equal(spokane.headingConfidence, 'low');
|
||
assert.equal(spokane.fovDeg, 44);
|
||
assert.ok(spokane.headingDeg >= 0 && spokane.headingDeg < 360);
|
||
});
|
||
|
||
test('WSDOT loader maps every CompassDirection code', async (t) => {
|
||
quiet(t);
|
||
withEnv(t, { CCTV_WSDOT_MAX_SOURCES: undefined });
|
||
const codes = {
|
||
N: 0,
|
||
NE: 45,
|
||
E: 90,
|
||
SE: 135,
|
||
S: 180,
|
||
SW: 225,
|
||
W: 270,
|
||
NW: 315,
|
||
// Lower-case and padded codes are the same code.
|
||
' nw ': 315,
|
||
};
|
||
// The live feed's no-facing codes, plus an unknown one.
|
||
const noFacing = ['B', 'O', null, 'BW'];
|
||
const entries = [...Object.keys(codes), ...noFacing];
|
||
t.mock.method(globalThis, 'fetch', async () =>
|
||
cp1252Response({
|
||
features: entries.map((code, i) =>
|
||
wsdotFeature(3000 + i, 47.0 + i * 0.05, -122.9 + i * 0.05, {
|
||
attributes: { CompassDirection: code },
|
||
}),
|
||
),
|
||
}),
|
||
);
|
||
|
||
const cameras = await loadWsdotSourcesFromOpenData();
|
||
const byId = new Map(cameras.map((camera) => [camera.id, camera]));
|
||
entries.forEach((code, i) => {
|
||
const camera = byId.get(`wsdot-${3000 + i}`);
|
||
if (code in codes) {
|
||
assert.equal(camera.headingDeg, codes[code], `heading for ${code}`);
|
||
assert.equal(camera.headingConfidence, 'high', `confidence for ${code}`);
|
||
assert.equal(camera.fovDeg, 56, `pose personality for ${code}`);
|
||
} else {
|
||
assert.equal(camera.headingConfidence, 'low', `confidence for ${code}`);
|
||
assert.equal(camera.fovDeg, 44, `pose personality for ${code}`);
|
||
}
|
||
});
|
||
});
|
||
|
||
test('WSDOT loader honors the max-sources cap', async (t) => {
|
||
quiet(t);
|
||
withEnv(t, { CCTV_WSDOT_MAX_SOURCES: '8' });
|
||
const features = [];
|
||
for (let i = 0; i < 24; i += 1) {
|
||
features.push(wsdotFeature(2000 + i, 47.0 + i * 0.05, -122.9 + i * 0.05));
|
||
}
|
||
t.mock.method(globalThis, 'fetch', async () => cp1252Response({ features }));
|
||
|
||
const cameras = await loadWsdotSourcesFromOpenData();
|
||
assert.equal(cameras.length, 8);
|
||
});
|
||
|
||
test('WSDOT loader defaults its cap and survives upstream failure', async (t) => {
|
||
quiet(t);
|
||
withEnv(t, { CCTV_WSDOT_MAX_SOURCES: undefined });
|
||
assert.equal(DEFAULT_WSDOT_MAX_SOURCES, 250);
|
||
|
||
t.mock.method(
|
||
globalThis,
|
||
'fetch',
|
||
async () => new Response('', { status: 503 }),
|
||
);
|
||
assert.deepEqual(await loadWsdotSourcesFromOpenData(), []);
|
||
|
||
t.mock.method(globalThis, 'fetch', async () => {
|
||
throw new Error('network down');
|
||
});
|
||
assert.deepEqual(await loadWsdotSourcesFromOpenData(), []);
|
||
});
|
||
|
||
test('the catalog fetch refuses a redirect and cancels its body', async (t) => {
|
||
quiet(t);
|
||
// A redirect is never followed, and its still-open body is cancelled.
|
||
const seen = [];
|
||
const state = { cancelled: false };
|
||
t.mock.method(globalThis, 'fetch', async (url, init) => {
|
||
seen.push([String(url), init.redirect]);
|
||
const body = new ReadableStream({
|
||
start(controller) {
|
||
controller.enqueue(new TextEncoder().encode('{'));
|
||
},
|
||
cancel() {
|
||
state.cancelled = true;
|
||
},
|
||
});
|
||
return new Response(body, {
|
||
status: 302,
|
||
headers: { location: 'https://evil.example/Cameras.json' },
|
||
});
|
||
});
|
||
assert.deepEqual(await loadWsdotSourcesFromOpenData(), []);
|
||
assert.deepEqual(seen, [[WSDOT_CAMERAS_URL, 'manual']]);
|
||
assert.equal(state.cancelled, true, 'the redirect body is cancelled');
|
||
});
|
||
|
||
test('the catalog fetch refuses a body over the byte cap', async (t) => {
|
||
quiet(t);
|
||
// A valid catalog padded past the cap is refused rather than buffered.
|
||
t.mock.method(globalThis, 'fetch', async () => {
|
||
const text = JSON.stringify({
|
||
features: [wsdotFeature(1001, 47.6097, -122.3331)],
|
||
});
|
||
const padded = text + ' '.repeat(WSDOT_MAX_CATALOG_BYTES);
|
||
return new Response(
|
||
new ReadableStream({
|
||
start(controller) {
|
||
controller.enqueue(new TextEncoder().encode(padded));
|
||
controller.close();
|
||
},
|
||
}),
|
||
{ headers: { 'Content-Type': 'application/json' } },
|
||
);
|
||
});
|
||
assert.deepEqual(await loadWsdotSourcesFromOpenData(), []);
|
||
});
|
||
|
||
test('CCTV_WSDOT_ENABLED=0 skips the WSDOT 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_WSDOT_ENABLED: undefined,
|
||
CCTV_WSDOT_MAX_SOURCES: undefined,
|
||
});
|
||
const requested = [];
|
||
t.mock.method(globalThis, 'fetch', async (url) => {
|
||
requested.push(String(url));
|
||
// Every other live pack is down; only WSDOT answers.
|
||
return String(url) === WSDOT_CAMERAS_URL
|
||
? cp1252Response({ features: [wsdotFeature(1001, 47.6097, -122.3331)] })
|
||
: new Response('unavailable', { status: 503 });
|
||
});
|
||
|
||
const sources = await createCctvCatalog({ sourceRoot })();
|
||
assert.deepEqual(
|
||
sources.map((source) => source.id),
|
||
['wsdot-1001'],
|
||
);
|
||
|
||
process.env.CCTV_WSDOT_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(WSDOT_CAMERAS_URL),
|
||
false,
|
||
'a disabled pack makes no request',
|
||
);
|
||
});
|