test: add config + widget helper coverage

This commit is contained in:
shamoon
2026-02-03 15:16:38 -08:00
parent 3b6ccd239f
commit 7df5aa9017
3 changed files with 352 additions and 0 deletions

View File

@@ -0,0 +1,93 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const { fs, yaml, config, widgetHelpers, serviceHelpers } = vi.hoisted(() => ({
fs: {
readFile: vi.fn(),
},
yaml: {
load: vi.fn(),
},
config: {
CONF_DIR: "/conf",
getSettings: vi.fn(),
substituteEnvironmentVars: vi.fn((s) => s),
default: vi.fn(),
},
widgetHelpers: {
widgetsFromConfig: vi.fn(),
cleanWidgetGroups: vi.fn(),
},
serviceHelpers: {
servicesFromDocker: vi.fn(),
servicesFromKubernetes: vi.fn(),
servicesFromConfig: vi.fn(),
cleanServiceGroups: vi.fn((g) => g),
findGroupByName: vi.fn(),
},
}));
vi.mock("fs", () => ({
promises: fs,
}));
vi.mock("js-yaml", () => ({
default: yaml,
...yaml,
}));
vi.mock("utils/config/config", () => config);
vi.mock("utils/config/widget-helpers", () => widgetHelpers);
vi.mock("utils/config/service-helpers", () => serviceHelpers);
import { bookmarksResponse, servicesResponse, widgetsResponse } from "./api-response";
describe("utils/config/api-response", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("bookmarksResponse sorts groups based on settings layout", async () => {
fs.readFile.mockResolvedValueOnce("ignored");
config.getSettings.mockResolvedValueOnce({ layout: { B: {}, A: {} } });
yaml.load.mockReturnValueOnce([{ A: [{ LinkA: [{ href: "a" }] }] }, { B: [{ LinkB: [{ href: "b" }] }] }]);
const res = await bookmarksResponse();
expect(res.map((g) => g.name)).toEqual(["B", "A"]);
});
it("widgetsResponse returns sanitized configured widgets", async () => {
widgetHelpers.widgetsFromConfig.mockResolvedValueOnce([{ type: "search", options: { url: "x" } }]);
widgetHelpers.cleanWidgetGroups.mockResolvedValueOnce([{ type: "search", options: { index: 0 } }]);
expect(await widgetsResponse()).toEqual([{ type: "search", options: { index: 0 } }]);
});
it("servicesResponse merges groups and sorts services by weight then name", async () => {
// Minimal stubs for findGroupByName used within servicesResponse.
serviceHelpers.findGroupByName.mockImplementation((groups, name) => groups.find((g) => g.name === name) ?? null);
serviceHelpers.servicesFromDocker.mockResolvedValueOnce([
{
name: "GroupA",
services: [
{ name: "b", weight: 200 },
{ name: "a", weight: 200 },
],
groups: [],
},
]);
serviceHelpers.servicesFromKubernetes.mockResolvedValueOnce([
{ name: "GroupA", services: [{ name: "c", weight: 100 }], groups: [] },
]);
serviceHelpers.servicesFromConfig.mockResolvedValueOnce([
{ name: "GroupA", services: [{ name: "d", weight: 50 }], groups: [] },
{ name: "Empty", services: [], groups: [] },
]);
config.getSettings.mockResolvedValueOnce({ layout: { GroupA: {}, GroupB: {} } });
const groups = await servicesResponse();
expect(groups.map((g) => g.name)).toEqual(["GroupA"]);
expect(groups[0].services.map((s) => s.name)).toEqual(["d", "c", "a", "b"]);
});
});

View File

