Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 28 additions & 11 deletions apps/dokploy/__test__/backup/rclone-command-boundary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,19 +195,36 @@ describe("destination rclone command boundary", () => {
additionalFlags: dangerousDestination.additionalFlags,
serverId: "none",
}),
).resolves.toBeUndefined();

const command = mocks.execAsync.mock.calls[0]?.[0] as string;
const args = parseShellArgs(command);
const rcloneIndex = args.indexOf("rclone");
).resolves.toEqual({
read: { ok: true },
write: { ok: true },
delete: { ok: true },
});

expect(args.slice(rcloneIndex, rcloneIndex + 2)).toEqual(["rclone", "ls"]);
expectS3CredentialsAsEnvironment(command, args);
expect(args).toContain(`dokploys3:${dangerousDestination.bucket}`);
expect(command).not.toContain(
'--s3-access-key-id="AKIA; touch /tmp/access"',
const commands = mocks.execAsync.mock.calls.map(
([command]) => command as string,
);
expect(command).not.toContain('":s3:bucket$(id);touch"');
expect(commands).toHaveLength(3);
for (const [index, operation] of ["ls", "rcat", "deletefile"].entries()) {
const command = commands[index] ?? "";
const args = parseShellArgs(command);
const rcloneIndex = args.indexOf("rclone");

expect(args.slice(rcloneIndex, rcloneIndex + 2)).toEqual([
"rclone",
operation,
]);
expectS3CredentialsAsEnvironment(command, args);
expect(args.at(-1)).toMatch(
operation === "ls"
? `dokploys3:${dangerousDestination.bucket}`
: `dokploys3:${dangerousDestination.bucket}/dokploy-connection-test/`,
);
expect(command).not.toContain(
'--s3-access-key-id="AKIA; touch /tmp/access"',
);
expect(command).not.toContain('":s3:bucket$(id);touch"');
}
});

it("quotes destination fields in scheduled backup upload rclone commands", async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { describeRcloneFailure } from "@dokploy/server/utils/destination/connection-test";
import {
type DestinationConnectionTestReport,
formatDestinationConnectionTestReport,
isDestinationConnectionTestPassed,
parseDestinationConnectionTestReport,
} from "@dokploy/server/utils/destination/connection-test-report";
import { describe, expect, it } from "vitest";

const readOnlyKeyReport: DestinationConnectionTestReport = {
read: { ok: true },
write: {
ok: false,
reason: "403 AccessDenied (the key cannot upload objects)",
},
delete: { ok: false, skipped: true, reason: "no test object was uploaded" },
};

describe("destination connection test report", () => {
it("formats one line per check and parses it back for the dialog", () => {
const message = formatDestinationConnectionTestReport(readOnlyKeyReport);

expect(message).toBe(
[
"Read: OK",
"Write: FAILED - 403 AccessDenied (the key cannot upload objects)",
"Delete: SKIPPED - no test object was uploaded",
].join("\n"),
);
expect(parseDestinationConnectionTestReport(message)).toEqual(
readOnlyKeyReport,
);
expect(isDestinationConnectionTestPassed(readOnlyKeyReport)).toBe(false);
expect(
isDestinationConnectionTestPassed({
read: { ok: true },
write: { ok: true },
delete: { ok: true },
}),
).toBe(true);
});

it("does not treat other errors as a check report", () => {
expect(
parseDestinationConnectionTestReport(
"S3 endpoint host is not allowed in cloud deployments",
),
).toBeNull();
expect(
parseDestinationConnectionTestReport("Read: OK\nWrite: OK"),
).toBeNull();
});
});

describe("rclone failure reasons", () => {
it("summarizes S3 status codes and error codes per operation", () => {
expect(
describeRcloneFailure(
{
stderr:
"Failed to rcat: InvalidAccessKeyId: The AWS Access Key Id you provided does not exist in our records.\n\tstatus code: 403, request id: tx01",
},
"write",
[],
),
).toBe("403 InvalidAccessKeyId (the access key ID is not recognized)");
expect(
describeRcloneFailure(
{
stderr:
"operation error S3: ListObjectsV2, https response error StatusCode: 404, api error NoSuchBucket: The specified bucket does not exist",
},
"read",
[],
),
).toBe("404 NoSuchBucket (the bucket does not exist)");
expect(
describeRcloneFailure(
{ stderr: "https response error StatusCode: 403" },
"delete",
[],
),
).toBe("403 (the key cannot delete objects)");
});

it("never echoes the command line of a failed local exec", () => {
expect(
describeRcloneFailure(
new Error(
"Command execution failed: Command failed: RCLONE_CONFIG_DOKPLOYS3_SECRET_ACCESS_KEY=abc rclone ls dokploys3:bucket",
),
"read",
[],
),
).toBe("rclone could not complete the request");
});

it("keeps connection errors that are not tied to the command", () => {
expect(
describeRcloneFailure(
new Error("SSH connection error: connect ECONNREFUSED"),
"read",
[],
),
).toBe("SSH connection error: connect ECONNREFUSED");
expect(
describeRcloneFailure(
{
stderr:
"2026/10/06 03:00:01 ERROR : : error reading source root directory: dial tcp: lookup s3.example.com: no such host\n2026/10/06 03:00:01 Failed to ls: dial tcp: lookup s3.example.com: no such host",
},
"read",
[],
),
).toBe("Failed to ls: dial tcp: lookup s3.example.com: no such host");
});
});
Loading
Loading