const Fastify = require("fastify"); const todosRoutes = require("../../../src/routes/todos"); const ACTOR = "11111111-1111-4111-8111-111111111111"; const TODO = "22222222-2222-4222-8222-222222222222"; const OTHER = "33333333-3333-4333-8333-333333333333"; async function buildApp({ owner = ACTOR } = {}) { const app = Fastify(); let row = { id: TODO, title: "合成待办", assignee_employee_id: owner, created_by: owner, due_date: new Date("2026-01-31T00:00:00.000Z"), status: "todo" }; const prisma = { featureFlag: { findUnique: vi.fn().mockResolvedValue({ flag_key: "todo_board_v1", override_mode: "force_on", rollout_type: "global", department_ids: [] }) }, todoItem: { findUnique: vi.fn(async () => row), create: vi.fn(async ({ data }) => (row = { id: TODO, ...data })), update: vi.fn(async ({ data }) => (row = { ...row, ...data })), }, 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 due date calendar contract", () => { it.each(["POST", "PATCH"].flatMap((method) => ["2026-02-29", "2026-02-30", "2026-04-31", "2026-13-01", ""].map((date) => [method, date])))("rejects %s date=%s before reads, writes or notification", async (method, date) => { const { app, prisma } = await buildApp(); try { const response = await app.inject({ method, url: `/api/todos${method === "PATCH" ? `/${TODO}` : ""}`, payload: { title: "合成待办", dueDate: date } }); expect(response.statusCode).toBe(400); expect(response.json().error.details.reasonCode).toBe("todo.input_invalid"); expect(response.json().error.details.fieldErrors.dueDate.length).toBeGreaterThan(0); for (const fn of [...Object.values(prisma.todoItem), ...Object.values(prisma.notification)]) expect(fn).not.toHaveBeenCalled(); } finally { await app.close(); } }); it.each(["POST", "PATCH"].flatMap((method) => ["2024-02-29", "2026-04-30", null, undefined].map((date) => [method, date])))("preserves %s nullable/optional date=%s", async (method, date) => { const { app, prisma } = await buildApp(); try { const response = await app.inject({ method, url: `/api/todos${method === "PATCH" ? `/${TODO}` : ""}`, payload: { title: "合成修改", ...(date === undefined ? {} : { dueDate: date }) } }); expect(response.statusCode).toBe(method === "POST" ? 201 : 200); expect(response.json().data.item.dueDate).toBe(date ?? (method === "PATCH" && date === undefined ? "2026-01-31" : null)); const data = prisma.todoItem[method === "POST" ? "create" : "update"].mock.calls[0][0].data; if (date === undefined && method === "PATCH") expect(data).not.toHaveProperty("due_date"); else expect(data.due_date).toEqual(date ? new Date(`${date}T00:00:00.000Z`) : null); for (const fn of Object.values(prisma.notification)) expect(fn).not.toHaveBeenCalled(); } finally { await app.close(); } }); it.each(["POST", "PATCH"])("keeps %s unauthorized assignment blocked for a valid date", async (method) => { const { app, prisma } = await buildApp(); try { const response = await app.inject({ method, url: `/api/todos${method === "PATCH" ? `/${TODO}` : ""}`, payload: { title: "合成指派", dueDate: "2024-02-29", assigneeEmployeeId: OTHER } }); expect(response.statusCode).toBe(403); expect(response.json().error.details.permission).toBe("report.dashboard.view"); expect(prisma.todoItem.create).not.toHaveBeenCalled(); expect(prisma.todoItem.update).not.toHaveBeenCalled(); for (const fn of Object.values(prisma.notification)) expect(fn).not.toHaveBeenCalled(); } finally { await app.close(); } }); it("keeps another employee's todo unwritable", async () => { const { app, prisma } = await buildApp({ owner: OTHER }); try { const response = await app.inject({ method: "PATCH", url: `/api/todos/${TODO}`, payload: { dueDate: "2024-02-29" } }); expect(response.statusCode).toBe(403); expect(response.json().error.details.permission).toBe("todo.write"); expect(prisma.todoItem.update).not.toHaveBeenCalled(); } finally { await app.close(); } }); });