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:
parent
d1ae4c3bb7
commit
471e4eaacf
1 changed files with 330 additions and 0 deletions
330
tests/int/wiki-documentation.int.spec.ts
Normal file
330
tests/int/wiki-documentation.int.spec.ts
Normal 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue