test(boss-dashboard): add route handler tests for dashboard and rep-activity

This commit is contained in:
Dave 2026-08-28 00:10:00 +02:00
parent f243132dc0
commit e9ce5f4fc4
2 changed files with 402 additions and 0 deletions

View file

@ -0,0 +1,216 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { NextRequest } from "next/server";
const assertBrowserSession = vi.fn();
const getBossRepActivity = vi.fn();
vi.mock("@/lib/server/browser-session", () => ({
assertBrowserSession: (...args: unknown[]) => assertBrowserSession(...args),
}));
vi.mock("@/lib/server/boss-dashboard-service", () => ({
getBossRepActivity: (...args: unknown[]) => getBossRepActivity(...args),
}));
import { GET } from "./route";
// --- Helpers --------------------------------------------------------------
function authedRequest(url: string) {
return new NextRequest(url, {
headers: { authorization: "Bearer test-token" },
});
}
function unauthedRequest(url: string) {
return new NextRequest(url);
}
// --- Tests ----------------------------------------------------------------
describe("GET /api/v1/boss-dashboard/rep-activity", () => {
beforeEach(() => {
vi.clearAllMocks();
assertBrowserSession.mockResolvedValue(undefined);
});
it("401s when the authorization header is missing", async () => {
// assertBrowserSession is what enforces auth; have it throw to mirror the
// real production behaviour when a missing header is forwarded.
assertBrowserSession.mockImplementation(() => {
throw Object.assign(new Error("Not authenticated."), { status: 401 });
});
const res = await GET(
unauthedRequest(
"http://localhost/api/v1/boss-dashboard/rep-activity?rep=u-1",
),
);
expect(res.status).toBe(401);
const body = await res.json();
expect(body.error).toMatch(/auth/i);
expect(getBossRepActivity).not.toHaveBeenCalled();
});
it("400s when the rep query param is missing", async () => {
const res = await GET(
authedRequest("http://localhost/api/v1/boss-dashboard/rep-activity"),
);
expect(res.status).toBe(400);
const body = await res.json();
expect(body.error).toMatch(/Missing rep/);
expect(getBossRepActivity).not.toHaveBeenCalled();
});
it("400s when the window query param is not in the allow-list", async () => {
const res = await GET(
authedRequest(
"http://localhost/api/v1/boss-dashboard/rep-activity?rep=u-1&window=garbage",
),
);
expect(res.status).toBe(400);
const body = await res.json();
expect(body.error).toMatch(/Invalid window/);
expect(getBossRepActivity).not.toHaveBeenCalled();
});
it("400s when the kind query param is not in the allow-list", async () => {
const res = await GET(
authedRequest(
"http://localhost/api/v1/boss-dashboard/rep-activity?rep=u-1&kind=garbage",
),
);
expect(res.status).toBe(400);
const body = await res.json();
expect(body.error).toMatch(/Invalid kind/);
expect(getBossRepActivity).not.toHaveBeenCalled();
});
it("returns 200 with the service payload (kind=quotes) on success", async () => {
getBossRepActivity.mockResolvedValueOnce({
items: [
{
id: "q-1",
number: "Q-2026-08-014",
customer: "North Pit",
status: "emailed",
ageMs: 1_000,
},
],
hasMore: false,
});
const res = await GET(
authedRequest(
"http://localhost/api/v1/boss-dashboard/rep-activity?rep=u-1&kind=quotes",
),
);
expect(res.status).toBe(200);
const body = await res.json();
expect(body.items).toHaveLength(1);
expect(body.items[0]).toMatchObject({
id: "q-1",
number: "Q-2026-08-014",
customer: "North Pit",
status: "emailed",
});
expect(body.hasMore).toBe(false);
// Defaults: window="today", page=1.
expect(getBossRepActivity).toHaveBeenCalledWith({
rep: "u-1",
window: "today",
kind: "quotes",
page: 1,
});
});
it("returns 200 with the service payload (kind=orders) on success", async () => {
getBossRepActivity.mockResolvedValueOnce({
items: [
{
id: "o-1",
number: "ORD-2026-08-009",
customer: "Anglo",
status: "pending",
ageMs: 5_000,
},
{
id: "o-2",
number: "ORD-2026-08-008",
customer: "Sasol",
status: "delivered",
ageMs: 10_000,
},
],
hasMore: true,
});
const res = await GET(
authedRequest(
"http://localhost/api/v1/boss-dashboard/rep-activity?rep=u-2&kind=orders",
),
);
expect(res.status).toBe(200);
const body = await res.json();
expect(body.items).toHaveLength(2);
expect(body.hasMore).toBe(true);
expect(getBossRepActivity).toHaveBeenCalledWith({
rep: "u-2",
window: "today",
kind: "orders",
page: 1,
});
});
it("forwards explicit window and page params to the service", async () => {
getBossRepActivity.mockResolvedValueOnce({ items: [], hasMore: false });
const res = await GET(
authedRequest(
"http://localhost/api/v1/boss-dashboard/rep-activity?rep=u-3&window=week&kind=quotes&page=4",
),
);
expect(res.status).toBe(200);
expect(getBossRepActivity).toHaveBeenCalledWith({
rep: "u-3",
window: "week",
kind: "quotes",
page: 4,
});
});
it("clamps page to the [1, 1000] range", async () => {
getBossRepActivity.mockResolvedValueOnce({ items: [], hasMore: false });
// page=0 → clamped up to 1.
await GET(
authedRequest(
"http://localhost/api/v1/boss-dashboard/rep-activity?rep=u-4&page=0",
),
);
expect(getBossRepActivity).toHaveBeenLastCalledWith(
expect.objectContaining({ page: 1 }),
);
// page=99999 → clamped down to 1000.
await GET(
authedRequest(
"http://localhost/api/v1/boss-dashboard/rep-activity?rep=u-4&page=99999",
),
);
expect(getBossRepActivity).toHaveBeenLastCalledWith(
expect.objectContaining({ page: 1000 }),
);
// Garbage page → falls back to 1.
await GET(
authedRequest(
"http://localhost/api/v1/boss-dashboard/rep-activity?rep=u-4&page=abc",
),
);
expect(getBossRepActivity).toHaveBeenLastCalledWith(
expect.objectContaining({ page: 1 }),
);
});
});

