实现报销材料合并预览与差旅概要(07-27-yang-jun-reimbursement-readiness)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-08-01 09:25:44 +08:00
父节点 3d9ec4c5a7
当前提交 da35591bd5
共修改 29 个文件,包含 907 行新增和 527 行删除
@@ -697,9 +697,9 @@ describe("reimbursement attachment routes", () => {
}
});
it("issues an ordered image bundle and audits each token-bound page access", async () => {
it("issues one token-bound PDF bundle with exact size and audits access", async () => {
const bundleId = "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee";
const previewBuffer = Buffer.from("synthetic-png-page");
const previewBuffer = Buffer.from("%PDF synthetic merged preview");
const materialPreviewStore = {
createBundle: vi.fn().mockImplementation(async ({ actorId, reimbursementId, materials, expiresAt }) => ({
bundleId,
@@ -707,16 +707,11 @@ describe("reimbursement attachment routes", () => {
actorId,
reimbursementId,
expiresAt,
pages: [{
buffer: previewBuffer,
materialId: materials[0].id,
materialType: materials[0].materialType,
materialIndex: materials[0].materialIndex,
pageNumber: 1,
}],
pageCount: 1,
},
fileSize: previewBuffer.length,
})),
getPage: vi.fn(),
getBundle: vi.fn(),
};
const { app, prisma, tempDir, reimbursementId } = await buildApp({ materialPreviewStore });
@@ -738,62 +733,58 @@ describe("reimbursement attachment routes", () => {
payload: {},
});
expect(issued.statusCode).toBe(200);
expect(issued.json().data.pages).toEqual([
expect.objectContaining({ materialType: "attachment", materialIndex: 1, pageNumber: 1 }),
]);
const pageUrl = new URL(issued.json().data.pages[0].url);
const verified = verifyToken(pageUrl.searchParams.get("previewToken"), JWT_SECRET);
expect(issued.json().data).toEqual(expect.objectContaining({
mimeType: "application/pdf",
fileSize: previewBuffer.length,
pageCount: 1,
}));
const previewUrl = new URL(issued.json().data.signedUrl);
const verified = verifyToken(previewUrl.searchParams.get("previewToken"), JWT_SECRET);
expect(verified).toEqual(expect.objectContaining({
ok: true,
payload: expect.objectContaining({
purpose: "reimbursement_material_preview",
reimbursementId,
bundleId,
pageIndex: 0,
actorId: ACTOR_ID,
}),
}));
const issuedToken = pageUrl.searchParams.get("previewToken");
const issuedToken = previewUrl.searchParams.get("previewToken");
const tamperedToken = `${issuedToken.slice(0, -1)}${issuedToken.endsWith("a") ? "b" : "a"}`;
const tamperedResponse = await app.inject({
method: "GET",
url: `${pageUrl.pathname}?previewToken=${encodeURIComponent(tamperedToken)}`,
url: `${previewUrl.pathname}?previewToken=${encodeURIComponent(tamperedToken)}`,
});
expect(tamperedResponse.statusCode).toBe(403);
expect(materialPreviewStore.getPage).not.toHaveBeenCalled();
expect(materialPreviewStore.getBundle).not.toHaveBeenCalled();
const missingBundleResponse = await app.inject({
method: "GET",
url: `${pageUrl.pathname}${pageUrl.search}`,
url: `${previewUrl.pathname}${previewUrl.search}`,
});
expect(missingBundleResponse.statusCode).toBe(403);
materialPreviewStore.getPage.mockReturnValue({
materialPreviewStore.getBundle.mockReturnValue({
bundle: {
actorId: ACTOR_ID,
reimbursementId,
expiresAt: verified.payload.exp,
},
page: {
buffer: previewBuffer,
materialId: "bbbbbbbb-bbbb-4bbb-8bbb-000000000001",
materialType: "attachment",
materialIndex: 1,
pageNumber: 1,
},
artifact: { buffer: previewBuffer },
});
const pageResponse = await app.inject({ method: "GET", url: `${pageUrl.pathname}${pageUrl.search}` });
expect(pageResponse.statusCode).toBe(200);
expect(pageResponse.rawPayload).toEqual(previewBuffer);
expect(pageResponse.headers["content-type"]).toContain("image/png");
expect(pageResponse.headers["cache-control"]).toBe("private, no-store");
const previewResponse = await app.inject({ method: "GET", url: `${previewUrl.pathname}${previewUrl.search}` });
expect(previewResponse.statusCode).toBe(200);
expect(previewResponse.rawPayload).toEqual(previewBuffer);
expect(previewResponse.headers["content-type"]).toContain("application/pdf");
expect(Number(previewResponse.headers["content-length"])).toBe(previewBuffer.length);
expect(previewResponse.headers["cache-control"]).toBe("private, no-store");
expect(prisma.auditLog.create).toHaveBeenCalledWith(expect.objectContaining({
data: expect.objectContaining({ action: "REIMBURSEMENT_MATERIAL_PREVIEWED", actor_id: ACTOR_ID }),
}));
prisma.auditLog.create.mockRejectedValueOnce(new Error("synthetic audit outage"));
const auditFailureResponse = await app.inject({ method: "GET", url: `${pageUrl.pathname}${pageUrl.search}` });
const auditFailureResponse = await app.inject({ method: "GET", url: `${previewUrl.pathname}${previewUrl.search}` });
expect(auditFailureResponse.statusCode).toBe(500);
expect(auditFailureResponse.body).not.toContain(previewBuffer.toString());
} finally {