mirror of
https://github.com/gethomepage/homepage.git
synced 2026-02-07 16:30: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