mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
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:
1 parent
e9a234094b
commit
0561277dcb
2 files changed
+95
-26
No files matched your search
@@ -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/);
|
||||
});
|
||||
}
|
||||
@@ -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)))
|
||||
|
||||
Reference in new issue
Block a user