Initial Commit
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* 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");
|
||||
}
|
||||
Reference in New Issue
Block a user