1
0
Fork 0
polaris-task-force/tests/int/division-admin-access.int.spec.ts
Z8MB1E 4dde790aa8 feat(access): division-scoped admin page access
- scopedAdminPageAccess replaces requireAdminPageAccess: technologies pass for
  intelligence division members, assets/resources/vehicles for logistics;
  everyone else still needs admin:<slug>:manage
- technology approval is stripped from create/update below admin so division
  members can never self-approve; technologies access moves to the
  intelligence permission
- assets/resources/vehicles move to logistics division permissions
2026-09-21 20:48:08 -04:00

451 lines
16 KiB
TypeScript

import { getPayload, Payload } from "payload";
import type { Access, AccessArgs } from "payload";
import config from "@/payload.config";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import type { Role, User } from "@/payload-types";
import {
canAccessAdminPanel,
requireApprovalPermission,
requireIntelligencePermission,
requireLogisticsPermission,
scopedAdminPageAccess,
} from "@/utils/access-control/divisionAccess";
import { invalidatePermissionCache } from "@/utils/access-control/loadUserPermissions";
let payload: Payload;
const RUN = `div-${Date.now().toString(36)}`;
const TIMEOUT = 30_000;
describe("Division-scoped admin access (intelligence / logistics)", () => {
const roleIds: number[] = [];
const userIds: number[] = [];
const assetIds: number[] = [];
const resourceIds: number[] = [];
const vehicleIds: number[] = [];
const technologyIds: number[] = [];
const createdQualificationIds: number[] = [];
let intelUser: User;
let logiUser: User;
let plainUser: 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,
});
};
// Invoke the scoped admin-page wrapper exactly as Payload would for an
// admin-panel request (pathname under /admin).
const adminDecision = async (slug: string, user: User | null): Promise<boolean> => {
const fn = scopedAdminPageAccess(slug);
return Boolean(
await (fn as (args: { req: unknown }) => Promise<unknown>)({
req: { user, payload, pathname: `/admin/collections/${slug}` },
}),
);
};
const apiDecision = async (slug: string, user: User | null, readAccess?: Access | boolean) => {
const fn = scopedAdminPageAccess(slug, readAccess);
return Boolean(
await (fn as (args: { req: unknown }) => Promise<unknown>)({
req: { user, payload, pathname: `/api/${slug}` },
}),
);
};
const accessFnDecision = async (fn: (args: AccessArgs) => Promise<boolean>, user: User) =>
Boolean(await fn({ req: { payload, user } } as unknown as AccessArgs));
const expectAccessDenied = async (fn: () => Promise<unknown>) => {
try {
await fn();
} catch (e) {
const name = (e as { name?: string })?.name ?? "";
const message = e instanceof Error ? e.message : "";
expect(
name === "AccessError" || name === "Forbidden" || /not permitted|not allowed|access denied/i.test(message),
).toBe(true);
return;
}
throw new Error("Expected operation to be denied");
};
beforeAll(async () => {
const payloadConfig = await config;
payload = await getPayload({ config: payloadConfig });
invalidatePermissionCache();
const bareRole = await makeRole("bare", { permissions: [] });
const superRole = await makeRole("super", { isSuperuser: true });
intelUser = await makeUser("intel", bareRole.id);
logiUser = await makeUser("logi", bareRole.id);
plainUser = await makeUser("plain", bareRole.id);
superUser = await makeUser("super", superRole.id);
const intelligenceId = await findOrCreateQualification("Intelligence");
const logisticsId = await findOrCreateQualification("Logistics");
await grantQualification(intelUser, intelligenceId);
await grantQualification(logiUser, logisticsId);
}, TIMEOUT);
afterAll(async () => {
if (!payload) return;
type TestSlug = "assets" | "resources" | "vehicles" | "technologies";
const docs: Array<[TestSlug, number]> = [
...assetIds.map((id) => ["assets", id] as [TestSlug, number]),
...resourceIds.map((id) => ["resources", id] as [TestSlug, number]),
...vehicleIds.map((id) => ["vehicles", id] as [TestSlug, number]),
...technologyIds.map((id) => ["technologies", id] as [TestSlug, number]),
];
for (const [collection, id] of docs) {
await payload.delete({ collection, id, overrideAccess: true }).catch(() => {});
}
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(() => {});
}
});
describe("admin panel visibility (scopedAdminPageAccess)", () => {
it("intelligence qualification grants technologies only", async () => {
expect(await adminDecision("technologies", intelUser)).toBe(true);
expect(await adminDecision("assets", intelUser)).toBe(false);
expect(await adminDecision("resources", intelUser)).toBe(false);
expect(await adminDecision("vehicles", intelUser)).toBe(false);
expect(await adminDecision("missions", intelUser)).toBe(false);
}, TIMEOUT);
it("logistics qualification grants assets, resources, and vehicles only", async () => {
expect(await adminDecision("assets", logiUser)).toBe(true);
expect(await adminDecision("resources", logiUser)).toBe(true);
expect(await adminDecision("vehicles", logiUser)).toBe(true);
expect(await adminDecision("technologies", logiUser)).toBe(false);
expect(await adminDecision("missions", logiUser)).toBe(false);
expect(await adminDecision("structures", logiUser)).toBe(false);
}, TIMEOUT);
it("plain users and anonymous users see none of the four", async () => {
for (const slug of ["technologies", "assets", "resources", "vehicles"]) {
expect(await adminDecision(slug, plainUser)).toBe(false);
expect(await adminDecision(slug, null)).toBe(false);
}
}, TIMEOUT);
it("superusers keep full admin panel access", async () => {
for (const slug of ["technologies", "assets", "resources", "vehicles"]) {
expect(await adminDecision(slug, superUser)).toBe(true);
}
}, TIMEOUT);
it("preserves the original read behavior on non-admin (REST) paths", async () => {
expect(await apiDecision("assets", logiUser)).toBe(true);
expect(await apiDecision("assets", null)).toBe(false);
expect(await apiDecision("technologies", logiUser, () => false)).toBe(false);
}, TIMEOUT);
});
describe("admin panel gate (canAccessAdminPanel)", () => {
it("division members pass; plain users and anonymous users do not", async () => {
expect(await canAccessAdminPanel(payload, intelUser)).toBe(true);
expect(await canAccessAdminPanel(payload, logiUser)).toBe(true);
expect(await canAccessAdminPanel(payload, superUser)).toBe(true);
expect(await canAccessAdminPanel(payload, plainUser)).toBe(false);
expect(await canAccessAdminPanel(payload, null)).toBe(false);
}, TIMEOUT);
});
describe("collection write access", () => {
it("logistics members fully manage assets but cannot self-approve", async () => {
const asset = (await payload.create({
collection: "assets",
data: {
name: `${RUN} Asset`,
className: "test-asset",
assetType: "weapon",
approvalStatus: "approved",
crafting: { craftingData: { craftingTimePerUnit: 1, batchSize: 1 } },
storageDimensions: { gridWidth: 1, gridHeight: 1 },
},
user: logiUser,
overrideAccess: false,
})) as unknown as { id: number; approvalStatus: string };
assetIds.push(asset.id);
expect(asset.approvalStatus).toBe("in_progress");
const renamed = (await payload.update({
collection: "assets",
id: asset.id,
data: { name: `${RUN} Asset v2`, approvalStatus: "rejected" },
user: logiUser,
overrideAccess: false,
})) as unknown as { name: string; approvalStatus: string };
expect(renamed.name).toBe(`${RUN} Asset v2`);
expect(renamed.approvalStatus).toBe("in_progress");
const approved = (await payload.update({
collection: "assets",
id: asset.id,
data: { approvalStatus: "approved", isLive: true },
user: superUser,
overrideAccess: false,
})) as unknown as { approvalStatus: string; isLive: boolean };
expect(approved.approvalStatus).toBe("approved");
expect(approved.isLive).toBe(true);
const demoted = (await payload.update({
collection: "assets",
id: asset.id,
data: { isLive: false },
user: logiUser,
overrideAccess: false,
})) as unknown as { isLive: boolean };
expect(demoted.isLive).toBe(true);
}, TIMEOUT);
it("intelligence members fully manage technologies but cannot self-approve", async () => {
const tech = (await payload.create({
collection: "technologies",
data: {
name: `${RUN} Tech`,
summary: "Division test tech",
type: "upgrade",
approvalStatus: "approved",
researchCosts: { minimumResearchDuration: 1 },
},
user: intelUser,
overrideAccess: false,
})) as unknown as { id: number; approvalStatus: string };
technologyIds.push(tech.id);
expect(tech.approvalStatus).toBe("in_progress");
const renamed = (await payload.update({
collection: "technologies",
id: tech.id,
data: { name: `${RUN} Tech v2`, approvalStatus: "approved" },
user: intelUser,
overrideAccess: false,
})) as unknown as { name: string; approvalStatus: string };
expect(renamed.name).toBe(`${RUN} Tech v2`);
expect(renamed.approvalStatus).toBe("in_progress");
await expectAccessDenied(() =>
payload.create({
collection: "technologies",
data: {
name: `${RUN} Tech Denied`,
summary: "no",
type: "upgrade",
approvalStatus: "in_progress",
researchCosts: { minimumResearchDuration: 1 },
},
user: logiUser,
overrideAccess: false,
}),
);
await expectAccessDenied(() =>
payload.create({
collection: "technologies",
data: {
name: `${RUN} Tech Denied 2`,
summary: "no",
type: "upgrade",
approvalStatus: "in_progress",
researchCosts: { minimumResearchDuration: 1 },
},
user: plainUser,
overrideAccess: false,
}),
);
}, TIMEOUT);
it("logistics members manage resources and vehicles; plain users are denied", async () => {
const resource = (await payload.create({
collection: "resources",
data: {
name: `${RUN} Fuel`,
codeName: `res_fuel_${RUN}`,
unitOfMeasure: "liter",
massPerUnit: 0,
gridWidth: 1,
gridHeight: 1,
type: "fluid",
baseValue: 1,
rarity: "common",
approvalStatus: "approved",
},
user: logiUser,
overrideAccess: false,
})) as unknown as { id: number; approvalStatus: string };
resourceIds.push(resource.id);
expect(resource.approvalStatus).toBe("in_progress");
await expectAccessDenied(() =>
payload.create({
collection: "resources",
data: {
name: `${RUN} Denied`,
codeName: `res_denied_${RUN}`,
unitOfMeasure: "unit",
massPerUnit: 0,
gridWidth: 1,
gridHeight: 1,
type: "physical",
baseValue: 1,
rarity: "common",
approvalStatus: "in_progress",
},
user: plainUser,
overrideAccess: false,
}),
);
const vehicle = (await payload.create({
collection: "vehicles",
data: {
name: `${RUN} Truck`,
transportMode: "ground",
approvalStatus: "approved",
fuel: { fuelType: resource.id, fuelCapacity: 100, fuelConsumptionRate: 1 },
},
user: logiUser,
overrideAccess: false,
})) as unknown as { id: number; approvalStatus: string };
vehicleIds.push(vehicle.id);
expect(vehicle.approvalStatus).toBe("in_progress");
await expectAccessDenied(() =>
payload.create({
collection: "vehicles",
data: {
name: `${RUN} Denied Truck`,
transportMode: "ground",
approvalStatus: "in_progress",
fuel: { fuelType: resource.id, fuelCapacity: 10, fuelConsumptionRate: 1 },
},
user: intelUser,
overrideAccess: false,
}),
);
}, TIMEOUT);
it("superusers create pre-approved documents directly", async () => {
const asset = (await payload.create({
collection: "assets",
data: {
name: `${RUN} Super Asset`,
className: "test-asset",
assetType: "weapon",
approvalStatus: "approved",
crafting: { craftingData: { craftingTimePerUnit: 1, batchSize: 1 } },
storageDimensions: { gridWidth: 1, gridHeight: 1 },
},
user: superUser,
overrideAccess: false,
})) as unknown as { id: number; approvalStatus: string };
assetIds.push(asset.id);
expect(asset.approvalStatus).toBe("approved");
}, TIMEOUT);
it("permission holders bypass the qualification requirement", async () => {
const assetsCreate = requireLogisticsPermission("assets:create");
expect(await accessFnDecision(assetsCreate, logiUser)).toBe(true);
const technologiesCreate = requireIntelligencePermission("technologies:create");
expect(await accessFnDecision(technologiesCreate, intelUser)).toBe(true);
}, TIMEOUT);
it("approval fields reject writes from everyone below the super-user tier", async () => {
const approvalUpdate = requireApprovalPermission();
expect(await accessFnDecision(approvalUpdate, superUser)).toBe(true);
expect(await accessFnDecision(approvalUpdate, logiUser)).toBe(false);
expect(await accessFnDecision(approvalUpdate, intelUser)).toBe(false);
expect(await accessFnDecision(approvalUpdate, plainUser)).toBe(false);
}, TIMEOUT);
});
});