import { afterEach, describe, expect, it } from "bun:test"; import { EventEmitter } from "node:events"; import { mkdtempSync, rmSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; import httpMocks from "node-mocks-http"; import { createApp } from "../src/app"; import { reseedDatabase } from "../src/database"; import { openApiDocument } from "../src/openapi"; let tempDir: string | null = null; function createTestApp() { tempDir = mkdtempSync(join(tmpdir(), "mock-task-api-http-")); const db = reseedDatabase(join(tempDir, "test.sqlite")); return createApp(db, { parseJson: false }); } async function sendRequest( app: ReturnType, options: { method: string; url: string; query?: Record; body?: unknown; headers?: Record; }, ) { const request = httpMocks.createRequest({ method: options.method, url: options.url, query: options.query, body: options.body, headers: options.headers, }); const response = httpMocks.createResponse({ eventEmitter: EventEmitter }); await new Promise((resolve) => { response.on("end", () => resolve()); app.handle(request, response); }); const contentType = String(response.getHeader("content-type") ?? ""); return { status: response.statusCode, headers: response._getHeaders(), body: contentType.includes("application/json") ? response._getJSONData() : response._getData(), }; } afterEach(() => { if (tempDir) { rmSync(tempDir, { recursive: true, force: true }); tempDir = null; } }); describe("mock task API", () => { it("serves a health endpoint", async () => { const app = createTestApp(); const response = await sendRequest(app, { method: "GET", url: "/health", }); expect(response.status).toBe(200); expect(response.body.status).toBe("ok"); }); it("lists organizations", async () => { const app = createTestApp(); const response = await sendRequest(app, { method: "GET", url: "/orgs", }); expect(response.status).toBe(200); expect(Array.isArray(response.body.organizations)).toBe(true); expect(response.body.organizations).toHaveLength(3); }); it("creates an organization through the HTTP contract", async () => { const app = createTestApp(); const response = await sendRequest(app, { method: "POST", url: "/orgs", body: { slug: "globex-systems", name: "Globex Systems", plan: "growth", industry: "Manufacturing", }, headers: { "content-type": "application/json", }, }); expect(response.status).toBe(201); expect(response.body.organization.slug).toBe("globex-systems"); expect(response.body.organization.name).toBe("Globex Systems"); }); it("creates a user through the HTTP contract", async () => { const app = createTestApp(); const response = await sendRequest(app, { method: "POST", url: "/users", body: { orgId: "org_acme", fullName: "Iris Quinn", email: "iris@acme.example", role: "engineer", timezone: "Europe/Dublin", }, headers: { "content-type": "application/json", }, }); expect(response.status).toBe(201); expect(response.body.user.orgId).toBe("org_acme"); expect(response.body.user.email).toBe("iris@acme.example"); }); it("creates a project through the HTTP contract", async () => { const app = createTestApp(); const response = await sendRequest(app, { method: "POST", url: "/projects", body: { orgId: "org_acme", key: "BIZ", name: "Business Ops Dashboard", status: "planning", ownerUserId: "user_acme_1", dueDate: "2026-06-30", }, headers: { "content-type": "application/json", }, }); expect(response.status).toBe(201); expect(response.body.project.orgId).toBe("org_acme"); expect(response.body.project.key).toBe("BIZ"); expect(response.body.project.ownerUserId).toBe("user_acme_1"); }); it("filters tasks by org and status", async () => { const app = createTestApp(); const response = await sendRequest(app, { method: "GET", url: "/tasks", query: { orgId: "org_acme", status: "blocked", }, }); expect(response.status).toBe(200); expect(response.body.tasks).toHaveLength(1); expect(response.body.tasks[0]?.id).toBe("task_1003"); }); it("creates a task through the HTTP contract", async () => { const app = createTestApp(); const response = await sendRequest(app, { method: "POST", url: "/tasks", body: { orgId: "org_acme", projectId: "proj_acme_ops", title: "Run API smoke test", priority: "low", }, headers: { "content-type": "application/json", }, }); expect(response.status).toBe(201); expect(response.body.task.title).toBe("Run API smoke test"); }); it("updates a task through the HTTP contract", async () => { const app = createTestApp(); const response = await sendRequest(app, { method: "PATCH", url: "/tasks/task_1001", body: { status: "done", assigneeUserId: null, }, headers: { "content-type": "application/json", }, }); expect(response.status).toBe(200); expect(response.body.task.status).toBe("done"); expect(response.body.task.assigneeUserId).toBeNull(); }); it("returns 400 for invalid create payloads", async () => { const app = createTestApp(); const response = await sendRequest(app, { method: "POST", url: "/tasks", body: { orgId: "org_acme", projectId: "proj_acme_ops", }, headers: { "content-type": "application/json", }, }); expect(response.status).toBe(400); expect(response.body.error).toBe("Validation failed."); expect(response.body.details).toBeDefined(); }); it("returns 404 when updating a missing task", async () => { const app = createTestApp(); const response = await sendRequest(app, { method: "PATCH", url: "/tasks/task_missing", body: { status: "done", }, headers: { "content-type": "application/json", }, }); expect(response.status).toBe(404); expect(response.body.error).toBe("Task not found."); }); it("returns 404 for unknown routes", async () => { const app = createTestApp(); const response = await sendRequest(app, { method: "GET", url: "/unknown", }); expect(response.status).toBe(404); expect(response.body.error).toBe("Route not found."); }); it("returns a valid openapi document at the import path", async () => { const app = createTestApp(); const response = await sendRequest(app, { method: "GET", url: "/openapi.json", }); const paths = response.body.paths as Record; expect(response.status).toBe(200); expect(response.body.openapi).toBe("3.0.0"); expect(paths["/tasks"]).toBeDefined(); expect(paths["/orgs/{orgId}/tasks"]).toBeDefined(); }); it("serves the same generated document on all json spec aliases", async () => { const app = createTestApp(); const openApiResponse = await sendRequest(app, { method: "GET", url: "/openapi.json", }); const swaggerResponse = await sendRequest(app, { method: "GET", url: "/swagger.json", }); expect(openApiResponse.body).toEqual(swaggerResponse.body); expect(openApiResponse.body).toEqual(openApiDocument); }); it("serves swagger ui at /api-docs", async () => { const app = createTestApp(); const response = await sendRequest(app, { method: "GET", url: "/api-docs", }); expect(response.status).toBe(301); }); });