import { afterEach, describe, expect, it } from "bun:test"; import { mkdtempSync, rmSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { reseedDatabase } from "../src/database"; import { createServer } from "../src/index"; import { openApiDocument } from "../src/openapi"; let tempDir: string | null = null; function createTestServer() { tempDir = mkdtempSync(join(tmpdir(), "mock-task-api-http-")); const db = reseedDatabase(join(tempDir, "test.sqlite")); return createServer(db); } async function readJson(response: Response) { return (await response.json()) as Record; } afterEach(() => { if (tempDir) { rmSync(tempDir, { recursive: true, force: true }); tempDir = null; } }); describe("mock task API", () => { it("serves a health endpoint", async () => { const server = createTestServer(); const response = await server.fetch(new Request("http://mock.local/health")); const body = await readJson(response); expect(response.status).toBe(200); expect(body.status).toBe("ok"); }); it("lists organizations", async () => { const server = createTestServer(); const response = await server.fetch(new Request("http://mock.local/orgs")); const body = await readJson(response); expect(response.status).toBe(200); expect(Array.isArray(body.organizations)).toBe(true); expect((body.organizations as unknown[]).length).toBe(3); }); it("filters tasks by org and status", async () => { const server = createTestServer(); const response = await server.fetch( new Request("http://mock.local/tasks?orgId=org_acme&status=blocked"), ); const body = await readJson(response); const tasks = body.tasks as Array>; expect(response.status).toBe(200); expect(tasks.length).toBe(1); expect(tasks[0]?.id).toBe("task_1003"); }); it("creates a task through the HTTP contract", async () => { const server = createTestServer(); const response = await server.fetch( new Request("http://mock.local/tasks", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ orgId: "org_acme", projectId: "proj_acme_ops", title: "Run API smoke test", priority: "low", }), }), ); const body = await readJson(response); expect(response.status).toBe(201); expect((body.task as Record).title).toBe("Run API smoke test"); }); it("updates a task through the HTTP contract", async () => { const server = createTestServer(); const response = await server.fetch( new Request("http://mock.local/tasks/task_1001", { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ status: "done", assigneeUserId: null, }), }), ); const body = await readJson(response); expect(response.status).toBe(200); expect((body.task as Record).status).toBe("done"); expect((body.task as Record).assigneeUserId).toBeNull(); }); it("returns 400 for invalid create payloads", async () => { const server = createTestServer(); const response = await server.fetch( new Request("http://mock.local/tasks", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ orgId: "org_acme", projectId: "proj_acme_ops", }), }), ); const body = await readJson(response); expect(response.status).toBe(400); expect(body.error).toBe("title is required."); }); it("returns 404 when updating a missing task", async () => { const server = createTestServer(); const response = await server.fetch( new Request("http://mock.local/tasks/task_missing", { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ status: "done", }), }), ); const body = await readJson(response); expect(response.status).toBe(404); expect(body.error).toBe("Task not found."); }); it("returns 404 for unknown routes", async () => { const server = createTestServer(); const response = await server.fetch(new Request("http://mock.local/unknown")); const body = await readJson(response); expect(response.status).toBe(404); expect(body.error).toBe("Route not found."); }); it("returns a valid openapi document at the import path", async () => { const server = createTestServer(); const response = await server.fetch(new Request("http://mock.local/openapi.json")); const body = await readJson(response); const paths = body.paths as Record; expect(response.status).toBe(200); expect(body.openapi).toBe("3.0.3"); expect(paths["/tasks"]).toBeDefined(); expect(paths["/orgs/{orgId}/tasks"]).toBeDefined(); }); it("advertises every implemented route in the openapi document", () => { const operations = Object.entries(openApiDocument.paths).flatMap(([path, pathItem]) => Object.keys(pathItem).map((method) => `${method.toUpperCase()} ${path}`), ); expect(operations).toEqual([ "GET /health", "GET /orgs", "GET /orgs/{orgId}", "GET /orgs/{orgId}/projects", "GET /orgs/{orgId}/tasks", "GET /users", "GET /projects", "GET /tasks", "POST /tasks", "GET /tasks/{taskId}", "PATCH /tasks/{taskId}", "GET /openapi.json", "GET /swagger.json", "GET /api-docs", ]); }); it("supports the swagger alias path used by import tools", async () => { const server = createTestServer(); const response = await server.fetch(new Request("http://mock.local/swagger.json")); expect(response.status).toBe(200); }); it("serves the same generated document on all openapi aliases", async () => { const server = createTestServer(); const openApiResponse = await server.fetch(new Request("http://mock.local/openapi.json")); const swaggerResponse = await server.fetch(new Request("http://mock.local/swagger.json")); const apiDocsResponse = await server.fetch(new Request("http://mock.local/api-docs")); const openApiBody = await readJson(openApiResponse); const swaggerBody = await readJson(swaggerResponse); const apiDocsBody = await readJson(apiDocsResponse); expect(openApiBody).toEqual(swaggerBody); expect(swaggerBody).toEqual(apiDocsBody); expect(openApiBody).toEqual(openApiDocument); }); });