const Fastify = require("fastify"); const approvalRulesRoutes = require("../../../src/routes/approvalRules"); const { AppError } = require("../../../src/utils/AppError"); const ACTOR_ID = "11111111-1111-4111-8111-111111111111"; const APPROVAL_RULE_ID = "22222222-2222-4222-8222-222222222222"; function buildCreatePayload(roleCode) { return { name: `合成规则-${roleCode}`, minAmount: "0.00", maxAmount: "1000.00", level1Role: roleCode, level2Role: null, level3Role: null, ownerSelfFallbackRole: "finance", enabled: true, priority: 1, }; } async function buildApp() { const app = Fastify(); const approvalRuleCreate = vi.fn(async ({ data }) => ({ id: APPROVAL_RULE_ID, name: data.name, min_amount: data.min_amount, max_amount: data.max_amount, level1_role: data.level1_role, level2_role: data.level2_role, level3_role: data.level3_role, owner_self_fallback_role: data.owner_self_fallback_role, enabled: data.enabled, priority: data.priority, created_at: new Date("2026-07-18T00:00:00.000Z"), updated_at: new Date("2026-07-18T00:00:00.000Z"), })); const transactionContext = { approvalRule: { aggregate: vi.fn().mockResolvedValue({ _max: { priority: 0 } }), create: approvalRuleCreate, }, auditLog: { create: vi.fn().mockResolvedValue({ id: "audit-1" }), }, }; const transaction = vi.fn(async (callback) => callback(transactionContext)); app.decorate("requirePermission", () => async (request) => { request.employee = { id: ACTOR_ID, permissionCodes: ["system.settings"], }; }); app.decorate("prisma", { $transaction: transaction, }); app.setErrorHandler((error, request, reply) => { if (error instanceof AppError) { return reply.status(error.statusCode).send({ requestId: request.id, error: { code: error.code, message: error.message, details: error.details ?? null, }, }); } return reply.status(500).send({ requestId: request.id, error: { code: "SYS_001", message: "服务器内部错误", details: null, }, }); }); await app.register(approvalRulesRoutes, { prefix: "/api" }); return { app, approvalRuleCreate, transaction }; } describe("reimbursement approval-rule role whitelist", () => { it.each(["finance", "finance_mgr"])( "allows owner self fallback role %s", async (fallbackRole) => { const testApp = await buildApp(); try { const response = await testApp.app.inject({ method: "POST", url: "/api/approval-rules", payload: { ...buildCreatePayload("manager"), ownerSelfFallbackRole: fallbackRole, }, }); expect(response.statusCode).toBe(200); expect(response.json().data.ownerSelfFallbackRole).toBe(fallbackRole); } finally { await testApp.app.close(); } }, ); it("defaults owner self fallback to finance when omitted", async () => { const testApp = await buildApp(); try { const payload = buildCreatePayload("manager"); delete payload.ownerSelfFallbackRole; const response = await testApp.app.inject({ method: "POST", url: "/api/approval-rules", payload, }); expect(response.statusCode).toBe(200); expect(response.json().data.ownerSelfFallbackRole).toBe("finance"); expect(testApp.approvalRuleCreate).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ owner_self_fallback_role: "finance" }), }), ); } finally { await testApp.app.close(); } }); it.each(["admin", "system_admin", "hr"])( "rejects owner self fallback role %s", async (fallbackRole) => { const testApp = await buildApp(); try { const response = await testApp.app.inject({ method: "POST", url: "/api/approval-rules", payload: { ...buildCreatePayload("manager"), ownerSelfFallbackRole: fallbackRole, }, }); expect(response.statusCode).toBe(400); expect(response.json().error.code).toBe("VALID_001"); expect(testApp.transaction).not.toHaveBeenCalled(); } finally { await testApp.app.close(); } }, ); it.each(["manager", "ceo", "finance", "finance_mgr"])( "allows role %s because it carries expense.approve", async (roleCode) => { const testApp = await buildApp(); try { const response = await testApp.app.inject({ method: "POST", url: "/api/approval-rules", payload: buildCreatePayload(roleCode), }); expect(response.statusCode).toBe(200); expect(response.json().data.level1Role).toBe(roleCode); expect(testApp.approvalRuleCreate).toHaveBeenCalledTimes(1); } finally { await testApp.app.close(); } }, ); it.each(["employee", "hr", "administration", "admin", "system_admin"])( "rejects role %s before a reimbursement approval rule is saved", async (roleCode) => { const testApp = await buildApp(); try { const response = await testApp.app.inject({ method: "POST", url: "/api/approval-rules", payload: buildCreatePayload(roleCode), }); expect(response.statusCode).toBe(400); expect(response.json().error.code).toBe("VALID_001"); expect(testApp.transaction).not.toHaveBeenCalled(); expect(testApp.approvalRuleCreate).not.toHaveBeenCalled(); } finally { await testApp.app.close(); } }, ); it.each(["level1Role", "level2Role", "level3Role"])( "applies the reimbursement role whitelist to %s on update", async (fieldName) => { const testApp = await buildApp(); try { const response = await testApp.app.inject({ method: "PATCH", url: `/api/approval-rules/${APPROVAL_RULE_ID}`, payload: { [fieldName]: "employee" }, }); expect(response.statusCode).toBe(400); expect(response.json().error.code).toBe("VALID_001"); expect(testApp.transaction).not.toHaveBeenCalled(); } finally { await testApp.app.close(); } }, ); });