const Fastify = require("fastify"); const todosRoutes = require("../../../src/routes/todos"); const ACTOR = "11111111-1111-4111-8111-111111111111"; const TODO = "22222222-2222-4222-8222-222222222222"; async function buildApp({ enabled = true } = {}) { const app = Fastify(); const prisma = { featureFlag: { findUnique: vi.fn().mockResolvedValue({ flag_key: "todo_board_v1", override_mode: enabled ? "force_on" : "force_off", rollout_type: "global", department_ids: [] }) }, todoItem: { findMany: vi.fn().mockResolvedValue([]), findUnique: vi.fn().mockResolvedValue(null), create: vi.fn(async ({ data }) => ({ id: TODO, ...data })), update: vi.fn(), }, notification: { create: vi.fn(), createMany: vi.fn() }, }; app.decorate("prisma", prisma); app.decorate("config", { todoBoardEnabled: true }); app.decorate("authenticate", async (request) => { request.employee = { id: ACTOR, department_id: null }; }); app.decorate("hasPermission", () => false); app.setErrorHandler((error, request, reply) => reply.status(error.statusCode || 500).send({ error: { code: error.code, details: error.details } })); await app.register(todosRoutes, { prefix: "/api" }); return { app, prisma }; } describe("todo error reason contract", () => { it.each([ ["POST", "/todos", { title: "" }, "todo.input_invalid", "title"], ["PATCH", `/todos/${TODO}`, { title: "" }, "todo.input_invalid", "title"], ["PATCH", "/todos/bad-id", { title: "合成待办" }, "todo.id_invalid", null], ["POST", "/todos/bad-id/complete", undefined, "todo.id_invalid", null], ["POST", "/todos/bad-id/cancel", undefined, "todo.id_invalid", null], ["GET", "/todos?status=unknown", undefined, "todo.query_invalid", "status"], ])("rejects %s %s with precise compatibility details before writes", async (method, url, payload, reasonCode, field) => { const { app, prisma } = await buildApp(); try { const response = await app.inject({ method, url: `/api${url}`, ...(payload ? { payload } : {}) }); expect(response.statusCode).toBe(400); expect(response.json().error.code).toBe("VALID_001"); expect(response.json().error.details.reasonCode).toBe(reasonCode); if (field) expect(response.json().error.details.fieldErrors[field].length).toBeGreaterThan(0); else expect(response.json().error.details.id).toBe("invalid_uuid"); expect(Object.values(prisma.todoItem).every((fn) => fn.mock.calls.length === 0)).toBe(true); expect(Object.values(prisma.notification).every((fn) => fn.mock.calls.length === 0)).toBe(true); } finally { await app.close(); } }); it("preserves disabled status and does not create a todo", async () => { const { app, prisma } = await buildApp({ enabled: false }); try { const response = await app.inject({ method: "POST", url: "/api/todos", payload: { title: "合成待办" } }); expect(response.statusCode).toBe(409); expect(response.json().error.details).toEqual({ reasonCode: "todo.disabled", featureFlag: "todo_board_v1_disabled", reason: "flag_force_off" }); expect(prisma.todoItem.create).not.toHaveBeenCalled(); } finally { await app.close(); } }); it("keeps self-create successful and unauthorized assignment blocked without notification", async () => { const { app, prisma } = await buildApp(); try { const own = await app.inject({ method: "POST", url: "/api/todos", payload: { title: "合成待办" } }); expect(own.statusCode).toBe(201); expect(own.json().data.item.assigneeEmployeeId).toBe(ACTOR); prisma.todoItem.create.mockClear(); const assigned = await app.inject({ method: "POST", url: "/api/todos", payload: { title: "合成待办", assigneeEmployeeId: "33333333-3333-4333-8333-333333333333" } }); expect(assigned.statusCode).toBe(403); expect(assigned.json().error.code).toBe("PERM_001"); expect(prisma.todoItem.create).not.toHaveBeenCalled(); expect(Object.values(prisma.notification).every((fn) => fn.mock.calls.length === 0)).toBe(true); } finally { await app.close(); } }); });