import { createServer } from "node:http" import type { IncomingMessage, ServerResponse } from "node:http" import type { Socket } from "node:net" import { afterEach, expect, test, vi } from "vitest" import { NodejsFetch, nodejsFetch } from "#Source/request/index.ts" const readRequestBody = async (request: IncomingMessage): Promise => { const chunks: Buffer[] = [] const bodyChunks = request as AsyncIterable for await (const chunk of bodyChunks) { chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk) } return Buffer.concat(chunks).toString("utf8") } const startTestServer = async ( handler: ( request: IncomingMessage, response: ServerResponse, ) => Promise | void, ): Promise<{ baseUrl: string close: () => Promise }> => { const sockets = new Set() const server = createServer((request, response) => { void Promise.resolve(handler(request, response)).catch((error: unknown) => { if (response.headersSent === false) { response.statusCode = 500 } response.end(error instanceof Error ? error.message : String(error)) }) }) server.on("connection", (socket) => { sockets.add(socket) socket.on("close", () => { sockets.delete(socket) }) }) await new Promise((resolve) => { server.listen(0, "127.0.0.1", () => { resolve() }) }) const address = server.address() if (address === null || typeof address === "string") { throw new Error("Expected the test server to expose a TCP address.") } const close = async (): Promise => { for (const socket of sockets) { socket.destroy() } if (server.listening === false) { return } await new Promise((resolve, reject) => { server.close((error) => { if (error !== undefined) { reject(error) return } resolve() }) }) } return { baseUrl: `http://127.0.0.1:${address.port}`, close, } } afterEach(() => { vi.useRealTimers() }) test("NodejsFetch.getJson builds request URL and JSON body", async () => { let capturedUrl: string | undefined let capturedMethod: string | undefined let capturedBody: string | undefined let capturedHeaders: IncomingMessage["headers"] | undefined const { baseUrl, close } = await startTestServer(async (request, response) => { capturedUrl = request.url ?? undefined capturedMethod = request.method ?? undefined capturedHeaders = request.headers capturedBody = await readRequestBody(request) response.setHeader("Content-Type", "application/json") response.end(JSON.stringify({ status: "ok" })) }) try { const fetcher = new NodejsFetch< { query: { page: number; tags: string[]; filter: { active: boolean } } body: { name: string } }, { type: "json"; data: { status: string } } >({ baseUrl, path: "/users", method: "post", headers: { Authorization: "Bearer token", }, query: { page: 2, tags: ["alpha", "beta"], filter: { active: true }, }, body: { name: "Mobius", }, }) const json = await fetcher.getJson() expect(json).toEqual({ status: "ok" }) expect(capturedUrl).toBe("/users?page=2&tags=alpha&tags=beta&filter=%7B%22active%22%3Atrue%7D") expect(capturedMethod).toBe("POST") expect(capturedBody).toBe('{"name":"Mobius"}') expect(capturedHeaders?.authorization).toBe("Bearer token") expect(capturedHeaders?.["content-type"]).toBe("application/json") } finally { await close() } }) test("NodejsFetch reads text, blob, arrayBuffer, and stream responses", async () => { const { baseUrl, close } = await startTestServer((request, response) => { const pathname = new URL(request.url ?? "/", "http://127.0.0.1").pathname if (pathname.endsWith("/text")) { response.setHeader("Content-Type", "text/plain; charset=utf-8") response.end("hello") return } if (pathname.endsWith("/blob")) { response.setHeader("Content-Type", "application/octet-stream") response.end("mobius") return } if (pathname.endsWith("/array-buffer")) { response.setHeader("Content-Type", "application/octet-stream") response.end(Buffer.from("matrix")) return } response.setHeader("Content-Type", "application/octet-stream") response.write(Buffer.from([1, 2])) response.end(Buffer.from([3])) }) try { const textFetcher = new NodejsFetch< { query?: undefined; body?: undefined }, { type: "text"; data: string } >({ baseUrl, path: "/text", method: "get", }) const blobFetcher = new NodejsFetch< { query?: undefined; body?: undefined }, { type: "blob"; data: Blob } >({ baseUrl, path: "/blob", method: "get", }) const arrayBufferFetcher = new NodejsFetch< { query?: undefined; body?: undefined }, { type: "arrayBuffer"; data: ArrayBuffer } >({ baseUrl, path: "/array-buffer", method: "get", }) const streamFetcher = new NodejsFetch< { query?: undefined; body?: undefined }, { type: "stream"; data: ReadableStream } >({ baseUrl, path: "/stream", method: "get", }) await expect(textFetcher.getText()).resolves.toBe("hello") const blob = await blobFetcher.getBlob() expect(await blob.text()).toBe("mobius") const arrayBuffer = await arrayBufferFetcher.getArrayBuffer() expect(new TextDecoder().decode(arrayBuffer)).toBe("matrix") const stream = await streamFetcher.getStream() const streamArrayBuffer = await new Response(stream).arrayBuffer() expect(Array.from(new Uint8Array(streamArrayBuffer))).toEqual([1, 2, 3]) } finally { await close() } }) test("NodejsFetch rejects with a timeout error when the request exceeds the limit", async () => { const { baseUrl, close } = await startTestServer(() => { // Keep the connection open until the fetch timeout aborts it. }) try { const fetcher = new NodejsFetch< { query?: undefined; body?: undefined }, { type: "json"; data: { ok: boolean } } >({ baseUrl, path: "/timeout", method: "get", timeout: 50, }) await expect(fetcher.getJson()).rejects.toThrow("Request timeout after 50ms.") } finally { await close() } }) test("NodejsFetch applies the external abortSignal option", async () => { const abortController = new AbortController() const { baseUrl, close } = await startTestServer(() => { // Keep the connection open until the external abortSignal aborts it. }) try { const fetcher = new NodejsFetch< { query?: undefined; body?: undefined }, { type: "json"; data: { ok: boolean } } >({ baseUrl, path: "/abort", method: "get", abortSignal: abortController.signal, }) const resultPromise = fetcher.getJson() const abortExpectation = expect(resultPromise).rejects.toThrow("Manual abort.") abortController.abort(new Error("Manual abort.")) await abortExpectation expect(abortController.signal.aborted).toBe(true) } finally { await close() } }) test("nodejsFetch creates a NodejsFetch instance", () => { const fetcher = nodejsFetch({ baseUrl: "https://api.example.com", path: "/factory", method: "get", }) expect(fetcher).toBeInstanceOf(NodejsFetch) })