1
0
Fork 0
polaris-task-force/tests/int/admin-page-access.int.spec.ts
Z8MB1E 822ac47c23 feat(auth): scope Payload admin pages by permissions
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@siisyphuslabs.ai>
2026-09-01 21:39:09 -04:00

164 lines
5.9 KiB
TypeScript

import { getPayload, Payload } from "payload";
import type { Access } from "payload";
import config from "@/payload.config";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import type { Role, User } from "@/payload-types";
import { requireAdminPageAccess } from "@/utils/access-control/hasPermission";
import { invalidatePermissionCache } from "@/utils/access-control/loadUserPermissions";
let payload: Payload;
const RUN = `adm-${Date.now().toString(36)}`;
const TIMEOUT = 30_000;
describe("Scoped admin page access control", () => {
const roleIds: number[] = [];
const userIds: number[] = [];
let readOnlyUser: User;
let manageOnlyUser: User;
let bothUser: User;
let devUser: User;
let neitherUser: 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",
// Permission resolution reads roleDocs (the dynamic RBAC relationship), not
// the legacy `roles` enum — so assign a real role doc to grant permissions.
roleDocs: [roleId],
},
overrideAccess: true,
depth: 0,
})) as unknown as User;
userIds.push(user.id);
return user;
};
// Invoke the admin-page wrapper exactly as Payload would for an admin-panel
// request (pathname under /admin). The vitest environment has no Next.js HTTP
// server, so calling the access function directly is the standard way to unit-test
// Payload access control.
const adminDecision = async (user: User | null): Promise<unknown> => {
const fn = requireAdminPageAccess("missions");
return await (fn as (args: { req: unknown }) => Promise<unknown>)({
req: { user, payload, pathname: "/admin/collections/missions" },
});
};
// Same wrapper, but on a REST API pathname — the original read access must be
// preserved unchanged outside the admin panel.
const apiDecision = async (user: User | null, readAccess?: Access | boolean): Promise<unknown> => {
const fn = requireAdminPageAccess("missions", readAccess);
return await (fn as (args: { req: unknown }) => Promise<unknown>)({
req: { user, payload, pathname: "/api/missions" },
});
};
beforeAll(async () => {
const payloadConfig = await config;
payload = await getPayload({ config: payloadConfig });
invalidatePermissionCache();
const readOnlyRole = await makeRole("readonly", { permissions: ["missions:read"] });
const manageOnlyRole = await makeRole("manageonly", {
permissions: ["admin:missions:manage"],
});
const bothRole = await makeRole("both", {
permissions: ["missions:read", "admin:missions:manage"],
});
const devRole = await makeRole("dev", { isSuperuser: true });
const neitherRole = await makeRole("neither", { permissions: [] });
readOnlyUser = await makeUser("readonly", readOnlyRole.id);
manageOnlyUser = await makeUser("manageonly", manageOnlyRole.id);
bothUser = await makeUser("both", bothRole.id);
devUser = await makeUser("dev", devRole.id);
neitherUser = await makeUser("neither", neitherRole.id);
}, 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 Promise.allSettled([
payload.delete({ collection: "profiles", id: p.id, overrideAccess: true }),
]);
}
await Promise.allSettled([payload.delete({ collection: "users", id, overrideAccess: true })]);
}
for (const id of roleIds) {
await Promise.allSettled([payload.delete({ collection: "roles", id, overrideAccess: true })]);
}
});
it("denies admin access with only the collection read permission", async () => {
expect(await adminDecision(readOnlyUser)).toBe(false);
}, TIMEOUT);
it("denies admin access with only the admin page-manage permission", async () => {
expect(await adminDecision(manageOnlyUser)).toBe(false);
}, TIMEOUT);
it("grants admin access with both permissions", async () => {
expect(await adminDecision(bothUser)).toBe(true);
}, TIMEOUT);
it("grants admin access to superuser roles", async () => {
expect(await adminDecision(devUser)).toBe(true);
}, TIMEOUT);
it("denies admin access with neither permission", async () => {
expect(await adminDecision(neitherUser)).toBe(false);
}, TIMEOUT);
it("denies anonymous admin access", async () => {
expect(await adminDecision(null)).toBe(false);
}, TIMEOUT);
it("preserves the default logged-in read on the API path", async () => {
expect(await apiDecision(bothUser)).toBe(true);
}, TIMEOUT);
it("preserves the default anonymous denial on the API path", async () => {
expect(await apiDecision(null)).toBe(false);
}, TIMEOUT);
it("delegates a Where-returning original access on the API path", async () => {
const where = { user: { equals: bothUser.id } };
expect(await apiDecision(bothUser, () => where)).toEqual(where);
}, TIMEOUT);
it("delegates a boolean original access on the API path", async () => {
expect(await apiDecision(bothUser, () => false)).toBe(false);
expect(await apiDecision(bothUser, () => true)).toBe(true);
}, TIMEOUT);
});