kanban-dev-webapp/tests/integration/client-permissions.test.ts

174 lines
5.3 KiB
TypeScript
Raw Normal View History

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);
});
});