pelagia-portal/App/tests/integration/manager-po-creation.test.ts
Hardik 938ff6df89 test+ci: green the test baseline and make type-check + unit tests hard gates
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>
2026-06-19 13:03:54 +05:30

127 lines
5.4 KiB
TypeScript

/**
* Integration tests for MANAGER role creating and submitting POs.
* Verifies the new create_po / submit_po permissions granted to MANAGER.
*/
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 } from "@/app/(portal)/approvals/[id]/actions";
import { discardDraftPo } from "@/app/(portal)/po/[id]/actions";
import {
makeSession, getSeedUser, getSeedVessel, getSeedAccount, getSeedVendor,
makePoForm, deletePosByTitle,
} from "./helpers";
const PREFIX = "INTTEST_MGR_CREATE_";
let managerId: string;
let accountsId: string;
let vesselId: string;
let accountId: string;
let vendorId: string;
beforeAll(async () => {
const [mgr, acct, vessel, account, vendor] = await Promise.all([
getSeedUser("manager@pelagia.local"),
getSeedUser("accounts@pelagia.local"),
getSeedVessel("MV Pelagia Star"),
getSeedAccount("TECH-OPS"),
getSeedVendor("Apar Industries Ltd"),
]);
managerId = mgr.id;
accountsId = acct.id;
vesselId = vessel.id;
accountId = account.id;
vendorId = vendor.id;
});
afterEach(async () => {
await deletePosByTitle(PREFIX);
});
// ── MANAGER can create POs ────────────────────────────────────────────────────
describe("MANAGER — create PO", () => {
it("MANAGER can save a PO as DRAFT", async () => {
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
const form = makePoForm({ title: `${PREFIX}Draft`, vesselId, accountId, intent: "draft" });
const result = await createPo(form);
expect(result).not.toHaveProperty("error");
const po = await db.purchaseOrder.findUnique({ where: { id: (result as { id: string }).id } });
expect(po?.status).toBe("DRAFT");
expect(po?.submitterId).toBe(managerId);
});
it("MANAGER can submit a PO directly", async () => {
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
const form = makePoForm({ title: `${PREFIX}Submit`, vesselId, accountId, intent: "submit" });
const result = await createPo(form);
expect(result).not.toHaveProperty("error");
const po = await db.purchaseOrder.findUnique({ where: { id: (result as { id: string }).id } });
expect(po?.status).toBe("MGR_REVIEW");
expect(po?.submittedAt).not.toBeNull();
});
it("MANAGER can discard their own DRAFT", async () => {
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
const form = makePoForm({ title: `${PREFIX}Discard`, vesselId, accountId, intent: "draft" });
const { id: poId } = (await createPo(form)) as { id: string };
const result = await discardDraftPo(poId);
expect(result).toEqual({ ok: true });
expect(await db.purchaseOrder.findUnique({ where: { id: poId } })).toBeNull();
});
it("stores correct submitterId on MANAGER-created PO", async () => {
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
const form = makePoForm({ title: `${PREFIX}SubmitterId`, vesselId, accountId });
const { id: poId } = (await createPo(form)) as { id: string };
const po = await db.purchaseOrder.findUnique({ where: { id: poId } });
expect(po?.submitterId).toBe(managerId);
});
});
// ── Negative permission tests ─────────────────────────────────────────────────
describe("role — negative permission tests for PO creation", () => {
it("ACCOUNTS cannot create a PO", async () => {
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(accountsId, "ACCOUNTS"));
const form = makePoForm({ title: `${PREFIX}AcctsForbidden`, vesselId, accountId });
const result = await createPo(form);
expect(result).toHaveProperty("error");
});
it("unauthenticated request returns Unauthorized", async () => {
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(null);
const form = makePoForm({ title: `${PREFIX}Unauth`, vesselId, accountId });
const result = await createPo(form);
expect(result).toEqual({ error: "Unauthorized" });
});
it("MANAGER cannot approve their own submitted PO (same user)", async () => {
// Manager creates and submits
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
const form = makePoForm({
title: `${PREFIX}SelfApprove`,
vesselId,
accountId,
vendorId,
intent: "submit",
});
const { id: poId } = (await createPo(form)) as { id: string };
// Approving as the same manager — the action itself doesn't block same-user approval
// because approval authority is role-based, not submitter-based.
// This test documents the current behaviour.
const result = await approvePo({ poId });
// Should succeed because MANAGER has approve_po permission and the PO has a vendor
expect(result).toEqual({ ok: true });
});
});