const crypto = require("node:crypto"); const { Readable } = require("node:stream"); const Fastify = require("fastify"); const multipart = require("@fastify/multipart"); const purchaseRequestRoutes = require("../../../src/routes/purchaseRequests"); const { AppError } = require("../../../src/utils/AppError"); const PURCHASE_REQUEST_ID = "11111111-1111-4111-8111-111111111111"; const ATTACHMENT_ID = "22222222-2222-4222-8222-222222222222"; const EMPLOYEE_ID = "33333333-3333-4333-8333-333333333333"; const OTHER_EMPLOYEE_ID = "44444444-4444-4444-8444-444444444444"; const NOW = new Date("2026-07-17T08:00:00.000Z"); const VALID_PDF = Buffer.from( "%PDF-1.4\n1 0 obj\n<<>>\nendobj\ntrailer\n<<>>\nstartxref\n0\n%%EOF\n", "ascii", ); class TestStorageNotFoundError extends Error { constructor() { super("not_found"); this.name = "StorageNotFoundError"; } } function buildMultipartRequest(parts) { const boundary = "----codex-purchase-attachment-boundary"; const segments = []; for (const part of parts) { if (part.type === "field") { segments.push( Buffer.from( `--${boundary}\r\nContent-Disposition: form-data; name="${part.name}"\r\n\r\n${part.value}\r\n`, ), ); continue; } segments.push( Buffer.from( `--${boundary}\r\nContent-Disposition: form-data; name="${part.fieldname ?? "file"}"; filename="${part.filename}"\r\nContent-Type: ${part.contentType}\r\n\r\n`, ), Buffer.isBuffer(part.content) ? part.content : Buffer.from(part.content), Buffer.from("\r\n"), ); } segments.push(Buffer.from(`--${boundary}--\r\n`)); return { payload: Buffer.concat(segments), headers: { authorization: "Bearer unit-test", "content-type": `multipart/form-data; boundary=${boundary}`, }, }; } function buildPurchaseRequest(overrides = {}) { return { id: PURCHASE_REQUEST_ID, applicant_id: EMPLOYEE_ID, submitter_id: null, status: "draft", ...overrides, }; } function buildAttachment(overrides = {}) { return { id: ATTACHMENT_ID, purchase_request_id: PURCHASE_REQUEST_ID, storage_path: `purchase-requests/${PURCHASE_REQUEST_ID}/${ATTACHMENT_ID}.pdf`, original_name: "采购报价.pdf", mime_type: "application/pdf", file_size: VALID_PDF.length, governance_status: "approved", sensitivity_level: "internal", attachment_type: "quote", download_policy: "standard", ...overrides, }; } async function buildApp({ actor = null, purchaseRequest = undefined, attachment = undefined, maxUploadSizeBytes = 1024 * 1024, storageOverrides = {}, auditCreate = null, multipartLimits = {}, } = {}) { const effectiveActor = actor ?? { id: EMPLOYEE_ID, roleCodes: ["employee"], permissionCodes: ["procurement.create", "procurement.view_own"], }; const state = { purchaseRequest: purchaseRequest === undefined ? buildPurchaseRequest() : purchaseRequest, attachment: attachment === undefined ? buildAttachment() : attachment, createdAttachmentData: null, }; const uploadAuditCreate = vi.fn().mockResolvedValue({ id: "upload-audit" }); const downloadAuditCreate = auditCreate ?? vi.fn().mockResolvedValue({ id: "download-audit" }); const attachmentCreate = vi.fn(async ({ data }) => { state.createdAttachmentData = data; return { id: ATTACHMENT_ID, original_name: data.original_name, mime_type: data.mime_type, file_size: data.file_size, file_sha256: data.file_sha256, governance_status: data.governance_status, sensitivity_level: data.sensitivity_level, attachment_type: data.attachment_type, reviewed_by: null, reviewed_at: null, review_note: null, archived_by: null, archived_at: null, archive_note: null, download_policy: data.download_policy, uploaded_by: data.uploaded_by, created_at: NOW, uploader: { id: effectiveActor.id, employee_no: "E001", name: "测试员工", }, }; }); const attachmentDelete = vi.fn(async ({ where }) => { if (state.attachment?.id === where.id) { state.attachment = null; } return { id: where.id }; }); const tx = { purchaseAttachment: { create: attachmentCreate, delete: attachmentDelete }, auditLog: { create: uploadAuditCreate }, }; const prisma = { purchaseRequest: { findUnique: vi.fn(async () => state.purchaseRequest), }, purchaseAttachment: { findFirst: vi.fn(async ({ where }) => { if (!state.attachment) return null; return where.id === state.attachment.id && where.purchase_request_id === state.attachment.purchase_request_id ? state.attachment : null; }), }, auditLog: { create: downloadAuditCreate }, $transaction: vi.fn(async (callback) => callback(tx)), }; const storage = { StorageNotFoundError: TestStorageNotFoundError, writeBuffer: vi.fn(async ({ key }) => key), openReadStream: vi.fn(async () => ({ stream: Readable.from([VALID_PDF]) })), ...storageOverrides, }; const app = Fastify(); app.decorate("prisma", prisma); app.decorate("config", { appTimezone: "Asia/Shanghai", maxUploadSizeBytes, }); app.decorate("hasPermission", (employee, permissionCode) => { const permissionCodes = employee?.permissionCodes ?? []; return permissionCodes.includes("*") || permissionCodes.includes(permissionCode); }); app.decorate("authenticate", async (request) => { if (typeof request.headers.authorization !== "string") { throw new AppError({ code: "AUTH_001", message: "未登录", statusCode: 401 }); } request.employee = effectiveActor; }); app.decorate("requirePermission", (permissionCode) => async (request) => { await app.authenticate(request); if (!app.hasPermission(request.employee, permissionCode)) { throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403 }); } }); app.setErrorHandler((error, request, reply) => { if (error instanceof AppError) { return reply.status(error.statusCode).send({ requestId: request.id, error: { code: error.code, message: error.message, details: error.details ?? {}, }, }); } return reply.status(500).send({ requestId: request.id, error: { code: "SYS_001", message: "服务器内部错误", details: {} }, }); }); await app.register(multipart, { attachFieldsToBody: false, limits: { files: 1, fileSize: maxUploadSizeBytes, ...multipartLimits }, }); await app.register(purchaseRequestRoutes, { prefix: "/api", attachmentStorage: storage, }); return { app, attachmentCreate, attachmentDelete, downloadAuditCreate, prisma, state, storage, uploadAuditCreate, }; } describe("purchase attachment upload", () => { it("consumes the file immediately, accepts trailing governance fields, and persists canonical bytes", async () => { const { app, attachmentCreate, state, storage } = await buildApp(); try { const response = await app.inject({ method: "POST", url: `/api/purchase-requests/${PURCHASE_REQUEST_ID}/attachments`, ...buildMultipartRequest([ { type: "file", filename: "采购报价.v2.pdf", contentType: "application/octet-stream", content: VALID_PDF, }, { type: "field", name: "sensitivityLevel", value: "confidential" }, ]), }); expect(response.statusCode).toBe(200); expect(storage.writeBuffer).toHaveBeenCalledTimes(1); const writeInput = storage.writeBuffer.mock.calls[0][0]; expect(writeInput.key).toMatch( new RegExp(`^purchase-requests/${PURCHASE_REQUEST_ID}/[0-9a-f-]{36}\\.pdf$`), ); expect(writeInput.key).not.toContain("采购报价"); expect(writeInput.contentType).toBe("application/pdf"); expect(writeInput.buffer.equals(VALID_PDF)).toBe(true); expect(state.createdAttachmentData).toEqual( expect.objectContaining({ original_name: "采购报价.v2.pdf", mime_type: "application/pdf", file_size: VALID_PDF.length, sensitivity_level: "confidential", }), ); expect(state.createdAttachmentData.file_sha256).toBe( crypto.createHash("sha256").update(writeInput.buffer).digest("hex"), ); expect(attachmentCreate).toHaveBeenCalledTimes(1); } finally { await app.close(); } }); it("accepts governance fields before the file without hanging", async () => { const { app, storage } = await buildApp(); try { const response = await app.inject({ method: "POST", url: `/api/purchase-requests/${PURCHASE_REQUEST_ID}/attachments`, ...buildMultipartRequest([ { type: "field", name: "attachmentType", value: "acceptance" }, { type: "file", filename: "验收.pdf", contentType: "application/pdf", content: VALID_PDF, }, ]), }); expect(response.statusCode).toBe(200); expect(storage.writeBuffer).toHaveBeenCalledTimes(1); } finally { await app.close(); } }); it.each([ ["non multipart", null, { file: "multipart_required" }], [ "wrong file field", buildMultipartRequest([ { type: "file", fieldname: "payload", filename: "报价.pdf", contentType: "application/pdf", content: VALID_PDF, }, ]), { file: "invalid_field" }, ], [ "path-shaped filename", buildMultipartRequest([ { type: "file", filename: "../报价.pdf", contentType: "application/pdf", content: VALID_PDF, }, ]), { file: "dangerous_filename" }, ], [ "active content", buildMultipartRequest([ { type: "file", filename: "报价.pdf", contentType: "application/pdf", content: "", }, ]), null, ], [ "empty content", buildMultipartRequest([ { type: "file", filename: "空附件.pdf", contentType: "application/pdf", content: Buffer.alloc(0), }, ]), { file: "empty_file" }, ], ])("rejects %s before storage or database writes", async (_label, multipartRequest, expectedDetails) => { const { app, attachmentCreate, storage, uploadAuditCreate } = await buildApp(); try { const response = await app.inject({ method: "POST", url: `/api/purchase-requests/${PURCHASE_REQUEST_ID}/attachments`, headers: multipartRequest?.headers ?? { authorization: "Bearer unit-test" }, payload: multipartRequest?.payload, }); expect(response.statusCode).toBe(400); expect(response.json().error.code).toBe("VALID_001"); if (expectedDetails) { expect(response.json().error.details).toEqual({ ...expectedDetails, reasonCode: ({ "non multipart": "purchase_attachment.multipart_required", "wrong file field": "purchase_attachment.field_invalid", "path-shaped filename": "purchase_attachment.dangerous_filename", "empty content": "purchase_attachment.empty_file" })[_label] }); } expect(storage.writeBuffer).not.toHaveBeenCalled(); expect(attachmentCreate).not.toHaveBeenCalled(); expect(uploadAuditCreate).not.toHaveBeenCalled(); } finally { await app.close(); } }); it("maps the production single-file plugin limit to a stable validation error", async () => { const { app, attachmentCreate, storage } = await buildApp(); const request = buildMultipartRequest([ { type: "file", filename: "报价一.pdf", contentType: "application/pdf", content: VALID_PDF, }, { type: "file", filename: "报价二.pdf", contentType: "application/pdf", content: VALID_PDF, }, ]); try { const response = await app.inject({ method: "POST", url: `/api/purchase-requests/${PURCHASE_REQUEST_ID}/attachments`, ...request, }); expect(response.statusCode).toBe(400); expect(response.json().error.details).toEqual({ file: "only_single_file_supported", reasonCode: "purchase_attachment.single_file_required" }); expect(storage.writeBuffer).not.toHaveBeenCalled(); expect(attachmentCreate).not.toHaveBeenCalled(); } finally { await app.close(); } }); it("maps the multipart byte limit to a stable validation error", async () => { const { app, attachmentCreate, storage } = await buildApp({ maxUploadSizeBytes: 32 }); try { const response = await app.inject({ method: "POST", url: `/api/purchase-requests/${PURCHASE_REQUEST_ID}/attachments`, ...buildMultipartRequest([ { type: "file", filename: "超限.pdf", contentType: "application/pdf", content: VALID_PDF, }, ]), }); expect(response.statusCode).toBe(400); expect(response.json().error.details).toEqual({ file: "file_too_large", reasonCode: "purchase_attachment.too_large" }); expect(storage.writeBuffer).not.toHaveBeenCalled(); expect(attachmentCreate).not.toHaveBeenCalled(); } finally { await app.close(); } }); it.each([ ["missing permission", { permissionCodes: ["procurement.view_own"] }, buildPurchaseRequest(), 403], [ "another applicant", { permissionCodes: ["procurement.create", "procurement.view_own"] }, buildPurchaseRequest({ applicant_id: OTHER_EMPLOYEE_ID }), 403, ], [ "non-draft request", { permissionCodes: ["procurement.create", "procurement.view_own"] }, buildPurchaseRequest({ status: "reviewing" }), 409, ], ])("does not write upload data for %s", async (_label, actorInput, purchaseRequest, statusCode) => { const { app, attachmentCreate, storage } = await buildApp({ actor: { id: EMPLOYEE_ID, roleCodes: ["employee"], ...actorInput, }, purchaseRequest, }); try { const response = await app.inject({ method: "POST", url: `/api/purchase-requests/${PURCHASE_REQUEST_ID}/attachments`, ...buildMultipartRequest([ { type: "file", filename: "报价.pdf", contentType: "application/pdf", content: VALID_PDF, }, ]), }); expect(response.statusCode).toBe(statusCode); expect(storage.writeBuffer).not.toHaveBeenCalled(); expect(attachmentCreate).not.toHaveBeenCalled(); } finally { await app.close(); } }); }); describe("purchase attachment delete", () => { it("requires a reason and records it with the attachment audit", async () => { const { app, attachmentDelete, state, uploadAuditCreate } = await buildApp(); try { const response = await app.inject({ method: "DELETE", url: `/api/purchase-requests/${PURCHASE_REQUEST_ID}/attachments/${ATTACHMENT_ID}`, headers: { authorization: "Bearer unit-test", "content-type": "application/json" }, payload: { reason: "重复上传" }, }); expect(response.statusCode).toBe(200); expect(response.json().data).toEqual({ outcome: "deleted", attachmentId: ATTACHMENT_ID }); expect(attachmentDelete).toHaveBeenCalledWith({ where: { id: ATTACHMENT_ID } }); expect(uploadAuditCreate).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ action: "PURCHASE_ATTACHMENT_DELETED", target_id: ATTACHMENT_ID, extra: expect.objectContaining({ purchaseRequestId: PURCHASE_REQUEST_ID, reason: "重复上传" }), }), }), ); expect(state.attachment).toBeNull(); } finally { await app.close(); } }); it("rejects a missing reason before attachment lookup or deletion", async () => { const { app, attachmentDelete, prisma, uploadAuditCreate } = await buildApp(); try { const response = await app.inject({ method: "DELETE", url: `/api/purchase-requests/${PURCHASE_REQUEST_ID}/attachments/${ATTACHMENT_ID}`, headers: { authorization: "Bearer unit-test", "content-type": "application/json" }, payload: {}, }); expect(response.statusCode).toBe(400); expect(response.json().error.code).toBe("VALID_001"); expect(prisma.purchaseRequest.findUnique).not.toHaveBeenCalled(); expect(attachmentDelete).not.toHaveBeenCalled(); expect(uploadAuditCreate).not.toHaveBeenCalled(); } finally { await app.close(); } }); }); describe("purchase attachment authenticated download", () => { const downloadUrl = `/api/purchase-requests/${PURCHASE_REQUEST_ID}/attachments/${ATTACHMENT_ID}/download`; it("requires current authentication before reading storage", async () => { const { app, storage } = await buildApp(); try { const response = await app.inject({ method: "POST", url: downloadUrl, payload: {} }); expect(response.statusCode).toBe(401); expect(storage.openReadStream).not.toHaveBeenCalled(); } finally { await app.close(); } }); it.each([ [ "missing purchase read permission", { actor: { id: EMPLOYEE_ID, roleCodes: ["employee"], permissionCodes: ["procurement.create"], }, }, 403, ], [ "another employee's purchase request", { purchaseRequest: buildPurchaseRequest({ applicant_id: OTHER_EMPLOYEE_ID, submitter_id: OTHER_EMPLOYEE_ID, }), }, 403, ], ["an attachment outside the purchase request", { attachment: null }, 404], ])("rejects %s before opening storage", async (_label, options, statusCode) => { const { app, storage } = await buildApp(options); try { const response = await app.inject({ method: "POST", url: downloadUrl, headers: { authorization: "Bearer unit-test" }, payload: {}, }); expect(response.statusCode).toBe(statusCode); expect(storage.openReadStream).not.toHaveBeenCalled(); } finally { await app.close(); } }); it("requires a JSON-body reason for sensitive attachments", async () => { const { app, storage } = await buildApp({ attachment: buildAttachment({ sensitivity_level: "confidential", download_policy: "reason_required", }), }); try { const response = await app.inject({ method: "POST", url: downloadUrl, headers: { authorization: "Bearer unit-test" }, payload: {}, }); expect(response.statusCode).toBe(400); expect(response.json().error.details).toEqual({ reason: "required", reasonCode: "purchase_attachment.download_reason_required" }); expect(storage.openReadStream).not.toHaveBeenCalled(); } finally { await app.close(); } }); it("truncates an overlong download reason before writing audit metadata", async () => { const { app, downloadAuditCreate } = await buildApp({ attachment: buildAttachment({ sensitivity_level: "confidential", download_policy: "reason_required", }), }); const reason = "复".repeat(201); try { const response = await app.inject({ method: "POST", url: downloadUrl, headers: { authorization: "Bearer unit-test", "content-type": "application/json", }, payload: JSON.stringify({ reason }), }); expect(response.statusCode).toBe(200); expect(downloadAuditCreate).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ action: "PURCHASE_ATTACHMENT_DOWNLOADED", extra: expect.objectContaining({ reason: "复".repeat(200) }), }), }), ); } finally { await app.close(); } }); it("blocks quarantined attachments before storage and records a fixed reason", async () => { const { app, downloadAuditCreate, storage } = await buildApp({ attachment: buildAttachment({ governance_status: "quarantined" }), }); try { const response = await app.inject({ method: "POST", url: downloadUrl, headers: { authorization: "Bearer unit-test" }, payload: {}, }); expect(response.statusCode).toBe(409); expect(response.json().error).toMatchObject({ message: "附件已隔离,无法下载", details: { reason: "quarantined", attachment: "security_validation_failed", reasonCode: "purchase_attachment.download_blocked" } }); expect(storage.openReadStream).not.toHaveBeenCalled(); expect(downloadAuditCreate).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ action: "PURCHASE_ATTACHMENT_DOWNLOAD_BLOCKED", extra: expect.objectContaining({ reasonCode: "quarantined" }), }), }), ); } finally { await app.close(); } }); it("blocks a recorded object above the hard limit before opening storage", async () => { const { app, storage } = await buildApp({ maxUploadSizeBytes: 64, attachment: buildAttachment({ file_size: 65 }), }); try { const response = await app.inject({ method: "POST", url: downloadUrl, headers: { authorization: "Bearer unit-test" }, payload: {}, }); expect(response.statusCode).toBe(409); expect(response.json().error).toMatchObject({ message: "附件未通过安全校验", details: { reason: "size_limit", attachment: "security_validation_failed", reasonCode: "purchase_attachment.download_blocked" } }); expect(storage.openReadStream).not.toHaveBeenCalled(); } finally { await app.close(); } }); it("returns a safe not-found response and blocked audit when the stored object is missing", async () => { const { app, downloadAuditCreate } = await buildApp({ storageOverrides: { openReadStream: vi.fn(async () => { throw new TestStorageNotFoundError(); }), }, }); try { const response = await app.inject({ method: "POST", url: downloadUrl, headers: { authorization: "Bearer unit-test" }, payload: {}, }); expect(response.statusCode).toBe(404); expect(response.json().error.message).toBe("附件不存在"); expect(downloadAuditCreate).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ action: "PURCHASE_ATTACHMENT_DOWNLOAD_BLOCKED", extra: expect.objectContaining({ reasonCode: "missing_object" }), }), }), ); } finally { await app.close(); } }); it("rejects an actual object larger than the configured limit and destroys its stream", async () => { const oversizedStream = { destroyed: false, destroy: vi.fn(function destroy() { this.destroyed = true; }), async *[Symbol.asyncIterator]() { yield Buffer.alloc(65, 0x41); }, }; const { app, storage } = await buildApp({ maxUploadSizeBytes: 64, attachment: buildAttachment({ file_size: 1 }), storageOverrides: { openReadStream: vi.fn(async () => ({ stream: oversizedStream })), }, }); try { const response = await app.inject({ method: "POST", url: downloadUrl, headers: { authorization: "Bearer unit-test" }, payload: {}, }); expect(response.statusCode).toBe(409); expect(response.json().error).toMatchObject({ message: "附件未通过安全校验", details: { reason: "size_limit", attachment: "security_validation_failed", reasonCode: "purchase_attachment.download_blocked" } }); expect(oversizedStream.destroy).toHaveBeenCalledTimes(1); expect(storage.openReadStream).toHaveBeenCalledTimes(1); } finally { await app.close(); } }); it("blocks historical active content after byte validation and sends no attachment headers", async () => { const { app, downloadAuditCreate } = await buildApp({ storageOverrides: { openReadStream: vi.fn(async () => ({ stream: Readable.from([Buffer.from("")]), })), }, }); try { const response = await app.inject({ method: "POST", url: downloadUrl, headers: { authorization: "Bearer unit-test" }, payload: {}, }); expect(response.statusCode).toBe(409); expect(response.json().error).toMatchObject({ message: "附件未通过安全校验", details: { attachment: "security_validation_failed", reasonCode: "purchase_attachment.download_blocked" } }); expect(response.headers["content-disposition"]).toBeUndefined(); expect(downloadAuditCreate).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ action: "PURCHASE_ATTACHMENT_DOWNLOAD_BLOCKED", }), }), ); } finally { await app.close(); } }); it("returns a validated historical attachment with the complete forced-download header contract", async () => { const { app, downloadAuditCreate } = await buildApp({ attachment: buildAttachment({ mime_type: "application/octet-stream", sensitivity_level: "confidential", download_policy: "reason_required", }), }); try { const response = await app.inject({ method: "POST", url: downloadUrl, headers: { authorization: "Bearer unit-test", "content-type": "application/json", }, payload: JSON.stringify({ reason: "审批复核" }), }); expect(response.statusCode).toBe(200); expect(response.rawPayload.equals(VALID_PDF)).toBe(true); expect(response.headers["content-type"]).toContain("application/octet-stream"); expect(response.headers["content-disposition"]).toContain("attachment;"); expect(response.headers["content-disposition"]).toContain("filename*="); expect(response.headers["content-disposition"]).not.toContain("inline"); expect(response.headers["x-content-type-options"]).toBe("nosniff"); expect(response.headers["cache-control"]).toBe("private, no-store, max-age=0"); expect(response.headers.pragma).toBe("no-cache"); expect(response.headers["referrer-policy"]).toBe("no-referrer"); expect(response.headers["content-security-policy"]).toContain("sandbox"); expect(response.headers["cross-origin-resource-policy"]).toBe("same-origin"); expect(downloadAuditCreate).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ action: "PURCHASE_ATTACHMENT_DOWNLOADED", extra: expect.objectContaining({ reason: "审批复核" }), }), }), ); } finally { await app.close(); } }); it("fails closed before setting attachment headers when success auditing fails", async () => { const auditCreate = vi.fn().mockRejectedValue(new Error("database unavailable")); const { app } = await buildApp({ auditCreate }); try { const response = await app.inject({ method: "POST", url: downloadUrl, headers: { authorization: "Bearer unit-test" }, payload: {}, }); expect(response.statusCode).toBe(500); expect(response.headers["content-disposition"]).toBeUndefined(); expect(response.json().error.details).toEqual({ attachment: "audit_failed" }); } finally { await app.close(); } }); it("retires both legacy signed and query-token routes", async () => { const { app, storage } = await buildApp(); try { const signedResponse = await app.inject({ method: "POST", url: `/api/purchase-requests/${PURCHASE_REQUEST_ID}/attachments/${ATTACHMENT_ID}/signed-url`, headers: { authorization: "Bearer unit-test" }, payload: {}, }); const fileResponse = await app.inject({ method: "GET", url: `/api/purchase-requests/${PURCHASE_REQUEST_ID}/attachments/${ATTACHMENT_ID}/file?token=legacy`, }); expect(signedResponse.statusCode).toBe(404); expect(fileResponse.statusCode).toBe(404); expect(storage.openReadStream).not.toHaveBeenCalled(); } finally { await app.close(); } }); }); describe("purchase attachment additional reason boundaries", () => { it.each([ ["field-only upload", {}, [{ type: "field", name: "attachmentType", value: "quote" }], "file_required", "required"], ["route single-file guard", { files: 2 }, Array.from({ length: 2 }, () => ({ type: "file", filename: "合成.pdf", contentType: "application/pdf", content: VALID_PDF })), "single_file_required", "only_single_file_supported"], ["fields limit", { fields: 1 }, [{ type: "field", name: "attachmentType", value: "quote" }, { type: "field", name: "sensitivityLevel", value: "internal" }], "multipart_limit", "multipart_limit_exceeded"], ["parts limit", { parts: 1 }, [{ type: "field", name: "attachmentType", value: "quote" }, { type: "file", filename: "合成.pdf", contentType: "application/pdf", content: VALID_PDF }], "multipart_limit", "multipart_limit_exceeded"], ])("explains %s before storage or mutation", async (_, multipartLimits, parts, reason, file) => { const { app, storage, attachmentCreate, uploadAuditCreate } = await buildApp({ multipartLimits }); try { const r = await app.inject({ method: "POST", url: `/api/purchase-requests/${PURCHASE_REQUEST_ID}/attachments`, ...buildMultipartRequest(parts) }); expect(r.statusCode).toBe(400); expect(r.json().error.details).toEqual({ file, reasonCode: `purchase_attachment.${reason}` }); expect(storage.writeBuffer).not.toHaveBeenCalled(); expect(attachmentCreate).not.toHaveBeenCalled(); expect(uploadAuditCreate).not.toHaveBeenCalled(); } finally { await app.close(); } }); it.each([ ["DELETE", {}, "delete_input_invalid"], ["POST", { reason: 123 }, "download_input_invalid"], ])("explains %s attachment body before lookup", async (method, payload, reason) => { const { app, prisma, storage, attachmentDelete } = await buildApp(); try { const r = await app.inject({ method, url: `/api/purchase-requests/${PURCHASE_REQUEST_ID}/attachments/${ATTACHMENT_ID}${method === "POST" ? "/download" : ""}`, headers: { authorization: "Bearer unit-test" }, payload }); expect(r.statusCode).toBe(400); expect(r.json().error.details).toMatchObject({ reasonCode: `purchase_attachment.${reason}`, fieldErrors: expect.any(Object) }); expect(prisma.purchaseRequest.findUnique).not.toHaveBeenCalled(); expect(prisma.purchaseAttachment.findFirst).not.toHaveBeenCalled(); expect(storage.openReadStream).not.toHaveBeenCalled(); expect(attachmentDelete).not.toHaveBeenCalled(); } finally { await app.close(); } }); it("explains non-draft delete before attachment lookup", async () => { const { app, prisma, attachmentDelete, uploadAuditCreate } = await buildApp({ purchaseRequest: buildPurchaseRequest({ status: "approved" }) }); try { const r = await app.inject({ method: "DELETE", url: `/api/purchase-requests/${PURCHASE_REQUEST_ID}/attachments/${ATTACHMENT_ID}`, headers: { authorization: "Bearer unit-test" }, payload: { reason: "合成删除" } }); expect(r.statusCode).toBe(409); expect(r.json().error.details).toEqual({ status: "only_draft_editable", reasonCode: "purchase_attachment.not_editable" }); expect(prisma.purchaseAttachment.findFirst).not.toHaveBeenCalled(); expect(attachmentDelete).not.toHaveBeenCalled(); expect(uploadAuditCreate).not.toHaveBeenCalled(); } finally { await app.close(); } }); });