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.
165 lines
5.9 KiB
TypeScript
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);
|
|
});
|
|
});
|