文件
hyb-oa/server/tests/unit/routes/riskFlags.test.js
T
wangsli 64d9ded024 R52 A1-A6:完成 AP 运营深度操作
- AP 概览页 Tab 工作台化(待付款/异常付款/风险标记/对账异常)
- AP 导出端点支持 CSV/XLSX 与筛选
- 风险标记 resolve/dismiss 支持说明审计留痕
- 支付批次深链、高风险复核提示、对账确认权限收口
- OpenClaw 浏览器级回归全部通过
2026-04-07 09:37:17 +08:00

126 行
3.6 KiB
JavaScript

const Fastify = require("fastify");
const riskFlagsRoutes = require("../../../src/routes/riskFlags");
const ACTOR_ID = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa";
const RISK_FLAG_ID = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb";
function buildRiskFlag(overrides = {}) {
return {
id: RISK_FLAG_ID,
entity_type: "reimbursement",
entity_id: "cccccccc-cccc-4ccc-8ccc-cccccccccccc",
risk_code: "budget_exceeded",
level: "high",
title: "预算已超支",
detail: "部门预算已超预算阈值",
source: "budget_control",
status: "open",
payload_json: null,
created_at: new Date("2026-04-06T08:00:00.000Z"),
created_by: null,
resolved_at: null,
resolved_by: null,
...overrides,
};
}
async function buildApp(options = {}) {
const app = Fastify();
const actor = options.actor ?? {
id: ACTOR_ID,
permissionCodes: ["risk.review.manage"],
};
const currentRecord = options.currentRecord ?? buildRiskFlag();
const findUnique = options.findUnique ?? vi.fn().mockResolvedValue(currentRecord);
const update = options.update ?? vi.fn().mockImplementation(async ({ data }) => ({
...currentRecord,
status: data.status,
resolved_at: data.resolved_at,
resolved_by: data.resolved_by,
}));
const auditCreate = options.auditCreate ?? vi.fn().mockResolvedValue({});
app.decorate("authenticate", async (request) => {
request.employee = actor;
});
app.decorate("hasPermission", (employee, permissionCode) => {
const permissionCodes = employee.permissionCodes ?? [];
return permissionCodes.includes("*") || permissionCodes.includes(permissionCode);
});
app.decorate("prisma", {
entityRiskFlag: {
findMany: vi.fn().mockResolvedValue([]),
findUnique,
update,
},
auditLog: {
create: auditCreate,
},
});
await app.register(riskFlagsRoutes, { prefix: "/api" });
return { app, findUnique, update, auditCreate };
}
describe("risk flags routes", () => {
it("writes resolve note into audit extra while keeping response shape", async () => {
const { app, update, auditCreate } = await buildApp();
const response = await app.inject({
method: "POST",
url: `/api/risk-flags/${RISK_FLAG_ID}/resolve`,
payload: {
note: "已人工核查,无需继续挂起",
},
});
expect(response.statusCode).toBe(200);
expect(update).toHaveBeenCalledWith(
expect.objectContaining({
where: { id: RISK_FLAG_ID },
data: expect.objectContaining({
status: "resolved",
resolved_by: ACTOR_ID,
}),
}),
);
expect(auditCreate).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
action: "ENTITY_RISK_FLAG_RESOLVED",
extra: expect.objectContaining({
riskCode: "budget_exceeded",
note: "已人工核查,无需继续挂起",
}),
}),
}),
);
const payload = response.json();
expect(payload.data.item.status).toBe("resolved");
expect(payload.data.item.resolvedBy).toBe(ACTOR_ID);
await app.close();
});
it("rejects unexpected fields for resolve action body", async () => {
const { app, update, auditCreate } = await buildApp();
const response = await app.inject({
method: "POST",
url: `/api/risk-flags/${RISK_FLAG_ID}/resolve`,
payload: {
note: "ok",
extra: "not-allowed",
},
});
expect(response.statusCode).toBe(400);
expect(update).not.toHaveBeenCalled();
expect(auditCreate).not.toHaveBeenCalled();
await app.close();
});
});