MEGAINFRA-WEBAPP/src/app/api/chat/threads/[id]/route.test.ts
DEls 9a6aed50a4 feat(management): add admin planning dashboards
Add turnover, project, retention, and year comparison pages from the supplied workbook, protected by named admin access. Include the local chart gallery, grouped monthly hover, smooth comparison toggle, and monthly income minus cost series.

Require named PocketBase sign-in in every environment and remove the shared PIN bypass. Update access documentation and business-route test fixtures.
2026-10-09 10:16:50 +02:00

102 lines
4.1 KiB
TypeScript

import "@/test-utils/named-admin-session";
import { describe, it, expect, vi, beforeEach } from "vitest";
const getFullListUsersMock = vi.fn();
const getOneThreadMock = vi.fn();
const getFullListMessagesMock = vi.fn();
const updateThreadMock = vi.fn();
const deleteThreadMock = vi.fn();
const filterMock = vi.fn((expr: string, params: Record<string, unknown>) => {
let out = expr;
for (const [k, v] of Object.entries(params)) out = out.replace(`{:${k}}`, String(v));
return out;
});
vi.mock("@/lib/pb", () => ({
pbAdmin: vi.fn(async () => ({
filter: filterMock,
collection: (name: string) => {
if (name === "users") return { getFullList: getFullListUsersMock };
if (name === "ask_threads") {
return { getOne: getOneThreadMock, update: updateThreadMock, delete: deleteThreadMock };
}
if (name === "ask_messages") return { getFullList: getFullListMessagesMock };
throw new Error(`unexpected collection in test: ${name}`);
},
})),
}));
import { GET, PATCH, DELETE } from "./route";
function req(method: string, headers: Record<string, string> = {}, body?: unknown): Request {
return new Request("http://x/api/chat/threads/t1", {
method,
headers,
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
});
}
const params = Promise.resolve({ id: "t1" });
describe("/api/chat/threads/[id]", () => {
beforeEach(() => {
getFullListUsersMock.mockReset();
getOneThreadMock.mockReset();
getFullListMessagesMock.mockReset();
updateThreadMock.mockReset();
deleteThreadMock.mockReset();
getFullListUsersMock.mockResolvedValue([{ id: "u1", name: "Dashboard" }]);
});
it("GET 404s a thread owned by someone else", async () => {
getOneThreadMock.mockResolvedValue({ id: "t1", owner: "someone-else" });
const res = await GET(req("GET", { "x-ask-user": "u1" }), { params });
expect(res.status).toBe(404);
});
it("GET 404s a thread that doesn't exist", async () => {
getOneThreadMock.mockRejectedValue({ status: 404 });
const res = await GET(req("GET", { "x-ask-user": "u1" }), { params });
expect(res.status).toBe(404);
});
it("GET returns the thread and its rows", async () => {
getOneThreadMock.mockResolvedValue({ id: "t1", owner: "u1", title: "Hi" });
getFullListMessagesMock.mockResolvedValue([{ id: "m1", kind: "user", text: "hi" }]);
const res = await GET(req("GET", { "x-ask-user": "u1" }), { params });
const body = await res.json();
expect(body.thread).toMatchObject({ id: "t1" });
expect(body.rows).toHaveLength(1);
});
it("GET sorts messages by created then id, so equal-created rows are stable", async () => {
getOneThreadMock.mockResolvedValue({ id: "t1", owner: "u1" });
getFullListMessagesMock.mockResolvedValue([]);
await GET(req("GET", { "x-ask-user": "u1" }), { params });
expect(getFullListMessagesMock.mock.calls[0][0]).toMatchObject({ sort: "created,id" });
});
it("PATCH renames and archives", async () => {
getOneThreadMock.mockResolvedValue({ id: "t1", owner: "u1" });
updateThreadMock.mockResolvedValue({ id: "t1", title: "Renamed", archived: true });
const res = await PATCH(
req("PATCH", { "x-ask-user": "u1" }, { title: "Renamed", archived: true }),
{ params },
);
expect(await res.json()).toEqual({ thread: { id: "t1", title: "Renamed", archived: true } });
expect(updateThreadMock.mock.calls[0]).toEqual(["t1", { title: "Renamed", archived: true }]);
});
it("DELETE removes an owned thread", async () => {
getOneThreadMock.mockResolvedValue({ id: "t1", owner: "u1" });
const res = await DELETE(req("DELETE", { "x-ask-user": "u1" }), { params });
expect(await res.json()).toEqual({ ok: true });
expect(deleteThreadMock).toHaveBeenCalledWith("t1");
});
it("DELETE 404s someone else's thread without deleting", async () => {
getOneThreadMock.mockResolvedValue({ id: "t1", owner: "someone-else" });
const res = await DELETE(req("DELETE", { "x-ask-user": "u1" }), { params });
expect(res.status).toBe(404);
expect(deleteThreadMock).not.toHaveBeenCalled();
});
});