addition of new dashboard
This commit is contained in:
169
backend/http/dashboard.test.js
Normal file
169
backend/http/dashboard.test.js
Normal file
@@ -0,0 +1,169 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user