214 行
6.3 KiB
JavaScript
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();
|
|
}
|
|
},
|
|
);
|
|
});
|