Files
arah2/routing/test/http-source.test.mjs
araharah-executor 0abe64e734 test(06-04): failing http-source suite H1-H8 + /api/route block RT1-RT11 + RP1/RP2 re-pin (RED)
- routing/test/http-source.test.mjs: backend #2 client seam — slug-guard-first,
  fail-honest on throw/non-200/malformed/null-body, defensive RouteResult shape
- webhook.test.mjs RT block: 401/413/400 classes, out-of-bbox origin (V5),
  200-null on unknown dest / absent graph / corrupt binary / snap miss / A* miss,
  happy path (engine-a, ONE option, steps) with the 1-GET isolate-memo probe
- route-privacy re-pin (06-04's own gate law): RP1 = claim-bound local routing
  (getClaimByToken, no fetch/proxy, arm wired to worker-router, no routing hosts),
  RP2 = workspace allowlist @araharah/{search,routing}/* subpaths only
2026-10-24 13:22:51 +08:00

180 lines
8.5 KiB
JavaScript

#!/usr/bin/env node
/*
* http-source.test.mjs — RoutingSource backend #2's client seam (06-04, D-01).
* ------------------------------------------------------------------------------
* Source-under-test: src/http-source.ts — createHttpRoutingSource(opts) turns
* the Worker's Bearer-gated POST /api/route into a RoutingSource. The fixture
* backend's laws carry over (fixture-source.ts is the posture reference):
*
* - slug guard FIRST: a hostile destinationId never reaches the network
* - fail-honest (T-05-04's .catch(() => null) law, network edition): a
* thrown fetch, a non-200, a malformed body, or a shape violation is an
* honest null — the designed router-pending state (D-03), never a throw
* in the webview's render path
* - a 200 with JSON null body IS the null contract crossing HTTP — also
* resolves null, not an error
* - the Authorization header rides ONLY when the injected bearer getter
* returns a token (a logged-out caller still sends the request; the
* server's 401 then degrades to null like any other miss)
*
* The fetcher is INJECTED (fetchImpl) — this suite is fully offline.
*
* Cases:
* H1 happy path → RouteResult; POST + JSON body {origin, destinationId};
* Bearer header attached from the injected getter
* H2 slug guard first — off-slug destinationId → null, fetch NEVER called
* H3 non-200 (401/404/500) → null
* H4 thrown fetch → null (fail-honest)
* H5 malformed JSON body → null
* H6 200 with JSON null body (router-pending over HTTP) → null
* H7 shape violations → null (destination mismatch, options 0/4, non-finite
* summary, cumulativeDistanceM length ≠ points length, non-finite
* point, missing backend, steps not an array)
* H8 bearer() → null ⇒ request goes WITHOUT an Authorization header
*
* Run: npm run routing:test (exit 0 = all green)
*/
import { strict as assert } from "node:assert";
import { test } from "./harness.mjs";
const ENDPOINT = "https://worker.example/api/route";
const OK_RESULT = {
destinationId: "suria-klcc",
origin: { lat: 3.1528, lng: 101.7038 },
options: [
{
geometry: {
points: [
{ lat: 3.1528, lng: 101.7038 },
{ lat: 3.1553, lng: 101.7078 },
{ lat: 3.1578, lng: 101.7117 },
],
cumulativeDistanceM: [0, 521.4, 1042.8],
},
summary: { distanceM: 1042.8, durationS: 62.6 },
steps: [
{ kind: "depart", name: "Jalan Koridor Ujian", distanceM: 1042.8, geometry: "oqv_Egt~~`Ewj@_q@" },
{ kind: "arrive", geometry: "{wv_Aeb~y`E" },
],
},
],
backend: "engine-a",
};
const REQUEST = { origin: { lat: 3.1528, lng: 101.7038 }, destinationId: "suria-klcc" };
/** A fetchImpl returning one fixed response; records the calls it saw. */
function fetching(status, body, { throws = false } = {}) {
const calls = [];
const impl = async (url, init) => {
calls.push({ url, init });
if (throws) throw new Error("network down");
return {
ok: status >= 200 && status < 300,
status,
json: async () => JSON.parse(body),
};
};
return { impl, calls };
}
async function load() {
return import("../src/http-source.ts");
}
test("H1 happy path → RouteResult passthrough; POST JSON {origin, destinationId}; Bearer attached from the getter", async () => {
const { createHttpRoutingSource } = await load();
const { impl, calls } = fetching(200, JSON.stringify(OK_RESULT));
const source = createHttpRoutingSource({ endpoint: ENDPOINT, bearer: () => "tok-123", fetchImpl: impl });
assert.equal(source.name, "engine-a");
const result = await source.route(REQUEST);
assert.deepEqual(result, OK_RESULT, "the validated payload crosses as the RouteResult");
assert.equal(calls.length, 1);
const { url, init } = calls[0];
assert.equal(url, ENDPOINT);
assert.equal(init.method, "POST");
assert.deepEqual(JSON.parse(init.body), { origin: REQUEST.origin, destinationId: REQUEST.destinationId });
assert.equal(init.headers.authorization, "Bearer tok-123");
});
test("H2 slug guard first — off-slug destinationId → null and the fetch is NEVER called", async () => {
const { createHttpRoutingSource } = await load();
const { impl, calls } = fetching(200, JSON.stringify(OK_RESULT));
const source = createHttpRoutingSource({ endpoint: ENDPOINT, bearer: () => "tok", fetchImpl: impl });
for (const bad of ["Suria KLCC", "", `${"x".repeat(65)}`, "klcc/../etc"]) {
const result = await source.route({ origin: REQUEST.origin, destinationId: bad });
assert.equal(result, null, `off-slug id ${JSON.stringify(bad)} is an honest null`);
}
assert.equal(calls.length, 0, "a hostile id never reaches the network (T-05-03 posture)");
});
test("H3 non-200 (401/404/500) → null", async () => {
const { createHttpRoutingSource } = await load();
for (const status of [401, 404, 500]) {
const { impl } = fetching(status, '{"error":"x"}');
const source = createHttpRoutingSource({ endpoint: ENDPOINT, bearer: () => "tok", fetchImpl: impl });
assert.equal(await source.route(REQUEST), null, `HTTP ${status} degrades to null`);
}
});
test("H4 thrown fetch → null (fail-honest, T-05-04 network edition)", async () => {
const { createHttpRoutingSource } = await load();
const { impl } = fetching(200, "{}", { throws: true });
const source = createHttpRoutingSource({ endpoint: ENDPOINT, bearer: () => "tok", fetchImpl: impl });
assert.equal(await source.route(REQUEST), null);
});
test("H5 malformed JSON body → null", async () => {
const { createHttpRoutingSource } = await load();
const { impl } = fetching(200, "<html>not json</html>");
const source = createHttpRoutingSource({ endpoint: ENDPOINT, bearer: () => "tok", fetchImpl: impl });
assert.equal(await source.route(REQUEST), null);
});
test("H6 200 with JSON null body (router-pending over HTTP) → null, not an error", async () => {
const { createHttpRoutingSource } = await load();
const { impl } = fetching(200, "null");
const source = createHttpRoutingSource({ endpoint: ENDPOINT, bearer: () => "tok", fetchImpl: impl });
assert.equal(await source.route(REQUEST), null);
});
test("H7 shape violations → null — every field defensive, the fixture-source posture", async () => {
const { createHttpRoutingSource } = await load();
const mutations = [
["destinationId mismatch", (r) => { r.destinationId = "kl-sentral"; }],
["options not an array", (r) => { r.options = "one"; }],
["zero options", (r) => { r.options = []; }],
["four options", (r) => { r.options = [OK_RESULT.options[0], OK_RESULT.options[0], OK_RESULT.options[0], OK_RESULT.options[0]]; }],
["non-finite distanceM", (r) => { r.options[0].summary.distanceM = Number.NaN; }],
["negative durationS", (r) => { r.options[0].summary.durationS = -5; }],
["cumulativeDistanceM length ≠ points length", (r) => { r.options[0].geometry.cumulativeDistanceM = [0]; }],
["non-finite point", (r) => { r.options[0].geometry.points[1].lat = Number.NaN; }],
["missing backend", (r) => { delete r.backend; }],
["steps not an array", (r) => { r.options[0].steps = "depart"; }],
["origin not a point", (r) => { r.origin = { lat: "far", lng: 101.7 }; }],
];
for (const [label, mutate] of mutations) {
const payload = structuredClone(OK_RESULT);
mutate(payload);
const { impl } = fetching(200, JSON.stringify(payload));
const source = createHttpRoutingSource({ endpoint: ENDPOINT, bearer: () => "tok", fetchImpl: impl });
const result = await source.route(REQUEST);
assert.equal(result, null, `${label} must degrade to null`);
}
for (const [label, raw] of [["array payload", "[]"], ["null payload", "null"], ["string payload", '"ok"']]) {
const { impl } = fetching(200, raw);
const source = createHttpRoutingSource({ endpoint: ENDPOINT, bearer: () => "tok", fetchImpl: impl });
assert.equal(await source.route(REQUEST), null, `${label} must degrade to null`);
}
});
test("H8 bearer() → null ⇒ the request goes out WITHOUT an Authorization header (server 401 degrades like any miss)", async () => {
const { createHttpRoutingSource } = await load();
const { impl, calls } = fetching(401, '{"error":"unauthorized"}');
const source = createHttpRoutingSource({ endpoint: ENDPOINT, bearer: () => null, fetchImpl: impl });
assert.equal(await source.route(REQUEST), null);
assert.equal(calls.length, 1, "the request still went out");
assert.equal(calls[0].init.headers.authorization, undefined, "no Authorization header without a token");
});