167 lines
6 KiB
TypeScript
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");
|
|
});
|
|
});
|