diff --git a/tests/utils/fetch-utils.ts b/tests/utils/fetch-utils.ts new file mode 100644 index 0000000..3fbe08f --- /dev/null +++ b/tests/utils/fetch-utils.ts @@ -0,0 +1,87 @@ +import { readFile } from "node:fs/promises"; +import { existsSync } from "node:fs"; +import { resolve } from "node:path"; +import { Fetch, HttpMethod, HttpRequest, HttpResponse, createFetch } from "@/utils/net"; + +type FetchInterceptorResponse = string | string[] | HttpResponse | unknown; + +type FetchInterceptorHandler = (args: string[], request: HttpRequest, url: string) => FetchInterceptorResponse | Promise; + +type FetchInterceptor = { + baseUrl?: string; + + requiredHeaders?: string[]; +} & { + [Method in HttpMethod]?: { + [url: string]: FetchInterceptorHandler; + }; +}; + +const FAKE_FETCH: Fetch = (url, request) => Promise.reject( + new Error(`Unsupported request: '${normalizeHttpMethod(request?.method)} ${url}'`) +); + +function normalizeHttpMethod(method: string): HttpMethod { + return (method?.toUpperCase() as HttpMethod) || "GET"; +} + +async function normalizeResponse(response: FetchInterceptorResponse): Promise { + if (typeof response === "string") { + return HttpResponse.text(response); + } + + if (Array.isArray(response) && response.length && response.every(x => typeof x === "string")) { + const path = resolve(...response); + if (existsSync(path)) { + const content = await readFile(path, "utf8"); + return HttpResponse.text(content); + } + } + + if (typeof response["url"] === "string" && typeof response["blob"] === "function") { + return response as HttpResponse; + } + + return HttpResponse.json(response); +} + +export function createFakeFetch(interceptor: FetchInterceptor): Fetch { + const baseUrl = interceptor.baseUrl?.endsWith("/") ? interceptor.baseUrl.slice(0, -1) : (interceptor.baseUrl || ""); + const requiredHeaders = interceptor.requiredHeaders || []; + const methods = interceptor; + + return createFetch({ handler: FAKE_FETCH }).use(async (url, request, next) => { + url = String(url); + const urlPath = url.startsWith(baseUrl) ? url.substring(baseUrl.length) : url; + + const handlerEntry = Object.entries(methods[normalizeHttpMethod(request?.method)] || {}) + .map(([urlMatcher, handler]) => [urlPath.match(urlMatcher), handler] as const) + .find(([match]) => match !== null); + + if (!handlerEntry || !requiredHeaders.every(header => request?.headers?.[header])) { + return await next(url, request); + } + + const args = handlerEntry[0].slice(1); + const response = await handlerEntry[1](args, request, url); + return await normalizeResponse(response); + }); +} + +export function createCombinedFetch(...fetchComponents: Fetch[]): Fetch { + const handlers = [...fetchComponents].reverse(); + + return async (url, request) => { + for (let i = handlers.length - 1; i >= 0; i--) { + try { + return await handlers[i](url, request); + } catch (error: unknown) { + if (i === 0) { + throw error; + } + } + } + + return HttpResponse.error(); + }; +}