test(intake): server-decided fields and project scoping
This commit is contained in:
parent
5a9d1cb97f
commit
8a2fb46788
1 changed files with 171 additions and 0 deletions
171
tests/integration/intake-submit.test.ts
Normal file
171
tests/integration/intake-submit.test.ts
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
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);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue