mirror of
https://github.com/gethomepage/homepage.git
synced 2026-02-08 00:40:52 +08:00
test: add config + widget helper coverage
This commit is contained in:
93
src/utils/config/api-response.test.js
Normal file
93
src/utils/config/api-response.test.js
Normal 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"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
171
src/utils/config/service-helpers.test.js
Normal file
171
src/utils/config/service-helpers.test.js
Normal 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" });
|
||||||
|
});
|
||||||
|
});
|
||||||
88
src/utils/config/widget-helpers.test.js
Normal file
88
src/utils/config/widget-helpers.test.js
Normal 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user