From aa7d4e69b5951849c266c56b7be4fc6905da7615 Mon Sep 17 00:00:00 2001 From: ajnart <49837342+ajnart@users.noreply.github.com> Date: Sat, 20 Jun 2026 15:07:18 +0200 Subject: [PATCH] fix(dashdot): normalize /api suffix from integration URL for Dashdot v6 compatibility Dashdot v6 removed the /api prefix from all endpoints (e.g. /info instead of /api/info). Users who configured their integration URL with a trailing /api suffix from Dashdot v5 setups would get incorrect paths like /api/info causing 404s. The fix strips any trailing /api suffix from the stored URL before constructing API paths. Closes #6027 --- packages/integrations/src/base/integration.ts | 2 +- .../src/dashdot/dashdot-integration.ts | 10 +++ .../integrations/test/dashdot-unit.spec.ts | 66 +++++++++++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 packages/integrations/test/dashdot-unit.spec.ts diff --git a/packages/integrations/src/base/integration.ts b/packages/integrations/src/base/integration.ts index e129ca283..72e7235d0 100644 --- a/packages/integrations/src/base/integration.ts +++ b/packages/integrations/src/base/integration.ts @@ -55,7 +55,7 @@ export abstract class Integration { return this.integration.decryptedSecrets.some((secret) => secret.kind === kind); } - private createUrl( + protected createUrl( inputUrl: string, path: `/${string}`, queryParams?: Record, diff --git a/packages/integrations/src/dashdot/dashdot-integration.ts b/packages/integrations/src/dashdot/dashdot-integration.ts index 3c835a302..ba2e5fd6b 100644 --- a/packages/integrations/src/dashdot/dashdot-integration.ts +++ b/packages/integrations/src/dashdot/dashdot-integration.ts @@ -16,6 +16,16 @@ import type { ISystemHealthMonitoringIntegration } from "../interfaces/health-mo import type { SystemHealthMonitoring } from "../interfaces/health-monitoring/health-monitoring-types"; export class DashDotIntegration extends Integration implements ISystemHealthMonitoringIntegration { + /** + * Dashdot v5 had endpoints at /api/info, /api/load/* etc. + * Dashdot v6 removed the /api prefix. Users who previously configured + * their integration URL as http://host:port/api need it normalized. + */ + protected override url(path: `/${string}`, queryParams?: Record) { + const normalizedUrl = this.integration.url.replace(/\/api\/?$/, ""); + return this.createUrl(normalizedUrl, path, queryParams); + } + protected async testingAsync(input: IntegrationTestingInput): Promise { const response = await input.fetchAsync(this.url("/info")); if (!response.ok) return TestConnectionError.StatusResult(response); diff --git a/packages/integrations/test/dashdot-unit.spec.ts b/packages/integrations/test/dashdot-unit.spec.ts new file mode 100644 index 000000000..ad0841bda --- /dev/null +++ b/packages/integrations/test/dashdot-unit.spec.ts @@ -0,0 +1,66 @@ +import { describe, expect, test, vi } from "vitest"; + +import { createDb } from "@homarr/db/test"; + +import type { IntegrationTestingInput } from "../src/base/integration"; +import { DashDotIntegration } from "../src/dashdot/dashdot-integration"; + +vi.mock("@homarr/db", async (importActual) => { + const actual = await importActual(); + return { + ...actual, + db: createDb(), + }; +}); + +vi.mock("@homarr/core/infrastructure/certificates", async (importActual) => { + const actual = await importActual(); + return { + ...actual, + getTrustedCertificateHostnamesAsync: vi.fn().mockResolvedValue([]), + }; +}); + +vi.mock("@homarr/redis", () => ({})); + +const createIntegration = (url: string) => + new DashDotIntegration({ + id: "test", + name: "Dashdot", + url, + decryptedSecrets: [], + externalUrl: null, + }); + +describe("DashDot URL normalization", () => { + test("url with /api suffix is stripped", async () => { + const integration = createIntegration("http://dashdot:3001/api"); + // Access protected url() via testingAsync which calls this.url("/info") + const url = (integration as unknown as { url: (path: string) => URL }).url("/info"); + expect(url.toString()).toBe("http://dashdot:3001/info"); + }); + + test("url with /api/ trailing slash is stripped", async () => { + const integration = createIntegration("http://dashdot:3001/api/"); + const url = (integration as unknown as { url: (path: string) => URL }).url("/info"); + expect(url.toString()).toBe("http://dashdot:3001/info"); + }); + + test("url without /api suffix is unchanged", async () => { + const integration = createIntegration("http://dashdot:3001"); + const url = (integration as unknown as { url: (path: string) => URL }).url("/info"); + expect(url.toString()).toBe("http://dashdot:3001/info"); + }); + + test("url with /api only at end is stripped (not mid-path)", async () => { + const integration = createIntegration("http://dashdot:3001/somepath/api"); + const url = (integration as unknown as { url: (path: string) => URL }).url("/info"); + expect(url.toString()).toBe("http://dashdot:3001/somepath/info"); + }); + + test("url with path prefix other than /api is preserved", async () => { + const integration = createIntegration("http://dashdot:3001/dashdot"); + const url = (integration as unknown as { url: (path: string) => URL }).url("/load/cpu"); + expect(url.toString()).toBe("http://dashdot:3001/dashdot/load/cpu"); + }); +});