kanban-dev-webapp/tests/integration/intake-submit.test.ts

171 lines
5.4 KiB
TypeScript

import { describe, it, expect, beforeAll, afterAll } from "vitest";
import PocketBase from "pocketbase";
import { APP_URL, DEMO, adminPb, requireServices, signIn } from "./helpers";
let admin: PocketBase;
let cookie: string;
let projectId: string;
let inboxId: string;
let doneId: string;
const createdCards: 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;
inboxId = (
await admin
.collection("columns")
.getFirstListItem(`project = "${projectId}" && is_inbox = true`)
).id;
doneId = (
await admin
.collection("columns")
.getFirstListItem(`project = "${projectId}" && is_done = true`)
).id;
});
afterAll(async () => {
for (const id of createdCards) {
await admin.collection("cards").delete(id).catch(() => {});
}
});
const brief = {
title: "Add a POS at the counter",
summary: "Till staff need to take card payments.",
subtasks: ["Choose a card reader", "Wire it to the till"],
openQuestions: ["Which reader is in use today?"],
area: "",
degraded: false,
};
describe("a brief becomes an Inbox card with sub-tasks", () => {
it("creates the card, the subtasks and the activity row", async () => {
const response = await fetch(`${APP_URL}/api/client/intake`, {
method: "POST",
headers: { "content-type": "application/json", cookie },
body: JSON.stringify({
project: projectId,
brief,
transcript: [{ question: "What would you like?", answer: "A POS." }],
sizeBand: "medium",
}),
});
expect(response.status).toBe(201);
const { card } = await response.json();
createdCards.push(card.id);
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");
expect(created.title).toBe(brief.title);
expect(created.size_band).toBe("medium");
expect(created.number).toBeGreaterThan(0);
const subtasks = await admin
.collection("subtasks")
.getFullList({ filter: `card = "${card.id}"`, sort: "position" });
expect(subtasks.map((s) => s.title)).toEqual(brief.subtasks);
const activity = await admin
.collection("activity")
.getFullList({ filter: `card = "${card.id}"` });
expect(activity.length).toBeGreaterThan(0);
});
});
describe("the server decides the fields that matter", () => {
it("ignores an escalating brief entirely", async () => {
// The whole security property in one test: whatever the body claims, the
// card lands in the Inbox at medium priority, unaccepted.
const response = await fetch(`${APP_URL}/api/client/intake`, {
method: "POST",
headers: { "content-type": "application/json", cookie },
body: JSON.stringify({
project: projectId,
brief: { ...brief, priority: "critical", accepted: true, column: doneId },
transcript: [],
sizeBand: "large",
// Top-level attempts too, not only inside the brief.
priority: "critical",
accepted: true,
column: doneId,
created_by_role: "owner",
}),
});
expect(response.status).toBe(201);
const { card } = await response.json();
createdCards.push(card.id);
const created = await admin.collection("cards").getOne(card.id);
expect(created.column).toBe(inboxId);
expect(created.priority).toBe("medium");
expect(created.accepted).toBe(false);
expect(created.created_by_role).toBe("client");
});
it("rejects an unknown size band rather than storing it", async () => {
const response = await fetch(`${APP_URL}/api/client/intake`, {
method: "POST",
headers: { "content-type": "application/json", cookie },
body: JSON.stringify({
project: projectId,
brief,
transcript: [],
sizeBand: "enormous",
}),
});
const { card } = await response.json();
createdCards.push(card.id);
const created = await admin.collection("cards").getOne(card.id);
expect(created.size_band).toBe("");
});
});
describe("the intake route is scoped like every other client route", () => {
it("refuses another client's project", async () => {
const stamp = Date.now();
const other = await admin
.collection("clients")
.create({ name: "Intake scope probe", slug: `isp-${stamp}`, active: true });
const otherProject = await admin.collection("projects").create({
client: other.id,
name: "Probe",
slug: `isp-${stamp}`,
status: "active",
});
const response = await fetch(`${APP_URL}/api/client/intake`, {
method: "POST",
headers: { "content-type": "application/json", cookie },
body: JSON.stringify({ project: otherProject.id, brief, transcript: [] }),
});
expect(response.status).toBe(404);
await admin.collection("projects").delete(otherProject.id);
await admin.collection("clients").delete(other.id);
});
it("refuses an unauthenticated request", async () => {
const response = await fetch(`${APP_URL}/api/client/intake`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ project: projectId, brief, transcript: [] }),
});
expect(response.status).toBe(401);
});
});