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
109 lines
3.9 KiB
TypeScript
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];
|
|
}
|
|
}
|