diff --git a/tests/integration/owner-actions.test.ts b/tests/integration/owner-actions.test.ts new file mode 100644 index 0000000..47c4560 --- /dev/null +++ b/tests/integration/owner-actions.test.ts @@ -0,0 +1,81 @@ +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 managerCookie: string; +let projectId: string; +let inboxId: string; + +beforeAll(async () => { + await requireServices(); + admin = await adminPb(); + managerCookie = await signIn(DEMO.managerEmail, DEMO.managerPassword); + + const project = await admin + .collection("projects") + .getFirstListItem(`slug = "${DEMO.projectSlug}"`); + projectId = project.id; + + inboxId = ( + await admin + .collection("columns") + .getFirstListItem(`project = "${projectId}" && is_inbox = true`) + ).id; +}); + +describe("a client manager cannot write card fields the owner owns", () => { + it("is refused by PocketBase on a direct subtask create", async () => { + const card = await admin.collection("cards").getFirstListItem( + `project = "${projectId}"` + ); + const pb = await userPb(DEMO.managerEmail, DEMO.managerPassword); + + await expect( + pb.collection("subtasks").create({ + card: card.id, + title: "injected", + done: false, + position: 1000, + }) + ).rejects.toThrow(); + }); + + it("is refused by PocketBase on a direct activity insert", async () => { + // activity.createRule is superuser-only. If this ever succeeds, someone + // relaxed the rule and the audit log can be forged. + const pb = await userPb(DEMO.managerEmail, DEMO.managerPassword); + + await expect( + pb.collection("activity").create({ + project: projectId, + actor: "x", + action: "card_accepted", + }) + ).rejects.toThrow(); + }); +}); + +describe("a client request carries the flags the accept flow depends on", () => { + it("lands unaccepted, client-authored, with no start date", async () => { + const response = await fetch(`${APP_URL}/api/client/cards`, { + method: "POST", + headers: { "content-type": "application/json", cookie: managerCookie }, + body: JSON.stringify({ project: projectId, title: "Accept-flow probe" }), + }); + expect(response.status).toBe(201); + + const { card } = await response.json(); + const created = await admin.collection("cards").getOne(card.id); + + expect(created.column).toBe(inboxId); + expect(created.accepted).toBe(false); + expect(created.created_by_role).toBe("client"); + // start_date must be empty until acceptance — Phase 2 uses it to bound + // which commits this card may claim. + expect(created.start_date).toBeFalsy(); + + await admin.collection("cards").delete(card.id); + }); +}); \ No newline at end of file