/** * Integration tests for Crewing Phase 5b appraisal: the * raise (PM) → verify (MPO) → approve (Manager) lifecycle, with rejection paths * and role gating per §5.4/§6. */ import { vi, describe, it, expect, beforeAll, afterAll, afterEach } from "vitest"; vi.mock("@/auth", () => ({ auth: vi.fn() })); vi.mock("next/cache", () => ({ revalidatePath: vi.fn() })); vi.mock("@/lib/feature-flags", () => ({ CREWING_ENABLED: true, INVENTORY_ENABLED: true })); vi.mock("@/lib/notifier", () => ({ notify: vi.fn(), notifyCrew: vi.fn() })); import { auth } from "@/auth"; import { db } from "@/lib/db"; import { raiseAppraisal, verifyAppraisal, approveAppraisal } from "@/app/(portal)/crewing/appraisals/actions"; import { makeSession, getSeedUser, fd } from "./helpers"; import type { Role } from "@prisma/client"; let managerId: string; let manningId: string; let siteStaffId: string; let rankId: string; let vesselId: string; const SS_EMAIL = "sitestaff@itapp2.local"; const as = (userId: string, role: Role) => vi.mocked(auth as unknown as () => Promise).mockResolvedValue(makeSession(userId, role)); async function assignment() { const c = await db.crewMember.create({ data: { name: "Appraisee", status: "EMPLOYEE", type: "NEW", source: "CAREERS" } }); const a = await db.crewAssignment.create({ data: { status: "ACTIVE", signOnDate: new Date("2026-01-01"), crewMemberId: c.id, rankId, vesselId } }); return { crewId: c.id, assignmentId: a.id }; } async function raise(assignmentId: string) { as(siteStaffId, "SITE_STAFF"); // PM / site staff raise (raise_appraisal) const res = await raiseAppraisal(fd({ assignmentId, period: "2026", competence: "4", conduct: "5", safety: "4", comments: "Solid" })); if (!("ok" in res)) throw new Error("raise failed"); return res.id!; } beforeAll(async () => { managerId = (await getSeedUser("manager@pelagia.local")).id; manningId = (await getSeedUser("manning@pelagia.local")).id; const ss = await db.user.upsert({ where: { email: SS_EMAIL }, update: { role: "SITE_STAFF", isActive: true }, create: { employeeId: "ITAPP2-SS", email: SS_EMAIL, name: "SS App2", role: "SITE_STAFF" } }); siteStaffId = ss.id; rankId = (await db.rank.findFirstOrThrow()).id; vesselId = (await db.vessel.findFirstOrThrow()).id; }); afterEach(async () => { await db.crewAction.deleteMany({}); await db.appraisal.deleteMany({}); await db.crewAssignment.deleteMany({}); await db.crewMember.deleteMany({}); vi.clearAllMocks(); }); afterAll(async () => { await db.user.deleteMany({ where: { email: SS_EMAIL } }); }); describe("appraisal lifecycle", () => { it("raise → verify (MPO) → approve (Manager)", async () => { const { assignmentId } = await assignment(); const id = await raise(assignmentId); expect((await db.appraisal.findUniqueOrThrow({ where: { id } })).status).toBe("SUBMITTED"); as(manningId, "MANNING"); expect("ok" in (await verifyAppraisal(id, true))).toBe(true); const verified = await db.appraisal.findUniqueOrThrow({ where: { id } }); expect(verified.status).toBe("MPO_VERIFIED"); expect(verified.verifiedById).toBe(manningId); // MPO cannot approve expect(await approveAppraisal(id, true)).not.toHaveProperty("ok"); as(managerId, "MANAGER"); expect("ok" in (await approveAppraisal(id, true))).toBe(true); const approved = await db.appraisal.findUniqueOrThrow({ where: { id } }); expect(approved.status).toBe("MANAGER_APPROVED"); expect(approved.approvedById).toBe(managerId); }); it("MPO rejects with remarks", async () => { const { assignmentId } = await assignment(); const id = await raise(assignmentId); as(manningId, "MANNING"); expect("error" in (await verifyAppraisal(id, false))).toBe(true); // remarks required expect("ok" in (await verifyAppraisal(id, false, "Incomplete"))).toBe(true); const a = await db.appraisal.findUniqueOrThrow({ where: { id } }); expect(a.status).toBe("REJECTED"); expect(a.rejectedReason).toBe("Incomplete"); }); it("raise is rejected for a role without raise_appraisal (MPO)", async () => { const { assignmentId } = await assignment(); as(manningId, "MANNING"); // MPO does not hold raise_appraisal expect(await raiseAppraisal(fd({ assignmentId, period: "2026" }))).toEqual({ error: "Unauthorized" }); }); it("verify is rejected for a role without verify_appraisal (site staff)", async () => { const { assignmentId } = await assignment(); const id = await raise(assignmentId); as(siteStaffId, "SITE_STAFF"); expect(await verifyAppraisal(id, true)).toEqual({ error: "Unauthorized" }); }); });