修复非参与人报销只读边界(08-28-reimbursement-nonparticipant-readonly-projection)

这个提交包含在:
2026-08-29 08:22:23 +08:00
父节点 174cb7acde
当前提交 e8d676634f
共修改 24 个文件,包含 891 行新增和 62 行删除
@@ -44,7 +44,13 @@ function buildMultipartRequest({ files, fields = {} }) {
};
}
async function buildApp({ reimbursementStatus = "draft", materialPreviewStore = null } = {}) {
async function buildApp({
reimbursementStatus = "draft",
materialPreviewStore = null,
employeeId = ACTOR_ID,
roleCodes = ["employee"],
permissionCodes = ["expense.edit_own", "expense.view_own"],
} = {}) {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "hyb-oa-r111-reimbursement-attachments-"));
const reimbursementId = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa";
const createdAttachments = [];
@@ -160,9 +166,9 @@ async function buildApp({ reimbursementStatus = "draft", materialPreviewStore =
throw new AppError({ code: "AUTH_001", message: "未登录", statusCode: 401 });
}
request.employee = {
id: ACTOR_ID,
roleCodes: ["employee"],
permissionCodes: ["expense.edit_own", "expense.view_own"],
id: employeeId,
roleCodes,
permissionCodes,
};
});
app.decorate("hasPermission", (employee, permissionCode) => {
@@ -202,6 +208,38 @@ async function buildApp({ reimbursementStatus = "draft", materialPreviewStore =
}
describe("reimbursement attachment routes", () => {
it("rejects an admin view-all nonparticipant before attachment side effects", async () => {
const { app, prisma, tempDir, reimbursementId, createdAttachments } = await buildApp({
employeeId: "finance-viewer",
roleCodes: ["admin"],
permissionCodes: ["expense.edit_own", "expense.view_all"],
});
try {
const requestPayload = buildMultipartRequest({
files: [{
filename: "synthetic.pdf",
contentType: "application/pdf",
content: "%PDF synthetic",
}],
});
const response = await app.inject({
method: "POST",
url: `/api/reimbursements/${reimbursementId}/attachments`,
headers: { authorization: "Bearer test", ...requestPayload.headers },
payload: requestPayload.payload,
});
expect(response.statusCode).toBe(403);
expect(createdAttachments).toHaveLength(0);
expect(prisma.reimbursementAttachment.create).not.toHaveBeenCalled();
expect(prisma.auditLog.create).not.toHaveBeenCalled();
} finally {
await app.close();
await fs.rm(tempDir, { recursive: true, force: true });
}
});
it("uploads five attachment materials with the production global one-file baseline", async () => {
const { app, prisma, tempDir, reimbursementId, createdAttachments } = await buildApp();