View file

@ -0,0 +1,186 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { NextRequest } from "next/server";
const getBossDashboardData = vi.fn();
vi.mock("@/lib/server/boss-dashboard-service", () => ({
getBossDashboardData: (...args: unknown[]) => getBossDashboardData(...args),
}));
import { GET } from "./route";
// --- Helpers --------------------------------------------------------------
const ORIGINAL_PB_URL = process.env.NEXT_PUBLIC_POCKETBASE_URL;
function authedRequest(url: string) {
return new NextRequest(url, {
headers: { authorization: "Bearer test-token" },
});
}
function unauthedRequest(url: string) {
return new NextRequest(url);
}
/**
* The dashboard route does an inline `fetch` to PB's auth-refresh endpoint
* (NOT the shared `assertBrowserSession` helper), so we mock globalThis.fetch
* here rather than mocking the helper module.
*/
function mockAuthRefreshOk(user: { id: string; name: string } = { id: "u1", name: "Boss" }) {
return vi
.spyOn(globalThis, "fetch")
.mockResolvedValueOnce(
new Response(JSON.stringify({ record: user }), {
status: 200,
headers: { "content-type": "application/json" },
}),
);
}
function mockAuthRefreshFail(status = 401) {
return vi
.spyOn(globalThis, "fetch")
.mockResolvedValueOnce(new Response("nope", { status }));
}
// --- Tests ----------------------------------------------------------------
describe("GET /api/v1/boss-dashboard", () => {
beforeEach(() => {
vi.clearAllMocks();
process.env.NEXT_PUBLIC_POCKETBASE_URL = "http://127.0.0.1:8090";
});
afterEach(() => {
if (ORIGINAL_PB_URL === undefined) delete process.env.NEXT_PUBLIC_POCKETBASE_URL;
else process.env.NEXT_PUBLIC_POCKETBASE_URL = ORIGINAL_PB_URL;
vi.restoreAllMocks();
});
it("401s when the authorization header is missing", async () => {
// No fetch mock — the route should bail before hitting PB.
const res = await GET(unauthedRequest("http://localhost/api/v1/boss-dashboard"));
expect(res.status).toBe(401);
const body = await res.json();
expect(body.error).toMatch(/auth/i);
expect(getBossDashboardData).not.toHaveBeenCalled();
});
it("401s when PB's auth-refresh rejects the token", async () => {
mockAuthRefreshFail(401);
const res = await GET(authedRequest("http://localhost/api/v1/boss-dashboard"));
expect(res.status).toBe(401);
const body = await res.json();
expect(body.error).toMatch(/invalid|expired|session/i);
expect(getBossDashboardData).not.toHaveBeenCalled();
});
it("400s when the window query param is not in the allow-list", async () => {
mockAuthRefreshOk();
const res = await GET(
authedRequest("http://localhost/api/v1/boss-dashboard?window=garbage"),
);
expect(res.status).toBe(400);
const body = await res.json();
expect(body.error).toMatch(/Invalid window/);
expect(getBossDashboardData).not.toHaveBeenCalled();
});
it("returns 200 with the merged dashboard payload + viewerName on success", async () => {
mockAuthRefreshOk({ id: "u-boss", name: "Dave" });
getBossDashboardData.mockResolvedValueOnce({
window: "today",
rangeStart: "2026-08-28T00:00:00.000Z",
rangeEnd: "2026-08-29T00:00:00.000Z",
orderTotals: { total: 7, delivered: 3, invoiced: 1 },
reps: [
{
id: "u1",
name: "Alice",
isUnknown: false,
quotesLoaded: 4,
quotesDoneFor: 2,
quotesDoneBy: 1,
ordersPlaced: 3,
},
],
drivers: [],
});
const res = await GET(
authedRequest("http://localhost/api/v1/boss-dashboard?window=today"),
);
expect(res.status).toBe(200);
const body = await res.json();
// Service payload passed through.
expect(body.window).toBe("today");
expect(body.rangeStart).toBe("2026-08-28T00:00:00.000Z");
expect(body.rangeEnd).toBe("2026-08-29T00:00:00.000Z");
expect(body.orderTotals).toEqual({ total: 7, delivered: 3, invoiced: 1 });
expect(body.reps).toHaveLength(1);
expect(body.reps[0].name).toBe("Alice");
// viewerName merged from the auth-refresh response.
expect(body.viewerName).toBe("Dave");
// Service was called with the validated window string.
expect(getBossDashboardData).toHaveBeenCalledWith({ window: "today" });
});
it("defaults window to 'today' when no window param is given", async () => {
mockAuthRefreshOk();
getBossDashboardData.mockResolvedValueOnce({
window: "today",
rangeStart: "2026-08-28T00:00:00.000Z",
rangeEnd: "2026-08-29T00:00:00.000Z",
orderTotals: { total: 0, delivered: 0, invoiced: 0 },
reps: [],
drivers: [],
});
const res = await GET(authedRequest("http://localhost/api/v1/boss-dashboard"));
expect(res.status).toBe(200);
expect(getBossDashboardData).toHaveBeenCalledWith({ window: "today" });
});
it("forwards the 'week' window to the service", async () => {
mockAuthRefreshOk();
getBossDashboardData.mockResolvedValueOnce({
window: "week",
rangeStart: "2026-08-24T00:00:00.000Z",
rangeEnd: "2026-08-31T00:00:00.000Z",
orderTotals: { total: 0, delivered: 0, invoiced: 0 },
reps: [],
drivers: [],
});
const res = await GET(
authedRequest("http://localhost/api/v1/boss-dashboard?window=week"),
);
expect(res.status).toBe(200);
expect(getBossDashboardData).toHaveBeenCalledWith({ window: "week" });
});
it("forwards the 'month' window to the service", async () => {
mockAuthRefreshOk();
getBossDashboardData.mockResolvedValueOnce({
window: "month",
rangeStart: "2026-08-01T00:00:00.000Z",
rangeEnd: "2026-09-01T00:00:00.000Z",
orderTotals: { total: 0, delivered: 0, invoiced: 0 },
reps: [],
drivers: [],
});
const res = await GET(
authedRequest("http://localhost/api/v1/boss-dashboard?window=month"),
);
expect(res.status).toBe(200);
expect(getBossDashboardData).toHaveBeenCalledWith({ window: "month" });
});
});