1
0
Fork 0

test(wiki): integration coverage for Documentation access gating

Covers the category registry sync, create and update gating per role (plain user, admin, developer, guest), the update-time escalation block, and read gating.
This commit is contained in:
Jason Fraley 2026-09-30 18:43:39 -04:00
parent d1ae4c3bb7
commit 471e4eaacf

View file

@ -0,0 +1,330 @@
import { getPayload, Payload } from "payload";
import config from "@/payload.config";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import type { User, WikiPage } from "@/payload-types";
import { WIKI_CATEGORIES } from "@/lib/wiki/categories";
import { WikiPages } from "@/collections/wiki/WikiPages";
import { slugify } from "@/lib/wiki/slugify";
let payload: Payload;
const RUN = `wikidoc-${Date.now().toString(36)}`;
/**
* User deletion trips FK constraints unless the hook-provisioned personal bank
* account and profile are removed first (same helper as wiki.int.spec).
*/
const deleteUserWithRelations = async (pg: Payload, id: number): Promise<void> => {
const accounts = await pg
.find({
collection: "bank-accounts",
where: { ownerUser: { equals: id } },
limit: 5,
depth: 0,
overrideAccess: true,
})
.catch(() => null);
for (const account of accounts?.docs ?? []) {
await pg
.delete({ collection: "bank-accounts", id: account.id, overrideAccess: true })
.catch(() => {});
}
const profiles = await pg
.find({
collection: "profiles",
where: { user: { equals: id } },
limit: 5,
depth: 0,
overrideAccess: true,
})
.catch(() => null);
for (const profile of profiles?.docs ?? []) {
await pg
.delete({ collection: "profiles", id: profile.id, overrideAccess: true })
.catch(() => {});
}
await pg.delete({ collection: "users", id, overrideAccess: true }).catch(() => {});
};
describe("Wiki Documentation category", () => {
let plainUser: User;
let adminUser: User;
let developerUser: User;
const userIds: number[] = [];
const pageIds: number[] = [];
const makeUser = async (username: string, roles: User["roles"]): Promise<User> => {
const u = (await payload.create({
collection: "users",
data: {
username,
discordUsername: username,
displayName: "DOC TEST",
steamId: `7656119${Math.floor(Math.random() * 1e9)}`,
password: "Test123",
roles,
},
overrideAccess: true,
depth: 0,
})) as unknown as User;
userIds.push(u.id);
return u;
};
beforeAll(async () => {
const payloadConfig = await config;
payload = await getPayload({ config: payloadConfig });
plainUser = await makeUser(`${RUN}-plain`, ["user"]);
adminUser = await makeUser(`${RUN}-admin`, ["admin"]);
developerUser = await makeUser(`${RUN}-dev`, ["developer"]);
});
afterAll(async () => {
for (const id of pageIds) {
const revisions = await payload
.find({
collection: "wiki-revisions",
where: { page: { equals: id } },
limit: 500,
depth: 0,
overrideAccess: true,
})
.catch(() => null);
for (const revision of revisions?.docs ?? []) {
await payload
.delete({ collection: "wiki-revisions", id: revision.id, overrideAccess: true })
.catch(() => {});
}
await payload.delete({ collection: "wiki-pages", id, overrideAccess: true }).catch(() => {});
}
for (const id of userIds) {
await deleteUserWithRelations(payload, id);
}
});
describe("categories list", () => {
it("includes Documentation in the shared category registry", () => {
expect(WIKI_CATEGORIES).toContain("Documentation");
expect(WIKI_CATEGORIES).toHaveLength(9);
});
it("keeps the collection select options in sync with the registry", () => {
const optionValues = WikiPages.fields.filter(
(field) => "name" in field && field.name === "category",
);
const options = (optionValues[0] as unknown as { options: Array<{ value: string }> }).options;
expect(options.map((option) => option.value)).toEqual([...WIKI_CATEGORIES]);
});
});
describe("create gating", () => {
it("a plain user cannot create a Documentation page", async () => {
await expect(
payload.create({
collection: "wiki-pages",
data: {
title: `${RUN} Doc`,
slug: slugify(`${RUN} Doc`),
category: "Documentation",
body: "Body",
},
user: plainUser,
overrideAccess: false,
}),
).rejects.toThrow();
});
it("an admin can create a Documentation page", async () => {
const page = (await payload.create({
collection: "wiki-pages",
data: {
title: `${RUN} Admin Doc`,
slug: slugify(`Admin Doc`),
category: "Documentation",
body: "Body",
},
user: adminUser,
overrideAccess: false,
})) as unknown as WikiPage;
pageIds.push(page.id);
expect(page.category).toBe("Documentation");
});
it("a developer can create a Documentation page", async () => {
const page = (await payload.create({
collection: "wiki-pages",
data: {
title: `${RUN} Dev Doc`,
slug: slugify(`Dev Doc`),
category: "Documentation",
body: "Body",
},
user: developerUser,
overrideAccess: false,
})) as unknown as WikiPage;
pageIds.push(page.id);
expect(page.category).toBe("Documentation");
});
it("a plain user can still create pages in other categories", async () => {
const page = (await payload.create({
collection: "wiki-pages",
data: {
title: `${RUN} User Lore`,
slug: slugify(`User Lore`),
category: "Lore",
body: "Body",
},
user: plainUser,
overrideAccess: false,
})) as unknown as WikiPage;
pageIds.push(page.id);
expect(page.category).toBe("Lore");
});
it("a guest cannot create anything", async () => {
await expect(
payload.create({
collection: "wiki-pages",
data: {
title: `${RUN} Guest`,
slug: slugify(`${RUN} Guest`),
category: "Guides",
body: "Body",
},
overrideAccess: false,
}),
).rejects.toThrow();
});
});
describe("update gating", () => {
it("a plain user cannot edit an existing Documentation page", async () => {
const page = (await payload.create({
collection: "wiki-pages",
data: {
title: `${RUN} Locked Doc`,
slug: slugify(`Locked Doc`),
category: "Documentation",
body: "v1",
},
user: adminUser,
overrideAccess: true,
})) as unknown as WikiPage;
pageIds.push(page.id);
await expect(
payload.update({
collection: "wiki-pages",
id: page.id,
data: { body: "v2 by plain user" },
user: plainUser,
overrideAccess: false,
}),
).rejects.toThrow();
});
it("a developer can edit an existing Documentation page", async () => {
const page = (await payload.create({
collection: "wiki-pages",
data: {
title: `${RUN} Dev Edit Doc`,
slug: slugify(`Dev Edit Doc`),
category: "Documentation",
body: "v1",
},
user: adminUser,
overrideAccess: true,
})) as unknown as WikiPage;
pageIds.push(page.id);
const updated = (await payload.update({
collection: "wiki-pages",
id: page.id,
data: { body: "v2 by developer" },
user: developerUser,
overrideAccess: false,
})) as unknown as WikiPage;
expect(updated.body).toBe("v2 by developer");
});
it("a plain user cannot escalate another category into Documentation", async () => {
const page = (await payload.create({
collection: "wiki-pages",
data: {
title: `${RUN} Escalate`,
slug: slugify(`Escalate`),
category: "Guides",
body: "Body",
},
user: plainUser,
overrideAccess: true,
})) as unknown as WikiPage;
pageIds.push(page.id);
await expect(
payload.update({
collection: "wiki-pages",
id: page.id,
data: { category: "Documentation" },
user: plainUser,
overrideAccess: false,
}),
).rejects.toThrow();
});
it("a plain user can still edit pages in other categories", async () => {
const page = (await payload.create({
collection: "wiki-pages",
data: {
title: `${RUN} Still Editable`,
slug: slugify(`Still Editable`),
category: "Meta",
body: "v1",
},
user: plainUser,
overrideAccess: true,
})) as unknown as WikiPage;
pageIds.push(page.id);
const updated = (await payload.update({
collection: "wiki-pages",
id: page.id,
data: { body: "v2" },
user: plainUser,
overrideAccess: false,
})) as unknown as WikiPage;
expect(updated.body).toBe("v2");
});
});
describe("read gating", () => {
it("any logged-in user can read Documentation pages", async () => {
const result = await payload.find({
collection: "wiki-pages",
where: { category: { equals: "Documentation" } },
user: plainUser,
overrideAccess: false,
});
expect(result.docs.length).toBeGreaterThan(0);
});
it("a guest is forbidden from reading wiki pages", async () => {
await expect(
payload.find({
collection: "wiki-pages",
where: { category: { equals: "Documentation" } },
overrideAccess: false,
}),
).rejects.toThrow(/not allowed/i);
});
});
describe("slug integration", () => {
it("slugifies Documentation titles the same as any other category", () => {
expect(slugify("Logistics and Storage Rules")).toBe("logistics-and-storage-rules");
});
});
});