86 lines
2.8 KiB
TypeScript
86 lines
2.8 KiB
TypeScript
/**
|
|
* Test-only helpers that stand up a real `node:http` server on a random
|
|
* port and route a `FetchLike` through it. Used by every provider's
|
|
* `*.test.ts` to exercise the full HTTP path without mocks.
|
|
*
|
|
* Lives under `tests/setup/` (not `src/`) so the universal dep-graph
|
|
* scan never sees it.
|
|
*/
|
|
import {
|
|
createServer,
|
|
type IncomingMessage,
|
|
type Server,
|
|
type ServerResponse,
|
|
} from "node:http";
|
|
import type { FetchLike } from "../../src/types/provider.js";
|
|
|
|
export interface RecordedRequest {
|
|
readonly method: string;
|
|
readonly path: string;
|
|
readonly headers: Record<string, string | string[] | undefined>;
|
|
readonly body: string;
|
|
}
|
|
|
|
export interface RecordingServer {
|
|
readonly origin: string;
|
|
readonly requests: RecordedRequest[];
|
|
readonly fetch: FetchLike;
|
|
close: () => Promise<void>;
|
|
}
|
|
|
|
export type Responder = (
|
|
req: IncomingMessage,
|
|
res: ServerResponse,
|
|
body: string
|
|
) => void | Promise<void>;
|
|
|
|
/**
|
|
* Stand up an HTTP server on a random port, capture every request, and
|
|
* return a `FetchLike` that proxies any URL (regardless of original
|
|
* scheme/host) to the local server preserving the path and method.
|
|
*/
|
|
export async function startRecordingServer(
|
|
respond: Responder
|
|
): Promise<RecordingServer> {
|
|
const requests: RecordedRequest[] = [];
|
|
const server: Server = createServer((req, res) => {
|
|
const chunks: Buffer[] = [];
|
|
req.on("data", (c) => chunks.push(c));
|
|
req.on("end", () => {
|
|
const body = Buffer.concat(chunks).toString("utf8");
|
|
requests.push({
|
|
method: req.method ?? "GET",
|
|
path: req.url ?? "/",
|
|
headers: { ...req.headers },
|
|
body,
|
|
});
|
|
respond(req, res, body);
|
|
});
|
|
});
|
|
await new Promise<void>((r) => server.listen(0, "127.0.0.1", r));
|
|
const addr = server.address();
|
|
if (addr === null || typeof addr === "string") {
|
|
throw new Error("server.address() returned an unexpected value");
|
|
}
|
|
const origin = `http://127.0.0.1:${addr.port}`;
|
|
const fetchImpl: FetchLike = async (url, init) => {
|
|
const u = new URL(url);
|
|
const rerouted = `${origin}${u.pathname}${u.search}`;
|
|
return globalThis.fetch(rerouted, init);
|
|
};
|
|
const close = async (): Promise<void> => {
|
|
await new Promise<void>((resolve, reject) => {
|
|
server.close((err) => (err ? reject(err) : resolve()));
|
|
});
|
|
};
|
|
return { origin, requests, fetch: fetchImpl, close };
|
|
}
|
|
|
|
/** Convenience: respond with a JSON body + status. */
|
|
export function jsonResponder(body: unknown, status = 200): Responder {
|
|
return (_req, res) => {
|
|
res.writeHead(status, { "content-type": "application/json" });
|
|
res.end(typeof body === "string" ? body : JSON.stringify(body));
|
|
};
|
|
}
|