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.
73 lines
2.7 KiB
TypeScript
73 lines
2.7 KiB
TypeScript
import "@/test-utils/named-admin-session";
|
|
import { getCurrentUser } from "@/lib/auth/session";
|
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
|
|
const getFullListUsersMock = vi.fn();
|
|
const getFullListThreadsMock = vi.fn();
|
|
const createThreadMock = 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 { getFullList: getFullListThreadsMock, create: createThreadMock };
|
|
}
|
|
throw new Error(`unexpected collection in test: ${name}`);
|
|
},
|
|
})),
|
|
}));
|
|
|
|
import { GET, POST } from "./route";
|
|
|
|
function req(method: string, headers: Record<string, string> = {}, body?: unknown): Request {
|
|
return new Request("http://x/api/chat/threads", {
|
|
method,
|
|
headers,
|
|
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
});
|
|
}
|
|
|
|
describe("/api/chat/threads", () => {
|
|
beforeEach(() => {
|
|
getFullListUsersMock.mockReset();
|
|
getFullListThreadsMock.mockReset();
|
|
createThreadMock.mockReset();
|
|
getFullListUsersMock.mockResolvedValue([{ id: "u1", name: "Dashboard" }]);
|
|
});
|
|
|
|
it("GET requires a named admin session", async () => {
|
|
vi.mocked(getCurrentUser).mockResolvedValueOnce(null);
|
|
const res = await GET(req("GET"));
|
|
expect(res.status).toBe(403);
|
|
});
|
|
|
|
it("GET lists the actor's threads newest first", async () => {
|
|
getFullListThreadsMock.mockResolvedValue([{ id: "t1", title: "Hi" }]);
|
|
const res = await GET(req("GET", { "x-ask-user": "u1" }));
|
|
expect(await res.json()).toEqual({ threads: [{ id: "t1", title: "Hi" }] });
|
|
expect(getFullListThreadsMock.mock.calls[0][0]).toMatchObject({
|
|
filter: "owner = u1",
|
|
sort: "-last_message_at,-created",
|
|
});
|
|
});
|
|
|
|
it("POST creates a thread owned by the actor", async () => {
|
|
createThreadMock.mockResolvedValue({ id: "t9", title: "New thread" });
|
|
const res = await POST(req("POST", { "x-ask-user": "u1" }));
|
|
expect(await res.json()).toEqual({ id: "t9", title: "New thread" });
|
|
expect(createThreadMock.mock.calls[0][0]).toMatchObject({ owner: "u1", archived: false });
|
|
});
|
|
|
|
it("POST uses a supplied title", async () => {
|
|
createThreadMock.mockResolvedValue({ id: "t9", title: "Fuel report" });
|
|
const res = await POST(req("POST", { "x-ask-user": "u1" }, { title: "Fuel report" }));
|
|
expect((await res.json()).title).toBe("Fuel report");
|
|
});
|
|
});
|