import type Database from "bun:sqlite"; import { describe, expect, test } from "bun:test"; import { mkdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import type { Project, RuntimeMode } from "../../../src/shared/api"; import { createDatabase } from "../../../src/server/db/connection"; import { runMigrations } from "../../../src/server/db/migrate"; import { createMemoryLogger } from "../../../src/server/logger"; const MODE: RuntimeMode = "test"; const MIGRATION_SQL = ` CREATE TABLE IF NOT EXISTS projects ( id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, description TEXT NOT NULL DEFAULT '', status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'archived')), archived_at TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS schema_migrations ( id TEXT PRIMARY KEY, checksum TEXT NOT NULL, applied_at TEXT NOT NULL ); `; async function archiveProjectViaHandler(req: Request, db: Database): Promise { const { handleArchiveProject: h } = await import("../../../src/server/routes/projects/archive"); return h(req, db, MODE); } // Inline imports for actual route handler tests (each handler is in separate file) async function createProjectViaHandler(req: Request, db: Database): Promise { const { handleCreateProject: h } = await import("../../../src/server/routes/projects/create"); return h(req, db, MODE); } function createTestProject(db: Database, name = "测试项目"): Project { const result = createProject(db, { name }); if ("error" in result) throw new Error(result.error); return result.project; } async function deleteProjectViaHandler(req: Request, db: Database): Promise { const { handleDeleteProject: h } = await import("../../../src/server/routes/projects/delete"); return h(req, db, MODE); } async function getProjectViaHandler(req: Request, db: Database): Promise { const { handleGetProject: h } = await import("../../../src/server/routes/projects/get"); return h(req, db, MODE); } async function listProjectsViaHandler(req: Request, db: Database): Promise { const { handleListProjects: h } = await import("../../../src/server/routes/projects/list"); return h(req, db, MODE); } function makeTempDir(): string { const dir = join(tmpdir(), `route-test-${Date.now()}-${Math.random().toString(36).slice(2)}`); mkdirSync(dir, { recursive: true }); return dir; } async function restoreProjectViaHandler(req: Request, db: Database): Promise { const { handleRestoreProject: h } = await import("../../../src/server/routes/projects/restore"); return h(req, db, MODE); } function setupDb(dir: string): Database { const logger = createMemoryLogger(); const db = createDatabase(dir, logger); runMigrations(db, [{ checksum: "init", id: "001_init", sql: MIGRATION_SQL }], dir, logger); return db; } async function updateProjectViaHandler(req: Request, db: Database): Promise { const { handleUpdateProject: h } = await import("../../../src/server/routes/projects/update"); return h(req, db, MODE); } // Need db/projects for setup import { archiveProject, createProject, getProject } from "../../../src/server/db/projects"; describe("项目 API 路由", () => { test("POST /api/projects 创建项目", async () => { const dir = makeTempDir(); try { const db = setupDb(dir); const req = new Request("http://localhost/api/projects", { body: JSON.stringify({ description: "路由测试", name: "路由项目" }), headers: { "Content-Type": "application/json" }, method: "POST", }); const res = await createProjectViaHandler(req, db); expect(res.status).toBe(201); const body = (await res.json()) as { project: Project }; expect(body.project.name).toBe("路由项目"); db.close(); } finally { rmSync(dir, { force: true, recursive: true }); } }); test("GET /api/projects 列表查询", async () => { const dir = makeTempDir(); try { const db = setupDb(dir); createTestProject(db, "A项目"); createTestProject(db, "B项目"); const req = new Request("http://localhost/api/projects?page=1&pageSize=20"); const res = await listProjectsViaHandler(req, db); expect(res.status).toBe(200); const body = (await res.json()) as { items: Project[]; total: number }; expect(body.total).toBe(2); expect(body.items.length).toBe(2); db.close(); } finally { rmSync(dir, { force: true, recursive: true }); } }); test("GET /api/projects/:id 获取详情", async () => { const dir = makeTempDir(); try { const db = setupDb(dir); const project = createTestProject(db, "详情路由"); const req = new Request(`http://localhost/api/projects/${project.id}`); const res = await getProjectViaHandler(req, db); expect(res.status).toBe(200); const body = (await res.json()) as { project: Project }; expect(body.project.name).toBe("详情路由"); db.close(); } finally { rmSync(dir, { force: true, recursive: true }); } }); test("PATCH /api/projects/:id 更新项目", async () => { const dir = makeTempDir(); try { const db = setupDb(dir); const project = createTestProject(db, "更新路由"); const req = new Request(`http://localhost/api/projects/${project.id}`, { body: JSON.stringify({ name: "已更新" }), headers: { "Content-Type": "application/json" }, method: "PATCH", }); const res = await updateProjectViaHandler(req, db); expect(res.status).toBe(200); const body = (await res.json()) as { project: Project }; expect(body.project.name).toBe("已更新"); db.close(); } finally { rmSync(dir, { force: true, recursive: true }); } }); test("POST /api/projects/:id/archive 归档项目", async () => { const dir = makeTempDir(); try { const db = setupDb(dir); const project = createTestProject(db, "归档路由"); const req = new Request(`http://localhost/api/projects/${project.id}/archive`, { method: "POST" }); const res = await archiveProjectViaHandler(req, db); expect(res.status).toBe(200); const body = (await res.json()) as { project: Project }; expect(body.project.status).toBe("archived"); db.close(); } finally { rmSync(dir, { force: true, recursive: true }); } }); test("POST /api/projects/:id/restore 恢复项目", async () => { const dir = makeTempDir(); try { const db = setupDb(dir); const project = createTestProject(db, "恢复路由"); archiveProject(db, project.id); const req = new Request(`http://localhost/api/projects/${project.id}/restore`, { method: "POST" }); const res = await restoreProjectViaHandler(req, db); expect(res.status).toBe(200); const body = (await res.json()) as { project: Project }; expect(body.project.status).toBe("active"); db.close(); } finally { rmSync(dir, { force: true, recursive: true }); } }); test("DELETE /api/projects/:id 永久删除已归档项目", async () => { const dir = makeTempDir(); try { const db = setupDb(dir); const project = createTestProject(db, "删除路由"); archiveProject(db, project.id); const req = new Request(`http://localhost/api/projects/${project.id}`, { method: "DELETE" }); const res = await deleteProjectViaHandler(req, db); expect(res.status).toBe(204); const after = getProject(db, project.id); expect("error" in after).toBe(true); db.close(); } finally { rmSync(dir, { force: true, recursive: true }); } }); test("创建同名项目返回 409", async () => { const dir = makeTempDir(); try { const db = setupDb(dir); const req1 = new Request("http://localhost/api/projects", { body: JSON.stringify({ name: "重复名" }), headers: { "Content-Type": "application/json" }, method: "POST", }); await createProjectViaHandler(req1, db); const req2 = new Request("http://localhost/api/projects", { body: JSON.stringify({ name: "重复名" }), headers: { "Content-Type": "application/json" }, method: "POST", }); const res = await createProjectViaHandler(req2, db); expect(res.status).toBe(409); db.close(); } finally { rmSync(dir, { force: true, recursive: true }); } }); test("删除 active 项目返回 409", async () => { const dir = makeTempDir(); try { const db = setupDb(dir); const project = createTestProject(db, "活项目"); const req = new Request(`http://localhost/api/projects/${project.id}`, { method: "DELETE" }); const res = await deleteProjectViaHandler(req, db); expect(res.status).toBe(409); db.close(); } finally { rmSync(dir, { force: true, recursive: true }); } }); });