kanban-dev-webapp/tests/integration/intake-link-submissions.test.ts

181 lines
5.2 KiB
TypeScript
Raw Normal View History

import { describe, it, expect, beforeAll, afterAll } from "vitest";
import PocketBase from "pocketbase";
import { randomBytes } from "node:crypto";
import { APP_URL, DEMO, adminPb, requireServices } from "./helpers";
let admin: PocketBase;
let projectId: string;
let secondProjectId: string;
let clientId: string;
const createdLinkIds: string[] = [];
const createdCardIds: string[] = [];
const createdSubmissionIds: string[] = [];
function makeToken(): string {
return randomBytes(24).toString("base64url");
}
async function newPhase9Link() {
const token = makeToken();
const link = await admin.collection("intake_links").create({
project: "",
client: clientId,
label: "phase9 cap test",
token,
active: true,
expires_at: "",
max_submissions_per_day: 2,
submission_count: 0,
submission_count_today: 0,
submission_count_today_date: "",
last_submission_at: "",
notes: "",
});
createdLinkIds.push(link.id);
return link;
}
beforeAll(async () => {
await requireServices();
admin = await adminPb();
const project = await admin
.collection("projects")
.getFirstListItem(`slug = "${DEMO.projectSlug}"`);
projectId = project.id;
clientId = project.client;
const existing = await admin
.collection("projects")
.getFullList({
filter: `client = "${clientId}" && name = "Phase9 cap test second"`,
});
if (existing.length > 0) {
secondProjectId = existing[0].id;
} else {
const second = await admin.collection("projects").create({
client: clientId,
name: "Phase9 cap test second",
slug: `phase9-cap-test-second-${Date.now()}`,
status: "active",
});
secondProjectId = second.id;
}
});
afterAll(async () => {
for (const id of createdCardIds) {
await admin.collection("cards").delete(id).catch(() => {});
}
for (const id of createdLinkIds) {
await admin.collection("intake_links").delete(id).catch(() => {});
}
for (const id of createdSubmissionIds) {
await admin
.collection("intake_link_submissions")
.delete(id)
.catch(() => {});
}
await admin
.collection("projects")
.update(projectId, { show_in_intake: false })
.catch(() => {});
await admin
.collection("projects")
.update(secondProjectId, { show_in_intake: false })
.catch(() => {});
});
describe("intake_link_submissions schema lands and tracks per-(link, project)", () => {
it("creates a row, reads it back with the expected fields", async () => {
const link = await newPhase9Link();
const today = new Date().toISOString().slice(0, 10);
// Direct PocketBase write — exercises the schema only.
const row = await admin.collection("intake_link_submissions").create({
link: link.id,
project: projectId,
date: today,
count: 1,
});
createdSubmissionIds.push(row.id);
expect(row.link).toBe(link.id);
expect(row.project).toBe(projectId);
expect(row.date).toContain(today);
expect(row.count).toBe(1);
const readBack = await admin
.collection("intake_link_submissions")
.getOne(row.id);
expect(readBack.count).toBe(1);
// A second row for the same link, different project, same date — must
// succeed because the unique index is on the COMPOSITE (link, project,
// date), not on date alone. This is the property Phase 9 needs.
const row2 = await admin.collection("intake_link_submissions").create({
link: link.id,
project: secondProjectId,
date: today,
count: 1,
});
createdSubmissionIds.push(row2.id);
expect(row2.id).not.toBe(row.id);
});
it("the unique index blocks a duplicate (link, project, date)", async () => {
const link = await newPhase9Link();
const today = new Date().toISOString().slice(0, 10);
await admin.collection("intake_link_submissions").create({
link: link.id,
project: projectId,
date: today,
count: 1,
}).then((r) => createdSubmissionIds.push(r.id));
// Same (link, project, date) again — should fail.
let rejected = false;
try {
await admin.collection("intake_link_submissions").create({
link: link.id,
project: projectId,
date: today,
count: 2,
});
} catch (e) {
rejected = true;
// Cleanup the row the rejected create might have left.
try {
const todayDate = new Date().toISOString().slice(0, 10);
const all = await admin.collection("intake_link_submissions").getFullList({
filter: `link = "${link.id}" && project = "${projectId}" && date = "${todayDate}"`,
});
for (const r of all) createdSubmissionIds.push(r.id);
} catch {
// ignore
}
}
expect(rejected).toBe(true);
});
});
describe("the public route exists for Phase 9", () => {
it("returns a sensible status, never 500, on a Phase 9 link", async () => {
const link = await newPhase9Link();
const response = await fetch(`${APP_URL}/api/public/intake`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
token: link.token,
brief: { title: "x", summary: "y", subtasks: [], area: "", degraded: false },
transcript: [],
}),
});
expect([400, 404, 409]).toContain(response.status);
});
});