test: cover info widget API routes
Some checks failed
Docker CI / Linting Checks (push) Has been cancelled
Docker CI / Docker Build & Push (push) Has been cancelled
Tests / vitest (push) Has been cancelled

This commit is contained in:
shamoon
2026-02-03 17:01:44 -08:00
parent 3a6faa3f41
commit 724894a27c
10 changed files with 775 additions and 2 deletions

View File

@@ -0,0 +1,76 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { getPrivateWidgetOptions, httpProxy, logger } = vi.hoisted(() => ({
getPrivateWidgetOptions: vi.fn(),
httpProxy: vi.fn(),
logger: { error: vi.fn() },
}));
vi.mock("utils/config/widget-helpers", () => ({
getPrivateWidgetOptions,
}));
vi.mock("utils/proxy/http", () => ({
httpProxy,
}));
vi.mock("utils/logger", () => ({
default: () => logger,
}));
import handler from "./glances";
describe("pages/api/widgets/glances", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns 400 when the widget URL is missing", async () => {
getPrivateWidgetOptions.mockResolvedValueOnce({});
const req = { query: { index: "0" } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(400);
expect(res.body.error).toBe("Missing Glances URL");
});
it("returns cpu/load/mem and includes optional endpoints when requested", async () => {
getPrivateWidgetOptions.mockResolvedValueOnce({ url: "http://glances", username: "u", password: "p" });
httpProxy
.mockResolvedValueOnce([200, null, Buffer.from(JSON.stringify({ total: 1 }))]) // cpu
.mockResolvedValueOnce([200, null, Buffer.from(JSON.stringify({ avg: 2 }))]) // load
.mockResolvedValueOnce([200, null, Buffer.from(JSON.stringify({ available: 3 }))]) // mem
.mockResolvedValueOnce([200, null, Buffer.from(JSON.stringify("1 days"))]) // uptime
.mockResolvedValueOnce([200, null, Buffer.from(JSON.stringify([{ label: "cpu_thermal", value: 50 }]))]) // sensors
.mockResolvedValueOnce([200, null, Buffer.from(JSON.stringify([{ mnt_point: "/", percent: 1 }]))]); // fs
const req = { query: { index: "0", uptime: "1", cputemp: "1", disk: "1", version: "4" } };
const res = createMockRes();
await handler(req, res);
expect(httpProxy).toHaveBeenCalledWith(
"http://glances/api/4/cpu",
expect.objectContaining({
method: "GET",
headers: expect.objectContaining({ Authorization: expect.any(String) }),
}),
);
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({
cpu: { total: 1 },
load: { avg: 2 },
mem: { available: 3 },
uptime: "1 days",
sensors: [{ label: "cpu_thermal", value: 50 }],
fs: [{ mnt_point: "/", percent: 1 }],
});
});
});

View File

@@ -0,0 +1,30 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { widgetsResponse } = vi.hoisted(() => ({
widgetsResponse: vi.fn(),
}));
vi.mock("utils/config/api-response", () => ({
widgetsResponse,
}));
import handler from "./index";
describe("pages/api/widgets/index", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns widgetsResponse()", async () => {
widgetsResponse.mockResolvedValueOnce([{ type: "logo", options: {} }]);
const req = { query: {} };
const res = createMockRes();
await handler(req, res);
expect(res.body).toEqual([{ type: "logo", options: {} }]);
});
});

View File

