diff --git a/.github/workflows/backendtests.yml b/.github/workflows/backendtests.yml
new file mode 100644
index 000000000..2e2a115c4
--- /dev/null
+++ b/.github/workflows/backendtests.yml
@@ -0,0 +1,289 @@
+name: Backend Live Tests
+'on':
+ - push
+ - pull_request
+jobs:
+ check_secrets:
+ runs-on: ubuntu-latest
+ outputs:
+ s3_secrets_available: '${{ steps.check_s3.outputs.available }}'
+ dropbox_secrets_available: '${{ steps.check_dropbox.outputs.available }}'
+ azure_secrets_available: '${{ steps.check_azure.outputs.available }}'
+ steps:
+ - id: check_s3
+ name: Check S3 secrets
+ shell: bash
+ run: |
+ echo "Starting S3 secrets check..."
+ if [[ -n "${{ secrets.TESTCREDENTIAL_S3_BUCKETNAME }}" ]] && \
+ [[ -n "${{ secrets.TESTCREDENTIAL_S3_KEY }}" ]] && \
+ [[ -n "${{ secrets.TESTCREDENTIAL_S3_REGION }}" ]] && \
+ [[ -n "${{ secrets.TESTCREDENTIAL_S3_SECRET }}" ]]; then
+ echo "All S3 secrets found"
+ echo "available=true" >> $GITHUB_OUTPUT
+ else
+ echo "Missing some S3 secrets"
+ echo "available=false" >> $GITHUB_OUTPUT
+ fi
+ echo "S3 check completed"
+ - id: check_dropbox
+ name: Check Dropbox secrets
+ shell: bash
+ run: |
+ echo "Starting Dropbox secrets check..."
+ if [[ -n "${{ secrets.TESTCREDENTIAL_DROPBOX_FOLDER }}" ]] && \
+ [[ -n "${{ secrets.TESTCREDENTIAL_DROPBOX_TOKEN }}" ]]; then
+ echo "All Dropbox secrets found"
+ echo "available=true" >> $GITHUB_OUTPUT
+ else
+ echo "Missing some Dropbox secrets"
+ echo "available=false" >> $GITHUB_OUTPUT
+ fi
+ echo "Dropbox check completed"
+ - id: check_azure
+ name: Check Azure secrets
+ shell: bash
+ run: |
+ echo "Starting Azure secrets check..."
+ if [[ -n "${{ secrets.TESTCREDENTIAL_AZURE_ACCOUNTNAME }}" ]] && \
+ [[ -n "${{ secrets.TESTCREDENTIAL_AZURE_ACCESSKEY }}" ]] && \
+ [[ -n "${{ secrets.TESTCREDENTIAL_AZURE_CONTAINERNAME }}" ]]; then
+ echo "All Azure secrets found"
+ echo "available=true" >> $GITHUB_OUTPUT
+ else
+ echo "Missing some Azure secrets"
+ echo "available=false" >> $GITHUB_OUTPUT
+ fi
+ echo "Azure check completed"
+ - name: Debug Output
+ shell: bash
+ run: |
+ echo "S3 Available: ${{ steps.check_s3.outputs.available }}"
+ echo "Dropbox Available: ${{ steps.check_dropbox.outputs.available }}"
+
+
+ test_ftp:
+ name: FTP Tests
+ runs-on: '${{ matrix.os }}'
+ strategy:
+ fail-fast: false
+ max-parallel: 1
+ matrix:
+ os:
+ - ubuntu-latest
+ - windows-latest
+ - macos-latest
+ steps:
+ - name: Set up .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 8.x
+ - name: Checkout source
+ uses: actions/checkout@v4
+ - name: Restore NuGet dependencies
+ run: >-
+ dotnet restore
+ LiveTests/Duplicati.Backend.Tests/Duplicati.Backend.Tests.sln
+ - name: Build project
+ run: >-
+ dotnet build --no-restore
+ LiveTests/Duplicati.Backend.Tests/Duplicati.Backend.Tests.sln
+ - name: Setup Testcontainers Cloud Client
+ uses: atomicjar/testcontainers-cloud-setup-action@v1
+ with:
+ token: '${{ secrets.TC_CLOUD_TOKEN }}'
+ - name: Run FTP tests
+ env:
+ TC_CLOUD_TOKEN: '${{ secrets.TC_CLOUD_TOKEN }}'
+ run: >-
+ dotnet test --no-build --filter="ClassName~FtpTests"
+ --logger:"console;verbosity=detailed"
+ LiveTests/Duplicati.Backend.Tests/Duplicati.Backend.Tests.sln
+ test_s3:
+ needs: check_secrets
+ if: needs.check_secrets.outputs.s3_secrets_available == 'true'
+ name: S3 Tests
+ runs-on: '${{ matrix.os }}'
+ strategy:
+ max-parallel: 1
+ fail-fast: false
+ matrix:
+ os:
+ - ubuntu-latest
+ - windows-latest
+ - macos-latest
+ steps:
+ - name: Set up .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 8.x
+ - name: Checkout source
+ uses: actions/checkout@v4
+ - name: Restore NuGet dependencies
+ run: >-
+ dotnet restore
+ LiveTests/Duplicati.Backend.Tests/Duplicati.Backend.Tests.sln
+ - name: Build project
+ run: >-
+ dotnet build --no-restore
+ LiveTests/Duplicati.Backend.Tests/Duplicati.Backend.Tests.sln
+ - name: Run S3 tests
+ env:
+ TC_CLOUD_TOKEN: '${{ secrets.TC_CLOUD_TOKEN }}'
+ TESTCREDENTIAL_S3_BUCKETNAME: '${{ secrets.TESTCREDENTIAL_S3_BUCKETNAME }}'
+ TESTCREDENTIAL_S3_KEY: '${{ secrets.TESTCREDENTIAL_S3_KEY }}'
+ TESTCREDENTIAL_S3_REGION: '${{ secrets.TESTCREDENTIAL_S3_REGION }}'
+ TESTCREDENTIAL_S3_SECRET: '${{ secrets.TESTCREDENTIAL_S3_SECRET }}'
+ run: >-
+ dotnet test --no-build --filter="ClassName~S3Tests"
+ --logger:"console;verbosity=detailed"
+ LiveTests/Duplicati.Backend.Tests/Duplicati.Backend.Tests.sln
+ test_azure:
+ needs: check_secrets
+ if: needs.check_secrets.outputs.azure_secrets_available == 'true'
+ name: Azure Tests
+ runs-on: '${{ matrix.os }}'
+ strategy:
+ max-parallel: 1
+ fail-fast: false
+ matrix:
+ os:
+ - ubuntu-latest
+ - windows-latest
+ - macos-latest
+ steps:
+ - name: Set up .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 8.x
+ - name: Checkout source
+ uses: actions/checkout@v4
+ - name: Restore NuGet dependencies
+ run: >-
+ dotnet restore
+ LiveTests/Duplicati.Backend.Tests/Duplicati.Backend.Tests.sln
+ - name: Build project
+ run: >-
+ dotnet build --no-restore
+ LiveTests/Duplicati.Backend.Tests/Duplicati.Backend.Tests.sln
+ - name: Run Azure tests
+ env:
+ TESTCREDENTIAL_AZURE_ACCOUNTNAME: '${{ secrets.TESTCREDENTIAL_AZURE_ACCOUNTNAME }}'
+ TESTCREDENTIAL_AZURE_ACCESSKEY: '${{ secrets.TESTCREDENTIAL_AZURE_ACCESSKEY }}'
+ TESTCREDENTIAL_AZURE_CONTAINERNAME: '${{ secrets.TESTCREDENTIAL_AZURE_CONTAINERNAME }}'
+ run: >-
+ dotnet test --no-build --filter="ClassName~AzureTests"
+ --logger:"console;verbosity=detailed"
+ LiveTests/Duplicati.Backend.Tests/Duplicati.Backend.Tests.sln
+
+ test_webdav:
+ name: Webdav Tests
+ runs-on: '${{ matrix.os }}'
+ strategy:
+ max-parallel: 1
+ fail-fast: false
+ matrix:
+ os:
+ - ubuntu-latest
+ - windows-latest
+ - macos-latest
+ steps:
+ - name: Set up .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 8.x
+ - name: Checkout source
+ uses: actions/checkout@v4
+ - name: Restore NuGet dependencies
+ run: >-
+ dotnet restore
+ LiveTests/Duplicati.Backend.Tests/Duplicati.Backend.Tests.sln
+ - name: Build project
+ run: >-
+ dotnet build --no-restore
+ LiveTests/Duplicati.Backend.Tests/Duplicati.Backend.Tests.sln
+ - name: Setup Testcontainers Cloud Client
+ uses: atomicjar/testcontainers-cloud-setup-action@v1
+ with:
+ token: '${{ secrets.TC_CLOUD_TOKEN }}'
+ - name: Run Webdav tests
+ env:
+ TC_CLOUD_TOKEN: '${{ secrets.TC_CLOUD_TOKEN }}'
+ run: >-
+ dotnet test --no-build --filter="ClassName~WebDavTests"
+ --logger:"console;verbosity=detailed"
+ LiveTests/Duplicati.Backend.Tests/Duplicati.Backend.Tests.sln
+ test_dropbox:
+ needs: check_secrets
+ if: needs.check_secrets.outputs.dropbox_secrets_available == 'true'
+ name: DropBox Tests
+ runs-on: '${{ matrix.os }}'
+ strategy:
+ max-parallel: 1
+ fail-fast: false
+ matrix:
+ os:
+ - ubuntu-latest
+ - windows-latest
+ - macos-latest
+ steps:
+ - name: Set up .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 8.x
+ - name: Checkout source
+ uses: actions/checkout@v4
+ - name: Restore NuGet dependencies
+ run: >-
+ dotnet restore
+ LiveTests/Duplicati.Backend.Tests/Duplicati.Backend.Tests.sln
+ - name: Build project
+ run: >-
+ dotnet build --no-restore
+ LiveTests/Duplicati.Backend.Tests/Duplicati.Backend.Tests.sln
+ - name: Run Dropbox tests
+ env:
+ TC_CLOUD_TOKEN: '${{ secrets.TC_CLOUD_TOKEN }}'
+ TESTCREDENTIAL_DROPBOX_FOLDER: '${{ secrets.TESTCREDENTIAL_DROPBOX_FOLDER }}'
+ TESTCREDENTIAL_DROPBOX_TOKEN: '${{ secrets.TESTCREDENTIAL_DROPBOX_TOKEN }}'
+ run: >-
+ dotnet test --no-build --filter="ClassName~DropBoxTests"
+ --logger:"console;verbosity=detailed"
+ LiveTests/Duplicati.Backend.Tests/Duplicati.Backend.Tests.sln
+ test_ssh:
+ name: SSH Tests
+ runs-on: '${{ matrix.os }}'
+ strategy:
+ fail-fast: false
+ max-parallel: 1
+ matrix:
+ os:
+ - ubuntu-latest
+ - windows-latest
+ - macos-latest
+ steps:
+ - name: Set up .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 8.x
+ - name: Checkout source
+ uses: actions/checkout@v4
+ - name: Restore NuGet dependencies
+ run: >-
+ dotnet restore
+ LiveTests/Duplicati.Backend.Tests/Duplicati.Backend.Tests.sln
+ - name: Build project
+ run: >-
+ dotnet build --no-restore
+ LiveTests/Duplicati.Backend.Tests/Duplicati.Backend.Tests.sln
+ - name: Setup Testcontainers Cloud Client
+ uses: atomicjar/testcontainers-cloud-setup-action@v1
+ with:
+ token: '${{ secrets.TC_CLOUD_TOKEN }}'
+ - name: Run SSH tests
+ env:
+ TC_CLOUD_TOKEN: '${{ secrets.TC_CLOUD_TOKEN }}'
+ run: >-
+ dotnet test --no-build --filter="ClassName~SshTests"
+ --logger:"console;verbosity=detailed"
+ LiveTests/Duplicati.Backend.Tests/Duplicati.Backend.Tests.sln
\ No newline at end of file
diff --git a/LiveTests/Duplicati.Backend.Tests/Azure/AzureTests.cs b/LiveTests/Duplicati.Backend.Tests/Azure/AzureTests.cs
new file mode 100644
index 000000000..361b42ee0
--- /dev/null
+++ b/LiveTests/Duplicati.Backend.Tests/Azure/AzureTests.cs
@@ -0,0 +1,50 @@
+// Copyright (C) 2024, The Duplicati Team
+// https://duplicati.com, hello@duplicati.com
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the "Software"),
+// to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+// and/or sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+namespace Duplicati.Backend.Tests.Azure;
+
+///
+/// Tests for Dropbox backend
+///
+[TestClass]
+public sealed class AzureTests : BaseTest
+{
+ ///
+ /// Basic Dropbox test. There are no adicional parameters to be set or tested.
+ ///
+ [TestMethod]
+ public Task TestAzureBlob()
+ {
+ CheckRequiredEnvironment(["TESTCREDENTIAL_AZURE_ACCOUNTNAME","TESTCREDENTIAL_AZURE_ACCESSKEY","TESTCREDENTIAL_AZURE_CONTAINERNAME"
+ ]);
+
+ var exitCode = CommandLine.BackendTester.Program.Main(
+ new[]
+ {
+ $"azure://{Environment.GetEnvironmentVariable("TESTCREDENTIAL_AZURE_CONTAINERNAME")}?auth-username={Environment.GetEnvironmentVariable("TESTCREDENTIAL_AZURE_ACCOUNTNAME")}&auth-password={Uri.EscapeDataString(Environment.GetEnvironmentVariable("TESTCREDENTIAL_AZURE_ACCESSKEY")!)}"
+ }.Concat(Parameters.GlobalTestParameters).ToArray());
+
+ if (exitCode != 0) Assert.Fail("BackendTester is returning non-zero exit code, check logs for details");
+
+ return Task.CompletedTask;
+ }
+
+}
\ No newline at end of file
diff --git a/LiveTests/Duplicati.Backend.Tests/Base/BaseSFTPGOTest.cs b/LiveTests/Duplicati.Backend.Tests/Base/BaseSFTPGOTest.cs
new file mode 100644
index 000000000..d99fe488e
--- /dev/null
+++ b/LiveTests/Duplicati.Backend.Tests/Base/BaseSFTPGOTest.cs
@@ -0,0 +1,99 @@
+// Copyright (C) 2024, The Duplicati Team
+// https://duplicati.com, hello@duplicati.com
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the "Software"),
+// to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+// and/or sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+namespace Duplicati.Backend.Tests.Base;
+
+///
+/// Base class for tests that require a running SFTPGo instance which will
+/// be used to test the FTP, Webdav & SSH backends.
+///
+/// It handles the creation of the necessary configuration files and certificates
+///
+public class BaseSftpgoTest : BaseTest
+{
+ protected static readonly string TEST_USER_NAME = "testuser";
+ protected static readonly string TEST_USER_PASSWORD = "testpassword";
+ protected static readonly string CERTIFICATE_FILE = "fullchain.pem";
+ protected static readonly string CERTIFICATE_PRIVATE_KEY_FILE = "privkey.pem";
+
+ ///
+ /// Creates a josn file with the configuration for the SFTPGo instance
+ ///
+ /// directory in which to create the file
+ protected void CreateUsersFile(DirectoryInfo directoryInfo)
+ {
+ var sftpgoConfig = "{\"users\":[{\"id\":1,\"status\":1,\"username\":\"testuser\",\"password\":\"$2a$10$BTqu.odQz6jLQAi70KbQJOYBcRHpbhP4uxqcfFXZalq4zUsQZGj/6\",\"has_password\":true,\"home_dir\":\"/srv/sftpgo/data/testuser\",\"uid\":0,\"gid\":0,\"max_sessions\":0,\"quota_size\":0,\"quota_files\":0,\"permissions\":{\"/\":[\"*\"]},\"upload_data_transfer\":0,\"download_data_transfer\":0,\"total_data_transfer\":0,\"created_at\":1729521839754,\"updated_at\":1729521839754,\"last_password_change\":1729521839754,\"filters\":{\"hooks\":{\"external_auth_disabled\":false,\"pre_login_disabled\":false,\"check_password_disabled\":false},\"totp_config\":{\"secret\":{}}},\"filesystem\":{\"provider\":0,\"osconfig\":{},\"s3config\":{\"access_secret\":{}},\"gcsconfig\":{\"credentials\":{}},\"azblobconfig\":{\"account_key\":{},\"sas_url\":{}},\"cryptconfig\":{\"passphrase\":{}},\"sftpconfig\":{\"password\":{},\"private_key\":{},\"key_passphrase\":{}},\"httpconfig\":{\"password\":{},\"api_key\":{}}}}],\"groups\":[],\"folders\":[],\"admins\":[{\"id\":1,\"status\":1,\"username\":\"admin\",\"password\":\"$2a$10$HJGY10EuEPPajgut3OMTte5NXqbn1RXMTzy8BUA639a6qzinOjIx6\",\"permissions\":[\"*\"],\"filters\":{\"require_two_factor\":false,\"totp_config\":{\"secret\":{}},\"preferences\":{}},\"created_at\":1729521817148,\"updated_at\":1729521817148,\"last_login\":1729521817152}],\"api_keys\":[],\"shares\":[],\"event_actions\":[],\"event_rules\":[],\"roles\":[],\"ip_lists\":[],\"configs\":{},\"version\":16}";
+
+ using var usersFile = File.Create(Path.Combine(directoryInfo.FullName, "users.json"));
+
+ using var userFileWritter = new StreamWriter(usersFile);
+
+ userFileWritter.WriteLine(sftpgoConfig);
+
+ }
+
+ ///
+ /// Create a self signed certificate to be used on WebDav and SFTP connections.
+ ///
+ /// Its created on a temporary directory and the private key and public key are saved in PEM format.
+ ///
+ /// It is up to the caller to delete the files albeit its in the OS temporary directory.
+ ///
+ /// Returns the directory in which certificate was created
+ protected DirectoryInfo CreateHttpsCertificates()
+ {
+ using var rsa = RSA.Create(2048);
+
+ var request = new CertificateRequest(
+ $"CN=TestCertificateForWebdav",
+ rsa,
+ HashAlgorithmName.SHA256,
+ RSASignaturePadding.Pkcs1);
+
+ request.CertificateExtensions.Add(
+ new X509KeyUsageExtension(
+ X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DigitalSignature,
+ false));
+
+ request.CertificateExtensions.Add(
+ new X509EnhancedKeyUsageExtension(
+ [new Oid("1.3.6.1.5.5.7.3.1")], false));
+
+ var certificate = request.CreateSelfSigned(
+ DateTimeOffset.Now.AddDays(-1),
+ DateTimeOffset.Now.AddDays(30));
+
+ if (OperatingSystem.IsWindows()) certificate.FriendlyName = "Test Certificate";
+
+ var directoryInfo = Directory.CreateTempSubdirectory("TemporaryCertificates");
+
+ using var privateKeyFile = File.Create(Path.Combine(directoryInfo.FullName, CERTIFICATE_PRIVATE_KEY_FILE));
+ using var publicKeyFile = File.Create(Path.Combine(directoryInfo.FullName, CERTIFICATE_FILE));
+
+ using var privateKeyWriter = new StreamWriter(privateKeyFile);
+ using var publicKeyWriter = new StreamWriter(publicKeyFile);
+ privateKeyWriter.WriteLine(certificate.GetRSAPrivateKey()!.ExportRSAPrivateKeyPem());
+ publicKeyWriter.WriteLine(certificate.ExportCertificatePem());
+
+ return directoryInfo;
+ }
+
+}
\ No newline at end of file
diff --git a/LiveTests/Duplicati.Backend.Tests/Base/BaseTest.cs b/LiveTests/Duplicati.Backend.Tests/Base/BaseTest.cs
new file mode 100644
index 000000000..988173690
--- /dev/null
+++ b/LiveTests/Duplicati.Backend.Tests/Base/BaseTest.cs
@@ -0,0 +1,18 @@
+namespace Duplicati.Backend.Tests.Base;
+
+public class BaseTest
+{
+
+ ///
+ /// Checks if the environment variables are set and if not fails with detailed about the missing variables
+ ///
+ /// Array of variables to be checked
+ protected void CheckRequiredEnvironment(string[] requiredEnvironmentVariables)
+ {
+ var missingVariables = requiredEnvironmentVariables
+ .Where(variable => string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(variable)))
+ .ToList();
+
+ if (missingVariables.Any()) Assert.Fail($"Required environment variables not set: {string.Join(", ", missingVariables)}");
+ }
+}
\ No newline at end of file
diff --git a/LiveTests/Duplicati.Backend.Tests/Base/PreliminaryTests.cs b/LiveTests/Duplicati.Backend.Tests/Base/PreliminaryTests.cs
new file mode 100644
index 000000000..68a1e710a
--- /dev/null
+++ b/LiveTests/Duplicati.Backend.Tests/Base/PreliminaryTests.cs
@@ -0,0 +1,52 @@
+// Copyright (C) 2024, The Duplicati Team
+// https://duplicati.com, hello@duplicati.com
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the "Software"),
+// to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+// and/or sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+namespace Duplicati.Backend.Tests.Base;
+
+
+///
+/// Tests to check if the TestContainers is available
+///
+[TestClass]
+public class PreliminaryTests
+{
+ [TestMethod]
+ public async Task TestContainersEngineReady()
+ {
+ using var dockerClientConfiguration =
+ TestcontainersSettings.OS.DockerEndpointAuthConfig.GetDockerClientConfiguration(ResourceReaper
+ .DefaultSessionId);
+
+ using var dockerClient = dockerClientConfiguration.CreateClient();
+
+ var versionResponse = await dockerClient.System.GetSystemInfoAsync()
+ .ConfigureAwait(false);
+
+ if (!(versionResponse.ServerVersion.Contains("Testcontainers Desktop") ||
+ versionResponse.ServerVersion.Contains("testcontainers cloud")))
+ {
+ Assert.Fail("Testcontainers engine not found");
+ }
+
+ Console.WriteLine($"Engine configured: {versionResponse.ServerVersion}");
+ }
+
+}
\ No newline at end of file
diff --git a/LiveTests/Duplicati.Backend.Tests/Dropbox/DropBoxTests.cs b/LiveTests/Duplicati.Backend.Tests/Dropbox/DropBoxTests.cs
new file mode 100644
index 000000000..9e143cd46
--- /dev/null
+++ b/LiveTests/Duplicati.Backend.Tests/Dropbox/DropBoxTests.cs
@@ -0,0 +1,49 @@
+// Copyright (C) 2024, The Duplicati Team
+// https://duplicati.com, hello@duplicati.com
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the "Software"),
+// to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+// and/or sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+namespace Duplicati.Backend.Tests.Dropbox;
+
+///
+/// Tests for Dropbox backend
+///
+[TestClass]
+public sealed class DropBoxTests : BaseTest
+{
+ ///
+ /// Basic Dropbox test. There are no adicional parameters to be set or tested.
+ ///
+ [TestMethod]
+ public Task TestDropBox()
+ {
+ CheckRequiredEnvironment(["TESTCREDENTIAL_DROPBOX_FOLDER","TESTCREDENTIAL_DROPBOX_TOKEN"]);
+
+ var exitCode = CommandLine.BackendTester.Program.Main(
+ new[]
+ {
+ $"dropbox://{Environment.GetEnvironmentVariable("TESTCREDENTIAL_DROPBOX_FOLDER")}/?authid={Environment.GetEnvironmentVariable("TESTCREDENTIAL_DROPBOX_TOKEN")}"
+ }.Concat(Parameters.GlobalTestParameters).ToArray());
+
+ if (exitCode != 0) Assert.Fail("BackendTester is returning non-zero exit code, check logs for details");
+
+ return Task.CompletedTask;
+ }
+
+}
\ No newline at end of file
diff --git a/LiveTests/Duplicati.Backend.Tests/Duplicati.Backend.Tests.csproj b/LiveTests/Duplicati.Backend.Tests/Duplicati.Backend.Tests.csproj
new file mode 100644
index 000000000..5b0b5f88b
--- /dev/null
+++ b/LiveTests/Duplicati.Backend.Tests/Duplicati.Backend.Tests.csproj
@@ -0,0 +1,27 @@
+
+
+
+ enable
+ Exe
+ net8.0
+ enable
+ enable
+ Commandline implementation of Duplicati
+ Copyright © 2024 Team Duplicati, MIT license
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LiveTests/Duplicati.Backend.Tests/Duplicati.Backend.Tests.sln b/LiveTests/Duplicati.Backend.Tests/Duplicati.Backend.Tests.sln
new file mode 100644
index 000000000..88b2a38ae
--- /dev/null
+++ b/LiveTests/Duplicati.Backend.Tests/Duplicati.Backend.Tests.sln
@@ -0,0 +1,22 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Backend.Tests", "Duplicati.Backend.Tests.csproj", "{6D888116-3424-4DE4-8AE3-D01A44A3EE38}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.CommandLine.BackendTester", "..\..\Duplicati\CommandLine\BackendTester\Duplicati.CommandLine.BackendTester.csproj", "{41756E86-799B-4142-9948-D9D94054BA15}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {6D888116-3424-4DE4-8AE3-D01A44A3EE38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6D888116-3424-4DE4-8AE3-D01A44A3EE38}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6D888116-3424-4DE4-8AE3-D01A44A3EE38}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6D888116-3424-4DE4-8AE3-D01A44A3EE38}.Release|Any CPU.Build.0 = Release|Any CPU
+ {41756E86-799B-4142-9948-D9D94054BA15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {41756E86-799B-4142-9948-D9D94054BA15}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {41756E86-799B-4142-9948-D9D94054BA15}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {41756E86-799B-4142-9948-D9D94054BA15}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+EndGlobal
diff --git a/LiveTests/Duplicati.Backend.Tests/FTP/FTPTests.cs b/LiveTests/Duplicati.Backend.Tests/FTP/FTPTests.cs
new file mode 100644
index 000000000..9af92a77f
--- /dev/null
+++ b/LiveTests/Duplicati.Backend.Tests/FTP/FTPTests.cs
@@ -0,0 +1,156 @@
+// Copyright (C) 2024, The Duplicati Team
+// https://duplicati.com, hello@duplicati.com
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the "Software"),
+// to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+// and/or sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+namespace Duplicati.Backend.Tests.FTP;
+
+///
+/// FTP Tests
+///
+[TestClass]
+public sealed class FtpTests : BaseSftpgoTest
+{
+
+ ///
+ /// Test a simple FTP connection, no SSL
+ ///
+ ///
+ [TestMethod]
+ public async Task TestSimpleFtp()
+ {
+ var outputConsumer = new OutputConsumer();
+ var filePermissions = UnixFileModes.UserRead | UnixFileModes.UserWrite | UnixFileModes.UserExecute |
+ UnixFileModes.GroupRead | UnixFileModes.GroupWrite | UnixFileModes.GroupExecute |
+ UnixFileModes.OtherRead | UnixFileModes.OtherWrite | UnixFileModes.OtherExecute;
+
+ var temporaryKeysDir = CreateHttpsCertificates();
+
+ CreateUsersFile(temporaryKeysDir);
+
+ var container = new ContainerBuilder()
+ .WithImage("drakkan/sftpgo")
+ .WithEnvironment("SFTPGO_LOG_LEVEL", "debug")
+ .WithEnvironment("SFTPGO_LOADDATA_FROM", "/var/lib/sftpgo/users.json")
+ .WithEnvironment("SFTPGO_LOADDATA_CLEAN", "0")
+ .WithEnvironment("SFTPGO_FTPD__BINDINGS__0__PORT", "2121")
+ .WithEnvironment("SFTPGO_FTPD__PASSIVE_PORT_RANGE__END", "21000")
+ .WithEnvironment("SFTPGO_FTPD__PASSIVE_PORT_RANGE__START", "20000")
+ .WithEnvironment("SFTPGO_FTPD__BINDINGS__0__ACTIVE_CONNECTIONS_SECURITY", "1")
+ .WithEnvironment("SFTPGO_FTPD__BINDINGS__0__CERTIFICATE_FILE", CERTIFICATE_FILE)
+ .WithEnvironment("SFTPGO_FTPD__BINDINGS__0__CERTIFICATE_KEY_FILE", CERTIFICATE_PRIVATE_KEY_FILE)
+ .WithResourceMapping(temporaryKeysDir, "/var/lib/sftpgo/", filePermissions)
+ .WithPortBinding(2121, 2121)
+
+ .WithOutputConsumer(outputConsumer);
+
+ container = Enumerable.Range(20000, 1000)
+ .Aggregate(container, (current, port) => current.WithPortBinding(port, port));
+
+ var builtContainer = container
+ .WithOutputConsumer(outputConsumer)
+ .Build();
+
+ Console.WriteLine("Starting container");
+ await builtContainer.StartAsync();
+
+ // Once started we will already cleanup temporary directory
+ temporaryKeysDir.Delete(true);
+
+ Console.WriteLine("Waiting X seconds");
+ await Task.Delay(TimeSpan.FromSeconds(5));
+
+ // Running this ignoring the certificate
+ var exitCode = CommandLine.BackendTester.Program.Main(
+ new[]
+ {
+ $"ftp://{TEST_USER_NAME}:{TEST_USER_PASSWORD}@localhost:2121/",
+ }.Concat(Parameters.GlobalTestParameters).ToArray());
+
+ if (exitCode != 0)
+ {
+ Console.WriteLine(await outputConsumer.GetStreamsOutput());
+ Assert.Fail("BackendTester is returning non-zero exit code, check logs for details");
+ }
+
+ await builtContainer.StopAsync();
+ }
+
+ ///
+ /// Test FTP connection with SSL/TLS
+ ///
+ ///
+ [TestMethod]
+ public async Task TestFtpSsl()
+ {
+ var outputConsumer = new OutputConsumer();
+ var filePermissions = UnixFileModes.UserRead | UnixFileModes.UserWrite | UnixFileModes.UserExecute |
+ UnixFileModes.GroupRead | UnixFileModes.GroupWrite | UnixFileModes.GroupExecute |
+ UnixFileModes.OtherRead | UnixFileModes.OtherWrite | UnixFileModes.OtherExecute;
+
+ var temporaryKeysDir = CreateHttpsCertificates();
+
+ CreateUsersFile(temporaryKeysDir);
+
+ var container = new ContainerBuilder()
+ .WithImage("drakkan/sftpgo")
+ .WithEnvironment("SFTPGO_LOG_LEVEL", "debug")
+ .WithEnvironment("SFTPGO_LOADDATA_FROM", "/var/lib/sftpgo/users.json")
+ .WithEnvironment("SFTPGO_LOADDATA_CLEAN", "0")
+ .WithEnvironment("SFTPGO_FTPD__BINDINGS__0__PORT", "2121")
+ .WithEnvironment("SFTPGO_FTPD__PASSIVE_PORT_RANGE__END", "23000")
+ .WithEnvironment("SFTPGO_FTPD__PASSIVE_PORT_RANGE__START", "22000")
+ .WithEnvironment("SFTPGO_FTPD__BINDINGS__0__ACTIVE_CONNECTIONS_SECURITY", "1")
+ .WithEnvironment("SFTPGO_FTPD__BINDINGS__0__CERTIFICATE_FILE", CERTIFICATE_FILE)
+ .WithEnvironment("SFTPGO_FTPD__BINDINGS__0__CERTIFICATE_KEY_FILE", CERTIFICATE_PRIVATE_KEY_FILE)
+ .WithResourceMapping(temporaryKeysDir, "/var/lib/sftpgo/", filePermissions)
+ .WithPortBinding(2122, 2121)
+
+ .WithOutputConsumer(outputConsumer);
+
+ container = Enumerable.Range(22000, 1000)
+ .Aggregate(container, (current, port) => current.WithPortBinding(port, port));
+
+ var builtContainer = container
+ .WithOutputConsumer(outputConsumer)
+ .Build();
+
+ Console.WriteLine("Starting container");
+ await builtContainer.StartAsync();
+
+ // Once started we will already cleanup temporary directory
+ temporaryKeysDir.Delete(true);
+
+ Console.WriteLine("Waiting X seconds");
+ await Task.Delay(TimeSpan.FromSeconds(5));
+
+ // Running this ignoring the certificate
+ var exitCode = CommandLine.BackendTester.Program.Main(
+ ["ftp://testuser:testpassword@localhost:2122/", "use-ssl=true"]);
+
+ if (exitCode != 0)
+ {
+ Console.WriteLine(await outputConsumer.GetStreamsOutput());
+ Assert.Fail("BackendTester is returning non-zero exit code, check logs for details");
+ }
+
+ await builtContainer.StopAsync();
+ }
+
+}
\ No newline at end of file
diff --git a/LiveTests/Duplicati.Backend.Tests/GlobalUsings.cs b/LiveTests/Duplicati.Backend.Tests/GlobalUsings.cs
new file mode 100644
index 000000000..c8ed2bf18
--- /dev/null
+++ b/LiveTests/Duplicati.Backend.Tests/GlobalUsings.cs
@@ -0,0 +1,30 @@
+// Copyright (C) 2024, The Duplicati Team
+// https://duplicati.com, hello@duplicati.com
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the "Software"),
+// to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+// and/or sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+global using System.Security.Cryptography;
+global using System.Security.Cryptography.X509Certificates;
+global using System.Text;
+global using DotNet.Testcontainers.Builders;
+global using DotNet.Testcontainers.Configurations;
+global using DotNet.Testcontainers.Containers;
+global using Duplicati.Backend.Tests.Base;
+global using Duplicati.Backend.Tests.Utility;
+global using Microsoft.VisualStudio.TestTools.UnitTesting;
\ No newline at end of file
diff --git a/LiveTests/Duplicati.Backend.Tests/Google Drive/GoogleDriveTests.cs b/LiveTests/Duplicati.Backend.Tests/Google Drive/GoogleDriveTests.cs
new file mode 100644
index 000000000..798a30cdc
--- /dev/null
+++ b/LiveTests/Duplicati.Backend.Tests/Google Drive/GoogleDriveTests.cs
@@ -0,0 +1,50 @@
+// Copyright (C) 2024, The Duplicati Team
+// https://duplicati.com, hello@duplicati.com
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the "Software"),
+// to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+// and/or sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+namespace Duplicati.Backend.Tests.Google_Drive;
+
+///
+/// Google Drive tests
+///
+[TestClass]
+public sealed class GoogleDriveTests : BaseTest
+{
+
+ [TestMethod]
+ public Task TestGoogleDriveSimple()
+ {
+ CheckRequiredEnvironment(["TESTCREDENTIAL_GOOGLEDRIVE_TOKEN","TESTCREDENTIAL_GOOGLEDRIVE_FOLDER"]);
+
+ var exitCode = CommandLine.BackendTester.Program.Main(
+ new[]
+ {
+ $"googledrive:///{Environment.GetEnvironmentVariable("TESTCREDENTIAL_GOOGLEDRIVE_FOLDER")}?authid={Environment.GetEnvironmentVariable("TESTCREDENTIAL_GOOGLEDRIVE_TOKEN")}",
+
+ }.Concat(Parameters.GlobalTestParameters).ToArray());
+
+ if (exitCode != 0) Assert.Fail("BackendTester is returning non-zero exit code, check logs for details");
+
+ return Task.CompletedTask;
+
+ }
+
+
+}
\ No newline at end of file
diff --git a/LiveTests/Duplicati.Backend.Tests/Parameters.cs b/LiveTests/Duplicati.Backend.Tests/Parameters.cs
new file mode 100644
index 000000000..bf2b2a26d
--- /dev/null
+++ b/LiveTests/Duplicati.Backend.Tests/Parameters.cs
@@ -0,0 +1,39 @@
+// Copyright (C) 2024, The Duplicati Team
+// https://duplicati.com, hello@duplicati.com
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the "Software"),
+// to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+// and/or sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+namespace Duplicati.Backend.Tests;
+
+///
+/// Glboal parameters to be used on all tests
+///
+public static class Parameters
+{
+ // Standard parameters for the backend tester
+ public static string[] GlobalTestParameters =>
+ [
+ "--auto-clean",
+ "--auto-create-folder",
+ "--extended-chars=_-",
+ "--force",
+ $"--max-file-size={Environment.GetEnvironmentVariable("MAX_FILE_SIZE") ?? "1000kb"}",
+ $"--number-of-files={Environment.GetEnvironmentVariable("NUMBER_OF_FILES") ?? "20"}"
+ ];
+}
\ No newline at end of file
diff --git a/LiveTests/Duplicati.Backend.Tests/README.md b/LiveTests/Duplicati.Backend.Tests/README.md
new file mode 100644
index 000000000..6c21d81dc
--- /dev/null
+++ b/LiveTests/Duplicati.Backend.Tests/README.md
@@ -0,0 +1,76 @@
+
+# Environment variables for tests
+
+On github actions these are mapped 1:1 to secrets, even the non password fields are stored in secrets.
+
+Tests like FTP, SSH and Webdav are self-contained, they do not require any environment variable.
+
+## General
+
+These control the size and number of files generated.
+
+```
+MAX_FILE_SIZE default is 1000kb
+NUMBER_OF_FILES default is 20
+```
+
+## Google Drive:
+
+Google Drive credentials are mapped to the following environment variables:
+
+```
+TESTCREDENTIAL_GOOGLEDRIVE_FOLDER
+TESTCREDENTIAL_GOOGLEDRIVE_TOKEN
+```
+## S3
+
+S3 credentials are mapped to the following environment variables:
+
+Attention: **AWS TESTCREDENTIAL_S3_SECRET is URI escaped automatically, supply the raw value.**
+
+```
+TESTCREDENTIAL_S3_KEY
+TESTCREDENTIAL_S3_SECRET
+TESTCREDENTIAL_S3_BUCKETNAME
+TESTCREDENTIAL_S3_REGION
+```
+
+## Dropbox
+
+Dropbox credentials are mapped to the following environment variables:
+
+```
+TESTCREDENTIAL_DROPBOX_FOLDER
+TESTCREDENTIAL_DROPBOX_TOKEN
+```
+
+## Azure Blob
+
+Attention: **TESTCREDENTIAL_AZURE_ACCESSKEY is URI escaped automatically, supply the raw value.**
+
+
+```
+TESTCREDENTIAL_AZURE_ACCOUNTNAME
+TESTCREDENTIAL_AZURE_ACCESSKEY
+TESTCREDENTIAL_AZURE_CONTAINERNAME
+```
+
+## Running the tests
+
+[TestContainers](https://testcontainers.org/) is a pre-requisite for SSH, FTP and Webdav tests. It is not required for the other tests.
+
+Set the environment variables as described above, then run the tests using the following commands:
+
+Minimal Verbosity:
+
+`dotnet test Duplicati.Backend.Tests.sln --logger:"console;verbosity=normal"`
+
+Running with full verbosity (useful if tests are failing):
+
+`dotnet test Duplicati.Backend.Tests.sln --logger:"console;verbosity=detailed"`
+
+Running specific tests:
+
+`dotnet test Duplicati.Backend.Tests.sln --logger:"console;verbosity=detailed" --filter="Name=TestDropBox"`
+
+
diff --git a/LiveTests/Duplicati.Backend.Tests/S3/S3Tests.cs b/LiveTests/Duplicati.Backend.Tests/S3/S3Tests.cs
new file mode 100644
index 000000000..402f3030e
--- /dev/null
+++ b/LiveTests/Duplicati.Backend.Tests/S3/S3Tests.cs
@@ -0,0 +1,76 @@
+// Copyright (C) 2024, The Duplicati Team
+// https://duplicati.com, hello@duplicati.com
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the "Software"),
+// to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+// and/or sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+namespace Duplicati.Backend.Tests.S3;
+
+///
+/// S3 Tests
+///
+[TestClass]
+public sealed class S3Tests : BaseTest
+{
+ ///
+ /// Perform tests using AWS Client
+ ///
+ ///
+ [TestMethod]
+ public Task TestS3WithAwsClient()
+ {
+ return TestS3WithClient("aws");
+ }
+
+ ///
+ /// Perform tests using Minio Client
+ ///
+ ///
+ [TestMethod]
+ public Task TestS3WithMinioClient()
+ {
+ return TestS3WithClient("minio");
+ }
+
+ ///
+ /// Performs tests with selected client. The S3 tests don't have any additional parameters to be set or tested.
+ ///
+ /// client to be used, either aws or minio
+ ///
+ private Task TestS3WithClient(string client)
+ {
+ CheckRequiredEnvironment([
+ "TESTCREDENTIAL_S3_KEY",
+ "TESTCREDENTIAL_S3_SECRET",
+ "TESTCREDENTIAL_S3_BUCKETNAME",
+ "TESTCREDENTIAL_S3_REGION"
+ ]);
+
+ Console.WriteLine( $"s3://{Environment.GetEnvironmentVariable("TESTCREDENTIAL_S3_BUCKETNAME")}/?s3-location-constraint={Environment.GetEnvironmentVariable("TESTCREDENTIAL_S3_REGION")}&s3-storage-class=&s3-client={client}&auth-username={Environment.GetEnvironmentVariable("TESTCREDENTIAL_S3_KEY")}&auth-password={Environment.GetEnvironmentVariable("TESTCREDENTIAL_S3_SECRET")}");
+ var exitCode = CommandLine.BackendTester.Program.Main(
+ new[]
+ {
+ $"s3://{Environment.GetEnvironmentVariable("TESTCREDENTIAL_S3_BUCKETNAME")}/?s3-location-constraint={Environment.GetEnvironmentVariable("TESTCREDENTIAL_S3_REGION")}&s3-storage-class=&s3-client={client}&auth-username={Environment.GetEnvironmentVariable("TESTCREDENTIAL_S3_KEY")}&auth-password={Uri.EscapeDataString(Environment.GetEnvironmentVariable("TESTCREDENTIAL_S3_SECRET")!)}"
+ }.Concat(Parameters.GlobalTestParameters).ToArray());
+
+ if (exitCode != 0) Assert.Fail("BackendTester is returning non-zero exit code, check logs for details");
+
+ return Task.CompletedTask;
+ }
+
+}
\ No newline at end of file
diff --git a/LiveTests/Duplicati.Backend.Tests/SSH/SSHTests.cs b/LiveTests/Duplicati.Backend.Tests/SSH/SSHTests.cs
new file mode 100644
index 000000000..4a4629999
--- /dev/null
+++ b/LiveTests/Duplicati.Backend.Tests/SSH/SSHTests.cs
@@ -0,0 +1,86 @@
+// Copyright (C) 2024, The Duplicati Team
+// https://duplicati.com, hello@duplicati.com
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the "Software"),
+// to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+// and/or sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+namespace Duplicati.Backend.Tests.SSH;
+
+///
+/// SSH Tests
+///
+[TestClass]
+public sealed class SshTests : BaseSftpgoTest
+{
+
+ ///
+ /// Tests connecting to SSH using password authentication
+ ///
+ [TestMethod]
+ public async Task TestSSHWithPassword()
+ {
+
+ var outputConsumer = new OutputConsumer();
+ var filePermissions = UnixFileModes.UserRead | UnixFileModes.UserWrite | UnixFileModes.UserExecute |
+ UnixFileModes.GroupRead | UnixFileModes.GroupWrite | UnixFileModes.GroupExecute |
+ UnixFileModes.OtherRead | UnixFileModes.OtherWrite | UnixFileModes.OtherExecute;
+
+ var temporaryKeysDir = CreateHttpsCertificates();
+
+ CreateUsersFile(temporaryKeysDir);
+
+ var container = new ContainerBuilder()
+ .WithImage("drakkan/sftpgo")
+ .WithEnvironment("SFTPGO_LOG_LEVEL", "debug")
+ .WithEnvironment("SFTPGO_LOADDATA_FROM", "/var/lib/sftpgo/users.json")
+ .WithEnvironment("SFTPGO_LOADDATA_CLEAN", "0")
+ .WithEnvironment("SFTPGO_SFTPD__BINDINGS__0__PORT", "3333")
+ .WithPortBinding(3000, 3333)
+ .WithResourceMapping(temporaryKeysDir, "/var/lib/sftpgo/", filePermissions)
+ .WithOutputConsumer(outputConsumer)
+ .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(3333))
+ .Build();
+
+ Console.WriteLine("Starting container");
+ await container.StartAsync();
+
+ // Once started we will already cleanup temporary directory
+ temporaryKeysDir.Delete(true);
+
+ Console.WriteLine("Waiting X seconds");
+ await Task.Delay(TimeSpan.FromSeconds(1));
+
+ // Console.WriteLine($"TCP Connect testing, @::1 is open = { await IsPortOpenAsync("::1",3000) }");
+ // Console.WriteLine($"TCP Connect testing, @localhost is open = { await IsPortOpenAsync("localhost",3000) }");
+
+ var exitCode = CommandLine.BackendTester.Program.Main(
+ new[]
+ {
+ $"ssh://{TEST_USER_NAME}:{TEST_USER_PASSWORD}@127.0.0.1:3000",
+ "--ssh-accept-any-fingerprints"
+ }.Concat(Parameters.GlobalTestParameters).ToArray());
+
+ if (exitCode != 0)
+ {
+ Console.WriteLine(await outputConsumer.GetStreamsOutput());
+ Assert.Fail("BackendTester is returning non-zero exit code, check logs for details");
+ }
+
+ await container.StopAsync();
+ }
+}
\ No newline at end of file
diff --git a/LiveTests/Duplicati.Backend.Tests/Utility/Output.cs b/LiveTests/Duplicati.Backend.Tests/Utility/Output.cs
new file mode 100644
index 000000000..f0c52fe81
--- /dev/null
+++ b/LiveTests/Duplicati.Backend.Tests/Utility/Output.cs
@@ -0,0 +1,60 @@
+// Copyright (C) 2024, The Duplicati Team
+// https://duplicati.com, hello@duplicati.com
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the "Software"),
+// to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+// and/or sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+namespace Duplicati.Backend.Tests.Utility;
+public class OutputConsumer : IOutputConsumer
+{
+ private readonly MemoryStream _stdout = new();
+ private readonly MemoryStream _stderr = new();
+
+ public bool Enabled => true;
+
+ public Stream Stdout => _stdout;
+
+ public Stream Stderr => _stderr;
+
+ public void Dispose()
+ {
+ _stdout.Dispose();
+ _stderr.Dispose();
+ }
+
+ public async Task GetStreamsOutput()
+ {
+ var output = new StringBuilder();
+
+ _stdout.Position = 0;
+ using (var reader = new StreamReader(_stdout, leaveOpen: true))
+ {
+ output.AppendLine("STDOUT:");
+ output.AppendLine(await reader.ReadToEndAsync());
+ }
+
+ _stderr.Position = 0;
+ using (var reader = new StreamReader(_stderr, leaveOpen: true))
+ {
+ output.AppendLine("STDERR:");
+ output.AppendLine(await reader.ReadToEndAsync());
+ }
+
+ return output.ToString();
+ }
+}
\ No newline at end of file
diff --git a/LiveTests/Duplicati.Backend.Tests/Webdav/WebdavTests.cs b/LiveTests/Duplicati.Backend.Tests/Webdav/WebdavTests.cs
new file mode 100644
index 000000000..47263c8fe
--- /dev/null
+++ b/LiveTests/Duplicati.Backend.Tests/Webdav/WebdavTests.cs
@@ -0,0 +1,141 @@
+// Copyright (C) 2024, The Duplicati Team
+// https://duplicati.com, hello@duplicati.com
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the "Software"),
+// to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+// and/or sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+namespace Duplicati.Backend.Tests.Webdav;
+
+[TestClass]
+public sealed class WebDavTests : BaseSftpgoTest
+{
+
+ [TestMethod]
+ public async Task TestWebdavHttps()
+ {
+ var outputConsumer = new OutputConsumer();
+ var filePermissions = UnixFileModes.UserRead | UnixFileModes.UserWrite | UnixFileModes.UserExecute |
+ UnixFileModes.GroupRead | UnixFileModes.GroupWrite | UnixFileModes.GroupExecute |
+ UnixFileModes.OtherRead | UnixFileModes.OtherWrite | UnixFileModes.OtherExecute;
+
+ var temporaryKeysDir = CreateHttpsCertificates();
+
+ CreateUsersFile(temporaryKeysDir);
+
+ var container = new ContainerBuilder()
+ .WithImage("drakkan/sftpgo")
+ .WithEnvironment("SFTPGO_LOG_LEVEL", "debug")
+ .WithEnvironment("SFTPGO_WEBDAVD__BINDINGS__0__ENABLE_HTTPS", "1")
+ .WithEnvironment("SFTPGO_WEBDAVD__BINDINGS__0__PORT", "8090")
+ .WithEnvironment("SFTPGO_WEBDAVD__BINDINGS__0__CERTIFICATE_FILE", "fullchain.pem")
+ .WithEnvironment("SFTPGO_WEBDAVD__BINDINGS__0__CERTIFICATE_KEY_FILE", "privkey.pem")
+ .WithEnvironment("SFTPGO_LOADDATA_FROM", "/var/lib/sftpgo/users.json")
+ .WithEnvironment("SFTPGO_LOADDATA_CLEAN", "0")
+ .WithResourceMapping(temporaryKeysDir, "/var/lib/sftpgo/", filePermissions)
+ .WithPortBinding(8090, 8090)
+ .WithOutputConsumer(outputConsumer)
+ .Build();
+
+ Console.WriteLine("Starting container");
+ await container.StartAsync();
+
+ // Once started we will already cleanup temporary directory
+ temporaryKeysDir.Delete(true);
+
+ Console.WriteLine("Waiting X seconds");
+ await Task.Delay(TimeSpan.FromSeconds(5));
+
+ // Print logs
+ Console.WriteLine("Fetching logs...");
+ var containerStartupLogs = await outputConsumer.GetStreamsOutput();
+ Console.WriteLine(containerStartupLogs);
+
+ // Running this ignoring the certificate
+ var exitCode = CommandLine.BackendTester.Program.Main(
+ new[] { $"webdavs://{TEST_USER_NAME}:{TEST_USER_PASSWORD}@localhost:8090/", "--accept-any-ssl-certificate=true" }.Concat(Parameters.GlobalTestParameters).ToArray());
+
+ if (exitCode != 0)
+ {
+ Assert.Fail("BackendTester is returning non-zero exit code, check logs for details");
+ }
+
+ // Running not ignoring the certificate, should return non-zero exit code
+ exitCode = CommandLine.BackendTester.Program.Main(
+ new[] { $"webdavs://{TEST_USER_NAME}:{TEST_USER_PASSWORD}@localhost:8090/", "--accept-any-ssl-certificate=false" }.Concat(Parameters.GlobalTestParameters).ToArray());
+
+ if (exitCode == 0)
+ {
+ Assert.Fail("BackendTester is returning zero when it should return non-zero exit code, check logs for details");
+ }
+
+ Console.WriteLine("Stopping container");
+ await container.StopAsync();
+
+ }
+
+ [TestMethod]
+ public async Task TestWebdavHttp()
+ {
+ var outputConsumer = new OutputConsumer();
+ var filePermissions = UnixFileModes.UserRead | UnixFileModes.UserWrite | UnixFileModes.UserExecute |
+ UnixFileModes.GroupRead | UnixFileModes.GroupWrite | UnixFileModes.GroupExecute |
+ UnixFileModes.OtherRead | UnixFileModes.OtherWrite | UnixFileModes.OtherExecute;
+
+ var temporaryKeysDir = CreateHttpsCertificates();
+
+ CreateUsersFile(temporaryKeysDir);
+
+ var container = new ContainerBuilder()
+ .WithImage("drakkan/sftpgo")
+ .WithEnvironment("SFTPGO_LOG_LEVEL", "debug")
+ .WithEnvironment("SFTPGO_WEBDAVD__BINDINGS__0__PORT", "8090")
+ .WithEnvironment("SFTPGO_LOADDATA_FROM", "/var/lib/sftpgo/users.json")
+ .WithEnvironment("SFTPGO_LOADDATA_CLEAN", "0")
+ .WithResourceMapping(temporaryKeysDir, "/var/lib/sftpgo/", filePermissions)
+ .WithPortBinding(8091, 8090)
+ .WithOutputConsumer(outputConsumer)
+ .Build();
+
+ Console.WriteLine("Starting container");
+ await container.StartAsync();
+
+ // Once started we will already cleanup temporary directory
+ temporaryKeysDir.Delete(true);
+
+ Console.WriteLine("Waiting X seconds");
+ await Task.Delay(TimeSpan.FromSeconds(5));
+
+ // Print logs
+ Console.WriteLine("Fetching logs...");
+ var containerStartupLogs = await outputConsumer.GetStreamsOutput();
+ Console.WriteLine(containerStartupLogs);
+
+ // Running this ignoring the certificate
+ var exitCode = CommandLine.BackendTester.Program.Main(
+ ["webdav://testuser:testpassword@localhost:8091/"]);
+
+ if (exitCode != 0)
+ {
+ Assert.Fail("BackendTester is returning non-zero exit code, check logs for details");
+ }
+
+ Console.WriteLine("Stopping container");
+ await container.StopAsync();
+
+ }
+}
\ No newline at end of file