172 lines
5.4 KiB
TypeScript
172 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);
|
||
|
|
});
|
||
|
|
});
|