test(boss-dashboard): add 500/null-fallback/edge-case coverage + tighten mock hygiene
This commit is contained in:
parent
e9ce5f4fc4
commit
15206eaf49
2 changed files with 93 additions and 7 deletions
|
|
@ -30,7 +30,7 @@ function unauthedRequest(url: string) {
|
|||
|
||||
describe("GET /api/v1/boss-dashboard/rep-activity", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.resetAllMocks();
|
||||
assertBrowserSession.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
|
|
@ -62,6 +62,20 @@ describe("GET /api/v1/boss-dashboard/rep-activity", () => {
|
|||
expect(getBossRepActivity).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("400s when rep is an empty string (not just absent)", async () => {
|
||||
// `?rep=` becomes "" — not nullish, so the route's `get(rep)` truthy guard
|
||||
// would let it through. Lock in that the route explicitly rejects it.
|
||||
const res = await GET(
|
||||
authedRequest(
|
||||
"http://localhost/api/v1/boss-dashboard/rep-activity?rep=",
|
||||
),
|
||||
);
|
||||
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(
|
||||
|
|
@ -181,36 +195,52 @@ describe("GET /api/v1/boss-dashboard/rep-activity", () => {
|
|||
});
|
||||
|
||||
it("clamps page to the [1, 1000] range", async () => {
|
||||
getBossRepActivity.mockResolvedValueOnce({ items: [], hasMore: false });
|
||||
// Default (not queued) — three GET calls in this test all return empty.
|
||||
getBossRepActivity.mockResolvedValue({ items: [], hasMore: false });
|
||||
|
||||
// page=0 → clamped up to 1.
|
||||
await GET(
|
||||
const res1 = await GET(
|
||||
authedRequest(
|
||||
"http://localhost/api/v1/boss-dashboard/rep-activity?rep=u-4&page=0",
|
||||
),
|
||||
);
|
||||
expect(res1.status).toBe(200);
|
||||
expect(getBossRepActivity).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ page: 1 }),
|
||||
);
|
||||
|
||||
// page=99999 → clamped down to 1000.
|
||||
await GET(
|
||||
const res2 = await GET(
|
||||
authedRequest(
|
||||
"http://localhost/api/v1/boss-dashboard/rep-activity?rep=u-4&page=99999",
|
||||
),
|
||||
);
|
||||
expect(res2.status).toBe(200);
|
||||
expect(getBossRepActivity).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ page: 1000 }),
|
||||
);
|
||||
|
||||
// Garbage page → falls back to 1.
|
||||
await GET(
|
||||
const res3 = await GET(
|
||||
authedRequest(
|
||||
"http://localhost/api/v1/boss-dashboard/rep-activity?rep=u-4&page=abc",
|
||||
),
|
||||
);
|
||||
expect(res3.status).toBe(200);
|
||||
expect(getBossRepActivity).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ page: 1 }),
|
||||
);
|
||||
});
|
||||
|
||||
it("500s and surfaces the message when the service throws", async () => {
|
||||
getBossRepActivity.mockRejectedValueOnce(new Error("PB down"));
|
||||
|
||||
const res = await GET(
|
||||
authedRequest(
|
||||
"http://localhost/api/v1/boss-dashboard/rep-activity?rep=rep-1",
|
||||
),
|
||||
);
|
||||
expect(res.status).toBe(500);
|
||||
expect((await res.json()).error).toBe("PB down");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ function unauthedRequest(url: string) {
|
|||
* 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.
|
||||
*
|
||||
* Both helpers return the spy so callers can either queue one-off responses
|
||||
* on top (`mockResolvedValueOnce`) or rely on the default fallback (500).
|
||||
*/
|
||||
function mockAuthRefreshOk(user: { id: string; name: string } = { id: "u1", name: "Boss" }) {
|
||||
return vi
|
||||
|
|
@ -36,20 +39,24 @@ function mockAuthRefreshOk(user: { id: string; name: string } = { id: "u1", name
|
|||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
}),
|
||||
)
|
||||
.mockResolvedValue(
|
||||
new Response("unexpected fetch", { status: 500 }),
|
||||
);
|
||||
}
|
||||
|
||||
function mockAuthRefreshFail(status = 401) {
|
||||
return vi
|
||||
.spyOn(globalThis, "fetch")
|
||||
.mockResolvedValueOnce(new Response("nope", { status }));
|
||||
.mockResolvedValueOnce(new Response("nope", { status }))
|
||||
.mockResolvedValue(new Response("unexpected fetch", { status: 500 }));
|
||||
}
|
||||
|
||||
// --- Tests ----------------------------------------------------------------
|
||||
|
||||
describe("GET /api/v1/boss-dashboard", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.resetAllMocks();
|
||||
process.env.NEXT_PUBLIC_POCKETBASE_URL = "http://127.0.0.1:8090";
|
||||
});
|
||||
|
||||
|
|
@ -90,6 +97,20 @@ describe("GET /api/v1/boss-dashboard", () => {
|
|||
expect(getBossDashboardData).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("400s when window is an empty string (not just absent)", async () => {
|
||||
// ?? only catches nullish; `?? "today"` does NOT apply to "". Locking this in
|
||||
// prevents a regression where empty strings silently become "today".
|
||||
mockAuthRefreshOk();
|
||||
|
||||
const res = await GET(
|
||||
authedRequest("http://localhost/api/v1/boss-dashboard?window="),
|
||||
);
|
||||
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({
|
||||
|
|
@ -148,6 +169,41 @@ describe("GET /api/v1/boss-dashboard", () => {
|
|||
expect(getBossDashboardData).toHaveBeenCalledWith({ window: "today" });
|
||||
});
|
||||
|
||||
it("returns viewerName as null when the PB record has no name", async () => {
|
||||
// The route does `user?.record?.name ?? null`. A record without a `name`
|
||||
// (or an empty record) should fall through to null.
|
||||
vi.spyOn(globalThis, "fetch")
|
||||
.mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({ record: { id: "u1" } }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
}),
|
||||
)
|
||||
.mockResolvedValue(new Response("unexpected fetch", { status: 500 }));
|
||||
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);
|
||||
const body = await res.json();
|
||||
expect(body.viewerName).toBeNull();
|
||||
});
|
||||
|
||||
it("500s and surfaces the message when the service throws", async () => {
|
||||
mockAuthRefreshOk();
|
||||
getBossDashboardData.mockRejectedValueOnce(new Error("PB down"));
|
||||
|
||||
const res = await GET(authedRequest("http://localhost/api/v1/boss-dashboard"));
|
||||
expect(res.status).toBe(500);
|
||||
expect((await res.json()).error).toBe("PB down");
|
||||
});
|
||||
|
||||
it("forwards the 'week' window to the service", async () => {
|
||||
mockAuthRefreshOk();
|
||||
getBossDashboardData.mockResolvedValueOnce({
|
||||
|
|
|
|||
Loading…
Reference in a new issue