Green-lights the test suite so the PR checks can enforce it:
- Fix the NextAuth v5 auth() mock typing across all integration tests (cast to a
simple async fn so mockResolvedValue accepts the session) — clears ~86 errors.
- Fix stale test values: intent 'resubmit'->'submit' / 'save'->'draft'; ParsedImportLine
.description -> .name; approvepo -> approvePo; add missing beforeEach/beforeAll imports.
- permissions: MANAGER *can* process_payment (intentional since e1340b9) — update the
stale assertion.
- po-import-parser: skip the Sample_PO.xlsx fixture tests when the file is absent (it
lives outside the repo); synthetic-workbook tests still cover the parser.
type-check is now 0 errors and unit tests pass (167 passed, 13 skipped). pr-checks.yml
flips type-check (whole project) and unit tests to HARD gates.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
167 lines
6.2 KiB
TypeScript
167 lines
6.2 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 as unknown as () => Promise<unknown>).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 as unknown as () => Promise<unknown>).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 as unknown as () => Promise<unknown>).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 as unknown as () => Promise<unknown>).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 as unknown as () => Promise<unknown>).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 as unknown as () => Promise<unknown>).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 as unknown as () => Promise<unknown>).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 as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(accountsId, "ACCOUNTS"));
|
|
|
|
const result = await provideVendorId({ poId, vendorId: verifiedVendorId });
|
|
expect(result).toHaveProperty("error");
|
|
});
|
|
});
|