test: move pages tests out of Next.js routes

This commit is contained in:
shamoon
2026-02-04 09:10:42 -08:00
parent 03a88c0f6b
commit bcdd4166a3
34 changed files with 47 additions and 46 deletions

View File

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

View File

@@ -1,71 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { fs, config, logger } = vi.hoisted(() => ({
fs: {
existsSync: vi.fn(),
readFileSync: vi.fn(),
},
config: {
CONF_DIR: "/conf",
},
logger: {
error: vi.fn(),
},
}));
vi.mock("fs", () => ({
default: fs,
...fs,
}));
vi.mock("utils/config/config", () => config);
vi.mock("utils/logger", () => ({
default: () => logger,
}));
import handler from "./[path]";
describe("pages/api/config/[path]", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns 422 for unsupported files", async () => {
const req = { query: { path: "not-supported.txt" } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(422);
});
it("returns empty content when the file doesn't exist", async () => {
fs.existsSync.mockReturnValueOnce(false);
const req = { query: { path: "custom.css" } };
const res = createMockRes();
await handler(req, res);
expect(res.headers["Content-Type"]).toBe("text/css");
expect(res.statusCode).toBe(200);
expect(res.body).toBe("");
});
it("returns file content when the file exists", async () => {
fs.existsSync.mockReturnValueOnce(true);
fs.readFileSync.mockReturnValueOnce("body{}");
const req = { query: { path: "custom.js" } };
const res = createMockRes();
await handler(req, res);
expect(res.headers["Content-Type"]).toBe("text/javascript");
expect(res.statusCode).toBe(200);
expect(res.body).toBe("body{}");
});
});

View File

