159 lines
6.4 KiB
TypeScript
159 lines
6.4 KiB
TypeScript
/**
|
|
* Integration tests for discardDraftPo server action.
|
|
* Verifies: ownership checks, status guard, cascade deletion, role permissions.
|
|
*/
|
|
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 { discardDraftPo } from "@/app/(portal)/po/[id]/actions";
|
|
import {
|
|
makeSession, getSeedUser, getSeedVessel, getSeedAccount,
|
|
makePoForm, deletePosByTitle,
|
|
} from "./helpers";
|
|
|
|
const PREFIX = "INTTEST_DISCARD_";
|
|
let techId: string;
|
|
let managerId: string;
|
|
let accountsId: string;
|
|
let vesselId: string;
|
|
let accountId: string;
|
|
|
|
beforeAll(async () => {
|
|
const [tech, mgr, acct, vessel, account] = await Promise.all([
|
|
getSeedUser("tech@pelagia.local"),
|
|
getSeedUser("manager@pelagia.local"),
|
|
getSeedUser("accounts@pelagia.local"),
|
|
getSeedVessel("MV Pelagia Star"),
|
|
getSeedAccount("TECH-OPS"),
|
|
]);
|
|
techId = tech.id;
|
|
managerId = mgr.id;
|
|
accountsId = acct.id;
|
|
vesselId = vessel.id;
|
|
accountId = account.id;
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await deletePosByTitle(PREFIX);
|
|
});
|
|
|
|
async function createDraft(title: string, asUserId = techId, asRole: Parameters<typeof makeSession>[1] = "TECHNICAL") {
|
|
vi.mocked(auth).mockResolvedValue(makeSession(asUserId, asRole));
|
|
const form = makePoForm({ title, vesselId, accountId, intent: "draft" });
|
|
const result = await createPo(form);
|
|
return (result as { id: string }).id;
|
|
}
|
|
|
|
// ── Happy path ────────────────────────────────────────────────────────────────
|
|
|
|
describe("discard — happy path", () => {
|
|
it("owner (TECHNICAL) can discard their own DRAFT", async () => {
|
|
const poId = await createDraft(`${PREFIX}OwnerDiscard`);
|
|
vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
|
|
|
const result = await discardDraftPo(poId);
|
|
expect(result).toEqual({ ok: true });
|
|
expect(await db.purchaseOrder.findUnique({ where: { id: poId } })).toBeNull();
|
|
});
|
|
|
|
it("MANAGER can discard any DRAFT PO (not their own)", async () => {
|
|
const poId = await createDraft(`${PREFIX}MgrDiscard`);
|
|
vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
|
|
|
const result = await discardDraftPo(poId);
|
|
expect(result).toEqual({ ok: true });
|
|
expect(await db.purchaseOrder.findUnique({ where: { id: poId } })).toBeNull();
|
|
});
|
|
|
|
it("SUPERUSER can discard any DRAFT PO", async () => {
|
|
const superuser = await getSeedUser("admin@pelagia.local");
|
|
const poId = await createDraft(`${PREFIX}SuperDiscard`);
|
|
vi.mocked(auth).mockResolvedValue(makeSession(superuser.id, "SUPERUSER"));
|
|
|
|
const result = await discardDraftPo(poId);
|
|
expect(result).toEqual({ ok: true });
|
|
expect(await db.purchaseOrder.findUnique({ where: { id: poId } })).toBeNull();
|
|
});
|
|
|
|
it("removes POActions cascade-lessly (no FK violation)", async () => {
|
|
const poId = await createDraft(`${PREFIX}Cascade`);
|
|
// Verify a CREATED action exists before discard
|
|
const before = await db.pOAction.findMany({ where: { poId } });
|
|
expect(before.length).toBeGreaterThan(0);
|
|
|
|
vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
|
await discardDraftPo(poId);
|
|
|
|
const after = await db.pOAction.findMany({ where: { poId } });
|
|
expect(after).toHaveLength(0);
|
|
});
|
|
|
|
it("removes line items along with the PO", async () => {
|
|
const poId = await createDraft(`${PREFIX}LineItemCleanup`);
|
|
const linesBefore = await db.pOLineItem.findMany({ where: { poId } });
|
|
expect(linesBefore.length).toBeGreaterThan(0);
|
|
|
|
vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
|
await discardDraftPo(poId);
|
|
|
|
const linesAfter = await db.pOLineItem.findMany({ where: { poId } });
|
|
expect(linesAfter).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
// ── Permission denials ────────────────────────────────────────────────────────
|
|
|
|
describe("discard — negative / permission tests", () => {
|
|
it("returns error for unauthenticated request", async () => {
|
|
const poId = await createDraft(`${PREFIX}Unauth`);
|
|
vi.mocked(auth).mockResolvedValue(null);
|
|
expect(await discardDraftPo(poId)).toHaveProperty("error");
|
|
});
|
|
|
|
it("TECHNICAL cannot discard another user's DRAFT", async () => {
|
|
// Create PO as manager, try to discard as tech
|
|
const poId = await createDraft(`${PREFIX}WrongOwner`, managerId, "MANAGER");
|
|
|
|
vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
|
const result = await discardDraftPo(poId);
|
|
expect(result).toHaveProperty("error");
|
|
// PO must still exist
|
|
expect(await db.purchaseOrder.findUnique({ where: { id: poId } })).not.toBeNull();
|
|
});
|
|
|
|
it("ACCOUNTS cannot discard any PO (not in allowed roles)", async () => {
|
|
const poId = await createDraft(`${PREFIX}AccountsForbidden`);
|
|
vi.mocked(auth).mockResolvedValue(makeSession(accountsId, "ACCOUNTS"));
|
|
const result = await discardDraftPo(poId);
|
|
expect(result).toHaveProperty("error");
|
|
expect(await db.purchaseOrder.findUnique({ where: { id: poId } })).not.toBeNull();
|
|
});
|
|
|
|
it("returns error for non-existent PO", async () => {
|
|
vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
|
const result = await discardDraftPo("non-existent-id");
|
|
expect(result).toHaveProperty("error");
|
|
});
|
|
});
|
|
|
|
// ── Status guard ──────────────────────────────────────────────────────────────
|
|
|
|
describe("discard — status guard", () => {
|
|
it("cannot discard a submitted (MGR_REVIEW) PO", async () => {
|
|
vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
|
const form = makePoForm({ title: `${PREFIX}Submitted`, vesselId, accountId, intent: "submit" });
|
|
const { id: poId } = (await createPo(form)) as { id: string };
|
|
|
|
vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
|
const result = await discardDraftPo(poId);
|
|
expect(result).toHaveProperty("error");
|
|
const po = await db.purchaseOrder.findUnique({ where: { id: poId } });
|
|
expect(po?.status).toBe("MGR_REVIEW");
|
|
});
|
|
});
|