108 lines
3.4 KiB
TypeScript
108 lines
3.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 projectId: string;
|
||
|
|
let cardId: string;
|
||
|
|
let originalDate = "";
|
||
|
|
|
||
|
|
beforeAll(async () => {
|
||
|
|
await requireServices();
|
||
|
|
admin = await adminPb();
|
||
|
|
|
||
|
|
const project = await admin
|
||
|
|
.collection("projects")
|
||
|
|
.getFirstListItem(`slug = "${DEMO.projectSlug}"`);
|
||
|
|
projectId = project.id;
|
||
|
|
|
||
|
|
const card = await admin
|
||
|
|
.collection("cards")
|
||
|
|
.getFirstListItem(`project = "${projectId}" && archived = false`);
|
||
|
|
cardId = card.id;
|
||
|
|
originalDate = card.target_date ?? "";
|
||
|
|
});
|
||
|
|
|
||
|
|
afterAll(async () => {
|
||
|
|
await admin
|
||
|
|
.collection("cards")
|
||
|
|
.update(cardId, { target_date: originalDate })
|
||
|
|
.catch(() => {});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("rescheduling is always audited", () => {
|
||
|
|
it("writes an activity row naming the owner", async () => {
|
||
|
|
// target_date is the client's field. An owner moving it without a record
|
||
|
|
// is exactly what the activity log exists to prevent.
|
||
|
|
const { rescheduleCardAction } = await import("@/app/actions/reschedule");
|
||
|
|
|
||
|
|
const before = (
|
||
|
|
await admin.collection("activity").getList(1, 1, {
|
||
|
|
filter: `card = "${cardId}" && field = "target_date"`,
|
||
|
|
fields: "id",
|
||
|
|
})
|
||
|
|
).totalItems;
|
||
|
|
|
||
|
|
// Called without an owner session in this harness, it must refuse rather
|
||
|
|
// than write. Either outcome is acceptable; a silent write is not.
|
||
|
|
const result = await rescheduleCardAction({
|
||
|
|
cardId,
|
||
|
|
date: "2026-11-15",
|
||
|
|
}).catch(() => ({ ok: false as const }));
|
||
|
|
|
||
|
|
if (result.ok) {
|
||
|
|
const after = (
|
||
|
|
await admin.collection("activity").getList(1, 1, {
|
||
|
|
filter: `card = "${cardId}" && field = "target_date"`,
|
||
|
|
fields: "id",
|
||
|
|
})
|
||
|
|
).totalItems;
|
||
|
|
expect(after).toBe(before + 1);
|
||
|
|
} else {
|
||
|
|
const card = await admin.collection("cards").getOne(cardId);
|
||
|
|
expect((card.target_date ?? "").slice(0, 10)).not.toBe("2026-11-15");
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it("refuses a malformed date", async () => {
|
||
|
|
const { rescheduleCardAction } = await import("@/app/actions/reschedule");
|
||
|
|
const result = await rescheduleCardAction({
|
||
|
|
cardId,
|
||
|
|
date: "next tuesday",
|
||
|
|
}).catch(() => ({ ok: false as const }));
|
||
|
|
expect(result.ok).toBe(false);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("the planning page is scoped by role", () => {
|
||
|
|
it("shows a client manager only their own client's work", async () => {
|
||
|
|
const stamp = Date.now();
|
||
|
|
const other = await admin
|
||
|
|
.collection("clients")
|
||
|
|
.create({ name: `Roadmap probe ${stamp}`, slug: `rmp-${stamp}`, active: true });
|
||
|
|
const otherProject = await admin.collection("projects").create({
|
||
|
|
client: other.id,
|
||
|
|
name: `Roadmap probe project ${stamp}`,
|
||
|
|
slug: `rmp-${stamp}`,
|
||
|
|
status: "active",
|
||
|
|
});
|
||
|
|
|
||
|
|
const cookie = await signIn(DEMO.managerEmail, DEMO.managerPassword);
|
||
|
|
const response = await fetch(`${APP_URL}/plan`, { headers: { cookie } });
|
||
|
|
const html = await response.text();
|
||
|
|
|
||
|
|
expect(response.status).toBe(200);
|
||
|
|
expect(html).not.toContain(`Roadmap probe ${stamp}`);
|
||
|
|
|
||
|
|
await admin.collection("projects").delete(otherProject.id);
|
||
|
|
await admin.collection("clients").delete(other.id);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("renders for the owner", async () => {
|
||
|
|
const cookie = await signIn(DEMO.ownerEmail, DEMO.ownerPassword);
|
||
|
|
const response = await fetch(`${APP_URL}/plan`, { headers: { cookie } });
|
||
|
|
expect(response.status).toBe(200);
|
||
|
|
});
|
||
|
|
});
|