import { expect, test, vi } from "vitest" import type { ApiCoreHandler, ApiCoreHeaders, ApiCoreRequestFormData, ApiCoreStream, ApiHostStartResult, ApiHandlerContext, } from "#Source/http/index.ts" import { ApiCore, ApiHost, ApiServer, SuccessResult, createApi, createApiSchema, createApiServer, } from "#Source/http/index.ts" import type { Standard } from "#Source/validation/index.ts" const isRecord = (value: unknown): value is Record => { return typeof value === "object" && value !== null } const createSchema = ( validate: (value: unknown) => Standard.Schema.Result, ): Standard.Schema => { return { "~standard": { version: 1 as const, vendor: "mobius-http-test", validate, }, } } const inputQuerySchema = createSchema<{ page: string }>((value) => { if (isRecord(value) && typeof value["page"] === "string") { return { value: { page: value["page"], }, } } return { issues: [{ message: "page is required" }], } }) const inputBodySchema = createSchema<{ name: string }>((value) => { if (isRecord(value) && typeof value["name"] === "string") { return { value: { name: value["name"], }, } } return { issues: [{ message: "name is required" }], } }) const outputErrorSchema = createSchema<{ status: "error" }>((value) => { if ( typeof value === "object" && value !== null && (value as { status?: unknown }).status === "error" ) { return { value: { status: "error", }, } } return { issues: [{ message: "status must be error" }], } }) const outputSuccessSchema = createSchema<{ status: "success" }>((value) => { if ( typeof value === "object" && value !== null && (value as { status?: unknown }).status === "success" ) { return { value: { status: "success", }, } } return { issues: [{ message: "status must be success" }], } }) interface TestApiCoreOptions { url: string method: string headers?: ApiCoreHeaders | undefined bodyText?: string | undefined formData?: ApiCoreRequestFormData | undefined } class TestApiCore extends ApiCore { readonly jsonPayloads: 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(): Promise { return await Promise.resolve() } async json(data: unknown): Promise { this.jsonPayloads.push(data) return await Promise.resolve() } async stream(): Promise { return await Promise.resolve({ write: async () => { await Promise.resolve() }, end: async () => { await Promise.resolve() }, }) } async error(): Promise { this.errorCallCount = this.errorCallCount + 1 return 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() } async dispatch(apiCore: ApiCore): Promise { if (this.apiCoreHandler === undefined) { throw new Error("ApiCore handler is not attached.") } await this.apiCoreHandler(apiCore) } } class TestSuccessResult extends SuccessResult<{ status: "success" }> { private readonly handleCallback: (apiCore: ApiCore) => Promise constructor(handleCallback: (apiCore: ApiCore) => Promise) { super({}) this.handleCallback = handleCallback } async handle(apiCore: ApiCore): Promise { await this.handleCallback(apiCore) } } const testApiSchema = createApiSchema< string, "get" | "post", Standard.Schema, Standard.Schema, Standard.Schema, Standard.Schema >({ path: "/users", method: "post", inputQuerySchema, inputBodySchema, outputErrorSchema, outputSuccessSchema, }) test("ApiServer.start starts and manages the configured ApiHost", async () => { const apiHost = new TestApiHost() const apiServer = createApiServer({ apiList: [], apiHost, }) const result = await apiServer.open() expect(result).toEqual({ urlList: ["http://127.0.0.1:0"], }) expect(apiHost.startCallCount).toBe(1) await apiServer.close() expect(apiHost.closeCallCount).toBe(1) expect(apiHost.isRunning()).toBe(false) }) test("ApiServer.attach reuses an already running ApiHost without taking over its lifecycle", async () => { const apiHost = new TestApiHost() await apiHost.start() const apiServer = createApiServer({ apiList: [], apiHost, }) expect(apiServer.attach()).toEqual({ urlList: ["http://127.0.0.1:0"], }) const attachedApiCore = new TestApiCore({ url: "http://127.0.0.1/missing?page=2", method: "GET", }) await apiHost.dispatch(attachedApiCore) expect(attachedApiCore.errorCallCount).toBe(1) await apiServer.close() expect(apiHost.closeCallCount).toBe(0) expect(apiHost.isRunning()).toBe(true) const detachedApiCore = new TestApiCore({ url: "http://127.0.0.1/missing?page=2", method: "GET", }) await expect(apiHost.dispatch(detachedApiCore)).rejects.toThrow( "ApiCore handler is not attached.", ) await apiHost.close() }) test("ApiServer dispatches matched requests, validates input, and handles the Api result", async () => { const apiHost = new TestApiHost() const apiHandler = vi.fn( async ( context: ApiHandlerContext< string, "get" | "post", Standard.Schema, Standard.Schema, Standard.Schema, Standard.Schema >, ): Promise => { await Promise.resolve() return new TestSuccessResult(async (apiCore) => { await apiCore.json({ path: context.path, method: context.method, inputQuery: context.inputQuery, inputBody: context.inputBody, }) }) }, ) const apiServer = createApiServer({ apiList: [createApi({ apiSchema: testApiSchema, apiHandler })], apiHost, }) expect(apiServer).toBeInstanceOf(ApiServer) const apiCore = new TestApiCore({ url: "http://127.0.0.1/users?page=2", method: "POST", bodyText: '{"name":"Mobius"}', }) await apiServer.open() await apiHost.dispatch(apiCore) expect(apiHandler).toHaveBeenCalledTimes(1) expect(apiHandler).toHaveBeenCalledWith( expect.objectContaining({ path: "/users", method: "post", inputQuery: { page: "2", }, inputBody: { name: "Mobius", }, apiCore, }), ) expect(apiCore.jsonPayloads).toEqual([ { path: "/users", method: "post", inputQuery: { page: "2", }, inputBody: { name: "Mobius", }, }, ]) expect(apiCore.errorCallCount).toBe(0) }) test("ApiServer calls ApiCore.error when no Api matches the request", async () => { const apiHost = new TestApiHost() const apiServer = createApiServer({ apiList: [], apiHost, }) expect(apiServer).toBeInstanceOf(ApiServer) const apiCore = new TestApiCore({ url: "http://127.0.0.1/missing?page=2", method: "GET", }) await apiServer.open() await apiHost.dispatch(apiCore) expect(apiCore.errorCallCount).toBe(1) expect(apiCore.jsonPayloads).toEqual([]) }) test("ApiServer calls ApiCore.error when input validation fails", async () => { const apiHost = new TestApiHost() const apiHandler = vi.fn( async ( context: ApiHandlerContext< string, "get" | "post", Standard.Schema, Standard.Schema, Standard.Schema, Standard.Schema >, ): Promise => { await Promise.resolve() return new TestSuccessResult(async (apiCore) => { await apiCore.json(context) }) }, ) const apiServer = createApiServer({ apiList: [createApi({ apiSchema: testApiSchema, apiHandler })], apiHost, }) expect(apiServer).toBeInstanceOf(ApiServer) const apiCore = new TestApiCore({ url: "http://127.0.0.1/users", method: "POST", bodyText: "{}", }) await apiServer.open() await apiHost.dispatch(apiCore) expect(apiHandler).not.toHaveBeenCalled() expect(apiCore.errorCallCount).toBe(1) expect(apiCore.jsonPayloads).toEqual([]) })