Files
Termix/plugins/host-metrics/tests/backend/routes.test.ts
T

230 lines
6.8 KiB
TypeScript

import { afterEach, describe, expect, it } from "vitest";
import { startServer, type TestServer } from "./server";
let server: TestServer | null = null;
afterEach(async () => {
await server?.close();
server = null;
});
const ROUTES: Array<[string, string, unknown?]> = [
["GET", "/defaults"],
["GET", "/metrics/7"],
["POST", "/metrics/start/7"],
["POST", "/metrics/stop/7", {}],
["POST", "/metrics/connect-totp", { sessionId: "x", totpCode: "1" }],
["POST", "/metrics/heartbeat", { viewerSessionId: "x" }],
["POST", "/metrics/register-viewer", { hostId: 7 }],
["POST", "/metrics/unregister-viewer", { hostId: 7, viewerSessionId: "x" }],
["GET", "/metrics/history/7"],
["GET", "/host-metrics/preferences/7"],
["POST", "/host-metrics/preferences/7", { slots: [], columns: 3 }],
["GET", "/host-metrics/platform/7"],
["POST", "/host-metrics/managers/health/7/config", { checks: [] }],
];
describe("permissions", () => {
it.each(ROUTES)(
"%s %s answers 403 without host-metrics.use",
async (method, path, body) => {
server = await startServer({ permissions: [] });
const response = await server.request(method, path, { body });
expect(response.status).toBe(403);
},
);
});
describe("metrics", () => {
it("answers 404 until a sample exists", async () => {
server = await startServer();
const response = await server.request("GET", "/metrics/7");
expect(response.status).toBe(404);
expect(response.body.error).toBe("Metrics not available");
});
it("refuses a host the user cannot see", async () => {
server = await startServer();
const response = await server.request("GET", "/metrics/99");
expect(response.status).toBe(403);
});
it("rejects a bad host id", async () => {
server = await startServer();
expect((await server.request("GET", "/metrics/abc")).status).toBe(400);
});
});
describe("viewers", () => {
it("registers, heartbeats and unregisters a viewer", async () => {
server = await startServer();
const registered = await server.request(
"POST",
"/metrics/register-viewer",
{
body: { hostId: 7 },
},
);
expect(registered.status).toBe(200);
const viewerSessionId = registered.body.viewerSessionId as string;
expect(viewerSessionId).toMatch(/^viewer-/);
expect(
(
await server.request("POST", "/metrics/heartbeat", {
body: { viewerSessionId },
})
).status,
).toBe(200);
// Another user cannot keep or drop someone else's viewer.
expect(
(
await server.request("POST", "/metrics/heartbeat", {
user: "user-2",
body: { viewerSessionId },
})
).status,
).toBe(404);
await server.request("POST", "/metrics/unregister-viewer", {
body: { hostId: 7, viewerSessionId },
});
expect(
(
await server.request("POST", "/metrics/heartbeat", {
body: { viewerSessionId },
})
).status,
).toBe(404);
});
it("skips a host with metrics turned off", async () => {
server = await startServer();
await server.mock.ctx.settings.setHost(7, "metricsEnabled", false);
const response = await server.request("POST", "/metrics/register-viewer", {
body: { hostId: 7 },
});
expect(response.body).toEqual({
success: true,
skipped: true,
reason: "metrics_disabled",
});
});
it("skips a host it cannot find", async () => {
server = await startServer();
const response = await server.request("POST", "/metrics/register-viewer", {
body: { hostId: 99 },
});
expect(response.body.reason).toBe("host_not_found");
});
});
describe("history", () => {
it("returns stored samples in the range", async () => {
server = await startServer();
const now = new Date();
const ts = now.toISOString().slice(0, 19).replace("T", " ");
server.db.sqlite
.prepare(
"INSERT INTO p_host_metrics_host_metrics_history (host_id, ts, cpu_percent) VALUES (?, ?, ?)",
)
.run(7, ts, 42);
const response = await server.request("GET", "/metrics/history/7?range=1h");
expect(response.status).toBe(200);
expect(response.body.rows).toEqual([
expect.objectContaining({ ts, cpu_percent: 42 }),
]);
});
it("rejects an unknown range", async () => {
server = await startServer();
const response = await server.request("GET", "/metrics/history/7?range=2y");
expect(response.status).toBe(400);
});
});
describe("layout preferences", () => {
it("defaults from the host's enabled widgets", async () => {
server = await startServer();
await server.mock.ctx.settings.setHost(7, "enabledWidgets", ["cpu"]);
const response = await server.request("GET", "/host-metrics/preferences/7");
expect(response.body.layout.slots.map((s: { id: string }) => s.id)).toEqual(
["cpu"],
);
});
it("saves a layout and keeps the owner's enabled widgets in step", async () => {
server = await startServer();
const layout = {
columns: 2,
slots: [
{ id: "memory", order: 0, colSpan: 1, height: null },
{ id: "service_manager", order: 1, colSpan: 2, height: null },
],
};
expect(
(
await server.request("POST", "/host-metrics/preferences/7", {
body: layout,
})
).status,
).toBe(200);
const read = await server.request("GET", "/host-metrics/preferences/7");
expect(read.body.layout).toEqual(layout);
expect(await server.mock.ctx.settings.getHost(7, "enabledWidgets")).toEqual(
["memory"],
);
});
it("rejects a malformed layout", async () => {
server = await startServer();
const response = await server.request(
"POST",
"/host-metrics/preferences/7",
{ body: { columns: 2 } },
);
expect(response.status).toBe(400);
});
});
describe("health checks", () => {
it("saves a user's checks for a host", async () => {
server = await startServer();
const checks = [
{ id: "web", name: "Web", type: "tcp", target: "x", port: 80 },
];
const response = await server.request(
"POST",
"/host-metrics/managers/health/7/config",
{ body: { checks, intervalSeconds: 120 } },
);
expect(response.status).toBe(200);
expect(
server.db.sqlite
.prepare(
"SELECT user_id, checks, interval_seconds FROM p_host_metrics_host_health_checks",
)
.all(),
).toEqual([
{
user_id: "user-1",
checks: JSON.stringify(checks),
interval_seconds: 120,
},
]);
});
it("rejects invalid checks", async () => {
server = await startServer();
const response = await server.request(
"POST",
"/host-metrics/managers/health/7/config",
{ body: { checks: [{ id: "x" }] } },
);
expect(response.status).toBe(400);
});
});