const fs = require("node:fs/promises"); const os = require("node:os"); const path = require("node:path"); const Fastify = require("fastify"); const multipart = require("@fastify/multipart"); const { AppError } = require("../../../src/utils/AppError"); const invoicePoolRoutes = require("../../../src/routes/invoicePool"); function buildMultipartRequest({ filename, contentType, content, fields = [] }) { const boundary = "----codex-invoice-upload-boundary"; const segments = [ Buffer.from( `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${filename}"\r\nContent-Type: ${contentType}\r\n\r\n`, ), Buffer.isBuffer(content) ? content : Buffer.from(content, "utf8"), Buffer.from("\r\n"), ]; for (const [fieldName, value] of fields) { segments.push( Buffer.from( `--${boundary}\r\nContent-Disposition: form-data; name="${fieldName}"\r\n\r\n${value}\r\n`, ), ); } segments.push(Buffer.from(`--${boundary}--\r\n`)); return { payload: Buffer.concat(segments), headers: { "content-type": `multipart/form-data; boundary=${boundary}`, }, }; } async function buildApp() { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "hyb-oa-r71-invoice-upload-")); const invoicePoolCreate = vi.fn(async ({ data }) => ({ id: "11111111-1111-4111-8111-111111111111", status: data.status, file_sha256: data.file_sha256, })); const auditCreate = vi.fn().mockResolvedValue({ id: "audit-1" }); const prisma = { invoicePool: { create: invoicePoolCreate, }, auditLog: { create: auditCreate, }, $transaction: vi.fn().mockImplementation(async (callback) => callback(prisma)), }; const app = Fastify(); app.decorate("prisma", prisma); app.decorate("config", { appTimezone: "Asia/Shanghai", storageDir: tempDir, maxUploadSizeBytes: 5 * 1024 * 1024, invoiceArchiveUnusedDays: 30, ocrProvider: "off", }); app.decorate("authenticate", async (request) => { if (!request.headers.authorization) { throw new AppError({ code: "AUTH_001", message: "未登录", statusCode: 401 }); } request.employee = { id: "actor-1", roleCodes: ["employee"], permissionCodes: ["invoice.upload"], }; }); app.decorate("hasPermission", (actor, permissionCode) => { return (actor?.permissionCodes ?? []).includes("*") || (actor?.permissionCodes ?? []).includes(permissionCode); }); 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: 5 * 1024 * 1024 }, }); await app.register(invoicePoolRoutes, { prefix: "/api" }); return { app, prisma, tempDir }; } describe("invoicePool upload route", () => { it("consumes the file stream before reading trailing multipart fields", async () => { const { app, prisma, tempDir } = await buildApp(); try { const response = await app.inject({ method: "POST", url: "/api/invoice-pool/upload", headers: { authorization: "Bearer test", ...buildMultipartRequest({ filename: "invoice.pdf", contentType: "application/pdf", content: "%PDF-1.4 invoice payload", fields: [["ocr_type", "vat"]], }).headers, }, payload: buildMultipartRequest({ filename: "invoice.pdf", contentType: "application/pdf", content: "%PDF-1.4 invoice payload", fields: [["ocr_type", "vat"]], }).payload, }); expect(response.statusCode).toBe(200); expect(response.json().data).toEqual( expect.objectContaining({ invoicePoolId: "11111111-1111-4111-8111-111111111111", status: "available", }), ); expect(prisma.invoicePool.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ owner_employee_id: "actor-1", original_name: "invoice.pdf", mime_type: "application/pdf", ocr_type: "vat", status: "available", }), }), ); const storedPath = prisma.invoicePool.create.mock.calls[0][0].data.storage_path; const storedBuffer = await fs.readFile(path.join(tempDir, storedPath)); expect(storedBuffer.toString("utf8")).toBe("%PDF-1.4 invoice payload"); expect(prisma.auditLog.create).toHaveBeenCalledTimes(1); } finally { await app.close(); await fs.rm(tempDir, { recursive: true, force: true }); } }); });