/** * Integration test for the server-side PII masking on the crew profile page. * Identity-document numbers (Aadhaar/PAN) must be masked BEFORE they cross to the * client component — full only for Accounts/SuperUser (Crewing-Implementation-Spec * §6 / Roles-and-Permissions §3). We invoke the server component and inspect the * props it hands to , so a regression that passes raw numbers to the * client is caught here. */ import { vi, describe, it, expect, beforeAll, afterAll, afterEach } from "vitest"; import React from "react"; // The integration runner compiles the page's JSX to classic React.createElement // without injecting React; provide it so invoking the server component works. (globalThis as unknown as { React: typeof React }).React = React; vi.mock("@/auth", () => ({ auth: vi.fn() })); vi.mock("@/lib/feature-flags", () => ({ CREWING_ENABLED: true, INVENTORY_ENABLED: true })); vi.mock("next/navigation", () => ({ redirect: vi.fn(), notFound: vi.fn() })); // The client component is irrelevant to this test — we read element.props directly. vi.mock("@/app/(portal)/crewing/crew/[id]/crew-profile", () => ({ CrewProfile: () => null })); import { auth } from "@/auth"; import { db } from "@/lib/db"; import CrewProfilePage from "@/app/(portal)/crewing/crew/[id]/page"; import { makeSession } from "./helpers"; import type { Role } from "@prisma/client"; const AADHAAR = "123456789012"; const PAN = "ABCDE1234F"; let crewId: string; const as = (role: Role) => vi.mocked(auth as unknown as () => Promise).mockResolvedValue(makeSession(`u-${role}`, role)); // Pull the documents prop the page would pass to the client component. async function docsFor(role: Role) { as(role); const element = (await CrewProfilePage({ params: Promise.resolve({ id: crewId }) })) as { props: { documents: Array<{ docType: string; number: string | null }> }; }; return element.props.documents; } const numberFor = (docs: Array<{ docType: string; number: string | null }>, docType: string) => docs.find((d) => d.docType === docType)?.number ?? null; beforeAll(async () => { const c = await db.crewMember.create({ data: { name: "PII Crew", status: "EMPLOYEE", type: "NEW", source: "CAREERS", employeeId: `CRW-PII${Date.now() % 100000}` }, }); crewId = c.id; await db.seafarerDocument.createMany({ data: [ { crewMemberId: c.id, docType: "AADHAAR", number: AADHAAR }, { crewMemberId: c.id, docType: "PAN", number: PAN }, { crewMemberId: c.id, docType: "PASSPORT", number: "P1234567" }, ], }); }); afterEach(() => vi.clearAllMocks()); afterAll(async () => { await db.seafarerDocument.deleteMany({ where: { crewMemberId: crewId } }); await db.crewMember.deleteMany({ where: { id: crewId } }); }); describe("crew profile — identity-document masking (server-side)", () => { it("masks Aadhaar/PAN for a MANAGER", async () => { const docs = await docsFor("MANAGER"); expect(numberFor(docs, "AADHAAR")).toBe("•••• 9012"); expect(numberFor(docs, "PAN")).toBe("•••• 234F"); // Non-identity documents are not restricted. expect(numberFor(docs, "PASSPORT")).toBe("P1234567"); }); it("masks Aadhaar/PAN for SITE_STAFF and the MPO too", async () => { expect(numberFor(await docsFor("SITE_STAFF"), "AADHAAR")).toBe("•••• 9012"); expect(numberFor(await docsFor("MANNING"), "PAN")).toBe("•••• 234F"); }); it("shows Aadhaar/PAN in full to ACCOUNTS and SUPERUSER", async () => { const acc = await docsFor("ACCOUNTS"); expect(numberFor(acc, "AADHAAR")).toBe(AADHAAR); expect(numberFor(acc, "PAN")).toBe(PAN); expect(numberFor(await docsFor("SUPERUSER"), "AADHAAR")).toBe(AADHAAR); }); });