import { API_PREFIX, listEndpoints } from "@umt/contracts"; import { describe, expect, it } from "vitest"; import { createApp } from "../src/http/app.ts"; import { PUBLIC_AUTH_PATHS } from "../src/http/auth.ts"; import { EventBus } from "../src/http/sse.ts"; import { SERVER_MODULES } from "../src/modules/index.ts"; import type { ServerContext } from "../src/modules/types.ts"; import { DEV_KEY, json, memoryLogger } from "./helpers.ts"; // OpenAPI 里出现的每条 /api/v1 路由都必须登记在 @umt/contracts 的 Api 注册表(web 与测试都从那里取路径/schema)。 describe("Api 注册表覆盖", () => { it("OpenAPI 文档中的每条路由都在 listEndpoints() 里", async () => { const app = createApp({ config: { NODE_ENV: "development", DEV_ADMIN_KEY: DEV_KEY }, logger: memoryLogger().logger, bus: new EventBus(5), role: "all", version: "0.0.0-test", modules: SERVER_MODULES, ctx: { role: "all" } as unknown as ServerContext, }); const res = await app.request("/api/openapi.json"); expect(res.status).toBe(200); const doc = (await json(res)) as { paths: Record> }; const served: string[] = []; for (const [path, ops] of Object.entries(doc.paths)) { if (!path.startsWith(`${API_PREFIX}/`)) continue; // /api/openapi.json 等非业务路由 const rel = path.slice(API_PREFIX.length); for (const method of Object.keys(ops)) served.push(`${method.toUpperCase()} ${rel}`); } expect(served.length).toBeGreaterThan(0); const registered = new Set(listEndpoints().map((e) => `${e.method.toUpperCase()} ${e.path}`)); const missing = served.filter((k) => !registered.has(k)).sort(); expect(missing, `以下路由未登记到 packages/contracts/src/api.ts:\n${missing.join("\n")}`).toEqual([]); // 反向:api.ts 登记的每条端点都必须有实现(OpenAPI 里可见) const servedSet = new Set(served); const unimplemented = [...registered].filter((k) => !servedSet.has(k)).sort(); expect(unimplemented, `以下端点登记在 api.ts 但未实现:\n${unimplemented.join("\n")}`).toEqual([]); }); }); // 验收 S10:注册表的鉴权标注与服务端实际行为一致(生成 scope 文档不误导集成方) describe("Api 注册表鉴权标注", () => { it("auth:'public' 的端点恰好是 PUBLIC_AUTH_PATHS", () => { const pub = listEndpoints() .filter((e) => e.auth === "public") .map((e) => e.path) .sort(); expect(pub).toEqual([...PUBLIC_AUTH_PATHS].sort()); }); it("只接受控制台会话的端点都标 auth:'session' 且 scope 为 admin", () => { const session = listEndpoints() .filter((e) => e.auth === "session") .map((e) => `${e.method.toUpperCase()} ${e.path}`) .sort(); expect(session).toEqual([ "GET /auth/totp", "PATCH /approvals/{id}", "POST /approvals", "POST /auth/password", "POST /auth/totp", ]); for (const e of listEndpoints().filter((x) => x.auth === "session")) expect(e.scope).toBe("admin"); }); it("auth:'session' 的端点拒绝 Bearer(DEV_ADMIN_KEY 也不行)", async () => { const app = createApp({ config: { NODE_ENV: "development", DEV_ADMIN_KEY: DEV_KEY }, logger: memoryLogger().logger, bus: new EventBus(5), role: "all", version: "0.0.0-test", modules: SERVER_MODULES, ctx: { role: "all" } as unknown as ServerContext, }); for (const e of listEndpoints().filter((x) => x.auth === "session")) { const path = e.path.replace("{id}", "00000000-0000-4000-8000-000000000000"); const res = await app.request(`${API_PREFIX}${path}`, { method: e.method.toUpperCase(), headers: { authorization: `Bearer ${DEV_KEY}`, "content-type": "application/json" }, ...(e.method === "get" ? {} : { body: "{}" }), }); expect(res.status, `${e.method} ${e.path}`).toBe(403); } }); });