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 test from 'node:test';
import { loadOntarioSourcesFromOpenData } from './sources.js';
import {
_resetOntarioKeyWarningForTest,
loadOntarioSourcesFromOpenData,
} from './sources.js';
const ROW = {
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) => {
const warnings = [];
t.mock.method(console, 'warn', (...args) => warnings.push(args.join(' ')));
t.mock.method(globalThis, 'fetch', async () =>
assert.fail('no request without a key'),
const lines = captureConsole(t);
let fetches = 0;
t.mock.method(globalThis, 'fetch', async () => {
fetches += 1;
return Response.json([ROW]);
});
assert.deepEqual(await loadOntarioSourcesFromOpenData({}, {}), []);
assert.deepEqual(
await loadOntarioSourcesFromOpenData({}, { ONTARIO_511_API_KEY: ' ' }),
[],
);
assert.deepEqual(await loadOntarioSourcesFromOpenData({}, {}), []);
assert.deepEqual(await loadOntarioSourcesFromOpenData({}, {}), []);
assert.equal(warnings.length, 1);
assert.match(warnings[0], /ONTARIO_511_API_KEY/);
assert.equal(fetches, 0);
assert.equal(lines.length, 1);
assert.match(lines[0], /ONTARIO_511_API_KEY/);
});
test('with a key the camera list is requested with it and parsed', async (t) => {
const lines = captureConsole(t);
const requested = [];
t.mock.method(globalThis, 'fetch', async (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' },
{ ONTARIO_511_API_KEY: ' test-key ' },
);
assert.equal(requested.length, 1);
const url = new URL(requested[0]);
assert.equal(url.host, '511on.ca');
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[0].id, 'on-42');
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) => {
const warnings = [];
t.mock.method(console, 'warn', (...args) => warnings.push(args.join(' ')));
t.mock.method(globalThis, 'fetch', async () =>
Response.json({ Message: 'Invalid Key' }, { status: 400 }),
);
test('the source cap is read from the same environment as the key', async (t) => {
captureConsole(t);
const rows = Array.from({ length: 12 }, (_, i) => ({
...ROW,
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(
{},
{ ONTARIO_511_API_KEY: 'secret-key-value' },
{ ONTARIO_511_API_KEY: 'test-key', CCTV_ONTARIO_MAX_SOURCES: '8' },
);
assert.deepEqual(cameras, []);
assert.match(warnings.join('\n'), /400 \(check ONTARIO_511_API_KEY\)/);
assert.doesNotMatch(warnings.join('\n'), /secret-key-value/);
assert.equal(cameras.length, 8);
});
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;
/** 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,
* which now requires a free developer key (511on.ca/developers/doc, "Requires a
@@ -521,7 +526,7 @@ let ontarioKeyMissingLogged = false;
* view are kept.
*
* @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.
*/
export async function loadOntarioSourcesFromOpenData(
@@ -547,12 +552,12 @@ export async function loadOntarioSourcesFromOpenData(
});
if (!resp.ok) {
// The API answers 400 "Invalid Key" to a wrong or revoked key.
console.warn(
'[CCTV] Ontario 511 camera download failed:',
resp.status,
const keyHint =
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 [];
}
@@ -616,7 +621,7 @@ export async function loadOntarioSourcesFromOpenData(
new Map(cameras.map((camera) => [camera.id, camera])).values(),
);
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)
? Math.max(8, Math.min(1000, Math.floor(maxRaw)))