import { expect, test, vi } from "vitest" import type { ApiCoreHandler, ApiCoreHeaders, ApiCoreRequestFiles, ApiCoreRequestFormData, ApiCoreStream, ApiHostStartResult, } from "#Source/http/index.ts" import { ApiCore, ApiHost } from "#Source/http/index.ts" interface TestApiCoreOptions { url: string method: string headers?: ApiCoreHeaders | undefined bodyText?: string | undefined formData?: ApiCoreRequestFormData | undefined } class TestApiCore extends ApiCore { readonly jsonPayloads: unknown[] = [] readonly textPayloads: unknown[] = [] errorCallCount = 0 private readonly url: string private readonly method: string private readonly headers: ApiCoreHeaders private readonly bodyText: string private readonly formData: ApiCoreRequestFormData constructor(options: TestApiCoreOptions) { super() this.url = options.url this.method = options.method this.headers = options.headers ?? {} this.bodyText = options.bodyText ?? "" this.formData = options.formData ?? [] } async getRequestHeaders(): Promise { return await Promise.resolve(this.headers) } async getRequestMethod(): Promise { return await Promise.resolve(this.method) } getRequestUrl(): string { return this.url } async getRequestBodyText(): Promise { return await Promise.resolve(this.bodyText) } async getRequestBodyFormData(): Promise { return await Promise.resolve(this.formData) } async text(data: unknown): Promise { this.textPayloads.push(data) await Promise.resolve() } async json(data: unknown): Promise { this.jsonPayloads.push(data) await Promise.resolve() } async stream(): Promise { return await Promise.resolve({ write: vi.fn(async () => { await Promise.resolve() }), end: vi.fn(async () => { await Promise.resolve() }), }) } async error(): Promise { this.errorCallCount = this.errorCallCount + 1 await Promise.resolve() } } class TestApiHost extends ApiHost<{ id: string }> { apiCoreHandler: ApiCoreHandler | undefined startCallCount = 0 closeCallCount = 0 constructor() { super({ port: 0 }) } attachApiCoreHandler(apiCoreHandler: ApiCoreHandler): void { this.apiCoreHandler = apiCoreHandler } detachApiCoreHandler(): void { this.apiCoreHandler = undefined } async start(): Promise { this.startCallCount = this.startCallCount + 1 const startResult = { urlList: ["http://127.0.0.1:0"], } this.setRunningServer({ id: "server-1" }, startResult) return await Promise.resolve(startResult) } async close(): Promise { this.closeCallCount = this.closeCallCount + 1 this.clearRunningServer() await Promise.resolve() } } test("ApiCore.getRequestHeader returns lowercase header values", async () => { const apiCore = new TestApiCore({ url: "http://127.0.0.1/users?page=2", method: "GET", headers: { "x-trace-id": ["trace-1"], }, }) await expect(apiCore.getRequestHeader("X-Trace-Id")).resolves.toEqual(["trace-1"]) }) test("ApiCore.getRequestPath extracts the pathname from the request URL", async () => { const apiCore = new TestApiCore({ url: "http://127.0.0.1/users?page=2", method: "GET", }) await expect(apiCore.getRequestPath()).resolves.toBe("/users") }) test("ApiCore.getRequestSearch extracts the search string from the request URL", async () => { const apiCore = new TestApiCore({ url: "http://127.0.0.1/users?page=2", method: "GET", }) await expect(apiCore.getRequestSearch()).resolves.toBe("?page=2") }) test("ApiCore.getRequestBodyJson parses JSON bodies and treats blank bodies as empty objects", async () => { const jsonApiCore = new TestApiCore({ url: "http://127.0.0.1/users", method: "POST", bodyText: '{"name":"Mobius"}', }) const emptyApiCore = new TestApiCore({ url: "http://127.0.0.1/users", method: "POST", bodyText: " ", }) await expect(jsonApiCore.getRequestBodyJson()).resolves.toEqual({ name: "Mobius", }) await expect(emptyApiCore.getRequestBodyJson()).resolves.toEqual({}) }) test("ApiCore.getRequestFiles returns only file entries from form data", async () => { const file = new File(["avatar"], "avatar.txt", { type: "text/plain" }) const apiCore = new TestApiCore({ url: "http://127.0.0.1/upload", method: "POST", formData: [ { name: "name", value: "Mobius" }, { name: "avatar", value: file }, ], }) const files = await apiCore.getRequestFiles() expect(files).toEqual([ { name: "avatar", value: file, }, ]) }) test("ApiHost.getServerOrThrow throws before start and returns the running server after start", async () => { const apiHost = new TestApiHost() expect(() => apiHost.getServerOrThrow()).toThrow("Server is not running.") await apiHost.start() expect(apiHost.getServerOrThrow()).toEqual({ id: "server-1" }) }) test("ApiHost.getStartResultOrThrow returns a stable running result after start", async () => { const apiHost = new TestApiHost() expect(() => apiHost.getStartResultOrThrow()).toThrow("Server is not running.") await apiHost.start() const startResult = apiHost.getStartResultOrThrow() expect(startResult).toEqual({ urlList: ["http://127.0.0.1:0"], }) startResult.urlList.push("http://localhost:0") expect(apiHost.getStartResultOrThrow()).toEqual({ urlList: ["http://127.0.0.1:0"], }) }) test("ApiHost.isRunning reflects whether a server is attached", async () => { const apiHost = new TestApiHost() expect(apiHost.isRunning()).toBe(false) await apiHost.start() expect(apiHost.isRunning()).toBe(true) await apiHost.close() expect(apiHost.isRunning()).toBe(false) })