Merge branch 'feat/openapi-envelope-integration'

This commit is contained in:
2026-05-11 01:55:26 +00:00
2 changed files with 218 additions and 6 deletions

View File

@@ -1,6 +1,8 @@
import { describe, it, expect } from "vitest";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { FromOpenAPI } from "../src/from_openapi.js";
import { OperationType } from "../src/types.js";
import { CallError } from "../src/error.js";
import { isResponseEnvelope } from "../src/response-envelope.js";
import { Value } from "@alkdev/typebox/value";
const simpleSpec = {
@@ -191,4 +193,206 @@ describe("FromOpenAPI", () => {
const ops = FromOpenAPI(specWithRef as any, config);
expect(ops.length).toBe(1);
});
});
describe("FromOpenAPI handler envelope behavior", () => {
const config = {
namespace: "api",
baseUrl: "https://api.example.com",
};
let originalFetch: typeof globalThis.fetch;
beforeEach(() => {
originalFetch = globalThis.fetch;
});
afterEach(() => {
globalThis.fetch = originalFetch;
});
it("returns httpEnvelope with JSON response data", async () => {
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
statusText: "OK",
headers: new Headers({ "Content-Type": "application/json", "X-Custom": "value" }),
json: async () => ({ users: ["alice", "bob"] }),
text: async () => "",
arrayBuffer: async () => new ArrayBuffer(0),
});
const ops = FromOpenAPI(simpleSpec as any, config);
const listUsers = ops.find((o) => o.name === "listUsers")!;
const result = await listUsers.handler({}, {} as any);
expect(isResponseEnvelope(result)).toBe(true);
if (isResponseEnvelope(result)) {
expect(result.meta.source).toBe("http");
expect(result.data).toEqual({ users: ["alice", "bob"] });
const meta = result.meta as { statusCode: number; headers: Record<string, string>; contentType: string };
expect(meta.statusCode).toBe(200);
expect(meta.contentType).toBe("application/json");
expect(meta.headers).toHaveProperty("x-custom", "value");
}
});
it("returns httpEnvelope with text response data", async () => {
const textSpec = {
openapi: "3.0.0",
info: { title: "Test", version: "1.0.0" },
paths: {
"/readme": {
get: {
operationId: "getReadme",
responses: {
"200": {
content: { "text/plain": { schema: { type: "string" } } },
},
},
},
},
},
};
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
statusText: "OK",
headers: new Headers({ "Content-Type": "text/plain" }),
json: async () => ({}),
text: async () => "Hello, world!",
arrayBuffer: async () => new ArrayBuffer(0),
});
const ops = FromOpenAPI(textSpec as any, config);
const getReadme = ops.find((o) => o.name === "getReadme")!;
const result = await getReadme.handler({}, {} as any);
expect(isResponseEnvelope(result)).toBe(true);
if (isResponseEnvelope(result)) {
expect(result.meta.source).toBe("http");
expect(result.data).toBe("Hello, world!");
const meta = result.meta as { statusCode: number; headers: Record<string, string>; contentType: string };
expect(meta.statusCode).toBe(200);
expect(meta.contentType).toBe("text/plain");
}
});
it("returns httpEnvelope with arrayBuffer for binary responses", async () => {
const binaryData = new Uint8Array([1, 2, 3, 4]).buffer;
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
statusText: "OK",
headers: new Headers({ "Content-Type": "application/octet-stream" }),
json: async () => ({}),
text: async () => "",
arrayBuffer: async () => binaryData,
});
const ops = FromOpenAPI(simpleSpec as any, config);
const listUsers = ops.find((o) => o.name === "listUsers")!;
const result = await listUsers.handler({}, {} as any);
expect(isResponseEnvelope(result)).toBe(true);
if (isResponseEnvelope(result)) {
expect(result.meta.source).toBe("http");
expect(result.data).toBe(binaryData);
const meta = result.meta as { statusCode: number; headers: Record<string, string>; contentType: string };
expect(meta.contentType).toBe("application/octet-stream");
}
});
it("throws CallError on HTTP error status", async () => {
globalThis.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 404,
statusText: "Not Found",
headers: new Headers(),
});
const ops = FromOpenAPI(simpleSpec as any, config);
const getUser = ops.find((o) => o.name === "getUser")!;
try {
await getUser.handler({ id: "123" }, {} as any);
expect.fail("Expected CallError to be thrown");
} catch (error) {
expect(error).toBeInstanceOf(CallError);
expect((error as CallError).code).toBe("EXECUTION_ERROR");
expect((error as CallError).message).toContain("HTTP 404");
}
});
it("throws CallError on 500 status", async () => {
globalThis.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 500,
statusText: "Internal Server Error",
headers: new Headers(),
});
const ops = FromOpenAPI(simpleSpec as any, config);
const listUsers = ops.find((o) => o.name === "listUsers")!;
try {
await listUsers.handler({}, {} as any);
expect.fail("Expected CallError to be thrown");
} catch (error) {
expect(error).toBeInstanceOf(CallError);
expect((error as CallError).code).toBe("EXECUTION_ERROR");
expect((error as CallError).message).toContain("HTTP 500");
}
});
it("includes statusCode from response in envelope", async () => {
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
status: 201,
statusText: "Created",
headers: new Headers({ "Content-Type": "application/json" }),
json: async () => ({ id: "abc" }),
text: async () => "",
arrayBuffer: async () => new ArrayBuffer(0),
});
const ops = FromOpenAPI(simpleSpec as any, config);
const createUser = ops.find((o) => o.name === "createUser")!;
const result = await createUser.handler({ body: { name: "Alice" } }, {} as any);
expect(isResponseEnvelope(result)).toBe(true);
if (isResponseEnvelope(result)) {
const meta = result.meta as { statusCode: number };
expect(meta.statusCode).toBe(201);
}
});
it("converts response headers to Record<string, string>", async () => {
const headers = new Headers();
headers.set("Content-Type", "application/json");
headers.set("X-Request-Id", "req-123");
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
statusText: "OK",
headers,
json: async () => ({ result: "ok" }),
text: async () => "",
arrayBuffer: async () => new ArrayBuffer(0),
});
const ops = FromOpenAPI(simpleSpec as any, config);
const listUsers = ops.find((o) => o.name === "listUsers")!;
const result = await listUsers.handler({}, {} as any);
expect(isResponseEnvelope(result)).toBe(true);
if (isResponseEnvelope(result)) {
const meta = result.meta as { headers: Record<string, string> };
expect(meta.headers["content-type"]).toBe("application/json");
expect(meta.headers["x-request-id"]).toBe("req-123");
}
});
});