@@ -0,0 +1,147 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { kc, coreApi, metricsApi, getKubeConfig, parseCpu, parseMemory, logger } = vi.hoisted(() => {
const coreApi = { listNode: vi.fn() };
const metricsApi = { getNodeMetrics: vi.fn() };
const kc = {
makeApiClient: vi.fn(() => coreApi),
};
return {
kc,
coreApi,
metricsApi,
getKubeConfig: vi.fn(),
parseCpu: vi.fn(),
parseMemory: vi.fn(),
logger: { error: vi.fn(), debug: vi.fn() },
};
});
vi.mock("@kubernetes/client-node", () => ({
CoreV1Api: class CoreV1Api {},
Metrics: class Metrics {
constructor() {
return metricsApi;
}
},
}));
vi.mock("../../../utils/config/kubernetes", () => ({
getKubeConfig,
}));
vi.mock("../../../utils/kubernetes/utils", () => ({
parseCpu,
parseMemory,
}));
vi.mock("../../../utils/logger", () => ({
default: () => logger,
}));
import handler from "./kubernetes";
describe("pages/api/widgets/kubernetes", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns 500 when no kube config is available", async () => {
getKubeConfig.mockReturnValueOnce(null);
const req = { query: {} };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(500);
expect(res.body.error).toBe("No kubernetes configuration");
});
it("returns 500 when listing nodes fails", async () => {
getKubeConfig.mockReturnValueOnce(kc);
coreApi.listNode.mockResolvedValueOnce(null);
const req = { query: {} };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(500);
expect(res.body.error).toContain("fetching nodes");
});
it("returns 500 when metrics lookup fails", async () => {
getKubeConfig.mockReturnValueOnce(kc);
parseMemory.mockReturnValue(100);
coreApi.listNode.mockResolvedValueOnce({
items: [
{
metadata: { name: "n1" },
status: { capacity: { cpu: "1", memory: "100" }, conditions: [{ type: "Ready", status: "True" }] },
},
],
});
metricsApi.getNodeMetrics.mockRejectedValueOnce(new Error("nope"));
const req = { query: {} };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(500);
expect(res.body.error).toContain("Error getting metrics");
});
it("returns cluster totals and per-node usage", async () => {
getKubeConfig.mockReturnValueOnce(kc);
parseMemory.mockImplementation((value) => {
if (value === "100") return 100;
if (value === "50") return 50;
if (value === "30") return 30;
return 0;
});
parseCpu.mockImplementation((value) => {
if (value === "100m") return 0.1;
if (value === "200m") return 0.2;
return 0;
});
coreApi.listNode.mockResolvedValueOnce({
items: [
{
metadata: { name: "n1" },
status: { capacity: { cpu: "1", memory: "100" }, conditions: [{ type: "Ready", status: "True" }] },
},
{
metadata: { name: "n2" },
status: { capacity: { cpu: "2", memory: "50" }, conditions: [{ type: "Ready", status: "False" }] },
},
],
});
metricsApi.getNodeMetrics.mockResolvedValueOnce({
items: [
{ metadata: { name: "n1" }, usage: { cpu: "100m", memory: "30" } },
{ metadata: { name: "n2" }, usage: { cpu: "200m", memory: "50" } },
],
});
const req = { query: {} };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(200);
expect(res.body.cluster.cpu.total).toBe(3);
expect(res.body.cluster.cpu.load).toBeCloseTo(0.3);
expect(res.body.cluster.memory.total).toBe(150);
expect(res.body.nodes).toHaveLength(2);
expect(res.body.nodes.find((n) => n.name === "n1").cpu.percent).toBeCloseTo(10);
});
});

View File

@@ -0,0 +1,87 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { getSettings, httpProxy, logger } = vi.hoisted(() => ({
getSettings: vi.fn(),
httpProxy: vi.fn(),
logger: { error: vi.fn() },
}));
vi.mock("../../../utils/config/config", () => ({
getSettings,
}));
vi.mock("../../../utils/proxy/http", () => ({
httpProxy,
}));
vi.mock("../../../utils/logger", () => ({
default: () => logger,
}));
import handler from "./longhorn";
describe("pages/api/widgets/longhorn", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns 400 when the longhorn URL isn't configured", async () => {
getSettings.mockReturnValueOnce({ providers: { longhorn: {} } });
const req = { query: {} };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(400);
expect(res.body.error).toBe("Missing Longhorn URL");
});
it("parses and aggregates node disk totals, including a total node", async () => {
getSettings.mockReturnValueOnce({
providers: { longhorn: { url: "http://lh", username: "u", password: "p" } },
});
const payload = {
data: [
{
id: "n1",
disks: {
d1: { storageAvailable: 1, storageMaximum: 10, storageReserved: 2, storageScheduled: 3 },
},
},
{
id: "n2",
disks: {
d1: { storageAvailable: 4, storageMaximum: 20, storageReserved: 5, storageScheduled: 6 },
d2: { storageAvailable: 1, storageMaximum: 1, storageReserved: 1, storageScheduled: 1 },
},
},
],
};
httpProxy.mockResolvedValueOnce([200, "application/json", JSON.stringify(payload)]);
const req = { query: {} };
const res = createMockRes();
await handler(req, res);
expect(res.headers["Content-Type"]).toBe("application/json");
expect(res.statusCode).toBe(200);
const nodes = res.body.nodes;
expect(nodes.map((n) => n.id)).toEqual(["n1", "n2", "total"]);
expect(nodes.find((n) => n.id === "total")).toEqual(
expect.objectContaining({
id: "total",
available: 6,
maximum: 31,
reserved: 8,
scheduled: 10,
}),
);
});
});

