修复非参与人报销只读边界(08-28-reimbursement-nonparticipant-readonly-projection)
这个提交包含在:
@@ -53,7 +53,7 @@ async function buildApp(options = {}) {
|
||||
id: EMPLOYEE_ID,
|
||||
department_id: DEPARTMENT_ID,
|
||||
roleCodes: [],
|
||||
permissionCodes: ["expense.submit"],
|
||||
permissionCodes: ["expense.submit", "expense.view_own"],
|
||||
};
|
||||
const reimbursement = options.reimbursement ?? {
|
||||
id: REIMBURSEMENT_ID,
|
||||
@@ -176,6 +176,8 @@ async function buildApp(options = {}) {
|
||||
reimbursementForm: {
|
||||
findUnique: vi.fn(async () => ({
|
||||
id: reimbursement.id,
|
||||
owner_id: reimbursement.owner_id,
|
||||
submitter_id: reimbursement.submitter_id,
|
||||
form_number: reimbursement.form_number,
|
||||
total_amount: reimbursement.total_amount,
|
||||
owner: {
|
||||
@@ -239,6 +241,29 @@ describe("reimbursements routes expense standard integration", () => {
|
||||
resolveRecipientsForAssigneeMock.mockResolvedValue([]);
|
||||
});
|
||||
|
||||
it("rejects an admin view-all nonparticipant before creating submission side effects", async () => {
|
||||
const { app, prisma, tx } = await buildApp({
|
||||
actor: {
|
||||
id: "finance-viewer",
|
||||
department_id: DEPARTMENT_ID,
|
||||
roleCodes: ["admin"],
|
||||
permissionCodes: ["expense.submit", "expense.view_all"],
|
||||
},
|
||||
});
|
||||
const response = await app.inject({
|
||||
method: "POST",
|
||||
url: `/api/reimbursements/${REIMBURSEMENT_ID}/submit`,
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(403);
|
||||
expect(tx.workflowInstance.create).not.toHaveBeenCalled();
|
||||
expect(tx.workflowTask.create).not.toHaveBeenCalled();
|
||||
expect(tx.reimbursementForm.update).not.toHaveBeenCalled();
|
||||
expect(tx.auditLog.create).not.toHaveBeenCalled();
|
||||
expect(prisma.reimbursementForm.findUnique).not.toHaveBeenCalled();
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it("blocks travel reimbursement submit until the structured summary is complete", async () => {
|
||||
const { app, tx } = await buildApp({ travelItem: true });
|
||||
const response = await app.inject({ method: "POST", url: `/api/reimbursements/${REIMBURSEMENT_ID}/submit` });
|
||||
@@ -347,6 +372,47 @@ describe("reimbursements routes expense standard integration", () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it("rejects an ordinary nonparticipant before resolving reimbursement precheck", async () => {
|
||||
const { app } = await buildApp({
|
||||
actor: {
|
||||
id: "ordinary-nonparticipant",
|
||||
department_id: DEPARTMENT_ID,
|
||||
roleCodes: ["employee"],
|
||||
permissionCodes: ["expense.submit"],
|
||||
},
|
||||
});
|
||||
|
||||
const response = await app.inject({
|
||||
method: "POST",
|
||||
url: `/api/reimbursements/${REIMBURSEMENT_ID}/precheck`,
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(403);
|
||||
expect(resolveEntityPrecheckMock).not.toHaveBeenCalled();
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it("allows a view-all nonparticipant to read reimbursement precheck", async () => {
|
||||
resolveEntityPrecheckMock.mockResolvedValue(buildPrecheck());
|
||||
const { app } = await buildApp({
|
||||
actor: {
|
||||
id: "finance-viewer",
|
||||
department_id: DEPARTMENT_ID,
|
||||
roleCodes: ["finance"],
|
||||
permissionCodes: ["expense.submit", "expense.view_all"],
|
||||
},
|
||||
});
|
||||
|
||||
const response = await app.inject({
|
||||
method: "POST",
|
||||
url: `/api/reimbursements/${REIMBURSEMENT_ID}/precheck`,
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(resolveEntityPrecheckMock).toHaveBeenCalledTimes(1);
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it("blocks submit with 400 and standardViolations when block standard is exceeded", async () => {
|
||||
resolveEntityPrecheckMock.mockResolvedValue(
|
||||
buildPrecheck({
|
||||
|
||||
在新工单中引用
屏蔽一个用户