diff --git a/tests/int/wiki-documentation.int.spec.ts b/tests/int/wiki-documentation.int.spec.ts new file mode 100644 index 0000000..397a2b6 --- /dev/null +++ b/tests/int/wiki-documentation.int.spec.ts @@ -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 => { + 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 => { + 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"); + }); + }); +});