This commit is contained in:
2026-03-03 15:23:00 +00:00
parent 5e3726de39
commit 8e223bfbec
3689 changed files with 955330 additions and 1011 deletions

View File

@@ -1,21 +1,53 @@
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 { createServer } from "../src/index";
import { openApiDocument } from "../src/openapi";
let tempDir: string | null = null;
function createTestServer() {
function createTestApp() {
tempDir = mkdtempSync(join(tmpdir(), "mock-task-api-http-"));
const db = reseedDatabase(join(tempDir, "test.sqlite"));
return createServer(db);
return createApp(db, { parseJson: false });
}
async function readJson(response: Response) {
return (await response.json()) as Record<string, unknown>;
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(() => {
@@ -27,185 +59,177 @@ afterEach(() => {
describe("mock task API", () => {
it("serves a health endpoint", async () => {
const server = createTestServer();
const app = createTestApp();
const response = await server.fetch(new Request("http://mock.local/health"));
const body = await readJson(response);
const response = await sendRequest(app, {
method: "GET",
url: "/health",
});
expect(response.status).toBe(200);
expect(body.status).toBe("ok");
expect(response.body.status).toBe("ok");
});
it("lists organizations", async () => {
const server = createTestServer();
const app = createTestApp();
const response = await server.fetch(new Request("http://mock.local/orgs"));
const body = await readJson(response);
const response = await sendRequest(app, {
method: "GET",
url: "/orgs",
});
expect(response.status).toBe(200);
expect(Array.isArray(body.organizations)).toBe(true);
expect((body.organizations as unknown[]).length).toBe(3);
expect(Array.isArray(response.body.organizations)).toBe(true);
expect(response.body.organizations).toHaveLength(3);
});
it("filters tasks by org and status", async () => {
const server = createTestServer();
const app = createTestApp();
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<Record<string, unknown>>;
const response = await sendRequest(app, {
method: "GET",
url: "/tasks",
query: {
orgId: "org_acme",
status: "blocked",
},
});
expect(response.status).toBe(200);
expect(tasks.length).toBe(1);
expect(tasks[0]?.id).toBe("task_1003");
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 server = createTestServer();
const app = createTestApp();
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);
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((body.task as Record<string, unknown>).title).toBe("Run API smoke test");
expect(response.body.task.title).toBe("Run API smoke test");
});
it("updates a task through the HTTP contract", async () => {
const server = createTestServer();
const app = createTestApp();
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);
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((body.task as Record<string, unknown>).status).toBe("done");
expect((body.task as Record<string, unknown>).assigneeUserId).toBeNull();
expect(response.body.task.status).toBe("done");
expect(response.body.task.assigneeUserId).toBeNull();
});
it("returns 400 for invalid create payloads", async () => {
const server = createTestServer();
const app = createTestApp();
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);
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(body.error).toBe("title is required.");
expect(response.body.error).toBe("Validation failed.");
expect(response.body.details).toBeDefined();
});
it("returns 404 when updating a missing task", async () => {
const server = createTestServer();
const app = createTestApp();
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);
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(body.error).toBe("Task not found.");
expect(response.body.error).toBe("Task not found.");
});
it("returns 404 for unknown routes", async () => {
const server = createTestServer();
const app = createTestApp();
const response = await server.fetch(new Request("http://mock.local/unknown"));
const body = await readJson(response);
const response = await sendRequest(app, {
method: "GET",
url: "/unknown",
});
expect(response.status).toBe(404);
expect(body.error).toBe("Route not found.");
expect(response.body.error).toBe("Route not found.");
});
it("returns a valid openapi document at the import path", async () => {
const server = createTestServer();
const app = createTestApp();
const response = await server.fetch(new Request("http://mock.local/openapi.json"));
const body = await readJson(response);
const paths = body.paths as Record<string, unknown>;
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(body.openapi).toBe("3.0.3");
expect(response.body.openapi).toBe("3.0.0");
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}`),
);
it("serves the same generated document on all json spec aliases", async () => {
const app = createTestApp();
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",
]);
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("supports the swagger alias path used by import tools", async () => {
const server = createTestServer();
it("serves swagger ui at /api-docs", async () => {
const app = createTestApp();
const response = await server.fetch(new Request("http://mock.local/swagger.json"));
const response = await sendRequest(app, {
method: "GET",
url: "/api-docs",
});
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);
expect(response.status).toBe(301);
});
});