@@ -1,124 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { state, getDockerArguments, Docker, logger } = vi.hoisted(() => {
const state = {
dockerArgs: { conn: {}, swarm: false },
listContainersResult: [],
tasks: [],
statsByContainer: {},
throwOnStatsFor: new Set(),
docker: null,
};
const getDockerArguments = vi.fn(() => state.dockerArgs);
const Docker = vi.fn(() => {
const docker = {
listContainers: vi.fn(async () => state.listContainersResult),
listTasks: vi.fn(async () => state.tasks),
getContainer: vi.fn((idOrName) => ({
stats: vi.fn(async () => {
if (state.throwOnStatsFor.has(idOrName)) throw new Error("nope");
return state.statsByContainer[idOrName] ?? { ok: true };
}),
})),
};
state.docker = docker;
return docker;
});
const logger = { error: vi.fn() };
return { state, getDockerArguments, Docker, logger };
});
vi.mock("dockerode", () => ({ default: Docker }));
vi.mock("utils/config/docker", () => ({ default: getDockerArguments }));
vi.mock("utils/logger", () => ({ default: () => logger }));
import handler from "./[...service]";
describe("pages/api/docker/stats/[...service]", () => {
beforeEach(() => {
vi.clearAllMocks();
state.dockerArgs = { conn: {}, swarm: false };
state.listContainersResult = [];
state.tasks = [];
state.statsByContainer = {};
state.throwOnStatsFor = new Set();
state.docker = null;
});
it("returns 400 when docker parameters are missing", async () => {
const req = { query: { service: [] } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(400);
expect(res.body.error).toContain("docker query parameters");
});
it("returns 500 when docker listContainers returns a non-array", async () => {
state.listContainersResult = Buffer.from("nope");
const req = { query: { service: ["c1", "docker-local"] } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(500);
expect(res.body.error).toBe("query failed");
});
it("returns container stats when the container exists", async () => {
state.listContainersResult = [{ Id: "id1", Names: ["/c1"] }];
state.statsByContainer.c1 = { cpu: 1 };
const req = { query: { service: ["c1", "docker-local"] } };
const res = createMockRes();
await handler(req, res);
expect(getDockerArguments).toHaveBeenCalledWith("docker-local");
expect(state.docker.getContainer).toHaveBeenCalledWith("c1");
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({ stats: { cpu: 1 } });
});
it("falls back to swarm tasks and returns a task container's stats when available", async () => {
state.dockerArgs = { conn: {}, swarm: true };
state.listContainersResult = [{ Id: "local1", Names: ["/other"] }];
state.tasks = [
{ Status: { ContainerStatus: { ContainerID: "local1" } } },
{ Status: { ContainerStatus: { ContainerID: "remote2" } } },
];
state.statsByContainer.local1 = { cpu: 2 };
const req = { query: { service: ["swarmservice", "docker-local"] } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(200);
expect(res.body.stats).toEqual({ cpu: 2 });
});
it("returns a 200 error payload when a swarm task exists but stats cannot be retrieved", async () => {
state.dockerArgs = { conn: {}, swarm: true };
state.listContainersResult = [{ Id: "local1", Names: ["/other"] }];
state.tasks = [{ Status: { ContainerStatus: { ContainerID: "local1" } } }];
state.throwOnStatsFor.add("local1");
const req = { query: { service: ["swarmservice", "docker-local"] } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(200);
expect(res.body.error).toBe("Unable to retrieve stats");
});
});

View File

@@ -1,121 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { state, getDockerArguments, Docker, logger } = vi.hoisted(() => {
const state = {
dockerArgs: { conn: {}, swarm: false },
listContainersResult: [],
tasks: [],
inspectByContainer: {},
serviceInfo: undefined,
docker: null,
};
const getDockerArguments = vi.fn(() => state.dockerArgs);
const Docker = vi.fn(() => {
const docker = {
listContainers: vi.fn(async () => state.listContainersResult),
listTasks: vi.fn(async () => state.tasks),
getContainer: vi.fn((idOrName) => ({
inspect: vi.fn(async () => state.inspectByContainer[idOrName]),
})),
getService: vi.fn(() => ({
inspect: vi.fn(async () => state.serviceInfo),
})),
};
state.docker = docker;
return docker;
});
const logger = { error: vi.fn() };
return { state, getDockerArguments, Docker, logger };
});
vi.mock("dockerode", () => ({ default: Docker }));
vi.mock("utils/config/docker", () => ({ default: getDockerArguments }));
vi.mock("utils/logger", () => ({ default: () => logger }));
import handler from "./[...service]";
describe("pages/api/docker/status/[...service]", () => {
beforeEach(() => {
vi.clearAllMocks();
state.dockerArgs = { conn: {}, swarm: false };
state.listContainersResult = [];
state.tasks = [];
state.inspectByContainer = {};
state.serviceInfo = undefined;
state.docker = null;
});
it("returns 400 when docker parameters are missing", async () => {
const req = { query: { service: [] } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(400);
expect(res.body.error).toContain("docker query parameters");
});
it("returns status/health when the container exists", async () => {
state.listContainersResult = [{ Id: "id1", Names: ["/c1"] }];
state.inspectByContainer.c1 = { State: { Status: "running", Health: { Status: "healthy" } } };
const req = { query: { service: ["c1", "docker-local"] } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({ status: "running", health: "healthy" });
});
it("returns replicated swarm service status based on task count", async () => {
state.dockerArgs = { conn: {}, swarm: true };
state.listContainersResult = [{ Id: "id1", Names: ["/other"] }];
state.serviceInfo = { Spec: { Mode: { Replicated: { Replicas: "3" } } } };
state.tasks = [{}, {}, {}];
const req = { query: { service: ["svc", "docker-local"] } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(200);
expect(res.body.status).toBe("running 3/3");
});
it("returns global swarm service status from a local task container when available", async () => {
state.dockerArgs = { conn: {}, swarm: true };
state.listContainersResult = [{ Id: "local1", Names: ["/other"] }];
state.serviceInfo = { Spec: { Mode: {} } };
state.tasks = [{ Status: { ContainerStatus: { ContainerID: "local1" }, State: "running" } }];
state.inspectByContainer.local1 = { State: { Status: "running" } };
const req = { query: { service: ["svc", "docker-local"] } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(200);
expect(res.body.status).toBe("running");
});
it("returns 404 when no container/service is found", async () => {
state.dockerArgs = { conn: {}, swarm: false };
state.listContainersResult = [{ Id: "id1", Names: ["/other"] }];
const req = { query: { service: ["missing", "docker-local"] } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(404);
expect(res.body.status).toBe("not found");
});
});

View File

@@ -1,64 +0,0 @@
import { createHash } from "crypto";
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
function sha256(input) {
return createHash("sha256").update(input).digest("hex");
}
const { readFileSync, checkAndCopyConfig, CONF_DIR } = vi.hoisted(() => ({
readFileSync: vi.fn(),
checkAndCopyConfig: vi.fn(),
CONF_DIR: "/conf",
}));
vi.mock("fs", () => ({
readFileSync,
}));
vi.mock("utils/config/config", () => ({
default: checkAndCopyConfig,
CONF_DIR,
}));
import handler from "./hash";
describe("pages/api/hash", () => {
const originalBuildTime = process.env.HOMEPAGE_BUILDTIME;
beforeEach(() => {
vi.clearAllMocks();
process.env.HOMEPAGE_BUILDTIME = originalBuildTime;
});
it("returns a combined sha256 hash of known config files and build time", async () => {
process.env.HOMEPAGE_BUILDTIME = "build-1";
// Return deterministic contents based on file name.
readFileSync.mockImplementation((filePath) => {
const name = filePath.split("/").pop();
return `content:${name}`;
});
const req = { query: {} };
const res = createMockRes();
await handler(req, res);
const configs = [
"docker.yaml",
"settings.yaml",
"services.yaml",
"bookmarks.yaml",
"widgets.yaml",
"custom.css",
"custom.js",
];
const hashes = configs.map((c) => sha256(`content:${c}`));
const expected = sha256(hashes.join("") + "build-1");
expect(checkAndCopyConfig).toHaveBeenCalled();
expect(res.body).toEqual({ hash: expected });
});
});

View File

@@ -1,16 +0,0 @@
import { describe, expect, it } from "vitest";
import createMockRes from "test-utils/create-mock-res";
import handler from "./healthcheck";
describe("pages/api/healthcheck", () => {
it("returns 'up'", () => {
const req = {};
const res = createMockRes();
handler(req, res);
expect(res.body).toBe("up");
});
});

View File

@@ -1,148 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { coreApi, metricsApi, kc, getKubeConfig, parseCpu, parseMemory, logger } = vi.hoisted(() => {
const coreApi = { listNamespacedPod: vi.fn() };
const metricsApi = { getPodMetrics: vi.fn() };
const kc = { makeApiClient: vi.fn(() => coreApi) };
const getKubeConfig = vi.fn();
const parseCpu = vi.fn();
const parseMemory = vi.fn();
const logger = { error: vi.fn() };
return { coreApi, metricsApi, kc, getKubeConfig, parseCpu, parseMemory, logger };
});
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 "./[...service]";
describe("pages/api/kubernetes/stats/[...service]", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns 400 when kubernetes parameters are missing", async () => {
const req = { query: { service: [] } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(400);
expect(res.body.error).toContain("kubernetes query parameters");
});
it("returns 500 when no kube config is available", async () => {
getKubeConfig.mockReturnValueOnce(null);
const req = { query: { service: ["ns", "app"] } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(500);
expect(res.body.error).toContain("No kubernetes configuration");
});
it("returns 404 when no pods are found", async () => {
getKubeConfig.mockReturnValueOnce(kc);
coreApi.listNamespacedPod.mockResolvedValueOnce({ items: [] });
const req = { query: { service: ["ns", "app"] } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(404);
expect(res.body.error).toContain("no pods found");
});
it("returns limits even when metrics are not available yet", async () => {
getKubeConfig.mockReturnValueOnce(kc);
parseCpu.mockImplementation((v) => (v === "500m" ? 0.5 : 0));
parseMemory.mockImplementation((v) => (v === "1Gi" ? 1024 : 0));
coreApi.listNamespacedPod.mockResolvedValueOnce({
items: [
{
metadata: { name: "p1" },
spec: { containers: [{ resources: { limits: { cpu: "500m", memory: "1Gi" } } }] },
},
],
});
metricsApi.getPodMetrics.mockRejectedValueOnce({ statusCode: 404 });
const req = { query: { service: ["ns", "app"] } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(200);
expect(res.body.stats).toEqual(
expect.objectContaining({
cpu: 0,
mem: 0,
cpuLimit: 0.5,
memLimit: 1024,
cpuUsage: 0,
memUsage: 0,
}),
);
});
it("returns usage calculated from pod metrics", async () => {
getKubeConfig.mockReturnValueOnce(kc);
parseCpu.mockImplementation((v) => (v === "100m" ? 0.1 : v === "500m" ? 0.5 : 0));
parseMemory.mockImplementation((v) => (v === "256Mi" ? 256 : v === "1Gi" ? 1024 : 0));
coreApi.listNamespacedPod.mockResolvedValueOnce({
items: [
{
metadata: { name: "p1" },
spec: { containers: [{ resources: { limits: { cpu: "500m", memory: "1Gi" } } }] },
},
],
});
metricsApi.getPodMetrics.mockResolvedValueOnce({
items: [
{
metadata: { name: "p1" },
containers: [{ usage: { cpu: "100m", memory: "256Mi" } }],
},
],
});
const req = { query: { service: ["ns", "app"] } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(200);
expect(res.body.stats.cpu).toBeCloseTo(0.1);
expect(res.body.stats.mem).toBe(256);
expect(res.body.stats.cpuUsage).toBeCloseTo(20);
expect(res.body.stats.memUsage).toBeCloseTo(25);
});
});

View File

@@ -1,89 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { coreApi, kc, getKubeConfig, logger } = vi.hoisted(() => {
const coreApi = { listNamespacedPod: vi.fn() };
const kc = { makeApiClient: vi.fn(() => coreApi) };
const getKubeConfig = vi.fn();
const logger = { error: vi.fn() };
return { coreApi, kc, getKubeConfig, logger };
});
vi.mock("@kubernetes/client-node", () => ({
CoreV1Api: class CoreV1Api {},
}));
vi.mock("../../../../utils/config/kubernetes", () => ({
getKubeConfig,
}));
vi.mock("../../../../utils/logger", () => ({
default: () => logger,
}));
import handler from "./[...service]";
describe("pages/api/kubernetes/status/[...service]", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns 400 when kubernetes parameters are missing", async () => {
const req = { query: { service: [] } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(400);
expect(res.body.error).toContain("kubernetes query parameters");
});
it("returns 500 when no kube config is available", async () => {
getKubeConfig.mockReturnValueOnce(null);
const req = { query: { service: ["ns", "app"] } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(500);
expect(res.body.error).toContain("No kubernetes configuration");
});
it("returns 404 when no pods are found", async () => {
getKubeConfig.mockReturnValueOnce(kc);
coreApi.listNamespacedPod.mockResolvedValueOnce({ items: [] });
const req = { query: { service: ["ns", "app"] } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(404);
expect(res.body.status).toBe("not found");
});
it("computes running/partial/down from pod phases", async () => {
getKubeConfig.mockReturnValueOnce(kc);
coreApi.listNamespacedPod.mockResolvedValueOnce({
items: [{ status: { phase: "Running" } }, { status: { phase: "Running" } }],
});
const resRunning = createMockRes();
await handler({ query: { service: ["ns", "app"] } }, resRunning);
expect(resRunning.statusCode).toBe(200);
expect(resRunning.body.status).toBe("running");
getKubeConfig.mockReturnValueOnce(kc);
coreApi.listNamespacedPod.mockResolvedValueOnce({
items: [{ status: { phase: "Running" } }, { status: { phase: "Pending" } }],
});
const resPartial = createMockRes();
await handler({ query: { service: ["ns", "app"] } }, resPartial);
expect(resPartial.statusCode).toBe(200);
expect(resPartial.body.status).toBe("partial");
});
});

View File

@@ -1,80 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { getServiceItem, ping, logger } = vi.hoisted(() => ({
getServiceItem: vi.fn(),
ping: { probe: vi.fn() },
logger: { debug: vi.fn() },
}));
vi.mock("utils/config/service-helpers", () => ({
getServiceItem,
}));
vi.mock("utils/logger", () => ({
default: () => logger,
}));
vi.mock("ping", () => ({
promise: ping,
}));
import handler from "./ping";
describe("pages/api/ping", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns 400 when service item isn't found", async () => {
getServiceItem.mockResolvedValueOnce(null);
const req = { query: { groupName: "g", serviceName: "s" } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(400);
expect(res.body.error).toContain("Unable to find service");
});
it("returns 400 when ping host isn't configured", async () => {
getServiceItem.mockResolvedValueOnce({ ping: "" });
const req = { query: { groupName: "g", serviceName: "s" } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(400);
expect(res.body.error).toBe("No ping host given");
});
it("pings the hostname extracted from a URL", async () => {
getServiceItem.mockResolvedValueOnce({ ping: "http://example.com:1234/path" });
ping.probe.mockResolvedValueOnce({ alive: true });
const req = { query: { groupName: "g", serviceName: "s" } };
const res = createMockRes();
await handler(req, res);
expect(ping.probe).toHaveBeenCalledWith("example.com");
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({ alive: true });
});
it("returns 400 when ping throws", async () => {
getServiceItem.mockResolvedValueOnce({ ping: "example.com" });
ping.probe.mockRejectedValueOnce(new Error("nope"));
const req = { query: { groupName: "g", serviceName: "s" } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(400);
expect(res.body.error).toContain("Error attempting ping");
});
});

View File

@@ -1,106 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { getProxmoxConfig, httpProxy, logger } = vi.hoisted(() => ({
getProxmoxConfig: vi.fn(),
httpProxy: vi.fn(),
logger: { error: vi.fn() },
}));
vi.mock("utils/config/proxmox", () => ({
getProxmoxConfig,
}));
vi.mock("utils/proxy/http", () => ({
httpProxy,
}));
vi.mock("utils/logger", () => ({
default: () => logger,
}));
import handler from "./[...service]";
describe("pages/api/proxmox/stats/[...service]", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns 400 when proxmox node parameter is missing", async () => {
const req = { query: { service: [], type: "qemu" } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(400);
expect(res.body.error).toContain("Proxmox node");
});
it("returns 500 when proxmox config is missing", async () => {
getProxmoxConfig.mockReturnValueOnce(null);
const req = { query: { service: ["pve", "100"], type: "qemu" } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(500);
expect(res.body.error).toContain("configuration");
});
it("returns 400 when node config is missing and legacy creds are not present", async () => {
getProxmoxConfig.mockReturnValueOnce({});
const req = { query: { service: ["pve", "100"], type: "qemu" } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(400);
expect(res.body.error).toContain("Proxmox config not found");
});
it("calls proxmox status endpoint and returns normalized stats", async () => {
getProxmoxConfig.mockReturnValueOnce({
pve: { url: "http://pve", token: "t", secret: "s" },
});
httpProxy.mockResolvedValueOnce([
200,
null,
Buffer.from(JSON.stringify({ data: { status: "running", cpu: 0.1, mem: 0.2 } })),
]);
const req = { query: { service: ["pve", "100"], type: "qemu" } };
const res = createMockRes();
await handler(req, res);
expect(httpProxy).toHaveBeenCalledWith(
"http://pve/api2/json/nodes/pve/qemu/100/status/current",
expect.objectContaining({
method: "GET",
headers: expect.objectContaining({
Authorization: "PVEAPIToken=t=s",
}),
}),
);
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({ status: "running", cpu: 0.1, mem: 0.2 });
});
it("returns proxmox http errors as response status codes", async () => {
getProxmoxConfig.mockReturnValueOnce({
pve: { url: "http://pve", token: "t", secret: "s" },
});
httpProxy.mockResolvedValueOnce([401, null, Buffer.from("nope")]);
const req = { query: { service: ["pve", "100"], type: "qemu" } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(401);
expect(res.body.error).toContain("Failed to fetch Proxmox");
});
});

View File

@@ -1,46 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { cachedRequest, logger } = vi.hoisted(() => ({
cachedRequest: vi.fn(),
logger: { error: vi.fn() },
}));
vi.mock("utils/logger", () => ({
default: () => logger,
}));
vi.mock("utils/proxy/http", () => ({
cachedRequest,
}));
import handler from "./releases";
describe("pages/api/releases", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns cached GitHub releases", async () => {
cachedRequest.mockResolvedValueOnce([{ tag_name: "v1" }]);
const req = {};
const res = createMockRes();
await handler(req, res);
expect(res.body).toEqual([{ tag_name: "v1" }]);
});
it("returns [] when cachedRequest throws", async () => {
cachedRequest.mockRejectedValueOnce(new Error("nope"));
const req = {};
const res = createMockRes();
await handler(req, res);
expect(res.body).toEqual([]);
});
});

View File

@@ -1,29 +0,0 @@
import { describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
import handler from "./revalidate";
describe("pages/api/revalidate", () => {
it("revalidates and returns {revalidated:true}", async () => {
const req = {};
const res = createMockRes();
res.revalidate = vi.fn().mockResolvedValueOnce(undefined);
await handler(req, res);
expect(res.revalidate).toHaveBeenCalledWith("/");
expect(res.body).toEqual({ revalidated: true });
});
it("returns 500 when revalidate throws", async () => {
const req = {};
const res = createMockRes();
res.revalidate = vi.fn().mockRejectedValueOnce(new Error("nope"));
await handler(req, res);
expect(res.statusCode).toBe(500);
expect(res.body).toBe("Error revalidating");
});
});

View File

@@ -1,106 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { providers, getSettings, widgetsFromConfig, cachedRequest } = vi.hoisted(() => ({
providers: {
custom: { name: "Custom", url: false, suggestionUrl: null },
google: { name: "Google", url: "https://google?q=", suggestionUrl: "https://google/suggest?q=" },
empty: { name: "NoSuggest", url: "x", suggestionUrl: null },
},
getSettings: vi.fn(),
widgetsFromConfig: vi.fn(),
cachedRequest: vi.fn(),
}));
vi.mock("components/widgets/search/search", () => ({
searchProviders: {
custom: providers.custom,
google: providers.google,
empty: providers.empty,
},
}));
vi.mock("utils/config/config", () => ({
getSettings,
}));
vi.mock("utils/config/widget-helpers", () => ({
widgetsFromConfig,
}));
vi.mock("utils/proxy/http", () => ({
cachedRequest,
}));
import handler from "./searchSuggestion";
describe("pages/api/search/searchSuggestion", () => {
beforeEach(() => {
vi.clearAllMocks();
// Reset provider objects since handler mutates the Custom provider.
providers.custom.url = false;
providers.custom.suggestionUrl = null;
});
it("returns empty suggestions when providerName is unknown", async () => {
const req = { query: { query: "hello", providerName: "Unknown" } };
const res = createMockRes();
await handler(req, res);
expect(res.body).toEqual(["hello", []]);
});
it("returns empty suggestions when provider has no suggestionUrl", async () => {
const req = { query: { query: "hello", providerName: "NoSuggest" } };
const res = createMockRes();
await handler(req, res);
expect(res.body).toEqual(["hello", []]);
});
it("calls cachedRequest for a standard provider", async () => {
cachedRequest.mockResolvedValueOnce(["q", ["a"]]);
const req = { query: { query: "hello world", providerName: "Google" } };
const res = createMockRes();
await handler(req, res);
expect(cachedRequest).toHaveBeenCalledWith("https://google/suggest?q=hello%20world", 5, "Mozilla/5.0");
expect(res.body).toEqual(["q", ["a"]]);
});
it("resolves Custom provider suggestionUrl from widgets.yaml when present", async () => {
widgetsFromConfig.mockResolvedValueOnce([
{ type: "search", options: { url: "https://custom?q=", suggestionUrl: "https://custom/suggest?q=" } },
]);
cachedRequest.mockResolvedValueOnce(["q", ["x"]]);
const req = { query: { query: "hello", providerName: "Custom" } };
const res = createMockRes();
await handler(req, res);
expect(cachedRequest).toHaveBeenCalledWith("https://custom/suggest?q=hello", 5, "Mozilla/5.0");
expect(res.body).toEqual(["q", ["x"]]);
});
it("falls back to quicklaunch custom settings when no search widget is configured", async () => {
widgetsFromConfig.mockResolvedValueOnce([]);
getSettings.mockReturnValueOnce({
quicklaunch: { provider: "custom", url: "https://ql?q=", suggestionUrl: "https://ql/suggest?q=" },
});
cachedRequest.mockResolvedValueOnce(["q", ["y"]]);
const req = { query: { query: "hello", providerName: "Custom" } };
const res = createMockRes();
await handler(req, res);
expect(cachedRequest).toHaveBeenCalledWith("https://ql/suggest?q=hello", 5, "Mozilla/5.0");
});
});

View File

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

View File

@@ -1,79 +0,0 @@
import { describe, expect, it, vi } from "vitest";
const { getServiceWidget } = vi.hoisted(() => ({ getServiceWidget: vi.fn() }));
vi.mock("utils/logger", () => ({
default: () => ({ debug: vi.fn(), error: vi.fn() }),
}));
vi.mock("utils/config/service-helpers", () => ({ default: getServiceWidget }));
const handlerFn = vi.hoisted(() => ({ handler: vi.fn() }));
vi.mock("utils/proxy/handlers/generic", () => ({ default: handlerFn.handler }));
// Calendar proxy is only used for an exception; keep it stubbed.
vi.mock("widgets/calendar/proxy", () => ({ default: vi.fn() }));
// Provide a minimal widget registry for mapping tests.
vi.mock("widgets/widgets", () => ({
default: {
linkwarden: {
api: "{url}/api/v1/{endpoint}",
mappings: {
collections: { endpoint: "collections" },
},
},
},
}));
import servicesProxy from "./proxy";
function createMockRes() {
const res = {
statusCode: undefined,
body: undefined,
status: (code) => {
res.statusCode = code;
return res;
},
json: (data) => {
res.body = data;
return res;
},
send: (data) => {
res.body = data;
return res;
},
end: () => res,
setHeader: vi.fn(),
};
return res;
}
describe("pages/api/services/proxy", () => {
it("maps opaque endpoints using widget.mappings and calls the handler", async () => {
getServiceWidget.mockResolvedValue({ type: "linkwarden" });
handlerFn.handler.mockImplementation(async (req, res) => res.status(200).json({ endpoint: req.query.endpoint }));
const req = { method: "GET", query: { group: "g", service: "s", index: "0", endpoint: "collections" } };
const res = createMockRes();
await servicesProxy(req, res);
expect(handlerFn.handler).toHaveBeenCalled();
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({ endpoint: "collections" });
});
it("returns 403 for unsupported endpoint mapping", async () => {
getServiceWidget.mockResolvedValue({ type: "linkwarden" });
const req = { method: "GET", query: { group: "g", service: "s", index: "0", endpoint: "nope" } };
const res = createMockRes();
await servicesProxy(req, res);
expect(res.statusCode).toBe(403);
expect(res.body).toEqual({ error: "Unsupported service endpoint" });
});
});

View File

@@ -1,103 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { getServiceItem, httpProxy, perf, logger } = vi.hoisted(() => ({
getServiceItem: vi.fn(),
httpProxy: vi.fn(),
perf: { now: vi.fn() },
logger: { debug: vi.fn() },
}));
vi.mock("perf_hooks", () => ({
performance: perf,
}));
vi.mock("utils/config/service-helpers", () => ({
getServiceItem,
}));
vi.mock("utils/proxy/http", () => ({
httpProxy,
}));
vi.mock("utils/logger", () => ({
default: () => logger,
}));
import handler from "./siteMonitor";
describe("pages/api/siteMonitor", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns 400 when the service item is missing", async () => {
getServiceItem.mockResolvedValueOnce(null);
const req = { query: { groupName: "g", serviceName: "s" } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(400);
expect(res.body.error).toContain("Unable to find service");
});
it("returns 400 when the monitor URL is missing", async () => {
getServiceItem.mockResolvedValueOnce({ siteMonitor: "" });
const req = { query: { groupName: "g", serviceName: "s" } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(400);
expect(res.body.error).toBe("No http monitor URL given");
});
it("uses HEAD and returns status + latency when the response is OK", async () => {
getServiceItem.mockResolvedValueOnce({ siteMonitor: "http://example.com" });
perf.now.mockReturnValueOnce(1).mockReturnValueOnce(11);
httpProxy.mockResolvedValueOnce([200]);
const req = { query: { groupName: "g", serviceName: "s" } };
const res = createMockRes();
await handler(req, res);
expect(httpProxy).toHaveBeenCalledWith("http://example.com", { method: "HEAD" });
expect(res.statusCode).toBe(200);
expect(res.body.status).toBe(200);
expect(res.body.latency).toBe(10);
});
it("falls back to GET when HEAD is rejected", async () => {
getServiceItem.mockResolvedValueOnce({ siteMonitor: "http://example.com" });
perf.now.mockReturnValueOnce(1).mockReturnValueOnce(2).mockReturnValueOnce(5).mockReturnValueOnce(15);
httpProxy.mockResolvedValueOnce([500]).mockResolvedValueOnce([200]);
const req = { query: { groupName: "g", serviceName: "s" } };
const res = createMockRes();
await handler(req, res);
expect(httpProxy).toHaveBeenNthCalledWith(1, "http://example.com", { method: "HEAD" });
expect(httpProxy).toHaveBeenNthCalledWith(2, "http://example.com");
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({ status: 200, latency: 10 });
});
it("returns 400 when httpProxy throws", async () => {
getServiceItem.mockResolvedValueOnce({ siteMonitor: "http://example.com" });
httpProxy.mockRejectedValueOnce(new Error("nope"));
const req = { query: { groupName: "g", serviceName: "s" } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(400);
expect(res.body.error).toContain("Error attempting http monitor");
});
});

View File

@@ -1,41 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { checkAndCopyConfig, getSettings } = vi.hoisted(() => ({
checkAndCopyConfig: vi.fn(),
getSettings: vi.fn(),
}));
vi.mock("utils/config/config", () => ({
default: checkAndCopyConfig,
getSettings,
}));
import handler from "./theme";
describe("pages/api/theme", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns defaults when settings are missing", () => {
getSettings.mockReturnValueOnce({});
const res = createMockRes();
handler({ res });
expect(checkAndCopyConfig).toHaveBeenCalledWith("settings.yaml");
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({ color: "slate", theme: "dark" });
});
it("returns configured color + theme when present", () => {
getSettings.mockReturnValueOnce({ color: "red", theme: "light" });
const res = createMockRes();
handler({ res });
expect(res.body).toEqual({ color: "red", theme: "light" });
});
});

View File

@@ -1,30 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { checkAndCopyConfig } = vi.hoisted(() => ({
checkAndCopyConfig: vi.fn(),
}));
vi.mock("utils/config/config", () => ({
default: checkAndCopyConfig,
}));
import handler from "./validate";
describe("pages/api/validate", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns errors for any configs that don't validate", async () => {
checkAndCopyConfig.mockReturnValueOnce(true).mockReturnValueOnce("settings bad").mockReturnValue(true);
const req = {};
const res = createMockRes();
await handler(req, res);
expect(res.body).toEqual(["settings bad"]);
});
});

View File

@@ -1,76 +0,0 @@
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

@@ -1,30 +0,0 @@
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

@@ -1,147 +0,0 @@
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

@@ -1,87 +0,0 @@
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

@@ -1,52 +0,0 @@
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

@@ -1,99 +0,0 @@
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,127 +0,0 @@
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

@@ -1,82 +0,0 @@
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

@@ -1,73 +0,0 @@
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",
);
});
});