diff --git a/apps/dokploy/__test__/utils/backups.test.ts b/apps/dokploy/__test__/utils/backups.test.ts index 2c1e5decc..d79a84c44 100644 --- a/apps/dokploy/__test__/utils/backups.test.ts +++ b/apps/dokploy/__test__/utils/backups.test.ts @@ -1,4 +1,8 @@ -import { normalizeS3Path } from "@dokploy/server/utils/backups/utils"; +import { RCLONE_DESTINATION_PROVIDERS } from "@dokploy/server/db/validations/destination"; +import { + getRclonePathAndFlags, + normalizeS3Path, +} from "@dokploy/server/utils/backups/utils"; import { describe, expect, test } from "vitest"; describe("normalizeS3Path", () => { @@ -59,3 +63,99 @@ describe("normalizeS3Path", () => { expect(normalizeS3Path("instance-backups")).toBe("instance-backups/"); }); }); + +const destination = (overrides: Record = {}) => + ({ + destinationId: "destination-id", + name: "Test", + provider: "AWS", + accessKey: "access", + secretAccessKey: "secret", + bucket: "bucket", + region: "us-east-1", + endpoint: "https://s3.example.com", + additionalFlags: [], + organizationId: "organization-id", + createdAt: new Date(0), + ...overrides, + }) as any; + +describe("getRclonePathAndFlags", () => { + test("preserves the existing S3 destination behavior", async () => { + const result = await getRclonePathAndFlags( + destination(), + "service/prefix/backup.sql.gz", + ); + + expect(result.path).toBe(":s3:bucket/service/prefix/backup.sql.gz"); + expect(result.flags).toContain("--s3-provider=AWS"); + expect(result.flags).toContain("--s3-access-key-id=access"); + expect(result.flags).toContain("--s3-secret-access-key=secret"); + }); + + test.each([ + RCLONE_DESTINATION_PROVIDERS.GOOGLE_DRIVE, + RCLONE_DESTINATION_PROVIDERS.ONEDRIVE, + RCLONE_DESTINATION_PROVIDERS.REMOTE, + ])("builds a safe named rclone remote for %s", async (provider) => { + const result = await getRclonePathAndFlags( + destination({ + provider, + endpoint: "team-drive", + bucket: "/dokploy/", + accessKey: "", + secretAccessKey: "", + region: "", + additionalFlags: ["--transfers=2"], + }), + "/service/backup.sql.gz", + ); + + expect(result.path).toBe("team-drive:dokploy/service/backup.sql.gz"); + expect(result.flags).toEqual(["--transfers=2"]); + }); + + test("rejects unsafe named remote names", async () => { + await expect( + getRclonePathAndFlags( + destination({ + provider: RCLONE_DESTINATION_PROVIDERS.GOOGLE_DRIVE, + endpoint: "drive;touch /tmp/pwned", + bucket: "", + }), + ), + ).rejects.toThrow("Invalid rclone remote name"); + }); + + test.each([ + [RCLONE_DESTINATION_PROVIDERS.FTP, "ftp", "21"], + [RCLONE_DESTINATION_PROVIDERS.SFTP, "sftp", "22"], + ] as const)( + "builds stateless %s flags and destination without invoking password obscuring when password is empty", + async (provider, backend, defaultPort) => { + const result = await getRclonePathAndFlags( + destination({ + provider, + endpoint: "storage.example.com", + accessKey: "backup-user", + secretAccessKey: "", + region: "", + bucket: "/backups/", + }), + "service/backup.tar", + ); + + expect(result.path).toBe( + `:${backend}:backups/service/backup.tar`, + ); + expect(result.flags).toContain( + `--${backend}-host=storage.example.com`, + ); + expect(result.flags).toContain(`--${backend}-user=backup-user`); + expect(result.flags).toContain(`--${backend}-port=${defaultPort}`); + expect(result.flags.some((flag) => flag.startsWith(`--${backend}-pass=`))).toBe( + false, + ); + }, + ); +});