import { getPayload, Payload } from "payload"; import config from "@/payload.config"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import type { Role, User } from "@/payload-types"; import { canViewIntelligence } from "@/utils/access-control/canViewIntelligence"; import { hasIntelligenceQualification } from "@/utils/access-control/hasIntelligenceQualification"; import { invalidatePermissionCache } from "@/utils/access-control/loadUserPermissions"; let payload: Payload; const RUN = `intelvis-${Date.now().toString(36)}`; const TIMEOUT = 30_000; describe("Intelligence navigation visibility (canViewIntelligence)", () => { const roleIds: number[] = []; const userIds: number[] = []; const createdQualificationIds: number[] = []; let readerUser: User; let outsiderUser: User; let qualifiedUser: User; let superUser: User; const makeRole = async (label: string, extra: Partial = {}): Promise => { const role = (await payload.create({ collection: "roles", data: { name: `${RUN}-${label}`, slug: `${RUN}-${label}`, ...extra }, overrideAccess: true, depth: 0, })) as unknown as Role; roleIds.push(role.id); return role; }; const makeUser = async (label: string, roleId: number): Promise => { const user = (await payload.create({ collection: "users", data: { username: `${RUN}-${label}`, discordUsername: `${RUN}-${label}`, displayName: label.toUpperCase(), steamId: `7656119${Math.floor(Math.random() * 1e9)}`, password: "Test123", roleDocs: [roleId], }, overrideAccess: true, depth: 0, })) as unknown as User; userIds.push(user.id); return user; }; const findOrCreateQualification = async (name: string): Promise => { const found = (await payload.find({ collection: "qualifications", where: { name: { equals: name } }, limit: 1, depth: 0, overrideAccess: true, })) as unknown as { docs: Array<{ id: number }> }; if (found.docs.length > 0) return found.docs[0].id; const created = (await payload.create({ collection: "qualifications", data: { name }, overrideAccess: true, depth: 0, })) as unknown as { id: number }; createdQualificationIds.push(created.id); return created.id; }; const grantQualification = async (user: User, qualificationId: number) => { const profile = (await payload.find({ collection: "profiles", where: { user: { equals: user.id } }, limit: 1, depth: 0, overrideAccess: true, })) as unknown as { docs: Array<{ id: number }> }; expect(profile.docs.length).toBeGreaterThan(0); await payload.update({ collection: "profiles", id: profile.docs[0].id, data: { progression: { qualifications: [qualificationId] } }, overrideAccess: true, depth: 0, }); }; beforeAll(async () => { const payloadConfig = await config; payload = await getPayload({ config: payloadConfig }); invalidatePermissionCache(); // Mirrors the standard "user" role: intel-domain read permissions, no writes. const readerRole = await makeRole("reader", { permissions: ["missions:read", "campaigns:read", "factions:read", "technologies:read"], }); const outsiderRole = await makeRole("outsider", { permissions: ["tickets:read"] }); const bareRole = await makeRole("bare", { permissions: [] }); const superRole = await makeRole("super", { isSuperuser: true }); readerUser = await makeUser("reader", readerRole.id); outsiderUser = await makeUser("outsider", outsiderRole.id); qualifiedUser = await makeUser("qualified", bareRole.id); superUser = await makeUser("super", superRole.id); const intelligenceId = await findOrCreateQualification("Intelligence"); await grantQualification(qualifiedUser, intelligenceId); }, TIMEOUT); afterAll(async () => { if (!payload) return; for (const id of userIds) { const profiles = await payload .find({ collection: "profiles", where: { user: { equals: id } }, limit: 5, depth: 0, overrideAccess: true, }) .catch(() => null); for (const p of profiles?.docs ?? []) { await payload.delete({ collection: "profiles", id: p.id, overrideAccess: true }).catch(() => {}); } await payload.delete({ collection: "users", id, overrideAccess: true }).catch(() => {}); } for (const id of roleIds) { await payload.delete({ collection: "roles", id, overrideAccess: true }).catch(() => {}); } for (const id of createdQualificationIds) { await payload .delete({ collection: "qualifications", id, overrideAccess: true }) .catch(() => {}); } }); it("member with intel read permissions sees the section but is not intel-qualified", async () => { expect(await canViewIntelligence(payload, readerUser)).toBe(true); // The strict gate must stay tight: read permissions alone never grant // moderation (wiki moderator, tech tree editing, intel admin pages). expect(await hasIntelligenceQualification(payload, readerUser)).toBe(false); }); it("member without intel permissions sees neither surface", async () => { expect(await canViewIntelligence(payload, outsiderUser)).toBe(false); expect(await hasIntelligenceQualification(payload, outsiderUser)).toBe(false); }); it("intel-qualified member keeps the section even without read grants", async () => { expect(await hasIntelligenceQualification(payload, qualifiedUser)).toBe(true); expect(await canViewIntelligence(payload, qualifiedUser)).toBe(true); }); it("superuser sees the section", async () => { expect(await canViewIntelligence(payload, superUser)).toBe(true); }); it("guest (no user) sees nothing", async () => { expect(await canViewIntelligence(payload, null)).toBe(false); }); });