1
0
Fork 0
polaris-task-force/tests/int/intelligence-visibility.int.spec.ts
Z8MB1E ef7295c0d4 fix(intel): show the intelligence section to anyone with read access
v0.2.33 tightened hasIntelligenceQualification to write permissions for wiki
moderation, but the same check also gated navigation: regular members lost the
Intelligence sidebar section, command palette entries, keyboard shortcuts,
dashboard widgets, and tour steps. Add canViewIntelligence (intel read
permissions, with the strict qualification as a fallback) and use it for those
visibility surfaces. Wiki moderation, the tech tree, and division admin pages
keep the strict qualification.
2026-10-04 02:53:24 -04:00

165 lines
5.9 KiB
TypeScript

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<Role> = {}): Promise<Role> => {
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<User> => {
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<number> => {
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);
});
});