/** * Shared test helpers for the HttpsTransport test suite. Centralises * fake fetch construction so each test file stays focused on the * behaviour under test. */ import type { MessageEnvelope, PeerId } from "@suaveplan/types"; import type { FetchFn } from "../src/types/transport.types.js"; export interface FakeFetchCall { readonly url: string; readonly method: string; readonly headers: Readonly>; readonly body: string | undefined; } export interface FakeFetchOptions { readonly responder: (call: FakeFetchCall) => Response | Promise; readonly history?: FakeFetchCall[]; } export function makeFakeFetch(options: FakeFetchOptions): FetchFn { const history = options.history ?? []; return async ( input: string | URL | Request, init?: RequestInit ): Promise => { const url = typeof input === "string" ? input : input.toString(); const method = init?.method ?? "GET"; const headers = init?.headers ? Object.fromEntries( Object.entries(init.headers as Record).map( ([k, v]) => [k.toLowerCase(), v] ) ) : {}; const body = typeof init?.body === "string" ? init.body : undefined; const call: FakeFetchCall = { url, method, headers, body }; history.push(call); if (init?.signal?.aborted) { throw new DOMException("aborted", "AbortError"); } return options.responder(call); }; } export function jsonResponse(value: unknown, status = 200): Response { return new Response(JSON.stringify(value), { status, headers: { "content-type": "application/json" }, }); } export function makeEnvelope( overrides: Partial = {} ): MessageEnvelope { return { id: overrides.id ?? "msg-1", type: overrides.type ?? "ping", senderId: overrides.senderId ?? "peer-a", targetId: overrides.targetId ?? "peer-b", payload: overrides.payload ?? new Uint8Array([1, 2, 3]), timestamp: overrides.timestamp ?? 1_000, headers: overrides.headers ?? {}, } as MessageEnvelope; } export const TEST_PEER: PeerId = "peer-b"; /** Yield the microtask queue several times to let async loops progress. */ export async function flushMicrotasks(times = 4): Promise { for (let i = 0; i < times; i++) { await Promise.resolve(); } } /** * Wait until `predicate()` returns true or the deadline expires. * Polls with a small real-time delay so backoff timers (which use * real `setTimeout`) get a chance to fire between checks. */ export async function waitUntil( predicate: () => boolean, timeoutMs = 2_000 ): Promise { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { if (predicate()) return; await new Promise((r) => setTimeout(r, 5)); } throw new Error("waitUntil: condition never satisfied"); }