/** * Integration tests for vendor-gated approval and provide-vendor-id. * Covers: * - Approval blocked when no vendor assigned * - Approval succeeds once vendor is set * - ACCOUNTS role can now call provideVendorId * - Unverified vendor rejected by provideVendorId * - AUDITOR cannot provide vendor ID */ 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 { approvepo, requestVendorId } from "@/app/(portal)/approvals/[id]/actions"; import { provideVendorId } from "@/app/(portal)/po/[id]/actions"; import { makeSession, getSeedUser, getSeedVessel, getSeedAccount, getSeedVendor, makePoForm, deletePosByTitle, } from "./helpers"; const PREFIX = "INTTEST_VENDOR_APPROVAL_"; let techId: string; let managerId: string; let accountsId: string; let auditorId: string; let vesselId: string; let accountId: string; let verifiedVendorId: string; let unverifiedVendorDbId: string; beforeAll(async () => { const [tech, mgr, acct, vessel, account, vendor] = await Promise.all([ getSeedUser("tech@pelagia.local"), getSeedUser("manager@pelagia.local"), getSeedUser("accounts@pelagia.local"), getSeedVessel("MV Pelagia Star"), getSeedAccount("TECH-OPS"), getSeedVendor("Apar Industries Ltd"), ]); techId = tech.id; managerId = mgr.id; accountsId = acct.id; vesselId = vessel.id; accountId = account.id; verifiedVendorId = vendor.id; // Auditor — create on-the-fly if not seeded const maybeAuditor = await db.user.findFirst({ where: { role: "AUDITOR" } }); if (maybeAuditor) { auditorId = maybeAuditor.id; } else { const created = await db.user.create({ data: { employeeId: "EMP-TEST-AUD", email: "auditor@test.local", name: "Test Auditor", passwordHash: "irrelevant", role: "AUDITOR", }, }); auditorId = created.id; } // Grab an unverified vendor const unverified = await db.vendor.findFirst({ where: { isVerified: false } }); unverifiedVendorDbId = unverified!.id; }); afterEach(async () => { await deletePosByTitle(PREFIX); }); async function makeReviewPo(title: string, withVendor = false) { vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL")); const form = makePoForm({ title, vesselId, accountId, intent: "submit", vendorId: withVendor ? verifiedVendorId : undefined, }); const result = await createPo(form); return (result as { id: string }).id; } // ── Vendor required for approval ────────────────────────────────────────────── describe("approval — vendor required", () => { it("blocks approval when PO has no vendor assigned", async () => { const poId = await makeReviewPo(`${PREFIX}NoVendorBlock`); vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER")); const result = await approvepo({ poId }); expect(result).toHaveProperty("error"); expect((result as { error: string }).error).toMatch(/vendor/i); const po = await db.purchaseOrder.findUnique({ where: { id: poId } }); expect(po?.status).toBe("MGR_REVIEW"); }); it("allows approval when PO has a vendor assigned", async () => { const poId = await makeReviewPo(`${PREFIX}VendorPresent`, true); vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER")); const result = await approvepo({ poId }); expect(result).toEqual({ ok: true }); const po = await db.purchaseOrder.findUnique({ where: { id: poId } }); expect(po?.status).toBe("MGR_APPROVED"); }); }); // ── provideVendorId — role expansion ───────────────────────────────────────── describe("provideVendorId — role expansion", () => { async function makePendingPo(title: string) { const poId = await makeReviewPo(title); vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER")); await requestVendorId({ poId }); return poId; } it("ACCOUNTS can provide a verified vendor ID", async () => { const poId = await makePendingPo(`${PREFIX}AccountsProvide`); vi.mocked(auth).mockResolvedValue(makeSession(accountsId, "ACCOUNTS")); const result = await provideVendorId({ poId, vendorId: verifiedVendorId }); expect(result).toEqual({ ok: true }); const po = await db.purchaseOrder.findUnique({ where: { id: poId } }); expect(po?.status).toBe("MGR_REVIEW"); expect(po?.vendorId).toBe(verifiedVendorId); }); it("rejects an unverified vendor (no vendorId field on Vendor record)", async () => { const poId = await makePendingPo(`${PREFIX}UnverifiedVendor`); vi.mocked(auth).mockResolvedValue(makeSession(accountsId, "ACCOUNTS")); const result = await provideVendorId({ poId, vendorId: unverifiedVendorDbId }); expect(result).toHaveProperty("error"); const po = await db.purchaseOrder.findUnique({ where: { id: poId } }); expect(po?.status).toBe("VENDOR_ID_PENDING"); }); it("AUDITOR cannot provide vendor ID", async () => { const poId = await makePendingPo(`${PREFIX}AuditorDenied`); vi.mocked(auth).mockResolvedValue(makeSession(auditorId, "AUDITOR")); const result = await provideVendorId({ poId, vendorId: verifiedVendorId }); expect(result).toHaveProperty("error"); }); it("returns error when called on a PO not in VENDOR_ID_PENDING state", async () => { // PO still in MGR_REVIEW — no requestVendorId called const poId = await makeReviewPo(`${PREFIX}WrongState`); vi.mocked(auth).mockResolvedValue(makeSession(accountsId, "ACCOUNTS")); const result = await provideVendorId({ poId, vendorId: verifiedVendorId }); expect(result).toHaveProperty("error"); }); });