diff --git a/src/utils/config/docker.js b/src/utils/config/docker.js index 4e2277f5d..fb60aa406 100644 --- a/src/utils/config/docker.js +++ b/src/utils/config/docker.js @@ -5,6 +5,14 @@ import yaml from "js-yaml"; import checkAndCopyConfig, { CONF_DIR, substituteEnvironmentVars } from "utils/config/config"; +export function getDefaultDockerArgs(platform = process.platform) { + if (platform !== "win32" && platform !== "darwin") { + return { socketPath: "/var/run/docker.sock" }; + } + + return { host: "127.0.0.1" }; +} + export default function getDockerArguments(server) { checkAndCopyConfig("docker.yaml"); @@ -14,11 +22,7 @@ export default function getDockerArguments(server) { const servers = yaml.load(configData); if (!server) { - if (process.platform !== "win32" && process.platform !== "darwin") { - return { socketPath: "/var/run/docker.sock" }; - } - - return { host: "127.0.0.1" }; + return getDefaultDockerArgs(); } if (servers[server]) { diff --git a/src/utils/config/docker.test.js b/src/utils/config/docker.test.js new file mode 100644 index 000000000..e4b7349bc --- /dev/null +++ b/src/utils/config/docker.test.js @@ -0,0 +1,95 @@ +import { describe, expect, it, vi } from "vitest"; + +const { fs, yaml, config, checkAndCopyConfig } = vi.hoisted(() => ({ + fs: { + readFileSync: vi.fn((filePath, encoding) => { + if (String(filePath).endsWith("/docker.yaml") && encoding === "utf8") return "docker-yaml"; + return Buffer.from(String(filePath)); + }), + }, + yaml: { + load: vi.fn(), + }, + config: { + CONF_DIR: "/conf", + substituteEnvironmentVars: vi.fn((s) => s), + }, + checkAndCopyConfig: vi.fn(), +})); + +vi.mock("fs", () => ({ + readFileSync: fs.readFileSync, +})); + +vi.mock("js-yaml", () => ({ + default: yaml, + ...yaml, +})); + +vi.mock("utils/config/config", () => ({ + default: checkAndCopyConfig, + ...config, +})); + +import getDockerArguments, { getDefaultDockerArgs } from "./docker"; + +describe("utils/config/docker", () => { + it("getDefaultDockerArgs returns a socketPath on linux and host on darwin", () => { + expect(getDefaultDockerArgs("linux")).toEqual({ socketPath: "/var/run/docker.sock" }); + expect(getDefaultDockerArgs("darwin")).toEqual({ host: "127.0.0.1" }); + }); + + it("returns default args when no server is given", () => { + yaml.load.mockReturnValueOnce({}); + + const args = getDockerArguments(); + + expect(checkAndCopyConfig).toHaveBeenCalledWith("docker.yaml"); + expect(args).toEqual(expect.objectContaining({ host: expect.any(String) })); + }); + + it("returns socket config when server has a socket", () => { + yaml.load.mockReturnValueOnce({ + "docker-local": { socket: "/tmp/docker.sock", swarm: true }, + }); + + const args = getDockerArguments("docker-local"); + + expect(args).toEqual({ conn: { socketPath: "/tmp/docker.sock" }, swarm: true }); + }); + + it("returns host/port/tls/protocol/headers config when provided", () => { + yaml.load.mockReturnValueOnce({ + remote: { + host: "10.0.0.1", + port: 2376, + swarm: false, + protocol: "http", + headers: { "X-Test": "1" }, + tls: { caFile: "ca.pem", certFile: "cert.pem", keyFile: "key.pem" }, + }, + }); + + const args = getDockerArguments("remote"); + + expect(args).toEqual( + expect.objectContaining({ + swarm: false, + conn: expect.objectContaining({ + host: "10.0.0.1", + port: 2376, + protocol: "http", + headers: { "X-Test": "1" }, + ca: expect.any(Buffer), + cert: expect.any(Buffer), + key: expect.any(Buffer), + }), + }), + ); + }); + + it("returns null when server is not configured", () => { + yaml.load.mockReturnValueOnce({ other: { host: "x" } }); + expect(getDockerArguments("missing")).toBeNull(); + }); +}); diff --git a/src/utils/config/kubernetes.test.js b/src/utils/config/kubernetes.test.js new file mode 100644 index 000000000..b86b3f1b2 --- /dev/null +++ b/src/utils/config/kubernetes.test.js @@ -0,0 +1,108 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const { fs, yaml, config, checkAndCopyConfig, kube, apiExt } = vi.hoisted(() => { + const apiExt = { + readCustomResourceDefinitionStatus: vi.fn(), + }; + + const kube = { + loadFromCluster: vi.fn(), + loadFromDefault: vi.fn(), + makeApiClient: vi.fn(() => apiExt), + }; + + return { + fs: { + readFileSync: vi.fn(() => "kube-yaml"), + }, + yaml: { + load: vi.fn(), + }, + config: { + CONF_DIR: "/conf", + substituteEnvironmentVars: vi.fn((s) => s), + }, + checkAndCopyConfig: vi.fn(), + kube, + apiExt, + }; +}); + +vi.mock("fs", () => ({ + readFileSync: fs.readFileSync, +})); + +vi.mock("js-yaml", () => ({ + default: yaml, + ...yaml, +})); + +vi.mock("utils/config/config", () => ({ + default: checkAndCopyConfig, + ...config, +})); + +vi.mock("@kubernetes/client-node", () => ({ + ApiextensionsV1Api: class ApiextensionsV1Api {}, + KubeConfig: class KubeConfig { + loadFromCluster() { + return kube.loadFromCluster(); + } + loadFromDefault() { + return kube.loadFromDefault(); + } + makeApiClient() { + return kube.makeApiClient(); + } + }, +})); + +import { checkCRD, getKubeConfig, getKubernetes } from "./kubernetes"; + +describe("utils/config/kubernetes", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("getKubernetes loads and parses kubernetes.yaml", () => { + yaml.load.mockReturnValueOnce({ mode: "disabled" }); + + expect(getKubernetes()).toEqual({ mode: "disabled" }); + expect(checkAndCopyConfig).toHaveBeenCalledWith("kubernetes.yaml"); + }); + + it("getKubeConfig returns null when disabled", () => { + yaml.load.mockReturnValueOnce({ mode: "disabled" }); + expect(getKubeConfig()).toBeNull(); + }); + + it("getKubeConfig loads from cluster/default based on mode", () => { + yaml.load.mockReturnValueOnce({ mode: "cluster" }); + const kc1 = getKubeConfig(); + expect(kube.loadFromCluster).toHaveBeenCalled(); + expect(kc1).not.toBeNull(); + + yaml.load.mockReturnValueOnce({ mode: "default" }); + const kc2 = getKubeConfig(); + expect(kube.loadFromDefault).toHaveBeenCalled(); + expect(kc2).not.toBeNull(); + }); + + it("checkCRD returns true when the CRD exists", async () => { + apiExt.readCustomResourceDefinitionStatus.mockResolvedValueOnce({ ok: true }); + const logger = { error: vi.fn() }; + + await expect(checkCRD("x.example", kube, logger)).resolves.toBe(true); + }); + + it("checkCRD returns false and logs on 403", async () => { + apiExt.readCustomResourceDefinitionStatus.mockRejectedValueOnce({ + statusCode: 403, + body: { message: "nope" }, + }); + const logger = { error: vi.fn() }; + + await expect(checkCRD("x.example", kube, logger)).resolves.toBe(false); + expect(logger.error).toHaveBeenCalled(); + }); +}); diff --git a/src/utils/config/proxmox.test.js b/src/utils/config/proxmox.test.js new file mode 100644 index 000000000..a2c49800b --- /dev/null +++ b/src/utils/config/proxmox.test.js @@ -0,0 +1,41 @@ +import { describe, expect, it, vi } from "vitest"; + +const { fs, yaml, config, checkAndCopyConfig } = vi.hoisted(() => ({ + fs: { + readFileSync: vi.fn(() => "proxmox-yaml"), + }, + yaml: { + load: vi.fn(), + }, + config: { + CONF_DIR: "/conf", + substituteEnvironmentVars: vi.fn((s) => s), + }, + checkAndCopyConfig: vi.fn(), +})); + +vi.mock("fs", () => ({ + readFileSync: fs.readFileSync, +})); + +vi.mock("js-yaml", () => ({ + default: yaml, + ...yaml, +})); + +vi.mock("utils/config/config", () => ({ + default: checkAndCopyConfig, + ...config, +})); + +import { getProxmoxConfig } from "./proxmox"; + +describe("utils/config/proxmox", () => { + it("loads and parses proxmox.yaml", () => { + yaml.load.mockReturnValueOnce({ pve: { url: "http://pve" } }); + + expect(getProxmoxConfig()).toEqual({ pve: { url: "http://pve" } }); + expect(checkAndCopyConfig).toHaveBeenCalledWith("proxmox.yaml"); + expect(fs.readFileSync).toHaveBeenCalledWith("/conf/proxmox.yaml", "utf8"); + }); +}); diff --git a/src/utils/config/shvl.test.js b/src/utils/config/shvl.test.js new file mode 100644 index 000000000..612f68b07 --- /dev/null +++ b/src/utils/config/shvl.test.js @@ -0,0 +1,30 @@ +import { describe, expect, it } from "vitest"; + +import { get, set } from "./shvl"; + +describe("utils/config/shvl", () => { + it("get reads nested paths with arrays and returns default when missing", () => { + const obj = { a: { b: [{ c: 1 }] } }; + + expect(get(obj, "a.b[0].c")).toBe(1); + expect(get(obj, "a.b[1].c", "dflt")).toBe("dflt"); + }); + + it("set creates nested objects/arrays as needed", () => { + const obj = {}; + set(obj, "a.b[0].c", 123); + + expect(obj).toEqual({ a: { b: [{ c: 123 }] } }); + }); + + it("set blocks prototype pollution", () => { + const obj = {}; + set(obj, "__proto__.polluted", true); + set(obj, "a.__proto__.polluted", true); + set(obj, "constructor.prototype.polluted", true); + + expect(obj.polluted).toBeUndefined(); + expect({}.polluted).toBeUndefined(); + expect(Object.prototype.polluted).toBeUndefined(); + }); +});