93 lines
3.0 KiB
TypeScript
93 lines
3.0 KiB
TypeScript
/**
|
|
* 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<Record<string, string>>;
|
|
readonly body: string | undefined;
|
|
}
|
|
|
|
export interface FakeFetchOptions {
|
|
readonly responder: (call: FakeFetchCall) => Response | Promise<Response>;
|
|
readonly history?: FakeFetchCall[];
|
|
}
|
|
|
|
export function makeFakeFetch(options: FakeFetchOptions): FetchFn {
|
|
const history = options.history ?? [];
|
|
return async (
|
|
input: string | URL | Request,
|
|
init?: RequestInit
|
|
): Promise<Response> => {
|
|
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<string, string>).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> = {}
|
|
): 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<void> {
|
|
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<void> {
|
|
const deadline = Date.now() + timeoutMs;
|
|
while (Date.now() < deadline) {
|
|
if (predicate()) return;
|
|
await new Promise<void>((r) => setTimeout(r, 5));
|
|
}
|
|
throw new Error("waitUntil: condition never satisfied");
|
|
}
|