import { type MockCall } from "../driver/mock.mjs"; import type { HttpMethod, Misina, MisinaOptions } from "../types.mjs"; export interface TestRouteContext { url: URL; method: HttpMethod; request: Request; params: Record; } export type TestRouteResponse = Response | TestResponseInit | Promise; export interface TestResponseInit { status?: number; statusText?: string; headers?: Record; body?: unknown; /** Simulate latency (ms) before responding. */ delay?: number; /** Throw a network-style error instead of responding. */ throw?: Error | string; } export type TestRouteHandler = (ctx: TestRouteContext) => TestRouteResponse; export interface TestRoutes { [pattern: string]: TestRouteHandler | TestResponseInit; } export interface CreateTestMisinaOptions extends MisinaOptions { routes?: TestRoutes; /** Throw when a request hits no route. Default: true. */ strict?: boolean; } export interface RouteCoverage { /** Routes that received at least one request. */ matched: string[]; /** Routes that were declared but never hit. */ unused: string[]; /** Requests that hit no declared route (only when strict: false). */ unmatched: MockCall[]; } export interface TestMisina { client: Misina; calls: readonly MockCall[]; reset: () => void; lastCall: () => MockCall | undefined; /** * Snapshot which routes have / haven't been exercised, plus any * requests that fell through to no route (only meaningful with * `strict: false`). */ coverage: () => RouteCoverage; } /** * Build a Misina instance backed by an in-memory mock driver. Routes are * matched by `METHOD /path` patterns supporting `:param` syntax. Records * every request for assertion in tests. */ export declare function createTestMisina(opts?: CreateTestMisinaOptions): TestMisina; export type { MockCall } from "../driver/mock.mjs"; /** A serialized request/response pair, JSON-stable across runtimes. */ export interface CassetteEntry { request: { method: string; url: string; headers: Record; /** UTF-8 body when present; binary bodies are base64-prefixed. */ body?: string; }; response: { status: number; statusText?: string; headers: Record; body?: string; }; } export type Cassette = CassetteEntry[]; export interface RecordedCall { request: Request; response: Response; } /** Match strategy when looking up a recorded entry on replay. */ export type CassetteMatcher = (request: Request, entry: CassetteEntry, index: number) => boolean; /** * Wrap a Misina with a recorder that captures every request/response * pair flowing through it. Returns the wrapped client plus a `calls` * array suitable for `recordToJSON`. * * Records run on the `afterResponse` hook, so any response that comes * back through the misina pipeline is captured — including 4xx/5xx and * driver-level mock responses. */ export declare function record(misina: Misina): { client: Misina; calls: RecordedCall[]; }; /** * Serialize recorded calls to a JSON-stable cassette. Bodies are read * fully via `clone().text()`, so the originals stay readable. Headers * are normalized to lowercase keys for deterministic equality. */ export declare function recordToJSON(calls: RecordedCall[]): Promise; /** * Build a replay handler from a cassette. The result plugs into * `createTestMisina({ replay: cassette, replayMatch })` (below) — when * a request is issued, the first cassette entry whose matcher returns * true is consumed; subsequent calls advance through unconsumed * entries. * * Default matcher pairs entries by method + url. Pass a custom * `match` callback to also branch on body, headers, or query order. */ export declare function replayFromJSON(cassette: Cassette, options?: { match?: CassetteMatcher; consume?: boolean; }): TestRouteHandler; /** * Pick a random status from a pool every time the route is hit. Useful * for resilience tests that want to hammer retry / breaker logic with a * mix of 200s and 5xx without scripting each call. */ export declare function randomStatus(statuses: readonly number[], body?: unknown): TestRouteHandler; /** * Throw a network-style error every time. Use as a route handler when * testing how callers react to "the connection just died" — this is * what misina's `NetworkError` wraps. */ export declare function randomNetworkError(message?: string): TestRouteHandler; /** * Minimal HAR 1.2 shape we care about. Chrome DevTools, Firefox, Safari, * Playwright, and Charles all emit this format. */ interface HarFile { log: { entries: Array<{ request: { method: string; url: string; headers: Array<{ name: string; value: string; }>; postData?: { text?: string; }; }; response: { status: number; statusText?: string; headers: Array<{ name: string; value: string; }>; content?: { text?: string; encoding?: string; }; }; }>; }; } /** * Convert an HTTP Archive (HAR) file into a misina cassette. The HAR * `entries[].request` and `entries[].response` arrays map directly onto * the `CassetteEntry` shape, so the result plugs into * `replayFromJSON(...)` without an intermediate step. * * Base64-encoded response bodies (HAR uses `encoding: "base64"` for * binary payloads) are decoded eagerly so callers don't have to know * about the wrapper. */ export declare function harToCassette(har: HarFile): Cassette; /** * Default volatile headers redacted by `misinaCallSerializer`. These * change every run and would otherwise make snapshots flaky. */ export declare const DEFAULT_VOLATILE_HEADERS: readonly string[]; export interface SerializerOptions { /** Headers to replace with `[redacted]`. Default: DEFAULT_VOLATILE_HEADERS. */ redactHeaders?: readonly string[]; } /** * Vitest snapshot serializer for `MockCall`. Redacts volatile headers * (authorization, idempotency-key, traceparent, etc.) so snapshots * compare cleanly across runs. Use it like: * * ```ts * import { misinaCallSerializer } from "misina/test" * expect.addSnapshotSerializer(misinaCallSerializer()) * ``` * * Returns the `{ test, serialize }` shape Vitest's serializer API * expects. */ export declare function misinaCallSerializer(options?: SerializerOptions): { test: (value: unknown) => boolean; serialize: (value: unknown) => string; };