Files
inbuxa-webmail/web/src/lib/__tests__/adminDashboard.test.ts
T

88 lines
4.2 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { client, JmapMethodError } from "@/jmap/client";
import { balancedColumns, countObjects, isRefused, loadMetrics, summarizeMetrics, type MetricRecord } from "@/lib/adminDashboard";
const counter = (metric: string, count: number, timestamp = "2026-09-15T14:00:00Z"): MetricRecord => ({ "@type": "Counter", metric, count, timestamp });
describe("the dashboard's message numbers", () => {
it("adds received and sent up over the metric names Stalwart's own dashboard uses", () => {
const stats = summarizeMetrics([
counter("queue.message-queued", 6),
counter("queue.message-queued", 4, "2026-09-15T13:00:00Z"),
counter("queue.authenticated-message-queued", 2),
counter("queue.dsn-queued", 1),
counter("queue.report-queued", 3),
// Recorded, but not either number.
counter("message-ingest.ham", 50),
]);
expect(stats.received).toBe(10);
expect(stats.sent).toBe(6);
});
it("reads memory from the newest gauge, not the first one listed", () => {
const stats = summarizeMetrics([
{ "@type": "Gauge", metric: "server.memory", count: 100, timestamp: "2026-09-15T12:00:00Z" },
{ "@type": "Gauge", metric: "server.memory", count: 300, timestamp: "2026-09-15T14:00:00Z" },
{ "@type": "Gauge", metric: "queue.count", count: 7, timestamp: "2026-09-15T15:00:00Z" },
]);
expect(stats.memory).toEqual({ bytes: 300, at: "2026-09-15T14:00:00Z" });
});
it("tells a history that records nothing from a quiet day", () => {
expect(summarizeMetrics([]).recorded).toBe(false);
const quiet = summarizeMetrics([{ "@type": "Gauge", metric: "server.memory", count: 1, timestamp: "2026-09-15T14:00:00Z" }]);
expect(quiet).toMatchObject({ recorded: true, received: 0, sent: 0 });
});
});
describe("the dashboard's queries", () => {
it("counts users rather than accounts, and asks for no ids", async () => {
const call = vi.spyOn(client, "call").mockResolvedValue({ ids: [], total: 5 });
expect(await countObjects("Account")).toBe(5);
expect(call).toHaveBeenCalledWith("x:Account/query", { filter: { "@type": "User" }, limit: 0, calculateTotal: true });
await countObjects("QueuedMessage");
expect(call).toHaveBeenLastCalledWith("x:QueuedMessage/query", { limit: 0, calculateTotal: true });
call.mockRestore();
});
it("filters the history with Stalwart's comparison names, and pages the gets", async () => {
// A bare `timestamp` or `after` is unsupportedFilter on a live server.
vi.spyOn(client, "maxObjectsInGet", "get").mockReturnValue(2);
const call = vi.spyOn(client, "call").mockImplementation(async (method, args) => {
if (method === "x:Metric/query") return (args as { position: number }).position === 0 ? { ids: ["a", "b"] } : { ids: ["c"] };
return { list: ((args as { ids: string[] }).ids).map((id) => counter("queue.message-queued", 1, id)) };
});
const records = await loadMetrics(new Date("2026-09-14T15:30:00.123Z"));
expect(records).toHaveLength(3);
expect(call.mock.calls[0]).toEqual([
"x:Metric/query",
{
filter: { timestampIsGreaterThanOrEqual: "2026-09-14T15:30:00Z", metric: ["queue.message-queued", "queue.authenticated-message-queued", "queue.dsn-queued", "queue.report-queued", "server.memory"] },
sort: [{ property: "timestamp", isAscending: false }],
position: 0,
limit: 2,
},
]);
vi.restoreAllMocks();
});
it("treats only a forbidden answer as the server refusing", () => {
expect(isRefused(new JmapMethodError("x:Metric/query", { type: "forbidden" }))).toBe(true);
expect(isRefused(new JmapMethodError("x:Metric/query", { type: "serverFail" }))).toBe(false);
expect(isRefused(new Error("offline"))).toBe(false);
});
});
describe("the card grid", () => {
it("never leaves a row short when the cards can be divided evenly", () => {
for (const n of [1, 2, 3, 4, 6]) {
const { wide, mid } = balancedColumns(n);
expect(n % wide, `${n} cards across ${wide}`).toBe(0);
expect(n % mid, `${n} cards across ${mid}`).toBe(0);
expect(wide).toBeLessThanOrEqual(4);
}
expect(balancedColumns(6)).toEqual({ wide: 3, mid: 2 });
expect(balancedColumns(3)).toEqual({ wide: 3, mid: 1 });
});
});