226 行
7.1 KiB
JavaScript
226 行
7.1 KiB
JavaScript
const Fastify = require("fastify");
|
|
|
|
const workflowRoutes = require("../../../src/routes/workflow");
|
|
|
|
const TASK_ID = "11111111-1111-4111-8111-111111111111";
|
|
const EMPLOYEE_ID = "22222222-2222-4222-8222-222222222222";
|
|
|
|
async function buildApp(options = {}) {
|
|
const app = Fastify();
|
|
const actor = options.actor ?? {
|
|
id: EMPLOYEE_ID,
|
|
roleCodes: [],
|
|
permissionCodes: [],
|
|
};
|
|
|
|
const prisma = {
|
|
delegationGrant: {
|
|
findMany: vi.fn(async () => []),
|
|
},
|
|
workflowTask: {
|
|
findFirst: vi.fn(async () => ({
|
|
id: TASK_ID,
|
|
instance_id: "33333333-3333-4333-8333-333333333333",
|
|
step_id: "approval_1_finance",
|
|
step_name: "财务审批",
|
|
assignee_id: EMPLOYEE_ID,
|
|
assignee_role: null,
|
|
status: "completed",
|
|
outcome: "approved",
|
|
comment: "已核验",
|
|
created_at: new Date("2026-04-08T08:00:00.000Z"),
|
|
completed_at: new Date("2026-04-08T09:00:00.000Z"),
|
|
workflow_instance: {
|
|
id: "33333333-3333-4333-8333-333333333333",
|
|
entity_type: "reimbursement",
|
|
entity_id: "44444444-4444-4444-8444-444444444444",
|
|
current_status: "pending",
|
|
current_step_id: "approval_1_finance",
|
|
context_data: options.contextData ?? {
|
|
standardViolations: [
|
|
{
|
|
itemId: "item-1",
|
|
expenseTypeId: "66666666-6666-4666-8666-666666666666",
|
|
expenseTypeName: "交通费",
|
|
amount: "180.00",
|
|
standardId: "77777777-7777-4777-8777-777777777777",
|
|
standardName: "交通费上限",
|
|
maxAmount: "120.00",
|
|
perUnit: "item",
|
|
exceededAmount: "60.00",
|
|
controlMode: "warn",
|
|
scopeType: "global",
|
|
scopeKey: null,
|
|
exceeded: true,
|
|
secretNestedField: "must-not-be-projected",
|
|
},
|
|
],
|
|
budgetCheck: {
|
|
budgetId: "55555555-5555-4555-8555-555555555555",
|
|
budgetCode: "BUDGET-2026",
|
|
controlMode: "warn",
|
|
usagePercent: 82.5,
|
|
exceededThreshold: true,
|
|
exceededBudget: false,
|
|
},
|
|
},
|
|
started_at: new Date("2026-04-08T08:00:00.000Z"),
|
|
completed_at: null,
|
|
reimbursement_form: {
|
|
id: "44444444-4444-4444-8444-444444444444",
|
|
form_number: "BX-20260408-001",
|
|
status: "reviewing",
|
|
total_amount: "180.00",
|
|
reimbursement_date: new Date("2026-04-08T00:00:00.000Z"),
|
|
description: "测试报销",
|
|
owner: {
|
|
id: EMPLOYEE_ID,
|
|
employee_no: "E001",
|
|
name: "申请人",
|
|
},
|
|
submitter: {
|
|
id: EMPLOYEE_ID,
|
|
employee_no: "E001",
|
|
name: "申请人",
|
|
},
|
|
items: [],
|
|
},
|
|
leave_request: null,
|
|
purchase_request: null,
|
|
travel_request: null,
|
|
},
|
|
})),
|
|
findUnique: vi.fn(async () => ({
|
|
id: TASK_ID,
|
|
})),
|
|
},
|
|
workflowHistory: {
|
|
findMany: vi.fn(async () => []),
|
|
},
|
|
};
|
|
|
|
app.decorate("prisma", prisma);
|
|
app.decorate("config", {
|
|
imNotifierEnabled: false,
|
|
emailEnabled: false,
|
|
});
|
|
app.decorate("authenticate", async (request) => {
|
|
request.employee = actor;
|
|
});
|
|
app.decorate("requirePermission", () => async (request) => {
|
|
request.employee = actor;
|
|
});
|
|
app.decorate("assertEntityPermission", () => {});
|
|
app.setErrorHandler((error, request, reply) => {
|
|
reply.status(error.statusCode || 500).send({
|
|
requestId: request.id,
|
|
error: {
|
|
code: error.code || "SYS_001",
|
|
message: error.message,
|
|
details: error.details ?? null,
|
|
},
|
|
});
|
|
});
|
|
|
|
await app.register(workflowRoutes, { prefix: "/api" });
|
|
return { app, prisma };
|
|
}
|
|
|
|
describe("workflow task detail route", () => {
|
|
it("returns reimbursement workflow contextData.standardViolations", async () => {
|
|
const { app } = await buildApp();
|
|
|
|
const response = await app.inject({
|
|
method: "GET",
|
|
url: `/api/workflow/tasks/${TASK_ID}`,
|
|
});
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
expect(response.json().data.workflowInstance.contextData.standardViolations).toEqual([
|
|
expect.objectContaining({
|
|
itemId: "item-1",
|
|
controlMode: "warn",
|
|
exceededAmount: "60.00",
|
|
}),
|
|
]);
|
|
expect(response.json().data.workflowInstance.contextData.standardViolations[0]).not.toHaveProperty("secretNestedField");
|
|
|
|
await app.close();
|
|
});
|
|
|
|
it("projects the allowed budget snapshot fields", async () => {
|
|
const { app } = await buildApp();
|
|
|
|
const response = await app.inject({ method: "GET", url: `/api/workflow/tasks/${TASK_ID}` });
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
expect(response.json().data.workflowInstance.contextData).toEqual(expect.objectContaining({
|
|
budgetCheck: {
|
|
budgetId: "55555555-5555-4555-8555-555555555555",
|
|
budgetCode: "BUDGET-2026",
|
|
controlMode: "warn",
|
|
usagePercent: 82.5,
|
|
exceededThreshold: true,
|
|
exceededBudget: false,
|
|
},
|
|
budgetCheckStatus: "available",
|
|
parseStatus: "ready",
|
|
}));
|
|
|
|
await app.close();
|
|
});
|
|
|
|
it("marks a historical missing budget snapshot without failing parsing", async () => {
|
|
const { app } = await buildApp({ contextData: { standardViolations: [] } });
|
|
|
|
const response = await app.inject({ method: "GET", url: `/api/workflow/tasks/${TASK_ID}` });
|
|
|
|
expect(response.json().data.workflowInstance.contextData).toEqual({
|
|
standardViolations: [],
|
|
budgetCheck: null,
|
|
budgetCheckStatus: "missing",
|
|
parseStatus: "ready",
|
|
});
|
|
|
|
await app.close();
|
|
});
|
|
|
|
it("distinguishes a recorded submission with no applicable budget from historical missing data", async () => {
|
|
const { app } = await buildApp({ contextData: { standardViolations: [], budgetCheck: null, budgetCheckRecorded: true } });
|
|
|
|
const response = await app.inject({ method: "GET", url: `/api/workflow/tasks/${TASK_ID}` });
|
|
|
|
expect(response.json().data.workflowInstance.contextData).toEqual({
|
|
standardViolations: [],
|
|
budgetCheck: null,
|
|
budgetCheckStatus: "not_applicable",
|
|
parseStatus: "ready",
|
|
});
|
|
|
|
await app.close();
|
|
});
|
|
|
|
it("fails the snapshot projection closed when stored fields are malformed", async () => {
|
|
const { app } = await buildApp({
|
|
contextData: {
|
|
standardViolations: { itemId: "not-an-array" },
|
|
budgetCheck: { budgetCode: "BUDGET-2026", usagePercent: "unknown" },
|
|
secretField: "must-not-be-projected",
|
|
},
|
|
});
|
|
|
|
const response = await app.inject({ method: "GET", url: `/api/workflow/tasks/${TASK_ID}` });
|
|
const contextData = response.json().data.workflowInstance.contextData;
|
|
|
|
expect(contextData).toEqual({
|
|
standardViolations: [],
|
|
budgetCheck: null,
|
|
budgetCheckStatus: "missing",
|
|
parseStatus: "invalid",
|
|
});
|
|
expect(contextData).not.toHaveProperty("secretField");
|
|
|
|
await app.close();
|
|
});
|
|
});
|