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