mirror of
https://github.com/gethomepage/homepage.git
synced 2026-02-08 08:50:52 +08:00
test: expand coverage for api-helpers and service-helpers
This commit is contained in:
@@ -7,6 +7,12 @@ const { state, fs, yaml, config, Docker, dockerCfg, kubeCfg, kubeApi } = vi.hois
|
|||||||
dockerContainers: [],
|
dockerContainers: [],
|
||||||
kubeConfig: null,
|
kubeConfig: null,
|
||||||
kubeServices: [],
|
kubeServices: [],
|
||||||
|
logger: {
|
||||||
|
debug: vi.fn(),
|
||||||
|
info: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const fs = {
|
const fs = {
|
||||||
@@ -72,12 +78,8 @@ vi.mock("utils/config/kubernetes", () => kubeCfg);
|
|||||||
vi.mock("utils/kubernetes/export", () => ({ default: kubeApi }));
|
vi.mock("utils/kubernetes/export", () => ({ default: kubeApi }));
|
||||||
|
|
||||||
vi.mock("utils/logger", () => ({
|
vi.mock("utils/logger", () => ({
|
||||||
default: vi.fn(() => ({
|
// Keep a stable logger instance so tests don't depend on module re-imports.
|
||||||
debug: vi.fn(),
|
default: vi.fn(() => state.logger),
|
||||||
info: vi.fn(),
|
|
||||||
warn: vi.fn(),
|
|
||||||
error: vi.fn(),
|
|
||||||
})),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe("utils/config/service-helpers", () => {
|
describe("utils/config/service-helpers", () => {
|
||||||
@@ -90,6 +92,126 @@ describe("utils/config/service-helpers", () => {
|
|||||||
state.kubeServices = [];
|
state.kubeServices = [];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("servicesFromConfig parses nested groups, assigns default weights, and skips invalid entries", async () => {
|
||||||
|
state.servicesYaml = [
|
||||||
|
{
|
||||||
|
Main: [
|
||||||
|
{
|
||||||
|
Child: [{ SvcA: { icon: "a" } }, { SvcB: { icon: "b", weight: 5 } }],
|
||||||
|
},
|
||||||
|
{ SvcRoot: { icon: "r" } },
|
||||||
|
{ BadSvc: null },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const mod = await import("./service-helpers");
|
||||||
|
const groups = await mod.servicesFromConfig();
|
||||||
|
|
||||||
|
expect(groups).toHaveLength(1);
|
||||||
|
expect(groups[0].name).toBe("Main");
|
||||||
|
expect(groups[0].type).toBe("group");
|
||||||
|
|
||||||
|
// Root services live on the group; child groups are nested.
|
||||||
|
expect(groups[0].services.map((s) => ({ name: s.name, weight: s.weight }))).toEqual([
|
||||||
|
{ name: "SvcRoot", weight: 100 },
|
||||||
|
]);
|
||||||
|
expect(groups[0].groups).toHaveLength(1);
|
||||||
|
expect(groups[0].groups[0].name).toBe("Child");
|
||||||
|
expect(groups[0].groups[0].services.map((s) => ({ name: s.name, weight: s.weight }))).toEqual([
|
||||||
|
{ name: "SvcA", weight: 100 },
|
||||||
|
{ name: "SvcB", weight: 5 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(state.logger.warn).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cleanServiceGroups normalizes weights, moves widget->widgets, and parses per-widget settings", async () => {
|
||||||
|
const mod = await import("./service-helpers");
|
||||||
|
const { cleanServiceGroups } = mod;
|
||||||
|
|
||||||
|
const rawGroups = [
|
||||||
|
{
|
||||||
|
name: "Group",
|
||||||
|
services: [
|
||||||
|
{
|
||||||
|
name: "svc",
|
||||||
|
showStats: "true",
|
||||||
|
weight: "not-a-number",
|
||||||
|
widgets: [
|
||||||
|
// Invalid fields/highlight should be dropped with a log message.
|
||||||
|
{ type: "iframe", fields: "{bad}", highlight: "{bad}", src: "https://example.com" },
|
||||||
|
// Type-specific boolean parsing.
|
||||||
|
{ type: "portainer", kubernetes: "true" },
|
||||||
|
{ type: "deluge", enableLeechProgress: "true", enableLeechSize: "false" },
|
||||||
|
],
|
||||||
|
// `widget` is appended after the `widgets` array.
|
||||||
|
widget: {
|
||||||
|
type: "glances",
|
||||||
|
metric: "cpu",
|
||||||
|
chart: false,
|
||||||
|
version: "3",
|
||||||
|
refreshInterval: 1500,
|
||||||
|
pointsLimit: 10,
|
||||||
|
diskUnits: "gb",
|
||||||
|
fields: '["cpu"]',
|
||||||
|
highlight: '{"level":"warning"}',
|
||||||
|
hideErrors: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "svc2",
|
||||||
|
weight: {},
|
||||||
|
widget: { type: "openwrt", interfaceName: "eth0" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
groups: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const cleaned = cleanServiceGroups(rawGroups);
|
||||||
|
expect(cleaned).toHaveLength(1);
|
||||||
|
expect(cleaned[0].type).toBe("group");
|
||||||
|
expect(cleaned[0].services).toHaveLength(2);
|
||||||
|
|
||||||
|
const svc = cleaned[0].services[0];
|
||||||
|
expect(svc.showStats).toBe(true);
|
||||||
|
expect(svc.weight).toBe(0);
|
||||||
|
expect(svc.widgets).toHaveLength(4);
|
||||||
|
|
||||||
|
// The last widget is the appended `widget` entry; it should carry service metadata.
|
||||||
|
const glancesWidget = svc.widgets[3];
|
||||||
|
expect(glancesWidget.type).toBe("glances");
|
||||||
|
expect(glancesWidget.service_group).toBe("Group");
|
||||||
|
expect(glancesWidget.service_name).toBe("svc");
|
||||||
|
expect(glancesWidget.index).toBe(3);
|
||||||
|
expect(glancesWidget.hide_errors).toBe(true);
|
||||||
|
expect(glancesWidget.fields).toEqual(["cpu"]);
|
||||||
|
expect(glancesWidget.highlight).toEqual({ level: "warning" });
|
||||||
|
expect(glancesWidget.chart).toBe(false);
|
||||||
|
expect(glancesWidget.version).toBe(3);
|
||||||
|
|
||||||
|
// Type-specific parsing for other widgets.
|
||||||
|
expect(svc.widgets[1].kubernetes).toBe(true);
|
||||||
|
expect(svc.widgets[2].enableLeechProgress).toBe(true);
|
||||||
|
expect(svc.widgets[2].enableLeechSize).toBe(false);
|
||||||
|
|
||||||
|
const svc2 = cleaned[0].services[1];
|
||||||
|
expect(svc2.weight).toBe(0);
|
||||||
|
expect(svc2.widgets).toHaveLength(1);
|
||||||
|
expect(svc2.widgets[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: "openwrt",
|
||||||
|
interfaceName: "eth0",
|
||||||
|
service_group: "Group",
|
||||||
|
service_name: "svc2",
|
||||||
|
index: 0,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(state.logger.error).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("findGroupByName deep-searches and annotates parent", async () => {
|
it("findGroupByName deep-searches and annotates parent", async () => {
|
||||||
const mod = await import("./service-helpers");
|
const mod = await import("./service-helpers");
|
||||||
const { findGroupByName } = mod;
|
const { findGroupByName } = mod;
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { asJson, formatApiCall, formatProxyUrl, getURLSearchParams, sanitizeErrorURL } from "./api-helpers";
|
import {
|
||||||
|
asJson,
|
||||||
|
formatApiCall,
|
||||||
|
formatProxyUrl,
|
||||||
|
getURLSearchParams,
|
||||||
|
jsonArrayFilter,
|
||||||
|
jsonArrayTransform,
|
||||||
|
sanitizeErrorURL,
|
||||||
|
} from "./api-helpers";
|
||||||
|
|
||||||
describe("utils/proxy/api-helpers", () => {
|
describe("utils/proxy/api-helpers", () => {
|
||||||
it("formatApiCall replaces placeholders and trims trailing slashes for {url}", () => {
|
it("formatApiCall replaces placeholders and trims trailing slashes for {url}", () => {
|
||||||
@@ -48,6 +56,18 @@ describe("utils/proxy/api-helpers", () => {
|
|||||||
expect(asJson(null)).toBeNull();
|
expect(asJson(null)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("jsonArrayTransform transforms arrays and returns non-arrays unchanged", () => {
|
||||||
|
const data = Buffer.from(JSON.stringify([{ a: 1 }, { a: 2 }]));
|
||||||
|
expect(jsonArrayTransform(data, (items) => items.map((i) => i.a))).toEqual([1, 2]);
|
||||||
|
|
||||||
|
expect(jsonArrayTransform(Buffer.from(JSON.stringify({ ok: true })), () => "nope")).toEqual({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("jsonArrayFilter filters arrays and returns non-arrays unchanged", () => {
|
||||||
|
const data = Buffer.from(JSON.stringify([{ a: 1 }, { a: 2 }]));
|
||||||
|
expect(jsonArrayFilter(data, (item) => item.a > 1)).toEqual([{ a: 2 }]);
|
||||||
|
});
|
||||||
|
|
||||||
it("sanitizeErrorURL redacts sensitive query params and hash fragments", () => {
|
it("sanitizeErrorURL redacts sensitive query params and hash fragments", () => {
|
||||||
const input = "https://example.com/path?apikey=123&token=abc#access_token=xyz&other=1";
|
const input = "https://example.com/path?apikey=123&token=abc#access_token=xyz&other=1";
|
||||||
const output = sanitizeErrorURL(input);
|
const output = sanitizeErrorURL(input);
|
||||||
@@ -58,4 +78,15 @@ describe("utils/proxy/api-helpers", () => {
|
|||||||
expect(url.hash).toContain("access_token=***");
|
expect(url.hash).toContain("access_token=***");
|
||||||
expect(url.hash).toContain("other=1");
|
expect(url.hash).toContain("other=1");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("sanitizeErrorURL only redacts known keys", () => {
|
||||||
|
const input = "https://example.com/path?api_key=123&safe=ok#auth=abc&safe_hash=1";
|
||||||
|
const output = sanitizeErrorURL(input);
|
||||||
|
|
||||||
|
const url = new URL(output);
|
||||||
|
expect(url.searchParams.get("api_key")).toBe("***");
|
||||||
|
expect(url.searchParams.get("safe")).toBe("ok");
|
||||||
|
expect(url.hash).toContain("auth=***");
|
||||||
|
expect(url.hash).toContain("safe_hash=1");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user