174 lines
5.3 KiB
TypeScript
174 lines
5.3 KiB
TypeScript
|
|
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);
|
||
|
|
});
|
||
|
|
});
|