diff --git a/tests/integration/admin.test.ts b/tests/integration/admin.test.ts new file mode 100644 index 0000000..7011ce9 --- /dev/null +++ b/tests/integration/admin.test.ts @@ -0,0 +1,110 @@ +import { describe, it, expect, beforeAll } from "vitest"; +import PocketBase from "pocketbase"; + +import { APP_URL, DEMO, adminPb, requireServices, signIn, userPb } from "./helpers"; + +let admin: PocketBase; + +beforeAll(async () => { + await requireServices(); + admin = await adminPb(); +}); + +describe("a project is never created without a usable board", () => { + it("every project has exactly one inbox and one done column", async () => { + // A project missing either makes POST /api/client/cards answer 409, which + // the client discovers days later when they try to file a request. + const projects = await admin.collection("projects").getFullList(); + expect(projects.length).toBeGreaterThan(0); + + for (const project of projects) { + const columns = await admin + .collection("columns") + .getFullList({ filter: `project = "${project.id}"` }); + + expect( + columns.filter((c) => c.is_inbox), + `project ${project.slug} inbox columns` + ).toHaveLength(1); + expect( + columns.filter((c) => c.is_done), + `project ${project.slug} done columns` + ).toHaveLength(1); + } + }); +}); + +describe("the admin area is owner-only", () => { + it("does not let a client manager list other accounts", async () => { + const pb = await userPb(DEMO.managerEmail, DEMO.managerPassword); + const users = await pb.collection("users").getFullList(); + expect(users).toHaveLength(1); + }); + + it("does not let a client manager create a client", async () => { + const pb = await userPb(DEMO.managerEmail, DEMO.managerPassword); + await expect( + pb.collection("clients").create({ + name: "Injected", + slug: `injected-${Date.now()}`, + active: true, + }) + ).rejects.toThrow(); + }); + + it("does not let a client manager create a project", async () => { + const pb = await userPb(DEMO.managerEmail, DEMO.managerPassword); + const client = await admin + .collection("clients") + .getFirstListItem(`slug = "${DEMO.clientSlug}"`); + + await expect( + pb.collection("projects").create({ + client: client.id, + name: "Injected", + slug: `injected-${Date.now()}`, + status: "active", + }) + ).rejects.toThrow(); + }); +}); + +describe("deactivated accounts cannot sign in", () => { + it("is refused with the same message as a wrong password", async () => { + const email = `deactivated-${Date.now()}@example.com`; + const password = "probe-password-123"; + const client = await admin + .collection("clients") + .getFirstListItem(`slug = "${DEMO.clientSlug}"`); + + const user = await admin.collection("users").create({ + email, + password, + passwordConfirm: password, + name: "Deactivated probe", + role: "client_manager", + client: client.id, + active: false, + verified: true, + }); + + const response = await fetch(`${APP_URL}/api/auth/login`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ email, password }), + }); + + expect(response.status).toBe(401); + const body = await response.json(); + expect(body.error).toBe("Invalid email or password"); + + await admin.collection("users").delete(user.id); + }); + + it("still lets an active account in", async () => { + // Guards against the active check rejecting everyone, which would look + // identical to a wrong password and lock the product. + const cookie = await signIn(DEMO.managerEmail, DEMO.managerPassword); + expect(cookie).toContain("pb_auth="); + }); +}); \ No newline at end of file