View File

@@ -0,0 +1,52 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { cachedRequest } = vi.hoisted(() => ({
cachedRequest: vi.fn(),
}));
vi.mock("utils/proxy/http", () => ({
cachedRequest,
}));
import handler from "./openmeteo";
describe("pages/api/widgets/openmeteo", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("builds the open-meteo URL with units + timezone and calls cachedRequest", async () => {
cachedRequest.mockResolvedValueOnce({ ok: true });
const req = {
query: { latitude: "1", longitude: "2", units: "metric", cache: "5" },
};
const res = createMockRes();
await handler(req, res);
expect(cachedRequest).toHaveBeenCalledWith(
"https://api.open-meteo.com/v1/forecast?latitude=1&longitude=2&daily=sunrise,sunset&current_weather=true&temperature_unit=celsius&timezone=auto",
"5",
);
expect(res.body).toEqual({ ok: true });
});
it("uses the provided timezone and fahrenheit for non-metric units", async () => {
cachedRequest.mockResolvedValueOnce({ ok: true });
const req = {
query: { latitude: "1", longitude: "2", units: "imperial", cache: 1, timezone: "UTC" },
};
const res = createMockRes();
await handler(req, res);
expect(cachedRequest).toHaveBeenCalledWith(
"https://api.open-meteo.com/v1/forecast?latitude=1&longitude=2&daily=sunrise,sunset&current_weather=true&temperature_unit=fahrenheit&timezone=UTC",
1,
);
});
});

View File

@@ -0,0 +1,99 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { getSettings, getPrivateWidgetOptions, cachedRequest } = vi.hoisted(() => ({
getSettings: vi.fn(),
getPrivateWidgetOptions: vi.fn(),
cachedRequest: vi.fn(),
}));
vi.mock("utils/config/config", () => ({
getSettings,
}));
vi.mock("utils/config/widget-helpers", () => ({
getPrivateWidgetOptions,
}));
vi.mock("utils/proxy/http", () => ({
cachedRequest,
}));
import handler from "./openweathermap";
describe("pages/api/widgets/openweathermap", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns 400 when no API key and no provider are supplied", async () => {
getPrivateWidgetOptions.mockResolvedValueOnce({});
const req = { query: { latitude: "1", longitude: "2", units: "metric", lang: "en", index: "0" } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(400);
expect(res.body).toEqual({ error: "Missing API key or provider" });
});
it("returns 400 when provider doesn't match endpoint and no per-widget key exists", async () => {
getPrivateWidgetOptions.mockResolvedValueOnce({});
const req = { query: { latitude: "1", longitude: "2", units: "metric", lang: "en", provider: "weatherapi" } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(400);
expect(res.body).toEqual({ error: "Invalid provider for endpoint" });
});
it("uses key from widget options when present", async () => {
getPrivateWidgetOptions.mockResolvedValueOnce({ apiKey: "from-widget" });
cachedRequest.mockResolvedValueOnce({ ok: true });
const req = {
query: { latitude: "1", longitude: "2", units: "metric", lang: "en", cache: "1", index: "2" },
};
const res = createMockRes();
await handler(req, res);
expect(getPrivateWidgetOptions).toHaveBeenCalledWith("openweathermap", "2");
expect(cachedRequest).toHaveBeenCalledWith(
"https://api.openweathermap.org/data/2.5/weather?lat=1&lon=2&appid=from-widget&units=metric&lang=en",
"1",
);
expect(res.body).toEqual({ ok: true });
});
it("falls back to settings provider key when provider=openweathermap", async () => {
getPrivateWidgetOptions.mockResolvedValueOnce({});
getSettings.mockReturnValueOnce({ providers: { openweathermap: "from-settings" } });
cachedRequest.mockResolvedValueOnce({ ok: true });
const req = {
query: {
latitude: "1",
longitude: "2",
units: "imperial",
lang: "en",
provider: "openweathermap",
cache: 2,
index: "0",
},
};
const res = createMockRes();
await handler(req, res);
expect(cachedRequest).toHaveBeenCalledWith(
"https://api.openweathermap.org/data/2.5/weather?lat=1&lon=2&appid=from-settings&units=imperial&lang=en",
2,
);
expect(res.body).toEqual({ ok: true });
});
});

