文件
hyb-oa/server/tests/unit/routes/approvalRules.permissions.test.js
T
wangsli 35948729cf
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s
修正CEO报销财务兜底与部署护栏(07-28-ceo-finance-fallback)
2026-07-28 13:42:48 +08:00

214 行
6.3 KiB
JavaScript

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();
}
},
);
});