/** * Integration tests for discardDraftPo server action. * Verifies: ownership checks, status guard, cascade deletion, role permissions. */ import { vi, describe, it, expect, beforeAll, afterEach } from "vitest"; vi.mock("@/auth", () => ({ auth: vi.fn() })); vi.mock("next/cache", () => ({ revalidatePath: vi.fn() })); vi.mock("@/lib/notifier", () => ({ notify: vi.fn() })); import { auth } from "@/auth"; import { db } from "@/lib/db"; import { createPo } from "@/app/(portal)/po/new/actions"; import { discardDraftPo } from "@/app/(portal)/po/[id]/actions"; import { makeSession, getSeedUser, getSeedVessel, getSeedAccount, makePoForm, deletePosByTitle, } from "./helpers"; const PREFIX = "INTTEST_DISCARD_"; let techId: string; let managerId: string; let accountsId: string; let vesselId: string; let accountId: string; beforeAll(async () => { const [tech, mgr, acct, vessel, account] = await Promise.all([ getSeedUser("tech@pelagia.local"), getSeedUser("manager@pelagia.local"), getSeedUser("accounts@pelagia.local"), getSeedVessel("MV Pelagia Star"), getSeedAccount("TECH-OPS"), ]); techId = tech.id; managerId = mgr.id; accountsId = acct.id; vesselId = vessel.id; accountId = account.id; }); afterEach(async () => { await deletePosByTitle(PREFIX); }); async function createDraft(title: string, asUserId = techId, asRole: Parameters[1] = "TECHNICAL") { vi.mocked(auth).mockResolvedValue(makeSession(asUserId, asRole)); const form = makePoForm({ title, vesselId, accountId, intent: "draft" }); const result = await createPo(form); return (result as { id: string }).id; } // ── Happy path ──────────────────────────────────────────────────────────────── describe("discard — happy path", () => { it("owner (TECHNICAL) can discard their own DRAFT", async () => { const poId = await createDraft(`${PREFIX}OwnerDiscard`); vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL")); const result = await discardDraftPo(poId); expect(result).toEqual({ ok: true }); expect(await db.purchaseOrder.findUnique({ where: { id: poId } })).toBeNull(); }); it("MANAGER can discard any DRAFT PO (not their own)", async () => { const poId = await createDraft(`${PREFIX}MgrDiscard`); vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER")); const result = await discardDraftPo(poId); expect(result).toEqual({ ok: true }); expect(await db.purchaseOrder.findUnique({ where: { id: poId } })).toBeNull(); }); it("SUPERUSER can discard any DRAFT PO", async () => { const superuser = await getSeedUser("admin@pelagia.local"); const poId = await createDraft(`${PREFIX}SuperDiscard`); vi.mocked(auth).mockResolvedValue(makeSession(superuser.id, "SUPERUSER")); const result = await discardDraftPo(poId); expect(result).toEqual({ ok: true }); expect(await db.purchaseOrder.findUnique({ where: { id: poId } })).toBeNull(); }); it("removes POActions cascade-lessly (no FK violation)", async () => { const poId = await createDraft(`${PREFIX}Cascade`); // Verify a CREATED action exists before discard const before = await db.pOAction.findMany({ where: { poId } }); expect(before.length).toBeGreaterThan(0); vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL")); await discardDraftPo(poId); const after = await db.pOAction.findMany({ where: { poId } }); expect(after).toHaveLength(0); }); it("removes line items along with the PO", async () => { const poId = await createDraft(`${PREFIX}LineItemCleanup`); const linesBefore = await db.pOLineItem.findMany({ where: { poId } }); expect(linesBefore.length).toBeGreaterThan(0); vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL")); await discardDraftPo(poId); const linesAfter = await db.pOLineItem.findMany({ where: { poId } }); expect(linesAfter).toHaveLength(0); }); }); // ── Permission denials ──────────────────────────────────────────────────────── describe("discard — negative / permission tests", () => { it("returns error for unauthenticated request", async () => { const poId = await createDraft(`${PREFIX}Unauth`); vi.mocked(auth).mockResolvedValue(null); expect(await discardDraftPo(poId)).toHaveProperty("error"); }); it("TECHNICAL cannot discard another user's DRAFT", async () => { // Create PO as manager, try to discard as tech const poId = await createDraft(`${PREFIX}WrongOwner`, managerId, "MANAGER"); vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL")); const result = await discardDraftPo(poId); expect(result).toHaveProperty("error"); // PO must still exist expect(await db.purchaseOrder.findUnique({ where: { id: poId } })).not.toBeNull(); }); it("ACCOUNTS cannot discard any PO (not in allowed roles)", async () => { const poId = await createDraft(`${PREFIX}AccountsForbidden`); vi.mocked(auth).mockResolvedValue(makeSession(accountsId, "ACCOUNTS")); const result = await discardDraftPo(poId); expect(result).toHaveProperty("error"); expect(await db.purchaseOrder.findUnique({ where: { id: poId } })).not.toBeNull(); }); it("returns error for non-existent PO", async () => { vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL")); const result = await discardDraftPo("non-existent-id"); expect(result).toHaveProperty("error"); }); }); // ── Status guard ────────────────────────────────────────────────────────────── describe("discard — status guard", () => { it("cannot discard a submitted (MGR_REVIEW) PO", async () => { vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL")); const form = makePoForm({ title: `${PREFIX}Submitted`, vesselId, accountId, intent: "submit" }); const { id: poId } = (await createPo(form)) as { id: string }; vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER")); const result = await discardDraftPo(poId); expect(result).toHaveProperty("error"); const po = await db.purchaseOrder.findUnique({ where: { id: poId } }); expect(po?.status).toBe("MGR_REVIEW"); }); });