Files
wehub-resource-sync 4ce4204b6c
CI / Lint (push) Failing after 2s
CI / Build (push) Has been skipped
SDK CI / PHP SDK (push) Failing after 1s
Split PHP SDK / PHP SDK tests (push) Failing after 1s
CI / Test (push) Failing after 1s
CI / Dashboard (push) Failing after 0s
SDK CI / JavaScript SDK (push) Failing after 0s
SDK CI / Python SDK (push) Failing after 2s
SDK CI / Java SDK (push) Failing after 1s
Split PHP SDK / Mirror sdk/php -> rmyndharis/openwa-php (push) Has been skipped
CI / Test (PostgreSQL migrations) (push) Failing after 7m47s
CI / Docker Build (push) Has been skipped
chore: import upstream snapshot with attribution
2026-07-13 12:24:08 +08:00

109 lines
3.9 KiB
TypeScript

/**
* Test helpers — a mock fetch transport that records calls and returns
* canned responses, so tests assert on exact method/url/body/headers without
* any real network or global monkey-patching.
*/
import type { FetchLike } from '../src';
export interface RecordedCall {
url: string;
method: string;
headers: Record<string, string>;
body: unknown;
}
export interface MockResponseSpec {
status?: number;
body?: unknown;
text?: string;
}
/** A scripted mock transport. Throws if a call doesn't match a route. */
export class MockTransport {
readonly calls: RecordedCall[] = [];
private readonly routes: { method: string; matcher: RegExp; respond: (call: RecordedCall) => MockResponseSpec }[] =
[];
private fallback: ((call: RecordedCall) => MockResponseSpec) | null = null;
/** Register a response for the first call matching `method` + `matcher`. */
on(
method: string,
matcher: string | RegExp,
respond: MockResponseSpec | ((call: RecordedCall) => MockResponseSpec),
): this {
const regex = typeof matcher === 'string' ? new RegExp(matcher) : matcher;
this.routes.push({
method,
matcher: regex,
respond: typeof respond === 'function' ? respond : () => respond,
});
return this;
}
/** Catch-all for any unmatched call. */
passthrough(respond: MockResponseSpec | ((call: RecordedCall) => MockResponseSpec)): this {
this.fallback = typeof respond === 'function' ? respond : () => respond;
return this;
}
/** @internal build the FetchLike impl consumed by the client. */
asFetch(): FetchLike {
return (async (input: RequestInfo | URL, init?: RequestInit) => {
const fullUrl = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
const method = (init?.method ?? 'GET').toUpperCase();
const rawHeaders = init?.headers;
const headers: Record<string, string> = {};
if (rawHeaders) {
const collect = (k: string, v: string): void => {
headers[String(k).toLowerCase()] = String(v);
};
if (rawHeaders instanceof Headers) {
rawHeaders.forEach((v, k) => collect(k, v));
} else if (Array.isArray(rawHeaders)) {
for (const [k, v] of rawHeaders) collect(k, String(v));
} else {
for (const [k, v] of Object.entries(rawHeaders as Record<string, string>)) collect(k, v);
}
}
let body: unknown = undefined;
if (init?.body) {
try {
body = JSON.parse(String(init.body));
} catch {
body = String(init.body);
}
}
const call: RecordedCall = { url: fullUrl, method, headers, body };
this.calls.push(call);
// Route matching uses the pathname only (query stripped) so regexes
// like `/contacts$` still match `…/contacts?limit=10`.
const matchUrl = fullUrl.split('?')[0];
let spec: MockResponseSpec;
const route = this.routes.find(r => r.method === method && r.matcher.test(matchUrl));
if (route) {
spec = route.respond(call);
} else if (this.fallback) {
spec = this.fallback(call);
} else {
throw new Error(`MockTransport: no route for ${method} ${fullUrl}`);
}
const status = spec.status ?? 200;
// 204/100/304 may not carry a body. Node's undici Response rejects
// an empty-string body with these statuses, so pass `null`.
const noBody = status === 204 || status === 100 || status === 304;
const responseBody: BodyInit | null = noBody
? null
: (spec.text ?? (spec.body === undefined ? '' : JSON.stringify(spec.body)));
const resHeaders: Record<string, string> = noBody ? {} : { 'content-type': 'application/json' };
return new Response(responseBody, { status, headers: resHeaders });
}) as FetchLike;
}
get lastCall(): RecordedCall | undefined {
return this.calls[this.calls.length - 1];
}
}