View File

@@ -1,9 +1,9 @@
import si from "systeminformation";
import createLogger from "utils/logger"; import createLogger from "utils/logger";
const logger = createLogger("resources"); const logger = createLogger("resources");
const si = require("systeminformation");
export default async function handler(req, res) { export default async function handler(req, res) {
const { type, target, interfaceName = "default" } = req.query; const { type, target, interfaceName = "default" } = req.query;

View File

@@ -0,0 +1,127 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { si, logger } = vi.hoisted(() => ({
si: {
currentLoad: vi.fn(),
fsSize: vi.fn(),
mem: vi.fn(),
cpuTemperature: vi.fn(),
time: vi.fn(),
networkStats: vi.fn(),
networkInterfaceDefault: vi.fn(),
},
logger: {
debug: vi.fn(),
warn: vi.fn(),
},
}));
vi.mock("utils/logger", () => ({
default: () => logger,
}));
vi.mock("systeminformation", () => ({ default: si }));
import handler from "./resources";
describe("pages/api/widgets/resources", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns CPU load data", async () => {
si.currentLoad.mockResolvedValueOnce({ currentLoad: 12.34, avgLoad: 1.23 });
const req = { query: { type: "cpu" } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(200);
expect(res.body.cpu).toEqual({ usage: 12.34, load: 1.23 });
});
it("returns 404 when requested disk target does not exist", async () => {
si.fsSize.mockResolvedValueOnce([{ mount: "/" }]);
const req = { query: { type: "disk", target: "/missing" } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(404);
expect(res.body).toEqual({ error: "Resource not available." });
expect(logger.warn).toHaveBeenCalled();
});
it("returns disk info for the requested mount", async () => {
si.fsSize.mockResolvedValueOnce([{ mount: "/data", size: 1 }]);
const req = { query: { type: "disk", target: "/data" } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(200);
expect(res.body.drive).toEqual({ mount: "/data", size: 1 });
});
it("returns memory, cpu temp and uptime", async () => {
si.mem.mockResolvedValueOnce({ total: 10 });
si.cpuTemperature.mockResolvedValueOnce({ main: 50 });
si.time.mockResolvedValueOnce({ uptime: 123 });
const resMem = createMockRes();
await handler({ query: { type: "memory" } }, resMem);
expect(resMem.statusCode).toBe(200);
expect(resMem.body.memory).toEqual({ total: 10 });
const resTemp = createMockRes();
await handler({ query: { type: "cputemp" } }, resTemp);
expect(resTemp.statusCode).toBe(200);
expect(resTemp.body.cputemp).toEqual({ main: 50 });
const resUptime = createMockRes();
await handler({ query: { type: "uptime" } }, resUptime);
expect(resUptime.statusCode).toBe(200);
expect(resUptime.body.uptime).toBe(123);
});
it("returns 404 when requested network interface does not exist", async () => {
si.networkStats.mockResolvedValueOnce([{ iface: "en0" }]);
const req = { query: { type: "network", interfaceName: "missing" } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(404);
expect(res.body).toEqual({ error: "Interface not found" });
});
it("returns default interface network stats", async () => {
si.networkStats.mockResolvedValueOnce([{ iface: "en0", rx_bytes: 1 }]);
si.networkInterfaceDefault.mockResolvedValueOnce("en0");
const req = { query: { type: "network" } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(200);
expect(res.body.interface).toBe("en0");
expect(res.body.network).toEqual({ iface: "en0", rx_bytes: 1 });
});
it("returns 400 for an invalid type", async () => {
const req = { query: { type: "nope" } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(400);
expect(res.body).toEqual({ error: "invalid type" });
});
});

View File

@@ -0,0 +1,82 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { getSettings, cachedRequest, logger } = vi.hoisted(() => ({
getSettings: vi.fn(),
cachedRequest: vi.fn(),
logger: { debug: vi.fn() },
}));
vi.mock("utils/config/config", () => ({
getSettings,
}));
vi.mock("utils/proxy/http", () => ({
cachedRequest,
}));
vi.mock("utils/logger", () => ({
default: () => logger,
}));
import handler from "./stocks";
describe("pages/api/widgets/stocks", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("validates watchlist and provider", async () => {
const res1 = createMockRes();
await handler({ query: {} }, res1);
expect(res1.statusCode).toBe(400);
const res2 = createMockRes();
await handler({ query: { watchlist: "null", provider: "finnhub" } }, res2);
expect(res2.statusCode).toBe(400);
const res3 = createMockRes();
await handler({ query: { watchlist: "AAPL,AAPL", provider: "finnhub" } }, res3);
expect(res3.statusCode).toBe(400);
expect(res3.body.error).toContain("duplicates");
const res4 = createMockRes();
await handler({ query: { watchlist: "AAPL", provider: "nope" } }, res4);
expect(res4.statusCode).toBe(400);
expect(res4.body.error).toContain("Invalid provider");
});
it("returns 400 when API key isn't configured for provider", async () => {
getSettings.mockReturnValueOnce({ providers: {} });
const req = { query: { watchlist: "AAPL", provider: "finnhub" } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(400);
expect(res.body.error).toContain("API Key");
});
it("returns a normalized stocks response and rounds values", async () => {
getSettings.mockReturnValueOnce({ providers: { finnhub: "k" } });
cachedRequest
.mockResolvedValueOnce({ c: 10.123, dp: -1.234 }) // AAPL
.mockResolvedValueOnce({ c: null, dp: null }); // MSFT
const req = { query: { watchlist: "AAPL,MSFT", provider: "finnhub", cache: "1" } };
const res = createMockRes();
await handler(req, res);
expect(cachedRequest).toHaveBeenCalledWith("https://finnhub.io/api/v1/quote?symbol=AAPL&token=k", "1");
expect(res.body).toEqual({
stocks: [
{ ticker: "AAPL", currentPrice: "10.12", percentChange: -1.23 },
{ ticker: "MSFT", currentPrice: null, percentChange: null },
],
});
});
});

View File

@@ -0,0 +1,73 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { getSettings, getPrivateWidgetOptions, cachedRequest } = vi.hoisted(() => ({
getSettings: vi.fn(),
getPrivateWidgetOptions: vi.fn(),
cachedRequest: vi.fn(),
}));
vi.mock("utils/config/config", () => ({
getSettings,
}));
vi.mock("utils/config/widget-helpers", () => ({
getPrivateWidgetOptions,
}));
vi.mock("utils/proxy/http", () => ({
cachedRequest,
}));
import handler from "./weather";
describe("pages/api/widgets/weatherapi", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns 400 when no API key and no provider are supplied", async () => {
getPrivateWidgetOptions.mockResolvedValueOnce({});
const req = { query: { latitude: "1", longitude: "2", lang: "en", index: "0" } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(400);
expect(res.body).toEqual({ error: "Missing API key or provider" });
});
it("uses key from widget options when present", async () => {
getPrivateWidgetOptions.mockResolvedValueOnce({ apiKey: "from-widget" });
cachedRequest.mockResolvedValueOnce({ ok: true });
const req = { query: { latitude: "1", longitude: "2", lang: "en", cache: 1, index: "0" } };
const res = createMockRes();
await handler(req, res);
expect(cachedRequest).toHaveBeenCalledWith(
"http://api.weatherapi.com/v1/current.json?q=1,2&key=from-widget&lang=en",
1,
);
expect(res.body).toEqual({ ok: true });
});
it("falls back to settings provider key when provider=weatherapi", async () => {
getPrivateWidgetOptions.mockResolvedValueOnce({});
getSettings.mockReturnValueOnce({ providers: { weatherapi: "from-settings" } });
cachedRequest.mockResolvedValueOnce({ ok: true });
const req = { query: { latitude: "1", longitude: "2", lang: "en", provider: "weatherapi", cache: "2" } };
const res = createMockRes();
await handler(req, res);
expect(cachedRequest).toHaveBeenCalledWith(
"http://api.weatherapi.com/v1/current.json?q=1,2&key=from-settings&lang=en",
"2",
);
});
});