From 9661dbf5c851458295fa8874c870c9b7ac61f902 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 4 Feb 2026 12:39:22 -0800 Subject: [PATCH] test: expand coverage for api-helpers and service-helpers --- src/utils/config/service-helpers.test.js | 134 ++++++++++++++++++++++- src/utils/proxy/api-helpers.test.js | 33 +++++- 2 files changed, 160 insertions(+), 7 deletions(-) diff --git a/src/utils/config/service-helpers.test.js b/src/utils/config/service-helpers.test.js index 6ef137780..90adf429d 100644 --- a/src/utils/config/service-helpers.test.js +++ b/src/utils/config/service-helpers.test.js @@ -7,6 +7,12 @@ const { state, fs, yaml, config, Docker, dockerCfg, kubeCfg, kubeApi } = vi.hois dockerContainers: [], kubeConfig: null, kubeServices: [], + logger: { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }, }; const fs = { @@ -72,12 +78,8 @@ 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(), - })), + // Keep a stable logger instance so tests don't depend on module re-imports. + default: vi.fn(() => state.logger), })); describe("utils/config/service-helpers", () => { @@ -90,6 +92,126 @@ describe("utils/config/service-helpers", () => { 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 () => { const mod = await import("./service-helpers"); const { findGroupByName } = mod; diff --git a/src/utils/proxy/api-helpers.test.js b/src/utils/proxy/api-helpers.test.js index 02185a87d..67031632e 100644 --- a/src/utils/proxy/api-helpers.test.js +++ b/src/utils/proxy/api-helpers.test.js @@ -1,6 +1,14 @@ 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", () => { it("formatApiCall replaces placeholders and trims trailing slashes for {url}", () => { @@ -48,6 +56,18 @@ describe("utils/proxy/api-helpers", () => { 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", () => { const input = "https://example.com/path?apikey=123&token=abc#access_token=xyz&other=1"; const output = sanitizeErrorURL(input); @@ -58,4 +78,15 @@ describe("utils/proxy/api-helpers", () => { expect(url.hash).toContain("access_token=***"); 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"); + }); });