/** * Integration test for PO document upload + visibility (regression for the * "documents uploaded but not visible anywhere" report). * * Drives the real `uploadPoDocuments` server action against a real DB and asserts * that a `PODocument` row is created AND surfaced by the exact include the PO * detail page uses — i.e. the attachment is actually visible. Storage I/O * (`uploadBuffer`) is stubbed so the test doesn't depend on R2 / the filesystem; * the bug was the missing DB row, which is asserted here for real. */ import { vi, describe, it, expect, beforeAll, afterEach } from "vitest"; vi.mock("@/auth", () => ({ auth: vi.fn() })); vi.mock("next/cache", () => ({ revalidatePath: vi.fn() })); // Keep buildStorageKey real (it shapes the storage key the UI groups on); stub // only the actual storage write so the test is hermetic. vi.mock("@/lib/storage", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, uploadBuffer: vi.fn().mockResolvedValue(undefined) }; }); import { auth } from "@/auth"; import { db } from "@/lib/db"; import { uploadBuffer } from "@/lib/storage"; import { createPo } from "@/app/(portal)/po/new/actions"; import { uploadPoDocuments } from "@/app/actions/upload-po-documents"; import { makeSession, getSeedUser, getSeedVessel, getSeedAccount, makePoForm, deletePosByTitle } from "./helpers"; const PREFIX = "INTTEST_PODOC_"; let submitterId: string; let vesselId: string; let accountId: string; beforeAll(async () => { const [user, vessel, account] = await Promise.all([ getSeedUser("manager@pelagia.local"), getSeedVessel("MV Pelagia Star"), getSeedAccount("700201"), ]); submitterId = user.id; vesselId = vessel.id; accountId = account.id; }); afterEach(async () => { await deletePosByTitle(PREFIX); vi.clearAllMocks(); }); async function makePo(title: string): Promise { vi.mocked(auth as unknown as () => Promise).mockResolvedValue(makeSession(submitterId, "MANAGER")); const result = await createPo(makePoForm({ title, vesselId, accountId, intent: "draft" })); expect(result).not.toHaveProperty("error"); return (result as { id: string }).id; } function pdf(name: string, contents = "%PDF-1.4 hello"): File { return new File([contents], name, { type: "application/pdf" }); } describe("uploadPoDocuments", () => { it("creates a PODocument row and stores the file, so it is visible on the PO", async () => { const poId = await makePo(`${PREFIX}Visible`); vi.mocked(auth as unknown as () => Promise).mockResolvedValue(makeSession(submitterId, "MANAGER")); const file = pdf("invoice.pdf"); const err = await uploadPoDocuments(poId, [file]); expect(err).toBeNull(); // The file was actually handed to storage with its bytes + mime type. expect(uploadBuffer).toHaveBeenCalledTimes(1); const [key, buffer, mime] = vi.mocked(uploadBuffer).mock.calls[0]; expect(key).toMatch(/^po-document\//); expect(mime).toBe("application/pdf"); expect(Buffer.isBuffer(buffer)).toBe(true); // The row exists — this is what was missing in the broken flow. const docs = await db.pODocument.findMany({ where: { poId } }); expect(docs).toHaveLength(1); expect(docs[0]).toMatchObject({ fileName: "invoice.pdf", mimeType: "application/pdf", storageKey: key, }); expect(docs[0].fileSize).toBeGreaterThan(0); // And it is surfaced by the exact include the PO detail page renders from. const po = await db.purchaseOrder.findUnique({ where: { id: poId }, include: { documents: { orderBy: { uploadedAt: "desc" } } }, }); expect(po?.documents.map((d) => d.fileName)).toEqual(["invoice.pdf"]); }); it("tags receipt uploads with the receipt prefix (delivery group)", async () => { const poId = await makePo(`${PREFIX}Receipt`); vi.mocked(auth as unknown as () => Promise).mockResolvedValue(makeSession(submitterId, "MANAGER")); const err = await uploadPoDocuments(poId, [pdf("delivery-note.pdf")], "receipt"); expect(err).toBeNull(); const doc = await db.pODocument.findFirstOrThrow({ where: { poId } }); expect(doc.storageKey).toMatch(/^receipt\//); }); it("stores every file when several are uploaded at once", async () => { const poId = await makePo(`${PREFIX}Multi`); vi.mocked(auth as unknown as () => Promise).mockResolvedValue(makeSession(submitterId, "MANAGER")); const err = await uploadPoDocuments(poId, [pdf("a.pdf"), pdf("b.pdf"), pdf("c.pdf")]); expect(err).toBeNull(); expect(await db.pODocument.count({ where: { poId } })).toBe(3); }); it("skips empty files without creating rows", async () => { const poId = await makePo(`${PREFIX}Empty`); vi.mocked(auth as unknown as () => Promise).mockResolvedValue(makeSession(submitterId, "MANAGER")); const err = await uploadPoDocuments(poId, [new File([], "blank.pdf", { type: "application/pdf" })]); expect(err).toBeNull(); expect(uploadBuffer).not.toHaveBeenCalled(); expect(await db.pODocument.count({ where: { poId } })).toBe(0); }); it("rejects an unauthenticated caller and writes nothing", async () => { const poId = await makePo(`${PREFIX}NoAuth`); vi.mocked(auth as unknown as () => Promise).mockResolvedValue(null); const err = await uploadPoDocuments(poId, [pdf("x.pdf")]); expect(err).toEqual({ error: "Unauthorized" }); expect(await db.pODocument.count({ where: { poId } })).toBe(0); }); it("errors when the PO does not exist", async () => { vi.mocked(auth as unknown as () => Promise).mockResolvedValue(makeSession(submitterId, "MANAGER")); const err = await uploadPoDocuments("nonexistent-po-id", [pdf("x.pdf")]); expect(err).toEqual({ error: "PO not found" }); expect(uploadBuffer).not.toHaveBeenCalled(); }); });