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
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
#!/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");
|
||||
});
|
||||
Reference in New Issue
Block a user