diff --git a/packages/integrations/src/nextcloud/nextcloud-url.ts b/packages/integrations/src/nextcloud/nextcloud-url.ts new file mode 100644 index 000000000..b276cb236 --- /dev/null +++ b/packages/integrations/src/nextcloud/nextcloud-url.ts @@ -0,0 +1,13 @@ +export const createNextcloudCalendarServerUrl = (integrationUrl: string) => { + const calendarServerUrl = new URL(integrationUrl); + const integrationPath = calendarServerUrl.pathname.replace(/\/+$/, ""); + const davPath = "/remote.php/dav"; + + calendarServerUrl.pathname = integrationPath.endsWith(davPath) + ? `${integrationPath}/` + : `${integrationPath}${davPath}/`; + calendarServerUrl.search = ""; + calendarServerUrl.hash = ""; + + return calendarServerUrl.toString(); +}; diff --git a/packages/integrations/src/nextcloud/nextcloud.integration.ts b/packages/integrations/src/nextcloud/nextcloud.integration.ts index f14bd11d7..4b3909610 100644 --- a/packages/integrations/src/nextcloud/nextcloud.integration.ts +++ b/packages/integrations/src/nextcloud/nextcloud.integration.ts @@ -21,6 +21,7 @@ import type { ICalendarIntegration } from "../interfaces/calendar/calendar-integ import type { CalendarEvent } from "../interfaces/calendar/calendar-types"; import type { Notification } from "../interfaces/notifications/notification-types"; import type { INotificationsIntegration } from "../interfaces/notifications/notifications-integration"; +import { createNextcloudCalendarServerUrl } from "./nextcloud-url"; // Register all existing timezones if (ICAL.TimezoneService.count === 0) { @@ -189,7 +190,7 @@ export class NextcloudIntegration extends Integration implements ICalendarIntegr private async createCalendarClientAsync(agent?: Agent) { return new DAVClient({ - serverUrl: this.integration.url, + serverUrl: createNextcloudCalendarServerUrl(this.integration.url), credentials: { username: this.getSecretValue("username"), password: this.getSecretValue("password"), diff --git a/packages/integrations/src/nextcloud/test/nextcloud-integration.spec.ts b/packages/integrations/src/nextcloud/test/nextcloud-integration.spec.ts new file mode 100644 index 000000000..a48d8e786 --- /dev/null +++ b/packages/integrations/src/nextcloud/test/nextcloud-integration.spec.ts @@ -0,0 +1,38 @@ +import type { Agent } from "node:https"; +import { DAVClient } from "tsdav"; +import { beforeEach, describe, expect, test, vi } from "vitest"; + +import { NextcloudIntegration } from "../nextcloud.integration"; + +vi.mock("tsdav", () => ({ DAVClient: vi.fn() })); + +interface NextcloudCalendarClientFactory { + createCalendarClientAsync(agent: Agent): Promise; +} + +describe("NextcloudIntegration", () => { + beforeEach(() => { + vi.mocked(DAVClient).mockClear(); + }); + + test("uses a calendar server URL that preserves the integration subpath", async () => { + const integration = new NextcloudIntegration({ + id: "integration-id", + name: "Nextcloud", + url: "https://example.com/nextcloud", + externalUrl: null, + decryptedSecrets: [ + { kind: "username", value: "admin" }, + { kind: "password", value: "password" }, + ], + }); + + await (integration as unknown as NextcloudCalendarClientFactory).createCalendarClientAsync({} as Agent); + + expect(DAVClient).toHaveBeenCalledWith( + expect.objectContaining({ + serverUrl: "https://example.com/nextcloud/remote.php/dav/", + }), + ); + }); +}); diff --git a/packages/integrations/src/nextcloud/test/nextcloud-url.spec.ts b/packages/integrations/src/nextcloud/test/nextcloud-url.spec.ts new file mode 100644 index 000000000..9546b5662 --- /dev/null +++ b/packages/integrations/src/nextcloud/test/nextcloud-url.spec.ts @@ -0,0 +1,29 @@ +import { describe, expect, test } from "vitest"; + +import { createNextcloudCalendarServerUrl } from "../nextcloud-url"; + +describe("createNextcloudCalendarServerUrl", () => { + test("uses the DAV endpoint for a root installation", () => { + expect(createNextcloudCalendarServerUrl("https://cloud.example.com")).toBe( + "https://cloud.example.com/remote.php/dav/", + ); + }); + + test("preserves the base path for a subpath installation", () => { + expect(createNextcloudCalendarServerUrl("https://example.com/nextcloud")).toBe( + "https://example.com/nextcloud/remote.php/dav/", + ); + }); + + test("does not duplicate a trailing slash", () => { + expect(createNextcloudCalendarServerUrl("https://example.com/nextcloud/")).toBe( + "https://example.com/nextcloud/remote.php/dav/", + ); + }); + + test("does not duplicate an existing DAV endpoint", () => { + expect(createNextcloudCalendarServerUrl("https://example.com/nextcloud/remote.php/dav")).toBe( + "https://example.com/nextcloud/remote.php/dav/", + ); + }); +});