import type { AddressInfo } from "node:net" import { afterEach, expect, test } from "vitest" import { Http } from "#Source/index.ts" afterEach(async () => { await Promise.resolve() }) test("ApiHostNodeHttp exposes usable URLs and forwards request data to ApiCoreNodeHttp", async () => { const apiHost = Http.createApiHostNodeHttp({ port: 0 }) apiHost.attachApiCoreHandler(async (apiCore) => { const path = await apiCore.getRequestPath() const method = await apiCore.getRequestMethod() const search = await apiCore.getRequestSearch() const bodyJson = await apiCore.getRequestBodyJson() await apiCore.json({ path, method, search, bodyJson, }) }) const startResult = await apiHost.start() const address = apiHost.getServerOrThrow().address() as AddressInfo expect(startResult.urlList).toContain(`http://127.0.0.1:${address.port}`) expect(startResult.urlList).toContain(`http://localhost:${address.port}`) expect(startResult.urlList).toContain(`http://[::1]:${address.port}`) const response = await fetch(`http://127.0.0.1:${address.port}/users?page=2`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ name: "Mobius" }), }) await expect(response.json()).resolves.toEqual({ path: "/users", method: "POST", search: "?page=2", bodyJson: { name: "Mobius" }, }) await apiHost.close() expect(apiHost.isRunning()).toBe(false) }) test("ApiCoreNodeHttp returns a stable error response when the handler throws", async () => { const apiHost = Http.createApiHostNodeHttp({ port: 0 }) apiHost.attachApiCoreHandler(() => { throw new Error("boom") }) await apiHost.start() const address = apiHost.getServerOrThrow().address() as AddressInfo const response = await fetch(`http://127.0.0.1:${address.port}/error`, { method: "GET", }) expect(response.status).toBe(500) await expect(response.json()).resolves.toEqual({ status: "error", data: { reason: "internal_server_error", }, }) await apiHost.close() }) test("ApiCoreNodeHttp treats an empty request body as an empty object", async () => { const apiHost = Http.createApiHostNodeHttp({ port: 0 }) apiHost.attachApiCoreHandler(async (apiCore) => { await apiCore.json({ bodyJson: await apiCore.getRequestBodyJson(), }) }) await apiHost.start() const address = apiHost.getServerOrThrow().address() as AddressInfo const response = await fetch(`http://127.0.0.1:${address.port}/empty`, { method: "GET", }) await expect(response.json()).resolves.toEqual({ bodyJson: {}, }) await apiHost.close() }) test("ApiCoreNodeHttp exposes normalized headers and parses multipart form data", async () => { const apiHost = Http.createApiHostNodeHttp({ port: 0 }) apiHost.attachApiCoreHandler(async (apiCore) => { const headers = await apiCore.getRequestHeaders() const formData = await apiCore.getRequestBodyFormData() const files = await apiCore.getRequestFiles() await apiCore.json({ contentType: headers["content-type"], customHeader: await apiCore.getRequestHeader("X-Custom-Header"), formData: formData.map((entry) => { if (entry.value instanceof File) { return { name: entry.name, value: { kind: "file", name: entry.value.name, size: entry.value.size, type: entry.value.type, }, } } return { name: entry.name, value: entry.value, } }), files: files.map((file) => { return { name: file.name, fileName: file.value.name, size: file.value.size, type: file.value.type, } }), }) }) await apiHost.start() const address = apiHost.getServerOrThrow().address() as AddressInfo const formData = new FormData() formData.append("user", "Mobius") formData.append("avatar", new File(["hello"], "avatar.txt", { type: "text/plain" })) const response = await fetch(`http://127.0.0.1:${address.port}/upload`, { method: "POST", headers: { "X-Custom-Header": "alpha", }, body: formData, }) await expect(response.json()).resolves.toEqual({ contentType: [expect.stringContaining("multipart/form-data")], customHeader: ["alpha"], formData: [ { name: "user", value: "Mobius", }, { name: "avatar", value: { kind: "file", name: "avatar.txt", size: 5, type: expect.stringMatching(/^text\/plain(?:;.*)?$/), }, }, ], files: [ { name: "avatar", fileName: "avatar.txt", size: 5, type: expect.stringMatching(/^text\/plain(?:;.*)?$/), }, ], }) await apiHost.close() })