pelagia-portal/App/tests/integration/vendor-approval.test.ts
2026-05-18 23:18:58 +05:30

167 lines
6 KiB
TypeScript

/**
* 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");
});
});