import { createContext, type Context, type ProblemDetails } from "@wrnexus/core"; export interface TestRequestOptions extends Omit { body?: BodyInit | Record | URLSearchParams | FormData | null; baseUrl?: string; } /** Build a web-standard Request with convenient JSON/FormData handling. */ export function testRequest(path = "/", options: TestRequestOptions = {}): Request { const base = options.baseUrl ?? "http://localhost"; const headers = new Headers(options.headers); let body: BodyInit | null | undefined = options.body as BodyInit | null | undefined; if ( body !== null && body !== undefined && typeof body === "object" && !(body instanceof URLSearchParams) && !(body instanceof FormData) && !(body instanceof Blob) && !(body instanceof ArrayBuffer) && !ArrayBuffer.isView(body) && !(body instanceof ReadableStream) ) { body = JSON.stringify(body); if (!headers.has("content-type")) headers.set("content-type", "application/json"); } const method = options.method ?? (body == null ? "GET" : "POST"); return new Request(new URL(path, base), { ...options, method, headers, body }); } /** Create a complete Context suitable for middleware and route unit tests. */ export function testContext(path = "/", options: TestRequestOptions = {}): Context { const request = testRequest(path, options); return createContext(request, new URL(request.url)); } export interface JsonResponse { response: Response; body: T; } export async function readJsonResponse(response: Response): Promise> { return { response, body: (await response.json()) as T }; } export async function expectProblem(response: Response, status?: number): Promise { const contentType = response.headers.get("content-type") ?? ""; if (!contentType.includes("application/problem+json")) { throw new Error(`Expected application/problem+json but received '${contentType || "none"}'.`); } if (status !== undefined && response.status !== status) { throw new Error(`Expected status ${status} but received ${response.status}.`); } const value = (await response.json()) as ProblemDetails; if (!value.title || !Number.isInteger(value.status)) { throw new Error("Response body is not RFC 9457 Problem Details."); } return value; } export interface Deferred { promise: Promise; resolve(value: T | PromiseLike): void; reject(reason?: unknown): void; } export function deferred(): Deferred { let resolve!: Deferred["resolve"]; let reject!: Deferred["reject"]; const promise = new Promise((resolvePromise, rejectPromise) => { resolve = resolvePromise; reject = rejectPromise; }); return { promise, resolve, reject }; } export interface WaitForOptions { timeoutMs?: number; intervalMs?: number; signal?: AbortSignal; } /** Poll a condition without depending on fake timers or a browser runtime. */ export async function waitFor( condition: () => boolean | Promise, options: WaitForOptions = {}, ): Promise { const timeoutMs = options.timeoutMs ?? 1_000; const intervalMs = options.intervalMs ?? 10; const startedAt = Date.now(); while (!(await condition())) { if (options.signal?.aborted) throw options.signal.reason ?? new DOMException("Aborted", "AbortError"); if (Date.now() - startedAt >= timeoutMs) throw new Error(`waitFor timed out after ${timeoutMs}ms`); await new Promise((resolve) => setTimeout(resolve, intervalMs)); } } export class MemoryCookieJar { readonly #cookies = new Map(); apply(response: Response): void { const value = response.headers.get("set-cookie"); if (!value) return; for (const segment of value.split(/,(?=[^;,]+=)/)) { const pair = segment.split(";", 1)[0]?.trim(); if (!pair) continue; const index = pair.indexOf("="); if (index <= 0) continue; this.#cookies.set(pair.slice(0, index), pair.slice(index + 1)); } } header(): string { return [...this.#cookies].map(([name, value]) => `${name}=${value}`).join("; "); } request(path: string, options: TestRequestOptions = {}): Request { const headers = new Headers(options.headers); const cookie = this.header(); if (cookie) headers.set("cookie", cookie); return testRequest(path, { ...options, headers }); } clear(): void { this.#cookies.clear(); } }