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>
87 lines
2.9 KiB
TypeScript
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()))),
|
|
};
|
|
}
|