import type Database from "bun:sqlite"; import { describe, expect, test } from "bun:test"; import type { Material, RuntimeMode } from "../../../src/shared/api"; import { archiveProject, createProject } from "../../../src/server/db/projects"; import { createNoopLogger } from "../../../src/server/logger"; import { createMigratedMemoryTestDatabase } from "../../helpers"; const MODE: RuntimeMode = "test"; const LOG = createNoopLogger(); async function createMaterialViaHandler(req: Request, db: Database): Promise { const { handleCreateMaterial: h } = await import("../../../src/server/routes/materials/create"); return h(req, db, MODE, LOG); } function createTestProject(db: Database, name = "测试项目") { const result = createProject(db, { name }, LOG); if ("error" in result) throw new Error(result.error); return result.project; } async function deleteMaterialViaHandler(req: Request, db: Database): Promise { const { handleDeleteMaterial: h } = await import("../../../src/server/routes/materials/delete"); return h(req, db, MODE, LOG); } async function getMaterialViaHandler(req: Request, db: Database): Promise { const { handleGetMaterial: h } = await import("../../../src/server/routes/materials/get"); return h(req, db, MODE, LOG); } async function listMaterialsViaHandler(req: Request, db: Database): Promise { const { handleListMaterials: h } = await import("../../../src/server/routes/materials/list"); return h(req, db, MODE, LOG); } async function withRouteDb(callback: (db: Database) => Promise): Promise { const handle = createMigratedMemoryTestDatabase("material-route-test"); try { await callback(handle.db); handle.close(); } finally { handle.cleanup(); } } describe("素材 API 路由", () => { describe("POST /api/projects/:id/materials", () => { test("正常创建素材", async () => { await withRouteDb(async (db) => { const project = createTestProject(db); const req = new Request(`http://localhost/api/projects/${project.id}/materials`, { body: JSON.stringify({ associatedDate: "2024-01-15", description: "测试素材" }), headers: { "Content-Type": "application/json" }, method: "POST", }); const res = await createMaterialViaHandler(req, db); expect(res.status).toBe(201); const body = (await res.json()) as { material: Material }; expect(body.material.description).toBe("测试素材"); expect(body.material.associatedDate).toBe("2024-01-15"); expect(body.material.projectId).toBe(project.id); expect(body.material.status).toBe("pending"); }); }); test("缺少 description 返回 400", async () => { await withRouteDb(async (db) => { const project = createTestProject(db); const req = new Request(`http://localhost/api/projects/${project.id}/materials`, { body: JSON.stringify({ associatedDate: "2024-01-15" }), headers: { "Content-Type": "application/json" }, method: "POST", }); const res = await createMaterialViaHandler(req, db); expect(res.status).toBe(400); }); }); test("缺少 associatedDate 返回 400", async () => { await withRouteDb(async (db) => { const project = createTestProject(db); const req = new Request(`http://localhost/api/projects/${project.id}/materials`, { body: JSON.stringify({ description: "测试素材" }), headers: { "Content-Type": "application/json" }, method: "POST", }); const res = await createMaterialViaHandler(req, db); expect(res.status).toBe(400); }); }); test("associatedDate 格式错误返回 400", async () => { await withRouteDb(async (db) => { const project = createTestProject(db); const req = new Request(`http://localhost/api/projects/${project.id}/materials`, { body: JSON.stringify({ associatedDate: "2024/01/15", description: "测试素材" }), headers: { "Content-Type": "application/json" }, method: "POST", }); const res = await createMaterialViaHandler(req, db); expect(res.status).toBe(400); }); }); test("项目不存在返回 404", async () => { await withRouteDb(async (db) => { const req = new Request("http://localhost/api/projects/nonexistent/materials", { body: JSON.stringify({ associatedDate: "2024-01-15", description: "测试素材" }), headers: { "Content-Type": "application/json" }, method: "POST", }); const res = await createMaterialViaHandler(req, db); expect(res.status).toBe(404); }); }); test("已归档项目返回 409", async () => { await withRouteDb(async (db) => { const project = createTestProject(db); archiveProject(db, project.id, LOG); const req = new Request(`http://localhost/api/projects/${project.id}/materials`, { body: JSON.stringify({ associatedDate: "2024-01-15", description: "测试素材" }), headers: { "Content-Type": "application/json" }, method: "POST", }); const res = await createMaterialViaHandler(req, db); expect(res.status).toBe(409); }); }); }); describe("GET /api/projects/:id/materials", () => { test("正常列表查询", async () => { await withRouteDb(async (db) => { const project = createTestProject(db); const req1 = new Request(`http://localhost/api/projects/${project.id}/materials`, { body: JSON.stringify({ associatedDate: "2024-01-15", description: "素材1" }), headers: { "Content-Type": "application/json" }, method: "POST", }); await createMaterialViaHandler(req1, db); const req2 = new Request(`http://localhost/api/projects/${project.id}/materials`, { body: JSON.stringify({ associatedDate: "2024-01-16", description: "素材2" }), headers: { "Content-Type": "application/json" }, method: "POST", }); await createMaterialViaHandler(req2, db); const req = new Request(`http://localhost/api/projects/${project.id}/materials?page=1&pageSize=20`); const res = await listMaterialsViaHandler(req, db); expect(res.status).toBe(200); const body = (await res.json()) as { items: Material[]; total: number }; expect(body.total).toBe(2); expect(body.items.length).toBe(2); }); }); test("空项目返回空列表", async () => { await withRouteDb(async (db) => { const project = createTestProject(db); const req = new Request(`http://localhost/api/projects/${project.id}/materials?page=1&pageSize=20`); const res = await listMaterialsViaHandler(req, db); expect(res.status).toBe(200); const body = (await res.json()) as { items: Material[]; total: number }; expect(body.total).toBe(0); expect(body.items.length).toBe(0); }); }); }); describe("GET /api/projects/:id/materials/:mid", () => { test("正常获取详情", async () => { await withRouteDb(async (db) => { const project = createTestProject(db); const createReq = new Request(`http://localhost/api/projects/${project.id}/materials`, { body: JSON.stringify({ associatedDate: "2024-01-15", description: "详情测试" }), headers: { "Content-Type": "application/json" }, method: "POST", }); const createRes = await createMaterialViaHandler(createReq, db); const createBody = (await createRes.json()) as { material: Material }; const req = new Request(`http://localhost/api/projects/${project.id}/materials/${createBody.material.id}`); const res = await getMaterialViaHandler(req, db); expect(res.status).toBe(200); const body = (await res.json()) as { material: Material }; expect(body.material.description).toBe("详情测试"); }); }); test("归属错误返回 403", async () => { await withRouteDb(async (db) => { const projectA = createTestProject(db, "项目A"); const projectB = createProject(db, { name: "项目B" }, LOG); if ("error" in projectB) throw new Error(projectB.error); const createReq = new Request(`http://localhost/api/projects/${projectA.id}/materials`, { body: JSON.stringify({ associatedDate: "2024-01-15", description: "测试" }), headers: { "Content-Type": "application/json" }, method: "POST", }); const createRes = await createMaterialViaHandler(createReq, db); const createBody = (await createRes.json()) as { material: Material }; const req = new Request( `http://localhost/api/projects/${projectB.project.id}/materials/${createBody.material.id}`, ); const res = await getMaterialViaHandler(req, db); expect(res.status).toBe(403); }); }); }); describe("DELETE /api/projects/:id/materials/:mid", () => { test("正常删除", async () => { await withRouteDb(async (db) => { const project = createTestProject(db); const createReq = new Request(`http://localhost/api/projects/${project.id}/materials`, { body: JSON.stringify({ associatedDate: "2024-01-15", description: "待删除" }), headers: { "Content-Type": "application/json" }, method: "POST", }); const createRes = await createMaterialViaHandler(createReq, db); const createBody = (await createRes.json()) as { material: Material }; const req = new Request(`http://localhost/api/projects/${project.id}/materials/${createBody.material.id}`, { method: "DELETE", }); const res = await deleteMaterialViaHandler(req, db); expect(res.status).toBe(204); }); }); test("删除后再查返回 404", async () => { await withRouteDb(async (db) => { const project = createTestProject(db); const createReq = new Request(`http://localhost/api/projects/${project.id}/materials`, { body: JSON.stringify({ associatedDate: "2024-01-15", description: "待删除" }), headers: { "Content-Type": "application/json" }, method: "POST", }); const createRes = await createMaterialViaHandler(createReq, db); const createBody = (await createRes.json()) as { material: Material }; const delReq = new Request(`http://localhost/api/projects/${project.id}/materials/${createBody.material.id}`, { method: "DELETE", }); await deleteMaterialViaHandler(delReq, db); const getReq = new Request(`http://localhost/api/projects/${project.id}/materials/${createBody.material.id}`); const res = await getMaterialViaHandler(getReq, db); expect(res.status).toBe(404); }); }); test("归属错误返回 403", async () => { await withRouteDb(async (db) => { const projectA = createTestProject(db, "项目A"); const projectB = createProject(db, { name: "项目B" }, LOG); if ("error" in projectB) throw new Error(projectB.error); const createReq = new Request(`http://localhost/api/projects/${projectA.id}/materials`, { body: JSON.stringify({ associatedDate: "2024-01-15", description: "测试" }), headers: { "Content-Type": "application/json" }, method: "POST", }); const createRes = await createMaterialViaHandler(createReq, db); const createBody = (await createRes.json()) as { material: Material }; const req = new Request( `http://localhost/api/projects/${projectB.project.id}/materials/${createBody.material.id}`, { method: "DELETE", }, ); const res = await deleteMaterialViaHandler(req, db); expect(res.status).toBe(403); }); }); }); });