/** * Integration tests for the Crewing Phase 3a candidate server actions * (addCandidate / updateCandidate). Mirrors the requisitions test setup. * * The CrewMember table is introduced in this phase, so afterEach wipes it (and * its CrewAction rows) wholesale — no pre-existing rows to preserve. */ 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 { addCandidate, updateCandidate } from "@/app/(portal)/crewing/candidates/actions"; import { makeSession, getSeedUser, fd } from "./helpers"; import type { Role } from "@prisma/client"; let managerId: string; let siteStaffId: string; let rankId: string; const SS_EMAIL = "sitestaff@itcand.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; const ss = await db.user.upsert({ where: { email: SS_EMAIL }, update: { role: "SITE_STAFF", isActive: true }, create: { employeeId: "ITCAND-SS", email: SS_EMAIL, name: "Site Staff Cand", role: "SITE_STAFF" }, }); siteStaffId = ss.id; rankId = (await db.rank.findFirstOrThrow()).id; }); afterEach(async () => { await db.crewAction.deleteMany({ where: { crewMemberId: { not: null } } }); await db.crewMember.deleteMany({}); vi.clearAllMocks(); }); afterAll(async () => { await db.user.deleteMany({ where: { email: SS_EMAIL } }); }); describe("addCandidate", () => { it("adds a NEW candidate with an audit action and sensible defaults", async () => { as(managerId, "MANAGER"); const res = await addCandidate(fd({ name: "Asha Rao", source: "CAREERS", appliedRankId: rankId, experienceMonths: "60" })); expect("ok" in res && res.ok).toBe(true); const c = await db.crewMember.findFirstOrThrow({ include: { actions: true } }); expect(c.name).toBe("Asha Rao"); expect(c.type).toBe("NEW"); expect(c.status).toBe("CANDIDATE"); expect(c.appliedRankId).toBe(rankId); expect(c.experienceMonths).toBe(60); expect(c.employeeId).toBeNull(); expect(c.actions[0].actionType).toBe("CANDIDATE_ADDED"); expect(c.actions[0].actorId).toBe(managerId); }); it("an EX_HAND source yields type EX_HAND and status EX_HAND", async () => { as(managerId, "MANAGER"); await addCandidate(fd({ name: "Returning Ravi", source: "EX_HAND" })); const c = await db.crewMember.findFirstOrThrow(); expect(c.type).toBe("EX_HAND"); expect(c.status).toBe("EX_HAND"); }); it("requires a name", async () => { as(managerId, "MANAGER"); const res = await addCandidate(fd({ name: " ", source: "CAREERS" })); expect("error" in res).toBe(true); expect(await db.crewMember.count()).toBe(0); }); it("is rejected for roles without manage_candidates (site staff, accounts)", async () => { as(siteStaffId, "SITE_STAFF"); expect(await addCandidate(fd({ name: "Nope" }))).toEqual({ error: "Unauthorized" }); as(managerId, "ACCOUNTS"); expect(await addCandidate(fd({ name: "Nope" }))).toEqual({ error: "Unauthorized" }); expect(await db.crewMember.count()).toBe(0); }); }); describe("updateCandidate", () => { it("edits fields and writes a CANDIDATE_UPDATED action", async () => { as(managerId, "MANAGER"); await addCandidate(fd({ name: "Edit Me", source: "CAREERS", experienceMonths: "12" })); const c = await db.crewMember.findFirstOrThrow(); const res = await updateCandidate(fd({ id: c.id, name: "Edited Name", source: "REFERRAL", experienceMonths: "24" })); expect("ok" in res && res.ok).toBe(true); const after = await db.crewMember.findUniqueOrThrow({ where: { id: c.id }, include: { actions: true } }); expect(after.name).toBe("Edited Name"); expect(after.source).toBe("REFERRAL"); expect(after.experienceMonths).toBe(24); expect(after.actions.some((a) => a.actionType === "CANDIDATE_UPDATED")).toBe(true); }); it("does not downgrade an onboarded EMPLOYEE back to a candidate", async () => { as(managerId, "MANAGER"); await addCandidate(fd({ name: "Hired Hannah", source: "CAREERS" })); const c = await db.crewMember.findFirstOrThrow(); await db.crewMember.update({ where: { id: c.id }, data: { status: "EMPLOYEE" } }); await updateCandidate(fd({ id: c.id, name: "Hired Hannah", source: "CAREERS" })); expect((await db.crewMember.findUniqueOrThrow({ where: { id: c.id } })).status).toBe("EMPLOYEE"); }); it("rejects an unknown id", async () => { as(managerId, "MANAGER"); const res = await updateCandidate(fd({ id: "nonexistent", name: "X", source: "CAREERS" })); expect("error" in res).toBe(true); }); });