@@ -0,0 +1,171 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const { state, fs, yaml, config, Docker, dockerCfg, kubeCfg, kubeApi } = vi.hoisted(() => {
const state = {
servicesYaml: null,
dockerYaml: null,
dockerContainers: [],
kubeConfig: null,
kubeServices: [],
};
const fs = {
readFile: vi.fn(async (filePath) => {
if (String(filePath).endsWith("/services.yaml")) return "services";
if (String(filePath).endsWith("/docker.yaml")) return "docker";
return "";
}),
};
const yaml = {
load: vi.fn((contents) => {
if (contents === "services") return state.servicesYaml;
if (contents === "docker") return state.dockerYaml;
return null;
}),
};
const config = {
CONF_DIR: "/conf",
getSettings: vi.fn(() => ({ instanceName: undefined })),
substituteEnvironmentVars: vi.fn((s) => s),
default: vi.fn(),
};
const Docker = vi.fn(() => ({
listContainers: vi.fn(async () => state.dockerContainers),
listServices: vi.fn(async () => state.dockerContainers),
}));
const dockerCfg = {
default: vi.fn(() => ({ conn: {} })),
};
const kubeCfg = {
getKubeConfig: vi.fn(() => state.kubeConfig),
};
const kubeApi = {
listIngress: vi.fn(async () => []),
listTraefikIngress: vi.fn(async () => []),
listHttpRoute: vi.fn(async () => []),
isDiscoverable: vi.fn(() => true),
constructedServiceFromResource: vi.fn(async () => state.kubeServices.shift()),
};
return { state, fs, yaml, config, Docker, dockerCfg, kubeCfg, kubeApi };
});
vi.mock("fs", () => ({
promises: fs,
}));
vi.mock("js-yaml", () => ({
default: yaml,
...yaml,
}));
vi.mock("utils/config/config", () => config);
vi.mock("dockerode", () => ({ default: Docker }));
vi.mock("utils/config/docker", () => dockerCfg);
vi.mock("utils/config/kubernetes", () => kubeCfg);
vi.mock("utils/kubernetes/export", () => ({ default: kubeApi }));
vi.mock("utils/logger", () => ({
default: vi.fn(() => ({
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
})),
}));
describe("utils/config/service-helpers", () => {
beforeEach(() => {
vi.clearAllMocks();
state.servicesYaml = null;
state.dockerYaml = null;
state.dockerContainers = [];
state.kubeConfig = null;
state.kubeServices = [];
});
it("findGroupByName deep-searches and annotates parent", async () => {
const mod = await import("./service-helpers");
const { findGroupByName } = mod;
const groups = [
{
name: "Parent",
groups: [{ name: "Child", services: [], groups: [] }],
services: [],
},
];
const found = findGroupByName(groups, "Child");
expect(found.name).toBe("Child");
expect(found.parent).toBe("Parent");
});
it("getServiceItem prefers configured services over docker/kubernetes", async () => {
// Service present in config -> should return early (no Docker init).
state.servicesYaml = [{ G: [{ S: { icon: "x" } }] }];
const mod = await import("./service-helpers");
const serviceItem = await mod.getServiceItem("G", "S");
expect(serviceItem).toEqual(expect.objectContaining({ name: "S", type: "service", icon: "x" }));
expect(Docker).not.toHaveBeenCalled();
expect(kubeCfg.getKubeConfig).not.toHaveBeenCalled();
});
it("getServiceItem falls back to docker then kubernetes", async () => {
const mod = await import("./service-helpers");
// Miss in config, hit in Docker.
state.servicesYaml = [{ G: [{ Other: { icon: "nope" } }] }];
state.dockerYaml = { "docker-local": {} };
state.dockerContainers = [
{
Names: ["/c1"],
Labels: {
"homepage.group": "G",
"homepage.name": "S",
},
},
];
expect(await mod.getServiceItem("G", "S")).toEqual(
expect.objectContaining({ name: "S", server: "docker-local", container: "c1" }),
);
// Miss in config, miss in Docker, hit in Kubernetes.
vi.resetModules();
state.servicesYaml = [{ G: [{ Other: { icon: "nope" } }] }];
state.dockerYaml = { "docker-local": {} };
state.dockerContainers = [];
state.kubeConfig = {}; // truthy => proceed
state.kubeServices = [{ name: "S", group: "G", type: "service" }];
kubeApi.listIngress.mockResolvedValueOnce([{}]);
const mod2 = await import("./service-helpers");
expect(await mod2.getServiceItem("G", "S")).toEqual(expect.objectContaining({ name: "S", type: "service" }));
});
it("getServiceWidget returns widget or widgets[index]", async () => {
state.servicesYaml = [
{
G: [
{
S: { widget: { id: "single" }, widgets: [{ id: "w0" }, { id: "w1" }] },
},
],
},
];
const mod = await import("./service-helpers");
expect(await mod.default("G", "S", -1)).toEqual({ id: "single" });
expect(await mod.default("G", "S", "1")).toEqual({ id: "w1" });
});
});

View File

@@ -0,0 +1,88 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const { fs, yaml, config } = vi.hoisted(() => ({
fs: {
readFile: vi.fn(),
},
yaml: {
load: vi.fn(),
},
config: {
CONF_DIR: "/conf",
substituteEnvironmentVars: vi.fn((s) => s),
default: vi.fn(),
},
}));
vi.mock("fs", () => ({
promises: fs,
}));
vi.mock("js-yaml", () => ({
default: yaml,
...yaml,
}));
vi.mock("utils/config/config", () => config);
import { cleanWidgetGroups, getPrivateWidgetOptions, widgetsFromConfig } from "./widget-helpers";
describe("utils/config/widget-helpers", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("widgetsFromConfig maps YAML into a typed widgets array with indices", async () => {
fs.readFile.mockResolvedValueOnce("ignored");
yaml.load.mockReturnValueOnce([{ search: { provider: "google", url: "http://x", key: "k" } }]);
const widgets = await widgetsFromConfig();
expect(widgets).toEqual([
{
type: "search",
options: { index: 0, provider: "google", url: "http://x", key: "k" },
},
]);
});
it("cleanWidgetGroups removes private options and hides url except for search/glances", async () => {
const cleaned = await cleanWidgetGroups([
{ type: "search", options: { index: 0, url: "http://x", username: "u", password: "p" } },
{ type: "something", options: { index: 1, url: "http://y", key: "k", foo: 1 } },
{ type: "glances", options: { index: 2, url: "http://z", apiKey: "k", bar: 2 } },
]);
expect(cleaned[0].options.url).toBe("http://x");
expect(cleaned[0].options.username).toBeUndefined();
expect(cleaned[1].options.url).toBeUndefined();
expect(cleaned[1].options.key).toBeUndefined();
expect(cleaned[1].options.foo).toBe(1);
expect(cleaned[2].options.url).toBe("http://z");
expect(cleaned[2].options.apiKey).toBeUndefined();
});
it("getPrivateWidgetOptions returns private options for a specific widget", async () => {
fs.readFile.mockResolvedValueOnce("ignored");
yaml.load.mockReturnValueOnce([{ search: { url: "http://x", username: "u", password: "p", key: "k" } }]);
const options = await getPrivateWidgetOptions("search", 0);
expect(options).toEqual(
expect.objectContaining({
index: 0,
url: "http://x",
username: "u",
password: "p",
key: "k",
}),
);
// And the full list when no args are provided
fs.readFile.mockResolvedValueOnce("ignored");
yaml.load.mockReturnValueOnce([{ search: { url: "http://x", username: "u" } }]);
const all = await getPrivateWidgetOptions();
expect(Array.isArray(all)).toBe(true);
expect(all[0].options.url).toBe("http://x");
});
});