import { jest } from "@jest/globals"; import express from "express"; import request from "supertest"; import { dashboardHandlers } from "./dashboard.js"; // Mount the dashboard routes on a bare app with an injectable fake service and a // settable user, so we can test auth, validation, response shaping, and the // error envelope without a database. function makeApp({ user = { id: 1, canViewJson: true }, service } = {}) { const app = express(); app.use((req, _res, next) => { if (user) req.user = user; next(); }); const h = dashboardHandlers({ service }); app.get("/api/dashboard/summary", h.summary); app.get("/api/dashboard/transactions", h.transactions); app.get("/api/dashboard/series", h.series); app.get("/api/dashboard/verify", h.verify); app.get("/api/dashboard/transactions/:eventUuid", h.event); return app; } const okService = () => ({ summary: jest.fn(async () => ({ apiUsage: { core: 2, audit: 1, total: 3 }, apiKeys: 4, endpoints: 2, storage: {} })), transactions: jest.fn(async (_uid, opts) => ({ rows: [{ eventUuid: "e1" }], total: 1, limit: opts.limit, offset: opts.offset })), series: jest.fn(async () => ({ usage: [{ date: "2026-06-01", core: 1, audit: 0, total: 1 }], storage: [{ date: "2026-06-01", coreDisk: 10, auditDisk: 0, totalDisk: 10 }], })), event: jest.fn(async () => ({ eventId: "3fa85f64-5717-4562-b3fc-2c963f66afa6", data: { a: 1 } })), verify: jest.fn(async (_uid, { eventUuids, auditIds }) => ({ results: [ ...eventUuids.map((eventUuid) => ({ eventUuid, auditId: null, status: "verified", verified: true })), ...auditIds.map((auditId) => ({ eventUuid: "e1", auditId, status: "verified", verified: true })), ], })), }); describe("GET /api/dashboard/* auth", () => { it("returns 401 with an error envelope when not logged in", async () => { const res = await request(makeApp({ user: null, service: okService() })).get("/api/dashboard/summary"); expect(res.status).toBe(401); expect(res.body).toEqual({ error: { code: "unauthorized", message: expect.any(String) } }); }); }); describe("GET /api/dashboard/summary", () => { it("calls the service with the user id + window and returns the payload", async () => { const service = okService(); const res = await request(makeApp({ service })).get("/api/dashboard/summary?from=2026-01-01&to=2026-01-31"); expect(res.status).toBe(200); expect(res.body.apiUsage).toEqual({ core: 2, audit: 1, total: 3 }); expect(service.summary).toHaveBeenCalledWith(1, { from: "2026-01-01", to: "2026-01-31" }); }); }); describe("GET /api/dashboard/transactions", () => { it("passes validated/normalized options to the service", async () => { const service = okService(); const res = await request(makeApp({ service })).get("/api/dashboard/transactions?type=audit&sort=size&dir=asc&limit=10"); expect(res.status).toBe(200); expect(res.body).toMatchObject({ total: 1, limit: 10, offset: 0 }); expect(service.transactions).toHaveBeenCalledWith(1, expect.objectContaining({ type: "audit", sort: "size", dir: "asc", limit: 10 })); }); it("returns 400 for an invalid type and does not call the service", async () => { const service = okService(); const res = await request(makeApp({ service })).get("/api/dashboard/transactions?type=evil"); expect(res.status).toBe(400); expect(res.body.error.code).toBe("bad_request"); expect(service.transactions).not.toHaveBeenCalled(); }); it("returns 400 for an unknown sort key (injection guard)", async () => { const res = await request(makeApp({ service: okService() })).get("/api/dashboard/transactions?sort=sender;DROP"); expect(res.status).toBe(400); }); }); describe("GET /api/dashboard/series (combined usage + storage)", () => { it("returns both usage and storage series for the window in one response", async () => { const service = okService(); const res = await request(makeApp({ service })).get("/api/dashboard/series?from=2026-06-01&to=2026-06-03"); expect(res.status).toBe(200); expect(res.body.usage).toHaveLength(1); expect(res.body.storage[0]).toMatchObject({ totalDisk: 10 }); expect(service.series).toHaveBeenCalledWith(1, { from: "2026-06-01", to: "2026-06-03" }); }); }); describe("GET /api/dashboard/verify", () => { const good = "3fa85f64-5717-4562-b3fc-2c963f66afa6"; it("verifies only the valid UUIDs from the ids list (core tab)", async () => { const service = okService(); const res = await request(makeApp({ service })).get(`/api/dashboard/verify?ids=${good},junk`); expect(res.status).toBe(200); expect(res.body.results).toHaveLength(1); expect(service.verify).toHaveBeenCalledWith(1, { eventUuids: [good], auditIds: [] }); }); // The audit tab targets audit RECORDS: two rows of one event must be two // separate targets, or verifying one reports a verdict on both. it("passes audit ids straight through as their own targets (audit tab)", async () => { const service = okService(); const res = await request(makeApp({ service })).get("/api/dashboard/verify?auditIds=9,10"); expect(res.status).toBe(200); expect(service.verify).toHaveBeenCalledWith(1, { eventUuids: [], auditIds: [9, 10] }); expect(res.body.results).toHaveLength(2); }); it("returns empty results when no valid targets are supplied", async () => { const service = okService(); const res = await request(makeApp({ service })).get("/api/dashboard/verify?ids=junk&auditIds=nope"); expect(res.status).toBe(200); expect(res.body.results).toEqual([]); expect(service.verify).toHaveBeenCalledWith(1, { eventUuids: [], auditIds: [] }); }); }); describe("GET /api/dashboard/transactions/:eventUuid", () => { it("400s on a non-uuid id", async () => { const res = await request(makeApp({ service: okService() })).get("/api/dashboard/transactions/not-a-uuid"); expect(res.status).toBe(400); }); it("404s when the event is not found", async () => { const service = okService(); service.event = jest.fn(async () => null); const res = await request(makeApp({ service })).get("/api/dashboard/transactions/3fa85f64-5717-4562-b3fc-2c963f66afa6"); expect(res.status).toBe(404); expect(res.body.error.code).toBe("not_found"); }); it("returns the record when found", async () => { const res = await request(makeApp({ service: okService() })).get("/api/dashboard/transactions/3fa85f64-5717-4562-b3fc-2c963f66afa6"); expect(res.status).toBe(200); expect(res.body.data).toEqual({ a: 1 }); }); it("403s for a user without View JSON, before touching the service", async () => { const service = okService(); const res = await request(makeApp({ user: { id: 1, canViewJson: false }, service })) .get("/api/dashboard/transactions/3fa85f64-5717-4562-b3fc-2c963f66afa6"); expect(res.status).toBe(403); expect(res.body.error.code).toBe("forbidden"); expect(service.event).not.toHaveBeenCalled(); }); it("allows a user with View JSON", async () => { const res = await request(makeApp({ user: { id: 1, canViewJson: true }, service: okService() })) .get("/api/dashboard/transactions/3fa85f64-5717-4562-b3fc-2c963f66afa6"); expect(res.status).toBe(200); }); }); describe("error envelope on a thrown service error", () => { it("returns 500 with the envelope and logs", async () => { const service = okService(); service.summary = jest.fn(async () => { throw new Error("boom"); }); const spy = jest.spyOn(console, "error").mockImplementation(() => {}); const res = await request(makeApp({ service })).get("/api/dashboard/summary"); expect(res.status).toBe(500); expect(res.body).toEqual({ error: { code: "internal", message: "Internal error" } }); expect(spy).toHaveBeenCalled(); spy.mockRestore(); }); });