import { describe, it, expect, beforeAll } from "vitest"; import PocketBase from "pocketbase"; import { APP_URL, DEMO, adminPb, requireServices, signIn, userPb, } from "./helpers"; let admin: PocketBase; let cookie: string; let projectId: string; let cardId: string; let inboxColumnId: string; let doneColumnId: string; beforeAll(async () => { await requireServices(); admin = await adminPb(); cookie = await signIn(DEMO.managerEmail, DEMO.managerPassword); const project = await admin .collection("projects") .getFirstListItem(`slug = "${DEMO.projectSlug}"`); projectId = project.id; const card = await admin .collection("cards") .getFirstListItem(`project = "${projectId}"`); cardId = card.id; inboxColumnId = ( await admin .collection("columns") .getFirstListItem(`project = "${projectId}" && is_inbox = true`) ).id; doneColumnId = ( await admin .collection("columns") .getFirstListItem(`project = "${projectId}" && is_done = true`) ).id; }); /** Spec §9 test 1 */ describe("a client manager cannot write non-whitelisted card fields", () => { it("leaves column, title and accepted untouched", async () => { const before = await admin.collection("cards").getOne(cardId); const response = await fetch(`${APP_URL}/api/client/cards/${cardId}`, { method: "PATCH", headers: { "content-type": "application/json", cookie }, body: JSON.stringify({ priority: "critical", column: doneColumnId, title: "hacked", accepted: false, archived: true, scope_rule: "stock", }), }); expect(response.status).toBe(200); const after = await admin.collection("cards").getOne(cardId); expect(after.priority).toBe("critical"); // whitelisted, applied expect(after.column).toBe(before.column); // not whitelisted, unchanged expect(after.title).toBe(before.title); expect(after.accepted).toBe(before.accepted); expect(after.archived).toBe(before.archived); expect(after.scope_rule).toBe(before.scope_rule); }); it("refuses a direct PocketBase update with the manager's own token", async () => { const pb = await userPb(DEMO.managerEmail, DEMO.managerPassword); await expect( pb.collection("cards").update(cardId, { title: "hacked directly" }) ).rejects.toThrow(); const after = await admin.collection("cards").getOne(cardId); expect(after.title).not.toBe("hacked directly"); }); }); /** Spec §9 test 2 */ describe("a client manager cannot reach another client's project", () => { it("returns no records for a foreign project", async () => { const otherClient = await admin.collection("clients").create({ name: "Mega Infrastructure", slug: `mega-${Date.now()}`, active: true, }); const otherProject = await admin.collection("projects").create({ client: otherClient.id, name: "Mega Plant", slug: `mega-plant-${Date.now()}`, status: "active", }); const pb = await userPb(DEMO.managerEmail, DEMO.managerPassword); await expect( pb.collection("projects").getOne(otherProject.id) ).rejects.toThrow(); const visible = await pb.collection("projects").getFullList(); expect(visible.map((p) => p.id)).not.toContain(otherProject.id); const response = await fetch(`${APP_URL}/api/client/cards`, { method: "POST", headers: { "content-type": "application/json", cookie }, body: JSON.stringify({ project: otherProject.id, title: "sneaky" }), }); expect(response.status).toBe(404); await admin.collection("projects").delete(otherProject.id); await admin.collection("clients").delete(otherClient.id); }); }); /** Spec §9 test 3 */ describe("client-created cards land in the Inbox", () => { it("ignores the requested column, priority and accepted flag", async () => { const response = await fetch(`${APP_URL}/api/client/cards`, { method: "POST", headers: { "content-type": "application/json", cookie }, body: JSON.stringify({ project: projectId, title: "Add a POS system", description: "Card payments at the counter.", column: doneColumnId, priority: "critical", accepted: true, }), }); expect(response.status).toBe(201); const { card } = await response.json(); const created = await admin.collection("cards").getOne(card.id); expect(created.column).toBe(inboxColumnId); expect(created.priority).toBe("medium"); expect(created.accepted).toBe(false); expect(created.created_by_role).toBe("client"); expect(created.title).toBe("Add a POS system"); await admin.collection("cards").delete(card.id); }); it("rejects a create with no title", async () => { const response = await fetch(`${APP_URL}/api/client/cards`, { method: "POST", headers: { "content-type": "application/json", cookie }, body: JSON.stringify({ project: projectId, description: "no title" }), }); expect(response.status).toBe(400); }); }); describe("unauthenticated requests", () => { it("are refused without a session cookie", async () => { const response = await fetch(`${APP_URL}/api/client/cards/${cardId}`, { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ priority: "low" }), }); expect(response.status).toBe(401); }); });