import { describe, expect, test } from "bun:test"; import type { StaticAssets } from "../../src/server/static"; import { startServer } from "../../src/server/server"; import { createMigratedTestDatabase } from "../helpers"; function createStaticAssets(): StaticAssets { return { files: { "/assets/app.js": new Blob(["console.log('app')"], { type: "text/javascript" }), }, indexHtml: new Blob(["
Alfred
"], { type: "text/html" }), }; } function createTestApp() { const handle = createMigratedTestDatabase("app-test"); const server = startServer({ config: { host: "127.0.0.1", port: 0 }, db: handle.db, logger: handle.logger, mode: "production", staticAssets: createStaticAssets(), version: "9.8.7-test", }); return { baseUrl: server.url.toString().replace(/\/$/, ""), close: () => { void server.stop(true); handle.cleanup(); }, }; } describe("后端应用集成", () => { test("/api/meta 返回服务元信息和生产安全 header", async () => { const app = createTestApp(); try { const res = await fetch(`${app.baseUrl}/api/meta`); expect(res.status).toBe(200); expect(res.headers.get("Content-Type")).toBe("application/json; charset=utf-8"); expect(res.headers.get("X-Content-Type-Options")).toBe("nosniff"); expect(res.headers.get("Referrer-Policy")).toBe("strict-origin-when-cross-origin"); const body = (await res.json()) as { ok: boolean; service: string; timestamp: string; version: string }; expect(body.ok).toBe(true); expect(body.service).toBe("alfred"); expect(body.version).toBe("9.8.7-test"); expect(Number.isNaN(Date.parse(body.timestamp))).toBe(false); } finally { app.close(); } }); test("未知 API 返回 JSON 404", async () => { const app = createTestApp(); try { const res = await fetch(`${app.baseUrl}/api/missing`); expect(res.status).toBe(404); expect(res.headers.get("Content-Type")).toBe("application/json; charset=utf-8"); expect(await res.json()).toEqual({ error: "API route not found", status: 404 }); } finally { app.close(); } }); test("静态资源和 SPA fallback 由真实服务返回", async () => { const app = createTestApp(); try { const assetRes = await fetch(`${app.baseUrl}/assets/app.js`); expect(assetRes.status).toBe(200); expect(assetRes.headers.get("Cache-Control")).toBe("public, max-age=31536000, immutable"); expect(await assetRes.text()).toBe("console.log('app')"); const fallbackRes = await fetch(`${app.baseUrl}/projects`); expect(fallbackRes.status).toBe(200); expect(fallbackRes.headers.get("Cache-Control")).toBe("no-cache"); expect(await fallbackRes.text()).toContain("Alfred"); } finally { app.close(); } }); test("项目 API 覆盖创建、列表、详情、更新、归档、恢复和删除", async () => { const app = createTestApp(); try { const createdRes = await fetch(`${app.baseUrl}/api/projects`, { body: JSON.stringify({ description: "集成测试", name: "集成项目" }), headers: { "Content-Type": "application/json" }, method: "POST", }); expect(createdRes.status).toBe(201); const createdBody = (await createdRes.json()) as { project: { id: string; name: string; status: string } }; const id = createdBody.project.id; expect(createdBody.project.name).toBe("集成项目"); const listRes = await fetch(`${app.baseUrl}/api/projects?keyword=集成&page=1&pageSize=20`); expect(listRes.status).toBe(200); const listBody = (await listRes.json()) as { items: Array<{ id: string }>; total: number }; expect(listBody.total).toBe(1); expect(listBody.items[0]!.id).toBe(id); const getRes = await fetch(`${app.baseUrl}/api/projects/${id}`); expect(getRes.status).toBe(200); const updateRes = await fetch(`${app.baseUrl}/api/projects/${id}`, { body: JSON.stringify({ name: "更新项目" }), headers: { "Content-Type": "application/json" }, method: "PATCH", }); expect(updateRes.status).toBe(200); expect(((await updateRes.json()) as { project: { name: string } }).project.name).toBe("更新项目"); const archiveRes = await fetch(`${app.baseUrl}/api/projects/${id}/archive`, { method: "POST" }); expect(archiveRes.status).toBe(200); expect(((await archiveRes.json()) as { project: { status: string } }).project.status).toBe("archived"); const restoreRes = await fetch(`${app.baseUrl}/api/projects/${id}/restore`, { method: "POST" }); expect(restoreRes.status).toBe(200); expect(((await restoreRes.json()) as { project: { status: string } }).project.status).toBe("active"); await fetch(`${app.baseUrl}/api/projects/${id}/archive`, { method: "POST" }); const deleteRes = await fetch(`${app.baseUrl}/api/projects/${id}`, { method: "DELETE" }); expect(deleteRes.status).toBe(204); } finally { app.close(); } }); test("项目 API 覆盖关键错误路径", async () => { const app = createTestApp(); try { const invalidIdRes = await fetch(`${app.baseUrl}/api/projects/-bad`); expect(invalidIdRes.status).toBe(400); const createOnce = await fetch(`${app.baseUrl}/api/projects`, { body: JSON.stringify({ name: "错误项目" }), headers: { "Content-Type": "application/json" }, method: "POST", }); const id = ((await createOnce.json()) as { project: { id: string } }).project.id; const duplicateRes = await fetch(`${app.baseUrl}/api/projects`, { body: JSON.stringify({ name: "错误项目" }), headers: { "Content-Type": "application/json" }, method: "POST", }); expect(duplicateRes.status).toBe(409); const deleteActiveRes = await fetch(`${app.baseUrl}/api/projects/${id}`, { method: "DELETE" }); expect(deleteActiveRes.status).toBe(409); await fetch(`${app.baseUrl}/api/projects/${id}/archive`, { method: "POST" }); const updateArchivedRes = await fetch(`${app.baseUrl}/api/projects/${id}`, { body: JSON.stringify({ name: "不可改" }), headers: { "Content-Type": "application/json" }, method: "PATCH", }); expect(updateArchivedRes.status).toBe(409); const missingRes = await fetch(`${app.baseUrl}/api/projects/00000000-0000-4000-8000-000000000000`); expect(missingRes.status).toBe(404); } finally { app.close(); } }); });