Files
0pt/tests/contracts/contract-test-server.ts
T
Simon AxelssonandClaude Fable 5 da58176735 test(contracts): per-client wire-contract suites + §3 production-guard tests
38 tests: recording provider double answers with the sibling services' real
response shapes; each suite asserts the exact request wire shape (path,
method, tenant/actor/idempotency headers, camelCase vs snake_case body) so a
regression to the pre-rewrite invented shapes fails loudly. Includes the
fail-closed policy cases (missing decision => deny, CL0UD down => typed
error, never implicit allow) and the production factory guard for all four
clients.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 16:37:57 +02:00

87 lines
2.9 KiB
TypeScript

/**
* tests/contracts/contract-test-server.ts
* ========================================
* In-process HTTP provider double for the per-client CONTRACT tests
* (Master-DoD §10 "Contract tests finns", §50 Contract level).
*
* NOT a mock of 0pt's clients — a recording provider that answers with
* response bodies shaped EXACTLY like the sibling services' real handlers
* (verified against sax3l/st0re, sax3l/aud0, sax3l/n0d, sax3l/cl0ud main
* this session). Each test asserts the REQUEST the client actually put on
* the wire: path, method, headers (tenant/actor/context propagation,
* idempotency), and body field names. That is the drift alarm: if a client
* regresses to its pre-2026-08-31 invented wire shapes, these tests fail.
*/
import http from "node:http";
import type { AddressInfo } from "node:net";
export interface RecordedRequest {
method: string;
/** Path WITHOUT query string. */
path: string;
/** Raw query params. */
query: Record<string, string>;
headers: http.IncomingHttpHeaders;
body: unknown;
}
export interface ContractRoute {
method: string;
/** Exact path to match (no query). */
path: string;
status: number;
/** Response body factory — receives the recorded request. */
respond: (req: RecordedRequest) => unknown;
}
export interface ContractServer {
baseUrl: string;
requests: RecordedRequest[];
close(): Promise<void>;
}
export async function startContractServer(routes: ContractRoute[]): Promise<ContractServer> {
const requests: RecordedRequest[] = [];
const server = http.createServer((req, res) => {
const chunks: Buffer[] = [];
req.on("data", (c: Buffer) => chunks.push(c));
req.on("end", () => {
const url = new URL(req.url ?? "/", "http://localhost");
const raw = Buffer.concat(chunks).toString("utf8");
let body: unknown;
try {
body = raw ? JSON.parse(raw) : undefined;
} catch {
body = raw;
}
const recorded: RecordedRequest = {
method: req.method ?? "GET",
path: url.pathname,
query: Object.fromEntries(url.searchParams.entries()),
headers: req.headers,
body,
};
requests.push(recorded);
const route = routes.find((r) => r.method === recorded.method && r.path === recorded.path);
if (!route) {
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: { code: "NOT_FOUND", message: `no contract route ${recorded.method} ${recorded.path}` } }));
return;
}
res.writeHead(route.status, { "Content-Type": "application/json" });
res.end(JSON.stringify(route.respond(recorded)));
});
});
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
const { port } = server.address() as AddressInfo;
return {
baseUrl: `http://127.0.0.1:${port}`,
requests,
close: () => new Promise<void>((resolve, reject) => server.close((e) => (e ? reject(e) : resolve()))),
};
}