Files
testapi/tests/api.test.ts

306 lines
7.8 KiB
TypeScript

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<typeof createApp>,
options: {
method: string;
url: string;
query?: Record<string, string>;
body?: unknown;
headers?: Record<string, string>;
},
) {
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<void>((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<string, unknown>;
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);
});
});