Compare commits

...
Author SHA1 Message Date
ajnart a0a3ef02f0 style: auto-fix lint and format 2026-06-20 13:08:45 +00:00
ajnart aa7d4e69b5 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
2026-06-20 15:07:18 +02:00
3 changed files with 80 additions and 1 deletions

No files matched your search

@@ -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<string, string | Date | number | boolean | null | undefined>,
@@ -16,6 +16,19 @@ 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<string, string | Date | number | boolean | null | undefined>,
) {
const normalizedUrl = this.integration.url.replace(/\/api\/?$/, "");
return this.createUrl(normalizedUrl, path, queryParams);
}
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const response = await input.fetchAsync(this.url("/info"));
if (!response.ok) return TestConnectionError.StatusResult(response);
@@ -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<typeof import("@homarr/db")>();
return {
...actual,
db: createDb(),
};
});
vi.mock("@homarr/core/infrastructure/certificates", async (importActual) => {
const actual = await importActual<typeof import("@homarr/core/infrastructure/certificates")>();
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");
});
});