test(cctv): pin Ontario 511 key handling against regressions

- Assert the key never reaches the camera records sent to the browser or
  the "Loaded" log line.
- Count fetches in the keyless test instead of relying on assert.fail
  inside the mock, which the loader's catch would swallow; a
  whitespace-only key is treated as missing.
- Cover the key hint for 400/401/403 and its absence for 429/500/503,
  and log the failure as one string without a trailing empty argument.
- Add _resetOntarioKeyWarningForTest so the logged-once check no longer
  depends on test order.
- Read CCTV_ONTARIO_MAX_SOURCES from the same env as the key, and keep
  console output out of the suite.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Said TurkseverandClaude Opus 5.5 committed 2026-10-19 16:59:52 +01:00
1 parent e9a234094b
commit 0561277dcb
2 files changed
+95 -26

No files matched your search

+83 -19
View File
@@ -1,7 +1,10 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import { loadOntarioSourcesFromOpenData } from './sources.js'; import {
_resetOntarioKeyWarningForTest,
loadOntarioSourcesFromOpenData,
} from './sources.js';
const ROW = { const ROW = {
Id: '42', Id: '42',
@@ -18,19 +21,35 @@ const ROW = {
], ],
}; };
function captureConsole(t) {
const lines = [];
const capture = (...args) => lines.push(args.join(' '));
t.mock.method(console, 'warn', capture);
t.mock.method(console, 'log', capture);
return lines;
}
test.beforeEach(() => _resetOntarioKeyWarningForTest());
test('without ONTARIO_511_API_KEY the Ontario pack is skipped, logged once', async (t) => { test('without ONTARIO_511_API_KEY the Ontario pack is skipped, logged once', async (t) => {
const warnings = []; const lines = captureConsole(t);
t.mock.method(console, 'warn', (...args) => warnings.push(args.join(' '))); let fetches = 0;
t.mock.method(globalThis, 'fetch', async () => t.mock.method(globalThis, 'fetch', async () => {
assert.fail('no request without a key'), fetches += 1;
return Response.json([ROW]);
});
assert.deepEqual(await loadOntarioSourcesFromOpenData({}, {}), []);
assert.deepEqual(
await loadOntarioSourcesFromOpenData({}, { ONTARIO_511_API_KEY: ' ' }),
[],
); );
assert.deepEqual(await loadOntarioSourcesFromOpenData({}, {}), []); assert.equal(fetches, 0);
assert.deepEqual(await loadOntarioSourcesFromOpenData({}, {}), []); assert.equal(lines.length, 1);
assert.equal(warnings.length, 1); assert.match(lines[0], /ONTARIO_511_API_KEY/);
assert.match(warnings[0], /ONTARIO_511_API_KEY/);
}); });
test('with a key the camera list is requested with it and parsed', async (t) => { test('with a key the camera list is requested with it and parsed', async (t) => {
const lines = captureConsole(t);
const requested = []; const requested = [];
t.mock.method(globalThis, 'fetch', async (url) => { t.mock.method(globalThis, 'fetch', async (url) => {
requested.push(String(url)); requested.push(String(url));
@@ -40,6 +59,7 @@ test('with a key the camera list is requested with it and parsed', async (t) =>
{ sourceRoot: '/ignored' }, { sourceRoot: '/ignored' },
{ ONTARIO_511_API_KEY: ' test-key ' }, { ONTARIO_511_API_KEY: ' test-key ' },
); );
assert.equal(requested.length, 1);
const url = new URL(requested[0]); const url = new URL(requested[0]);
assert.equal(url.host, '511on.ca'); assert.equal(url.host, '511on.ca');
assert.equal(url.searchParams.get('key'), 'test-key'); assert.equal(url.searchParams.get('key'), 'test-key');
@@ -47,19 +67,63 @@ test('with a key the camera list is requested with it and parsed', async (t) =>
assert.equal(cameras.length, 1); assert.equal(cameras.length, 1);
assert.equal(cameras[0].id, 'on-42'); assert.equal(cameras[0].id, 'on-42');
assert.equal(cameras[0].url, 'https://511on.ca/map/Cctv/4201'); assert.equal(cameras[0].url, 'https://511on.ca/map/Cctv/4201');
// The records go to the browser through /api/cctv; the key must stay on
// the server-side catalog request.
assert.doesNotMatch(JSON.stringify(cameras), /test-key/);
assert.doesNotMatch(lines.join('\n'), /test-key/);
}); });
test('a rejected key is reported by status, never by the key itself', async (t) => { test('the source cap is read from the same environment as the key', async (t) => {
const warnings = []; captureConsole(t);
t.mock.method(console, 'warn', (...args) => warnings.push(args.join(' '))); const rows = Array.from({ length: 12 }, (_, i) => ({
t.mock.method(globalThis, 'fetch', async () => ...ROW,
Response.json({ Message: 'Invalid Key' }, { status: 400 }), Id: String(100 + i),
); Latitude: ROW.Latitude + i * 0.01,
Views: [{ ...ROW.Views[0], Url: `https://511on.ca/map/Cctv/${100 + i}` }],
}));
t.mock.method(globalThis, 'fetch', async () => Response.json(rows));
const cameras = await loadOntarioSourcesFromOpenData( const cameras = await loadOntarioSourcesFromOpenData(
{}, {},
{ ONTARIO_511_API_KEY: 'secret-key-value' }, { ONTARIO_511_API_KEY: 'test-key', CCTV_ONTARIO_MAX_SOURCES: '8' },
); );
assert.deepEqual(cameras, []); assert.equal(cameras.length, 8);
assert.match(warnings.join('\n'), /400 \(check ONTARIO_511_API_KEY\)/);
assert.doesNotMatch(warnings.join('\n'), /secret-key-value/);
}); });
for (const status of [400, 401, 403]) {
test(`a ${status} points at the key by name, never by value`, async (t) => {
const lines = captureConsole(t);
t.mock.method(globalThis, 'fetch', async () =>
Response.json({ Message: 'Invalid Key' }, { status }),
);
const cameras = await loadOntarioSourcesFromOpenData(
{},
{ ONTARIO_511_API_KEY: 'secret-key-value' },
);
assert.deepEqual(cameras, []);
assert.match(
lines.join('\n'),
new RegExp(`failed: ${status} \\(check ONTARIO_511_API_KEY\\)`),
);
assert.doesNotMatch(lines.join('\n'), /secret-key-value/);
});
}
for (const status of [429, 500, 503]) {
test(`a ${status} is reported without blaming the key`, async (t) => {
const lines = captureConsole(t);
t.mock.method(globalThis, 'fetch', async () =>
Response.json({}, { status }),
);
const cameras = await loadOntarioSourcesFromOpenData(
{},
{ ONTARIO_511_API_KEY: 'secret-key-value' },
);
assert.deepEqual(cameras, []);
assert.equal(lines.length, 1);
assert.equal(
lines[0],
`[CCTV] Ontario 511 camera download failed: ${status}`,
);
assert.doesNotMatch(lines[0], /secret-key-value/);
});
}
+12 -7
View File
@@ -511,6 +511,11 @@ function pickOntarioCctvView(views) {
let ontarioKeyMissingLogged = false; let ontarioKeyMissingLogged = false;
/** Test seam: let the next keyless call log the missing-key line again. */
export function _resetOntarioKeyWarningForTest() {
ontarioKeyMissingLogged = false;
}
/** /**
* Fetch Ontario 511 CCTV cameras. The camera list comes from the 511 API, * Fetch Ontario 511 CCTV cameras. The camera list comes from the 511 API,
* which now requires a free developer key (511on.ca/developers/doc, "Requires a * which now requires a free developer key (511on.ca/developers/doc, "Requires a
@@ -521,7 +526,7 @@ let ontarioKeyMissingLogged = false;
* view are kept. * view are kept.
* *
* @param {object} [_options] The catalog's loader options (unused). * @param {object} [_options] The catalog's loader options (unused).
* @param {object} [env] Environment to read the key from. * @param {object} [env] Environment to read the key and the source cap from.
* @returns {Promise<Array<object>>} Normalized camera source objects. * @returns {Promise<Array<object>>} Normalized camera source objects.
*/ */
export async function loadOntarioSourcesFromOpenData( export async function loadOntarioSourcesFromOpenData(
@@ -547,12 +552,12 @@ export async function loadOntarioSourcesFromOpenData(
}); });
if (!resp.ok) { if (!resp.ok) {
// The API answers 400 "Invalid Key" to a wrong or revoked key. // The API answers 400 "Invalid Key" to a wrong or revoked key.
console.warn( const keyHint =
'[CCTV] Ontario 511 camera download failed:',
resp.status,
resp.status === 400 || resp.status === 401 || resp.status === 403 resp.status === 400 || resp.status === 401 || resp.status === 403
? '(check ONTARIO_511_API_KEY)' ? ' (check ONTARIO_511_API_KEY)'
: '', : '';
console.warn(
`[CCTV] Ontario 511 camera download failed: ${resp.status}${keyHint}`,
); );
return []; return [];
} }
@@ -616,7 +621,7 @@ export async function loadOntarioSourcesFromOpenData(
new Map(cameras.map((camera) => [camera.id, camera])).values(), new Map(cameras.map((camera) => [camera.id, camera])).values(),
); );
const maxRaw = Number( const maxRaw = Number(
process.env.CCTV_ONTARIO_MAX_SOURCES || DEFAULT_ONTARIO_MAX_SOURCES, env.CCTV_ONTARIO_MAX_SOURCES || DEFAULT_ONTARIO_MAX_SOURCES,
); );
const maxCount = Number.isFinite(maxRaw) const maxCount = Number.isFinite(maxRaw)
? Math.max(8, Math.min(1000, Math.floor(maxRaw))) ? Math.max(8, Math.min(1000, Math.floor(maxRaw)))