/** * Integration tests for the crewing-admin actions: admin crew CRUD, Manager * direct placement (no requisition), and per-vessel/per-rank strength config — * all gated by the new manage_crew permission. */ 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 })); import { auth } from "@/auth"; import { db } from "@/lib/db"; import { createCrewMember, updateCrewMember, deleteCrewMember, placeCrew } from "@/app/(portal)/admin/crew/actions"; import { upsertRequirement, deleteRequirement } from "@/app/(portal)/admin/crew-strength/actions"; import { makeSession, getSeedUser, fd } from "./helpers"; import type { Role } from "@prisma/client"; let managerId: string; let adminId: string; let siteStaffId: string; let rankId: string; let vesselId: string; const SS_EMAIL = "sitestaff@itadm.local"; const as = (userId: string, role: Role) => vi.mocked(auth as unknown as () => Promise).mockResolvedValue(makeSession(userId, role)); beforeAll(async () => { managerId = (await getSeedUser("manager@pelagia.local")).id; adminId = (await getSeedUser("admin@pelagia.local")).id; const ss = await db.user.upsert({ where: { email: SS_EMAIL }, update: { role: "SITE_STAFF", isActive: true }, create: { employeeId: "ITADM-SS", email: SS_EMAIL, name: "SS Adm", 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.crewAssignment.deleteMany({}); await db.vesselRankRequirement.deleteMany({}); await db.crewMember.deleteMany({}); vi.clearAllMocks(); }); afterAll(async () => { await db.user.deleteMany({ where: { email: SS_EMAIL } }); }); describe("admin crew CRUD (manage_crew)", () => { it("admin creates and edits a crew member", async () => { as(adminId, "ADMIN"); const res = await createCrewMember(fd({ name: "Direct Hire", status: "CANDIDATE", source: "WALK_IN" })); expect("ok" in res && res.ok).toBe(true); const c = await db.crewMember.findFirstOrThrow({ where: { name: "Direct Hire" } }); expect(c.source).toBe("WALK_IN"); await updateCrewMember(fd({ id: c.id, name: "Direct Hire", status: "BLACKLISTED", source: "WALK_IN" })); expect((await db.crewMember.findUniqueOrThrow({ where: { id: c.id } })).status).toBe("BLACKLISTED"); }); it("is rejected for roles without manage_crew (site staff)", async () => { as(siteStaffId, "SITE_STAFF"); expect(await createCrewMember(fd({ name: "Nope" }))).toEqual({ error: "Unauthorized" }); expect(await db.crewMember.count()).toBe(0); }); it("blocks deletion of crew with assignments", async () => { as(managerId, "MANAGER"); const c = await db.crewMember.create({ data: { name: "Has Assignment", status: "EMPLOYEE", type: "NEW", source: "CAREERS" } }); await db.crewAssignment.create({ data: { status: "ACTIVE", signOnDate: new Date(), crewMemberId: c.id, rankId, vesselId } }); expect("error" in (await deleteCrewMember(c.id))).toBe(true); expect(await db.crewMember.findUnique({ where: { id: c.id } })).not.toBeNull(); }); it("deletes a crew member with no assignments/applications", async () => { as(managerId, "MANAGER"); const c = await db.crewMember.create({ data: { name: "Removable", status: "CANDIDATE", type: "NEW", source: "CAREERS" } }); expect("ok" in (await deleteCrewMember(c.id))).toBe(true); expect(await db.crewMember.findUnique({ where: { id: c.id } })).toBeNull(); }); }); describe("direct placement (Manager, no requisition)", () => { it("places a candidate → ACTIVE assignment + promoted to EMPLOYEE with a CRW- number", async () => { as(managerId, "MANAGER"); const c = await db.crewMember.create({ data: { name: "To Place", status: "CANDIDATE", type: "NEW", source: "CAREERS" } }); const res = await placeCrew(fd({ crewMemberId: c.id, rankId, vesselId, signOnDate: "2026-07-01" })); expect("ok" in res && res.ok).toBe(true); const assignment = await db.crewAssignment.findFirstOrThrow({ where: { crewMemberId: c.id } }); expect(assignment.status).toBe("ACTIVE"); expect(assignment.requisitionId).toBeNull(); // no requisition const after = await db.crewMember.findUniqueOrThrow({ where: { id: c.id } }); expect(after.status).toBe("EMPLOYEE"); expect(after.employeeId).toMatch(/^CRW-\d+$/); expect(after.currentRankId).toBe(rankId); }); it("refuses to place crew that already has an active assignment", async () => { as(managerId, "MANAGER"); const c = await db.crewMember.create({ data: { name: "Already Placed", status: "EMPLOYEE", type: "NEW", source: "CAREERS" } }); await db.crewAssignment.create({ data: { status: "ACTIVE", signOnDate: new Date(), crewMemberId: c.id, rankId, vesselId } }); expect("error" in (await placeCrew(fd({ crewMemberId: c.id, rankId, vesselId, signOnDate: "2026-07-01" })))).toBe(true); }); it("is rejected for roles without manage_crew", async () => { as(siteStaffId, "SITE_STAFF"); const c = await db.crewMember.create({ data: { name: "X", status: "CANDIDATE", type: "NEW", source: "CAREERS" } }); expect(await placeCrew(fd({ crewMemberId: c.id, rankId, vesselId, signOnDate: "2026-07-01" }))).toEqual({ error: "Unauthorized" }); }); }); describe("crew strength config (manage_crew)", () => { it("upserts and removes a vessel/rank requirement", async () => { as(managerId, "MANAGER"); expect("ok" in (await upsertRequirement(fd({ vesselId, rankId, minStrength: "3" })))).toBe(true); let req = await db.vesselRankRequirement.findUniqueOrThrow({ where: { vesselId_rankId: { vesselId, rankId } } }); expect(req.minStrength).toBe(3); // Upsert updates in place. await upsertRequirement(fd({ vesselId, rankId, minStrength: "5" })); req = await db.vesselRankRequirement.findUniqueOrThrow({ where: { vesselId_rankId: { vesselId, rankId } } }); expect(req.minStrength).toBe(5); expect(await db.vesselRankRequirement.count()).toBe(1); expect("ok" in (await deleteRequirement(req.id))).toBe(true); expect(await db.vesselRankRequirement.count()).toBe(0); }); it("is rejected for roles without manage_crew", async () => { as(siteStaffId, "SITE_STAFF"); expect(await upsertRequirement(fd({ vesselId, rankId, minStrength: "2" }))).toEqual({ error: "Unauthorized" }); }); });