Files
gods-eye-view/src/layers/perimeters/source.test.mjs
T

182 lines
5.2 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { createWfigsPerimeterSource } from './source.js';
import { normalizeFirePerimeterSnapshot } from './records.js';
import { firePerimetersProxy } from '../../../server/providers/firePerimeters.js';
const ring = [
[-108.1, 35.2],
[-108.0, 35.2],
[-108.0, 35.3],
[-108.1, 35.2],
];
const validPayload = {
features: [
{
id: 1,
geometry: { type: 'Polygon', coordinates: [ring] },
properties: { attr_UniqueFireIdentifier: '2026-NMGNF-000123' },
},
],
};
test('a successful response yields normalized perimeter rows', async () => {
let requested;
const source = createWfigsPerimeterSource({
fetchImpl: async (url) => {
requested = String(url);
return Response.json({
rows: normalizeFirePerimeterSnapshot(validPayload),
});
},
});
const rows = await source.getSnapshot();
assert.equal(rows.length, 1);
assert.equal(rows[0].stableId, '2026-NMGNF-000123');
assert.equal(requested, '/api/fire-perimeters');
});
test('a truncated response pages until the feed is complete', async () => {
const pageRing = (id) => ({
id,
geometry: { type: 'Polygon', coordinates: [ring] },
properties: { attr_UniqueFireIdentifier: `fire-${id}` },
});
const requests = [];
let handler;
const plugin = firePerimetersProxy({
fetchImpl: async (url) => {
requests.push(new URL(String(url)).searchParams.get('resultOffset'));
const page = requests.length;
return Response.json({
features: [pageRing(page)],
properties: { exceededTransferLimit: page < 3 },
});
},
});
plugin.configureServer({
middlewares: {
use: (_path, callback) => {
handler = callback;
},
},
});
let payload;
await handler(
{ url: '/', method: 'GET' },
{
writeHead(status) {
assert.equal(status, 200);
},
end(body) {
payload = JSON.parse(body);
},
},
);
const rows = payload.rows;
assert.deepEqual(
rows.map((row) => row.stableId),
['fire-1', 'fire-2', 'fire-3'],
);
assert.deepEqual(requests, [null, '1', '2']);
});
test('an upstream failure surfaces its HTTP status', async () => {
const source = createWfigsPerimeterSource({
fetchImpl: async () => ({ ok: false, status: 503 }),
});
await assert.rejects(source.getSnapshot(), /WFIGS HTTP 503/);
});
test('a malformed successful response is never accepted as an empty snapshot', async () => {
for (const payload of [{}, { rows: null }, { rows: {} }]) {
const source = createWfigsPerimeterSource({
fetchImpl: async () => Response.json(payload),
});
await assert.rejects(source.getSnapshot(), /Malformed perimeter snapshot/);
}
});
test('response-body completion honors cancellation without replacing records', async () => {
const abort = new AbortController();
const source = createWfigsPerimeterSource({
fetchImpl: async () => ({
ok: true,
headers: new Headers(),
text: async () => {
abort.abort();
return JSON.stringify({
rows: normalizeFirePerimeterSnapshot(validPayload),
});
},
}),
});
await assert.rejects(source.getSnapshot({ signal: abort.signal }), {
name: 'AbortError',
});
});
test('a failed refresh serves the last snapshot marked stale for a day, then refuses', async () => {
let clock = 1_000_000_000;
let up = true;
const plugin = firePerimetersProxy({
now: () => clock,
fetchImpl: async () => {
if (!up) throw new Error('WFIGS unreachable');
return Response.json(validPayload);
},
});
let handler;
plugin.configureServer({
middlewares: { use: (_path, callback) => (handler = callback) },
});
const ask = async () => {
const reply = {};
await handler(
{ url: '/', method: 'GET', socket: { remoteAddress: '127.0.0.1' } },
{
writeHead(status, headers) {
reply.status = status;
reply.staleHeader = headers?.['X-Data-Stale'] === 'true';
},
end(body) {
reply.body = JSON.parse(body);
},
},
);
return reply;
};
const fresh = await ask();
assert.equal(fresh.status, 200);
assert.equal(fresh.body.stale, undefined);
up = false;
clock += 23 * 60 * 60 * 1000;
const stale = await ask();
assert.equal(stale.status, 200);
assert.equal(stale.staleHeader, true);
assert.equal(stale.body.stale, true);
assert.equal(stale.body.fetchedAt, fresh.body.fetchedAt);
clock += 2 * 60 * 60 * 1000;
assert.equal((await ask()).status, 502, 'a day-old snapshot is not served');
});
test('the snapshot detail carries the proxy staleness and fetch time', async () => {
const fetchedAt = Date.UTC(2026, 9, 25, 12);
const source = createWfigsPerimeterSource({
fetchImpl: async () => Response.json({ rows: [], stale: true, fetchedAt }),
});
assert.deepEqual(await source.getSnapshotDetail(), {
rows: [],
stale: true,
fetchedAt,
});
assert.deepEqual(await source.getSnapshot(), []);
});
test('getSnapshot works when taken off its source object', async () => {
const { getSnapshot } = createWfigsPerimeterSource({
fetchImpl: async () => Response.json({ rows: [] }),
});
assert.deepEqual(await getSnapshot(), []);
});