Real weather, real aircraft, a heightfield off the main thread, and instruments
Three things that were built and never connected, connected.
**The weather was already there.** `observe()` has always taken a
`WeatherObservation` and `main.ts` has always passed null, so the cloud,
precipitation, visibility and marine-layer paths in atmosphere.ts had never run
outside a test. The server already shipped NWS, met.no and Open-Meteo, all
configured off. What was actually missing was that a single TERA_ORIGIN_LAT/LNG
served one metro and lied to the other — so weather and traffic are per-region
now, derived from the city's own bounds, and the Bay Area gets its fog while
Long Beach gets its own sky. The route takes ?city= or a validated ?lat=&lng=
and refuses to become an open geocoding proxy for the planet.
**The heightfield moved to a Worker.** 2.3 s of blocked main thread at boot, and
another ~950 ms of point-in-polygon on top of it: the park mask is filled in the
worker now, and block placement samples four corners and only runs the exact
test on a cell that straddles an edge — 8 buildings differ out of 185,036.
createScene is async and takes a Stage as a consequence, and there is a
main-thread fallback because "clone it and it works" has no exception clause.
**Spaces is a chunk you fetch when you reach for the door**, not one everybody
downloads. Same for the godmode tools. The entry chunk is 722 kB rather than
772; three.js is most of what is left and splitting it is a different job.
**Godmode is an instrument panel now** rather than one slider: the date and the
season, not just the hour, so the Meeus moon and the sun's seasonal arc become
visible instead of merely correct; a weather override that says on screen when
it is lying; a frame-time and draw-call readout; and a pose editor that emits a
paste-ready Chapter block, which is the thing that makes adding New York cheap.
Two blockers the review caught:
- Every city switch leaked 8 GPU textures — one of them a 2048x2048 shadow map
— and ~10.5 shader programs, and deleteTexture had never been called once in
the app's lifetime. The renderer was being built per scene; it belongs to the
canvas, for the life of the page.
- An upstream fetch that threw rather than returning null skipped the cache
stamp, so the TTL — the only rate limit on outbound calls — collapsed to one
upstream request per inbound request, and the caller got a 500.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
/**
|
||||
* Live traffic, and the budget that keeps it welcome.
|
||||
*
|
||||
* adsb.lol is a volunteer-fed community feed that asks for no more than one
|
||||
* request per second and will rate-limit a caller who ignores it. Most of this
|
||||
* file is therefore about **how often this server is capable of calling out**,
|
||||
* not about what comes back: one poll per region per TTL, a TTL with a floor
|
||||
* under it, and a refused query that costs the upstream nothing. The arithmetic
|
||||
* those tests pin down is written out in `flights/index.ts`.
|
||||
*
|
||||
* The one that is not about rate is the URL assertion. A caller's coordinate
|
||||
* must select a configured region and must never itself be fetched.
|
||||
*/
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { after, before, beforeEach, describe, it } from "node:test";
|
||||
import { buildApp } from "../app.ts";
|
||||
import { loadConfig } from "../config.ts";
|
||||
import type { FlightsBody } from "../../../src/server/wire.ts";
|
||||
|
||||
const realFetch = globalThis.fetch;
|
||||
let calls: string[] = [];
|
||||
let feedIsUp = true;
|
||||
/** A body to serve instead of the usual one, for the wrong-shape tests. */
|
||||
let feedOverride: unknown = undefined;
|
||||
|
||||
/** One aircraft, whose id encodes the point that was asked about. */
|
||||
function feed(url: string): unknown | undefined {
|
||||
if (!feedIsUp) return undefined;
|
||||
const point = /\/v2\/point\/(-?[\d.]+)\/(-?[\d.]+)\/(\d+)$/.exec(url);
|
||||
if (point === null) return undefined;
|
||||
if (feedOverride !== undefined) return feedOverride;
|
||||
return {
|
||||
now: 1_770_000_000_000,
|
||||
ac: [{ hex: `a${point[1]}`, flight: "LMB1 ", lat: 37.5, lon: -122.3, alt_baro: 10_000, track: 90 }],
|
||||
};
|
||||
}
|
||||
|
||||
globalThis.fetch = (async (input: unknown) => {
|
||||
const url = String(input);
|
||||
calls.push(url);
|
||||
const body = feed(url);
|
||||
if (body === undefined) return new Response("nope", { status: 503 });
|
||||
return new Response(JSON.stringify(body), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}) as unknown as typeof globalThis.fetch;
|
||||
|
||||
after(() => {
|
||||
globalThis.fetch = realFetch;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
calls = [];
|
||||
feedIsUp = true;
|
||||
feedOverride = undefined;
|
||||
});
|
||||
|
||||
function appWith(env: Record<string, string>) {
|
||||
const config = loadConfig(env);
|
||||
config.logLevel = "silent";
|
||||
return buildApp(config);
|
||||
}
|
||||
|
||||
const adsbEnv = { TERA_FLIGHTS_SOURCE: "adsb" };
|
||||
|
||||
describe("the ADS-B source", () => {
|
||||
it("polls each city's own centre, and never the caller's coordinate", async () => {
|
||||
const app = appWith(adsbEnv);
|
||||
after(() => app.close());
|
||||
|
||||
// Pasadena, which is inside the Southland region and is not a point this
|
||||
// deployment serves.
|
||||
await app.inject({ method: "GET", url: "/api/v1/flights?lat=34.1478&lng=-118.1445" });
|
||||
await app.inject({ method: "GET", url: "/api/v1/flights?city=sf" });
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
"https://api.adsb.lol/v2/point/33.8200/-118.0500/40",
|
||||
"https://api.adsb.lol/v2/point/37.7749/-122.4194/40",
|
||||
]);
|
||||
});
|
||||
|
||||
it("clamps a radius the feeds would reject, and says so", async () => {
|
||||
const config = loadConfig({ ...adsbEnv, TERA_ADSB_RADIUS_NM: "2500" });
|
||||
config.logLevel = "silent";
|
||||
const app = buildApp(config);
|
||||
after(() => app.close());
|
||||
|
||||
assert.equal(config.flights.radiusNm, 250);
|
||||
assert.match(config.degraded[0] ?? "", /TERA_ADSB_RADIUS_NM/);
|
||||
|
||||
await app.inject({ method: "GET", url: "/api/v1/flights" });
|
||||
assert.equal(calls[0], "https://api.adsb.lol/v2/point/37.7749/-122.4194/250");
|
||||
});
|
||||
|
||||
it("polls once per region per TTL, whatever the request rate is", async () => {
|
||||
const app = appWith(adsbEnv);
|
||||
after(() => app.close());
|
||||
|
||||
const ask = (city: string) =>
|
||||
app.inject({ method: "GET", url: `/api/v1/flights?city=${city}` });
|
||||
await Promise.all([ask("sf"), ask("sf"), ask("socal"), ask("sf"), ask("socal")]);
|
||||
for (let i = 0; i < 6; i++) await ask("sf");
|
||||
|
||||
// Two regions, eleven requests, two upstream calls.
|
||||
assert.equal(calls.length, 2);
|
||||
});
|
||||
|
||||
it("floors the poll interval so TERA_FLIGHTS_TTL=0 is not a flood", async () => {
|
||||
// This is the bug the floor exists for: a zero TTL used to mean one
|
||||
// outbound request per inbound request, straight through the rate limit,
|
||||
// from a setting that reads like "as fresh as possible".
|
||||
const app = appWith({ ...adsbEnv, TERA_FLIGHTS_TTL: "0" });
|
||||
after(() => app.close());
|
||||
|
||||
let body: FlightsBody | null = null;
|
||||
for (let i = 0; i < 8; i++) {
|
||||
body = (await app.inject({ method: "GET", url: "/api/v1/flights" })).json<FlightsBody>();
|
||||
}
|
||||
assert.equal(calls.length, 1);
|
||||
assert.ok(body !== null && body.mode === "live" && body.ttlSeconds === 5);
|
||||
});
|
||||
|
||||
it("caps the poll interval too, because aircraft move", async () => {
|
||||
const app = appWith({ ...adsbEnv, TERA_FLIGHTS_TTL: "3600" });
|
||||
after(() => app.close());
|
||||
|
||||
const body = (await app.inject({ method: "GET", url: "/api/v1/flights" })).json<FlightsBody>();
|
||||
assert.ok(body.mode === "live" && body.ttlSeconds === 15);
|
||||
});
|
||||
|
||||
it("costs the upstream nothing when the query is refused", async () => {
|
||||
const app = appWith(adsbEnv);
|
||||
after(() => app.close());
|
||||
|
||||
const res = await app.inject({ method: "GET", url: "/api/v1/flights?city=atlantis" });
|
||||
assert.equal(res.statusCode, 400);
|
||||
assert.deepEqual(calls, []);
|
||||
});
|
||||
|
||||
it("falls back to the simulated plan rather than to an empty sky", async () => {
|
||||
feedIsUp = false;
|
||||
const app = appWith(adsbEnv);
|
||||
after(() => app.close());
|
||||
|
||||
const body = (await app.inject({ method: "GET", url: "/api/v1/flights" })).json<FlightsBody>();
|
||||
assert.equal(body.mode, "plan");
|
||||
assert.ok(body.mode === "plan" && body.routes.length > 0);
|
||||
});
|
||||
|
||||
it("credits the feed it took the positions from", async () => {
|
||||
const app = appWith(adsbEnv);
|
||||
after(() => app.close());
|
||||
|
||||
const body = (await app.inject({ method: "GET", url: "/api/v1/flights" })).json<FlightsBody>();
|
||||
assert.ok(body.mode === "live");
|
||||
assert.match(body.attribution?.[0] ?? "", /adsb\.lol/);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* The failure that turns a rate limit inside out.
|
||||
*
|
||||
* A feed that goes *down* is handled everywhere. A feed that stays up and
|
||||
* answers 200 with a field of the wrong type used to throw out of `normalise`,
|
||||
* past `upstream.ts` — which then never stamped its clock — and out to the
|
||||
* route as a 500. The TTL is the only rate limit on outbound calls, so losing it
|
||||
* meant one request to adsb.lol per inbound request, from the operator's
|
||||
* address, for as long as the feed stayed broken.
|
||||
*/
|
||||
describe("a feed that changed shape", () => {
|
||||
it("answers with the plan instead of a 500", async () => {
|
||||
// Valid JSON, wrong shape: `ac` is a number where an array belongs.
|
||||
feedOverride = { ac: 5, now: 1 };
|
||||
const app = appWith(adsbEnv);
|
||||
after(() => app.close());
|
||||
|
||||
const res = await app.inject({ method: "GET", url: "/api/v1/flights" });
|
||||
assert.equal(res.statusCode, 200);
|
||||
assert.equal(res.json<FlightsBody>().mode, "plan");
|
||||
});
|
||||
|
||||
it("still polls once per TTL, which is the whole of the rate limit", async () => {
|
||||
feedOverride = { ac: 5, now: 1 };
|
||||
const app = appWith(adsbEnv);
|
||||
after(() => app.close());
|
||||
|
||||
for (let i = 0; i < 5; i++) await app.inject({ method: "GET", url: "/api/v1/flights" });
|
||||
assert.equal(calls.length, 1);
|
||||
});
|
||||
|
||||
it("survives rows that are not objects", async () => {
|
||||
feedOverride = { ac: [null, 7, "LMB1", { hex: "ok", lat: 37.5, lon: -122.3 }], now: 1 };
|
||||
const app = appWith(adsbEnv);
|
||||
after(() => app.close());
|
||||
|
||||
const body = (await app.inject({ method: "GET", url: "/api/v1/flights" })).json<FlightsBody>();
|
||||
assert.ok(body.mode === "live" && body.aircraft.length === 1);
|
||||
});
|
||||
|
||||
it("caps how much of somebody else's sky it will hold and serve", async () => {
|
||||
feedOverride = {
|
||||
now: 1,
|
||||
ac: Array.from({ length: 6000 }, (_, i) => ({
|
||||
hex: `x${i}`,
|
||||
lat: 37.5,
|
||||
lon: -122.3,
|
||||
alt_baro: 1000,
|
||||
})),
|
||||
};
|
||||
const app = appWith(adsbEnv);
|
||||
after(() => app.close());
|
||||
|
||||
const body = (await app.inject({ method: "GET", url: "/api/v1/flights" })).json<FlightsBody>();
|
||||
assert.ok(body.mode === "live" && body.aircraft.length === 5000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("a local receiver", () => {
|
||||
let path = "";
|
||||
|
||||
before(async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "tera-flights-"));
|
||||
path = join(dir, "aircraft.json");
|
||||
await writeFile(
|
||||
path,
|
||||
JSON.stringify({
|
||||
now: 1_770_000_000,
|
||||
aircraft: [{ hex: "abc123", flight: "LMB9 ", lat: 37.6, lon: -122.4, alt_baro: 3000 }],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("has one antenna, so every region reads the same snapshot once", async () => {
|
||||
const app = appWith({ TERA_FLIGHTS_SOURCE: "dump1090", TERA_DUMP1090_PATH: path });
|
||||
after(() => app.close());
|
||||
|
||||
const sf = (
|
||||
await app.inject({ method: "GET", url: "/api/v1/flights?city=sf" })
|
||||
).json<FlightsBody>();
|
||||
assert.ok(sf.mode === "live" && sf.aircraft.length === 1);
|
||||
|
||||
// Take the file away, then ask for the other city inside the TTL. A live
|
||||
// answer proves the two regions share one cache entry — a per-region key
|
||||
// would have gone back to disk here and found nothing.
|
||||
await rm(path);
|
||||
const socal = (
|
||||
await app.inject({ method: "GET", url: "/api/v1/flights?city=socal" })
|
||||
).json<FlightsBody>();
|
||||
assert.ok(socal.mode === "live");
|
||||
assert.deepEqual(socal.aircraft, sf.aircraft);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,263 @@
|
||||
/**
|
||||
* The marker feed: the refusal that makes the `member` tier real, and a file an
|
||||
* operator can actually use.
|
||||
*
|
||||
* The first half is one assertion said several ways. `src/access.ts` has three
|
||||
* tiers and the middle one gated nothing — `member` was a word in a type union
|
||||
* with no server behaviour behind it. A tier that never refuses anybody is not a
|
||||
* tier, so a configured feed takes a session, and the test that matters is the
|
||||
* negative: an anonymous caller gets 401 and no rows, with no query parameter,
|
||||
* header or cleared cookie that changes it.
|
||||
*
|
||||
* The second half is the file itself — what a malformed row does to the rest of
|
||||
* the snapshot, and the reload path, which has to work without a restart because
|
||||
* the sync oneshot runs on a timer and nothing signals this process.
|
||||
*
|
||||
* Follows `offices.test.ts`: a temp directory, `buildApp` over a fake
|
||||
* environment, `inject()` rather than a socket, and HS256 by hand.
|
||||
*/
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { createHmac } from "node:crypto";
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { after, before, beforeEach, describe, it } from "node:test";
|
||||
import { setTimeout as sleep } from "node:timers/promises";
|
||||
import { buildApp } from "../app.ts";
|
||||
import { loadConfig } from "../config.ts";
|
||||
import type { ErrorBody, MarkersBody } from "../../../src/server/wire.ts";
|
||||
|
||||
const SECRET = "not-a-real-secret-and-never-was";
|
||||
|
||||
const FERRY = {
|
||||
id: "ferry-building",
|
||||
label: "Ferry Building",
|
||||
colorKey: "sector.civic",
|
||||
lat: 37.7955,
|
||||
lng: -122.3937,
|
||||
provenance: "hand-placed",
|
||||
};
|
||||
|
||||
let file = "";
|
||||
|
||||
before(async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "tera-markers-"));
|
||||
file = join(dir, "markers.json");
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await write({ generatedAt: "2026-08-05T09:00:00Z", markers: [FERRY] });
|
||||
});
|
||||
|
||||
async function write(snapshot: unknown): Promise<void> {
|
||||
await writeFile(file, JSON.stringify(snapshot));
|
||||
}
|
||||
|
||||
/** A feed that is configured, with an issuer that can produce a member. */
|
||||
const feedEnv = {
|
||||
TERA_MARKERS_SOURCE: "file",
|
||||
TERA_AUTH_MODE: "jwt",
|
||||
TERA_AUTH_JWT_SECRET: SECRET,
|
||||
};
|
||||
|
||||
function appWith(env: Record<string, string>) {
|
||||
const config = loadConfig({ TERA_MARKERS_FILE: file, ...env });
|
||||
config.logLevel = "silent";
|
||||
return buildApp(config);
|
||||
}
|
||||
|
||||
function member(): string {
|
||||
const encode = (value: unknown): string =>
|
||||
Buffer.from(JSON.stringify(value)).toString("base64url");
|
||||
const claims = { sub: "someone", exp: Math.floor(Date.now() / 1000) + 600 };
|
||||
const signed = `${encode({ alg: "HS256", typ: "JWT" })}.${encode(claims)}`;
|
||||
return `${signed}.${createHmac("sha256", SECRET).update(signed).digest("base64url")}`;
|
||||
}
|
||||
|
||||
function asMember(app: ReturnType<typeof buildApp>) {
|
||||
return app.inject({
|
||||
method: "GET",
|
||||
url: "/api/v1/markers",
|
||||
headers: { authorization: `Bearer ${member()}` },
|
||||
});
|
||||
}
|
||||
|
||||
describe("a configured marker feed", () => {
|
||||
it("refuses an anonymous caller", async () => {
|
||||
const app = appWith(feedEnv);
|
||||
after(() => app.close());
|
||||
|
||||
const res = await app.inject({ method: "GET", url: "/api/v1/markers" });
|
||||
assert.equal(res.statusCode, 401);
|
||||
assert.equal(res.json<ErrorBody>().error, "unauthorized");
|
||||
assert.equal(res.headers["www-authenticate"], "Bearer");
|
||||
// No rows anywhere in the refusal, and nothing shared may keep it.
|
||||
assert.ok(!res.body.includes("Ferry"));
|
||||
assert.equal(res.headers["cache-control"], "private, no-store");
|
||||
});
|
||||
|
||||
it("refuses every shape of not-being-signed-in", async () => {
|
||||
const app = appWith(feedEnv);
|
||||
after(() => app.close());
|
||||
|
||||
const attempts = [
|
||||
{},
|
||||
{ authorization: "Bearer not-a-jwt" },
|
||||
{ authorization: `Bearer ${member()}tampered` },
|
||||
{ authorization: "Basic Zm9vOmJhcg==" },
|
||||
{ cookie: "tera_session=" },
|
||||
{ cookie: "tera_session=%zz" },
|
||||
];
|
||||
for (const headers of attempts) {
|
||||
const res = await app.inject({ method: "GET", url: "/api/v1/markers", headers });
|
||||
assert.equal(res.statusCode, 401, `${JSON.stringify(headers)} must not get the feed`);
|
||||
}
|
||||
});
|
||||
|
||||
it("serves the snapshot to a member", async () => {
|
||||
const app = appWith(feedEnv);
|
||||
after(() => app.close());
|
||||
|
||||
const res = await asMember(app);
|
||||
assert.equal(res.statusCode, 200);
|
||||
const body = res.json<MarkersBody>();
|
||||
assert.equal(body.markers.length, 1);
|
||||
assert.equal(body.markers[0]?.id, "ferry-building");
|
||||
assert.equal(body.generatedAt, "2026-08-05T09:00:00Z");
|
||||
});
|
||||
|
||||
it("never lets a shared cache keep a member's copy", async () => {
|
||||
const app = appWith(feedEnv);
|
||||
after(() => app.close());
|
||||
assert.equal((await asMember(app)).headers["cache-control"], "private, no-store");
|
||||
});
|
||||
|
||||
it("is unreachable, loudly, on a box where nobody can sign in", async () => {
|
||||
const config = loadConfig({ TERA_MARKERS_FILE: file, TERA_MARKERS_SOURCE: "file" });
|
||||
config.logLevel = "silent";
|
||||
const app = buildApp(config);
|
||||
after(() => app.close());
|
||||
|
||||
// Fail-closed, and it says so rather than leaving an operator to work it out
|
||||
// from an empty map.
|
||||
assert.equal(config.auth.mode, "none");
|
||||
assert.ok(config.degraded.some((line) => line.includes("TERA_MARKERS_SOURCE=file")));
|
||||
|
||||
const anonymous = await app.inject({ method: "GET", url: "/api/v1/markers" });
|
||||
assert.equal(anonymous.statusCode, 401);
|
||||
// Even a token that would be valid elsewhere: mode=none verifies nothing.
|
||||
assert.equal((await asMember(app)).statusCode, 401);
|
||||
});
|
||||
|
||||
it("still answers the public empty body on a box with no feed", async () => {
|
||||
// The acceptance test's box. There is nothing here to protect, and making a
|
||||
// stranger sign in to be told "no markers" would gain nobody anything.
|
||||
const app = appWith({ TERA_MARKERS_SOURCE: "none" });
|
||||
after(() => app.close());
|
||||
|
||||
const res = await app.inject({ method: "GET", url: "/api/v1/markers" });
|
||||
assert.equal(res.statusCode, 200);
|
||||
assert.deepEqual(res.json<MarkersBody>().markers, []);
|
||||
assert.match(String(res.headers["cache-control"]), /^public, max-age=/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the marker file itself", () => {
|
||||
it("accepts a bare array, because that is what a person writes first", async () => {
|
||||
await write([FERRY]);
|
||||
const app = appWith(feedEnv);
|
||||
after(() => app.close());
|
||||
|
||||
assert.equal((await asMember(app)).json<MarkersBody>().markers.length, 1);
|
||||
});
|
||||
|
||||
it("refuses a bad row without losing the good ones, and reports the count", async () => {
|
||||
await write({
|
||||
markers: [
|
||||
FERRY,
|
||||
{ ...FERRY, id: "osm-row", provenance: "nominatim" },
|
||||
{ ...FERRY, id: "leaky", ownerEmail: "someone@example.com" },
|
||||
{ ...FERRY, id: "broken", lat: 200 },
|
||||
],
|
||||
});
|
||||
const app = appWith(feedEnv);
|
||||
after(() => app.close());
|
||||
|
||||
const body = (await asMember(app)).json<MarkersBody>();
|
||||
assert.deepEqual(
|
||||
body.markers.map((marker) => marker.id),
|
||||
["ferry-building"],
|
||||
);
|
||||
assert.equal(body.refused.length, 3);
|
||||
assert.ok(body.refused.some((entry) => entry.reason.includes("allowlist")));
|
||||
assert.ok(body.refused.some((entry) => entry.reason.includes("ownerEmail")));
|
||||
});
|
||||
|
||||
it("picks up a rewritten file without a restart", async () => {
|
||||
// TERA_MARKERS_TTL is the reload interval. Zero means every request checks,
|
||||
// which is what makes this assertable without sleeping for five minutes.
|
||||
const app = appWith({ ...feedEnv, TERA_MARKERS_TTL: "0" });
|
||||
after(() => app.close());
|
||||
|
||||
assert.equal((await asMember(app)).json<MarkersBody>().markers.length, 1);
|
||||
|
||||
await write({ markers: [FERRY, { ...FERRY, id: "coit-tower", label: "Coit Tower" }] });
|
||||
// A zero TTL means "check on the next millisecond", not "check twice inside
|
||||
// the same one" — `inject()` is fast enough that both requests can land on
|
||||
// the same `Date.now()`, which is a property of the test and not of the
|
||||
// reload.
|
||||
await sleep(5);
|
||||
const reloaded = (await asMember(app)).json<MarkersBody>();
|
||||
assert.deepEqual(
|
||||
reloaded.markers.map((marker) => marker.id),
|
||||
["ferry-building", "coit-tower"],
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the last good snapshot when a read fails", async () => {
|
||||
const app = appWith({ ...feedEnv, TERA_MARKERS_TTL: "0" });
|
||||
after(() => app.close());
|
||||
|
||||
assert.equal((await asMember(app)).json<MarkersBody>().markers.length, 1);
|
||||
|
||||
// The file goes away mid-flight — a rewrite by something less careful than
|
||||
// the sync oneshot, a permissions change, a full disk. Emptying the map for
|
||||
// a whole TTL is the expensive mistake; serving what was there is the cheap
|
||||
// one.
|
||||
await rm(file);
|
||||
await sleep(5);
|
||||
const survived = (await asMember(app)).json<MarkersBody>();
|
||||
assert.equal(survived.markers.length, 1);
|
||||
|
||||
// And it recovers on its own once the file comes back.
|
||||
await write({ markers: [{ ...FERRY, id: "coit-tower" }] });
|
||||
await sleep(5);
|
||||
const recovered = (await asMember(app)).json<MarkersBody>();
|
||||
assert.deepEqual(
|
||||
recovered.markers.map((marker) => marker.id),
|
||||
["coit-tower"],
|
||||
);
|
||||
});
|
||||
|
||||
it("says it cannot read the file rather than pretending there are no markers", async () => {
|
||||
await rm(file);
|
||||
const app = appWith(feedEnv);
|
||||
after(() => app.close());
|
||||
|
||||
const body = (await asMember(app)).json<MarkersBody>();
|
||||
assert.deepEqual(body.markers, []);
|
||||
assert.equal(body.refused.length, 1);
|
||||
assert.match(body.refused[0]?.reason ?? "", /could not be read/);
|
||||
});
|
||||
|
||||
it("survives a file that is not JSON at all", async () => {
|
||||
await writeFile(file, "{ this is not json");
|
||||
const app = appWith(feedEnv);
|
||||
after(() => app.close());
|
||||
|
||||
const res = await asMember(app);
|
||||
assert.equal(res.statusCode, 200);
|
||||
assert.deepEqual(res.json<MarkersBody>().markers, []);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,280 @@
|
||||
/**
|
||||
* The served-region allowlist, and everything it refuses.
|
||||
*
|
||||
* Two halves. The first is that a box answers for the *places it was configured
|
||||
* for* rather than for one origin, because the map has two cities six hundred
|
||||
* kilometres apart and the Bay Area's fog is not Los Angeles's weather.
|
||||
*
|
||||
* The second is the one with teeth: **a caller's coordinate must never reach an
|
||||
* upstream.** An endpoint that fetches any point on demand is an amplifier
|
||||
* aimed at somebody else's public-good API, with this deployment's contact
|
||||
* string on every request. So `?lat=&lng=` selects among configured points and
|
||||
* nothing else, and everything it cannot select is a 400. See the header of
|
||||
* `regions.ts`; the assertion that this refusal actually holds at the HTTP layer
|
||||
* is `weather.test.ts`.
|
||||
*/
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { after, describe, it } from "node:test";
|
||||
import { buildApp } from "../app.ts";
|
||||
import { loadConfig } from "../config.ts";
|
||||
import { loadRegions, resolveRegion, type RegionSet } from "../regions.ts";
|
||||
import type { ErrorBody, FlightsBody, WeatherBody } from "../../../src/server/wire.ts";
|
||||
|
||||
function appWith(env: Record<string, string>) {
|
||||
const config = loadConfig(env);
|
||||
config.logLevel = "silent";
|
||||
return buildApp(config);
|
||||
}
|
||||
|
||||
/** The two shipped cities, which is what an empty environment resolves to. */
|
||||
function shipped(): RegionSet {
|
||||
return loadConfig({}).regions;
|
||||
}
|
||||
|
||||
describe("the region set a box ends up with", () => {
|
||||
it("serves both shipped cities when handed nothing, San Francisco first", () => {
|
||||
const config = loadConfig({});
|
||||
assert.deepEqual(
|
||||
config.regions.map((region) => region.id),
|
||||
["sf", "socal"],
|
||||
);
|
||||
assert.deepEqual(config.degraded, []);
|
||||
// The default region is still the origin a pre-existing env file named, so
|
||||
// a bare GET answers exactly as it did before regions existed.
|
||||
assert.equal(config.regions[0].lat, config.origin.lat);
|
||||
});
|
||||
|
||||
it("promotes the shipped city an operator's origin falls inside", () => {
|
||||
const config = loadConfig({ TERA_ORIGIN_LAT: "34.05", TERA_ORIGIN_LNG: "-118.24" });
|
||||
assert.deepEqual(
|
||||
config.regions.map((region) => region.id),
|
||||
["socal", "sf"],
|
||||
);
|
||||
assert.deepEqual(config.degraded, []);
|
||||
});
|
||||
|
||||
it("adds an origin that is neither city, and makes it the default", () => {
|
||||
const config = loadConfig({ TERA_ORIGIN_LAT: "39.7392", TERA_ORIGIN_LNG: "-104.9903" });
|
||||
assert.deepEqual(
|
||||
config.regions.map((region) => region.id),
|
||||
["origin", "sf", "socal"],
|
||||
);
|
||||
assert.equal(config.regions[0].lat, 39.7392);
|
||||
});
|
||||
|
||||
it("takes TERA_REGIONS literally, in order, with an optional radius", () => {
|
||||
const config = loadConfig({
|
||||
TERA_REGIONS: "pdx:45.5152,-122.6784:80; sea:47.6062,-122.3321",
|
||||
});
|
||||
assert.deepEqual(config.regions, [
|
||||
{ id: "pdx", lat: 45.5152, lng: -122.6784, radiusKm: 80 },
|
||||
{ id: "sea", lat: 47.6062, lng: -122.3321, radiusKm: 120 },
|
||||
]);
|
||||
assert.deepEqual(config.degraded, []);
|
||||
});
|
||||
|
||||
it("drops a malformed entry with a sentence and keeps the good ones", () => {
|
||||
const config = loadConfig({ TERA_REGIONS: "pdx:45.5152,-122.6784; nowhere; sea:200,0" });
|
||||
assert.deepEqual(
|
||||
config.regions.map((region) => region.id),
|
||||
["pdx"],
|
||||
);
|
||||
assert.equal(config.degraded.length, 2);
|
||||
assert.match(config.degraded[0] ?? "", /nowhere/);
|
||||
assert.match(config.degraded[1] ?? "", /not a place on Earth/);
|
||||
});
|
||||
|
||||
it("falls back to the shipped cities when nothing in TERA_REGIONS parses", () => {
|
||||
const config = loadConfig({ TERA_REGIONS: "?????" });
|
||||
assert.deepEqual(
|
||||
config.regions.map((region) => region.id),
|
||||
["sf", "socal"],
|
||||
);
|
||||
assert.ok(config.degraded.some((line) => line.includes("nothing in it parsed")));
|
||||
});
|
||||
|
||||
it("keeps the first of two entries sharing an id", () => {
|
||||
const config = loadConfig({ TERA_REGIONS: "sf:37.7749,-122.4194; sf:0,0" });
|
||||
assert.equal(config.regions.length, 1);
|
||||
assert.ok(config.degraded.some((line) => line.includes("twice")));
|
||||
});
|
||||
|
||||
it("never ends up with an empty set, whatever it was handed", () => {
|
||||
for (const spec of ["", " ", ";;;", "sf:", "@:1,2", "x".repeat(200)]) {
|
||||
const regions = loadRegions({
|
||||
spec,
|
||||
origin: { lat: 37.7749, lng: -122.4194 },
|
||||
originConfigured: false,
|
||||
degraded: [],
|
||||
});
|
||||
assert.ok(regions.length > 0, `"${spec}" produced no regions`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolving a request to a region", () => {
|
||||
const regions = shipped();
|
||||
|
||||
it("answers for the default when asked for nothing", () => {
|
||||
const resolved = resolveRegion(regions, {});
|
||||
assert.ok(resolved.ok && resolved.region.id === "sf");
|
||||
});
|
||||
|
||||
it("answers for a city by id", () => {
|
||||
const resolved = resolveRegion(regions, { city: "socal" });
|
||||
assert.ok(resolved.ok && resolved.region.id === "socal");
|
||||
});
|
||||
|
||||
it("snaps a nearby coordinate to the region that claims it", () => {
|
||||
// Berkeley, Pasadena: neither is a configured point, and both resolve to the
|
||||
// configured point that will actually be fetched.
|
||||
const berkeley = resolveRegion(regions, { lat: "37.8715", lng: "-122.2730" });
|
||||
assert.ok(berkeley.ok && berkeley.region.id === "sf");
|
||||
const pasadena = resolveRegion(regions, { lat: "34.1478", lng: "-118.1445" });
|
||||
assert.ok(pasadena.ok && pasadena.region.id === "socal");
|
||||
});
|
||||
|
||||
it("refuses a coordinate this deployment has nothing to say about", () => {
|
||||
for (const point of [
|
||||
{ lat: "36.7378", lng: "-119.7871" }, // Fresno, between the two boards.
|
||||
{ lat: "40.7128", lng: "-74.0060" }, // New York.
|
||||
{ lat: "0", lng: "0" }, // Null Island, the classic probe.
|
||||
]) {
|
||||
const resolved = resolveRegion(regions, point);
|
||||
assert.ok(!resolved.ok, `${point.lat},${point.lng} must be refused`);
|
||||
assert.match(resolved.message, /serves: sf, socal/);
|
||||
}
|
||||
});
|
||||
|
||||
it("refuses a coordinate that is not a coordinate", () => {
|
||||
for (const lat of [
|
||||
"banana",
|
||||
"NaN",
|
||||
"Infinity",
|
||||
"1e5",
|
||||
"0x2f",
|
||||
"37.7749deg",
|
||||
"91",
|
||||
"37.77490001",
|
||||
"",
|
||||
" ",
|
||||
]) {
|
||||
const resolved = resolveRegion(regions, { lat, lng: "-122.4194" });
|
||||
assert.ok(!resolved.ok, `lat=${lat} must be refused`);
|
||||
}
|
||||
assert.ok(!resolveRegion(regions, { lat: "37.7", lng: "181" }).ok);
|
||||
});
|
||||
|
||||
it("refuses a repeated parameter rather than picking one", () => {
|
||||
// `?lat=1&lat=2` arrives as an array, and quietly taking either half is how
|
||||
// a parser disagreement becomes a security bug somewhere downstream.
|
||||
assert.ok(!resolveRegion(regions, { lat: ["37.7", "0"], lng: "-122.4" }).ok);
|
||||
assert.ok(!resolveRegion(regions, { city: ["sf", "socal"] }).ok);
|
||||
});
|
||||
|
||||
it("refuses half a coordinate", () => {
|
||||
assert.ok(!resolveRegion(regions, { lat: "37.7749" }).ok);
|
||||
assert.ok(!resolveRegion(regions, { lng: "-122.4194" }).ok);
|
||||
});
|
||||
|
||||
it("refuses a request that asks two ways at once", () => {
|
||||
const resolved = resolveRegion(regions, { city: "sf", lat: "37.7", lng: "-122.4" });
|
||||
assert.ok(!resolved.ok);
|
||||
assert.match(resolved.message, /not both/);
|
||||
});
|
||||
|
||||
it("refuses an unknown city without pretending it might exist elsewhere", () => {
|
||||
const resolved = resolveRegion(regions, { city: "atlantis" });
|
||||
assert.ok(!resolved.ok);
|
||||
assert.match(resolved.message, /sf, socal/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the routes that take a region", () => {
|
||||
for (const route of ["weather", "flights"]) {
|
||||
it(`serves ${route} for either city, from that city's own point`, async () => {
|
||||
const app = appWith({});
|
||||
after(() => app.close());
|
||||
|
||||
const sf = await app.inject({ method: "GET", url: `/api/v1/${route}?city=sf` });
|
||||
const socal = await app.inject({ method: "GET", url: `/api/v1/${route}?city=socal` });
|
||||
assert.equal(sf.statusCode, 200);
|
||||
assert.equal(socal.statusCode, 200);
|
||||
assert.notDeepEqual(sf.json(), socal.json());
|
||||
});
|
||||
|
||||
it(`refuses a nonsense ${route} query with a 400 and no cached copy of it`, async () => {
|
||||
const app = appWith({});
|
||||
after(() => app.close());
|
||||
|
||||
const res = await app.inject({ method: "GET", url: `/api/v1/${route}?lat=banana&lng=0` });
|
||||
assert.equal(res.statusCode, 400);
|
||||
assert.equal(res.json<ErrorBody>().error, "bad_request");
|
||||
// The fail-closed default still applies: nothing shared may keep a refusal.
|
||||
assert.equal(res.headers["cache-control"], "private, no-store");
|
||||
});
|
||||
|
||||
it(`refuses a ${route} request for somewhere this box does not serve`, async () => {
|
||||
const app = appWith({});
|
||||
after(() => app.close());
|
||||
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: `/api/v1/${route}?lat=51.5072&lng=-0.1276`,
|
||||
});
|
||||
assert.equal(res.statusCode, 400);
|
||||
assert.match(res.json<ErrorBody>().message, /sf, socal/);
|
||||
});
|
||||
}
|
||||
|
||||
it("puts the weather where it was asked for, not where the box is", async () => {
|
||||
const app = appWith({});
|
||||
after(() => app.close());
|
||||
|
||||
const res = await app.inject({ method: "GET", url: "/api/v1/weather?city=socal" });
|
||||
const body = res.json<WeatherBody>();
|
||||
assert.deepEqual(body.location, { lat: 33.82, lng: -118.05 });
|
||||
// Still the synthetic clear day, because a zero-config box has no source.
|
||||
assert.equal(body.synthetic, true);
|
||||
});
|
||||
|
||||
it("flies the Southland's own airports over the Southland", async () => {
|
||||
const app = appWith({});
|
||||
after(() => app.close());
|
||||
|
||||
const body = (
|
||||
await app.inject({ method: "GET", url: "/api/v1/flights?city=socal" })
|
||||
).json<FlightsBody>();
|
||||
assert.ok(body.mode === "plan");
|
||||
// LAX's published reference point, as a departure or an arrival. The Bay
|
||||
// Area plan cannot produce it, which is the whole point of the assertion.
|
||||
const lax = body.routes.some(
|
||||
(leg) =>
|
||||
(leg.from[0] === 33.9425 && leg.from[1] === -118.4081) ||
|
||||
(leg.to[0] === 33.9425 && leg.to[1] === -118.4081),
|
||||
);
|
||||
assert.ok(lax, "the SoCal plan should fly out of LAX");
|
||||
});
|
||||
|
||||
it("lays out spokes for a city it has never heard of", async () => {
|
||||
const app = appWith({ TERA_REGIONS: "pdx:45.5152,-122.6784" });
|
||||
after(() => app.close());
|
||||
|
||||
const body = (await app.inject({ method: "GET", url: "/api/v1/flights" })).json<FlightsBody>();
|
||||
assert.ok(body.mode === "plan" && body.routes.length === 8);
|
||||
});
|
||||
|
||||
it("publishes the allowlist on health so a client can stop guessing", async () => {
|
||||
const app = appWith({});
|
||||
after(() => app.close());
|
||||
|
||||
const body = (await app.inject({ method: "GET", url: "/api/v1/health" })).json<{
|
||||
regions: { id: string }[];
|
||||
}>();
|
||||
assert.deepEqual(
|
||||
body.regions.map((region) => region.id),
|
||||
["sf", "socal"],
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* Weather, once a source is actually turned on.
|
||||
*
|
||||
* `api.weather.gov` is stood up as a stub here rather than called, for the
|
||||
* obvious reason and for a less obvious one: the assertions that matter are
|
||||
* about **which URL this server constructs**, and a real upstream would answer
|
||||
* the right way for the wrong reason. The load-bearing one is that a caller
|
||||
* asking for Berkeley causes a fetch of San Francisco's point and never of
|
||||
* Berkeley's — the difference between a map and an open proxy pointed at a
|
||||
* public-good API with this deployment's contact address on it.
|
||||
*
|
||||
* Global `fetch` is replaced for the file. `node --test` runs each test file in
|
||||
* its own process, so nothing here leaks into another one, and `after` puts the
|
||||
* real one back anyway.
|
||||
*/
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { after, beforeEach, describe, it } from "node:test";
|
||||
import { buildApp } from "../app.ts";
|
||||
import { loadConfig } from "../config.ts";
|
||||
import type { WeatherBody } from "../../../src/server/wire.ts";
|
||||
|
||||
const CONTACT = "ops@example.com";
|
||||
|
||||
const nwsEnv = { TERA_WEATHER_SOURCE: "nws", TERA_WEATHER_CONTACT: CONTACT };
|
||||
|
||||
interface Call {
|
||||
url: string;
|
||||
userAgent: string;
|
||||
}
|
||||
|
||||
const realFetch = globalThis.fetch;
|
||||
let calls: Call[] = [];
|
||||
/** Flipped by a test that wants to watch the upstream go away mid-flight. */
|
||||
let upstreamIsUp = true;
|
||||
/**
|
||||
* Flipped by a test that wants the upstream to stay *up* and answer 200 with
|
||||
* something the parser cannot walk. `cloudLayers` becomes a number, which is
|
||||
* the shape that used to throw straight through the cache.
|
||||
*/
|
||||
let upstreamIsGarbled = false;
|
||||
|
||||
/**
|
||||
* The three hops NWS makes you take, and nothing else: an unrecognised URL is a
|
||||
* 404, so a request this server should not be making shows up as a failure
|
||||
* rather than as a plausible answer.
|
||||
*/
|
||||
function nws(url: string): unknown | undefined {
|
||||
if (!upstreamIsUp) return undefined;
|
||||
|
||||
const point = /\/points\/(-?[\d.]+),(-?[\d.]+)$/.exec(url);
|
||||
if (point !== null) {
|
||||
return { properties: { observationStations: `https://api.weather.gov/zones/${point[1]}` } };
|
||||
}
|
||||
const zone = /\/zones\/(-?[\d.]+)$/.exec(url);
|
||||
if (zone !== null) {
|
||||
// One station id per latitude, so a body can be traced back to the point
|
||||
// that was asked about.
|
||||
return { features: [{ properties: { stationIdentifier: `K${zone[1]}` } }] };
|
||||
}
|
||||
const station = /\/stations\/K(-?[\d.]+)\/observations\/latest$/.exec(url);
|
||||
if (station !== null) {
|
||||
return {
|
||||
properties: {
|
||||
timestamp: "2026-08-05T09:00:00+00:00",
|
||||
temperature: { value: Number(station[1]), unitCode: "wmoUnit:degC" },
|
||||
windSpeed: { value: 9, unitCode: "wmoUnit:km_h-1" },
|
||||
cloudLayers: upstreamIsGarbled ? 7 : [{ amount: "BKN" }],
|
||||
presentWeather: [],
|
||||
visibility: { value: 16_000, unitCode: "wmoUnit:m" },
|
||||
},
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
globalThis.fetch = (async (input: unknown, init?: { headers?: Record<string, string> }) => {
|
||||
const url = String(input);
|
||||
// `http.ts` always passes a plain object, so this needs no `Headers` dance.
|
||||
calls.push({ url, userAgent: init?.headers?.["user-agent"] ?? "" });
|
||||
|
||||
const body = nws(url);
|
||||
if (body === undefined) return new Response("nope", { status: 503 });
|
||||
return new Response(JSON.stringify(body), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}) as unknown as typeof globalThis.fetch;
|
||||
|
||||
after(() => {
|
||||
globalThis.fetch = realFetch;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
calls = [];
|
||||
upstreamIsUp = true;
|
||||
upstreamIsGarbled = false;
|
||||
});
|
||||
|
||||
function appWith(env: Record<string, string>) {
|
||||
const config = loadConfig(env);
|
||||
config.logLevel = "silent";
|
||||
return buildApp(config);
|
||||
}
|
||||
|
||||
function observations(): string[] {
|
||||
return calls.filter((call) => call.url.includes("/observations/")).map((call) => call.url);
|
||||
}
|
||||
|
||||
describe("a configured weather source", () => {
|
||||
it("fetches the region centre and never the coordinate the caller sent", async () => {
|
||||
const app = appWith(nwsEnv);
|
||||
after(() => app.close());
|
||||
|
||||
// Berkeley. Inside the Bay Area region, and not a point this deployment
|
||||
// serves — so it selects San Francisco and San Francisco is what gets asked.
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/v1/weather?lat=37.8715&lng=-122.2730",
|
||||
});
|
||||
assert.equal(res.statusCode, 200);
|
||||
|
||||
assert.ok(
|
||||
calls.every((call) => !call.url.includes("37.8715")),
|
||||
`the caller's coordinate reached the upstream: ${calls.map((c) => c.url).join(" ")}`,
|
||||
);
|
||||
assert.ok(calls.some((call) => call.url.endsWith("/points/37.7749,-122.4194")));
|
||||
assert.deepEqual(res.json<WeatherBody>().location, { lat: 37.7749, lng: -122.4194 });
|
||||
});
|
||||
|
||||
it("holds one observation per city rather than one per box", async () => {
|
||||
const app = appWith(nwsEnv);
|
||||
after(() => app.close());
|
||||
|
||||
const sf = (
|
||||
await app.inject({ method: "GET", url: "/api/v1/weather?city=sf" })
|
||||
).json<WeatherBody>();
|
||||
const socal = (
|
||||
await app.inject({ method: "GET", url: "/api/v1/weather?city=socal" })
|
||||
).json<WeatherBody>();
|
||||
|
||||
// The stub encodes the latitude in the temperature, so two different
|
||||
// numbers here means two different stations were actually consulted.
|
||||
assert.equal(sf.temperatureC, 37.7749);
|
||||
assert.equal(socal.temperatureC, 33.82);
|
||||
assert.equal(observations().length, 2);
|
||||
assert.equal(sf.source, "nws");
|
||||
assert.equal(sf.synthetic, false);
|
||||
});
|
||||
|
||||
it("asks once per region per TTL however many callers turn up", async () => {
|
||||
const app = appWith(nwsEnv);
|
||||
after(() => app.close());
|
||||
|
||||
const ask = () => app.inject({ method: "GET", url: "/api/v1/weather?city=sf" });
|
||||
// Concurrent, then sequential: the first is the single-flight collapse, the
|
||||
// second is the TTL. Both used to be one fetch each and only one of them was.
|
||||
await Promise.all([ask(), ask(), ask(), ask()]);
|
||||
await ask();
|
||||
await ask();
|
||||
|
||||
assert.equal(observations().length, 1);
|
||||
});
|
||||
|
||||
it("keeps the cities apart under load, not merely on the first request", async () => {
|
||||
const app = appWith(nwsEnv);
|
||||
after(() => app.close());
|
||||
|
||||
const bodies = await Promise.all(
|
||||
["sf", "socal", "sf", "socal", "sf"].map(async (city) =>
|
||||
(await app.inject({ method: "GET", url: `/api/v1/weather?city=${city}` })).json<WeatherBody>(),
|
||||
),
|
||||
);
|
||||
assert.deepEqual(
|
||||
bodies.map((body) => body.temperatureC),
|
||||
[37.7749, 33.82, 37.7749, 33.82, 37.7749],
|
||||
);
|
||||
assert.equal(observations().length, 2);
|
||||
});
|
||||
|
||||
/**
|
||||
* A source that is *up* and answering in a shape this build cannot read is a
|
||||
* different failure from one that is down, and it used to be a much worse
|
||||
* one: the parser threw, `upstream.ts` never reached its clock stamp, and the
|
||||
* TTL — the only thing standing between a public-good API and one outbound
|
||||
* request per inbound request — stopped existing. `current()` also stopped
|
||||
* being the thing its own header calls it, which is a function that never
|
||||
* throws.
|
||||
*/
|
||||
it("treats a body it cannot parse as a source that did not answer", async () => {
|
||||
upstreamIsGarbled = true;
|
||||
const app = appWith(nwsEnv);
|
||||
after(() => app.close());
|
||||
|
||||
const res = await app.inject({ method: "GET", url: "/api/v1/weather?city=sf" });
|
||||
assert.equal(res.statusCode, 200);
|
||||
// The clear day, which is what "nobody answered" has always meant here.
|
||||
assert.equal(res.json<WeatherBody>().synthetic, true);
|
||||
});
|
||||
|
||||
it("keeps the TTL when the body is garbled, not just when the socket dies", async () => {
|
||||
upstreamIsGarbled = true;
|
||||
const app = appWith(nwsEnv);
|
||||
after(() => app.close());
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await app.inject({ method: "GET", url: "/api/v1/weather?city=sf" });
|
||||
}
|
||||
assert.equal(observations().length, 1);
|
||||
});
|
||||
|
||||
it("identifies the operator to a source that requires it", async () => {
|
||||
const app = appWith(nwsEnv);
|
||||
after(() => app.close());
|
||||
|
||||
await app.inject({ method: "GET", url: "/api/v1/weather" });
|
||||
assert.ok(calls.length > 0);
|
||||
for (const call of calls) assert.match(call.userAgent, new RegExp(CONTACT));
|
||||
});
|
||||
|
||||
it("serves the last good observation when the upstream goes away", async () => {
|
||||
// TTL 0 makes every request a refetch, which is what makes the failure
|
||||
// reachable in a test without waiting ten minutes for one.
|
||||
const app = appWith({ ...nwsEnv, TERA_WEATHER_TTL: "0" });
|
||||
after(() => app.close());
|
||||
|
||||
const first = (
|
||||
await app.inject({ method: "GET", url: "/api/v1/weather?city=sf" })
|
||||
).json<WeatherBody>();
|
||||
assert.equal(first.temperatureC, 37.7749);
|
||||
|
||||
upstreamIsUp = false;
|
||||
const second = await app.inject({ method: "GET", url: "/api/v1/weather?city=sf" });
|
||||
assert.equal(second.statusCode, 200);
|
||||
const body = second.json<WeatherBody>();
|
||||
// Not a 503, not a clear day: the observation from a minute ago, which is
|
||||
// the only answer that keeps the sky looking like the sky.
|
||||
assert.equal(body.temperatureC, 37.7749);
|
||||
assert.equal(body.synthetic, false);
|
||||
});
|
||||
|
||||
it("falls back to a clear day for a city that has never answered", async () => {
|
||||
upstreamIsUp = false;
|
||||
const app = appWith(nwsEnv);
|
||||
after(() => app.close());
|
||||
|
||||
const res = await app.inject({ method: "GET", url: "/api/v1/weather?city=socal" });
|
||||
assert.equal(res.statusCode, 200);
|
||||
const body = res.json<WeatherBody>();
|
||||
assert.equal(body.synthetic, true);
|
||||
assert.equal(body.condition, "clear");
|
||||
assert.deepEqual(body.location, { lat: 33.82, lng: -118.05 });
|
||||
});
|
||||
|
||||
it("retries a dead source on the TTL, not on every request", async () => {
|
||||
upstreamIsUp = false;
|
||||
const app = appWith(nwsEnv);
|
||||
after(() => app.close());
|
||||
|
||||
for (let i = 0; i < 5; i++) await app.inject({ method: "GET", url: "/api/v1/weather" });
|
||||
// One attempt, one failure, one clock stamp. Somebody else's outage must not
|
||||
// turn into this box's outbound flood.
|
||||
assert.equal(calls.length, 1);
|
||||
});
|
||||
|
||||
it("makes no outbound request at all until somebody asks", async () => {
|
||||
const app = appWith(nwsEnv);
|
||||
after(() => app.close());
|
||||
await app.inject({ method: "GET", url: "/api/v1/health" });
|
||||
assert.deepEqual(calls, []);
|
||||
});
|
||||
|
||||
it("never calls anybody when the source is off", async () => {
|
||||
const app = appWith({});
|
||||
after(() => app.close());
|
||||
|
||||
await app.inject({ method: "GET", url: "/api/v1/weather?city=sf" });
|
||||
await app.inject({ method: "GET", url: "/api/v1/weather?city=socal" });
|
||||
assert.deepEqual(calls, []);
|
||||
});
|
||||
|
||||
it("refuses the request before it would have fetched anything", async () => {
|
||||
const app = appWith(nwsEnv);
|
||||
after(() => app.close());
|
||||
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/v1/weather?lat=51.5072&lng=-0.1276",
|
||||
});
|
||||
assert.equal(res.statusCode, 400);
|
||||
// The point of the allowlist: a refused request costs the upstream nothing.
|
||||
assert.deepEqual(calls, []);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user