From c56c4958c68637aac61a6278011fc6b77ba74b9e Mon Sep 17 00:00:00 2001 From: Wim Van Laer Date: Mon, 10 Feb 2025 18:07:28 +0100 Subject: [PATCH] feat: Add SFTP module (#1362) Co-authored-by: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> --- .github/workflows/cicd.yml | 1 + Testcontainers.sln | 14 ++ src/Testcontainers.Sftp/.editorconfig | 1 + src/Testcontainers.Sftp/SftpBuilder.cs | 132 ++++++++++++++++++ src/Testcontainers.Sftp/SftpConfiguration.cs | 80 +++++++++++ src/Testcontainers.Sftp/SftpContainer.cs | 15 ++ .../Testcontainers.Sftp.csproj | 12 ++ src/Testcontainers.Sftp/Usings.cs | 6 + tests/Testcontainers.Sftp.Tests/.editorconfig | 1 + .../SftpContainerTest.cs | 35 +++++ .../Testcontainers.Sftp.Tests.csproj | 17 +++ tests/Testcontainers.Sftp.Tests/Usings.cs | 5 + 12 files changed, 319 insertions(+) create mode 100644 src/Testcontainers.Sftp/.editorconfig create mode 100644 src/Testcontainers.Sftp/SftpBuilder.cs create mode 100644 src/Testcontainers.Sftp/SftpConfiguration.cs create mode 100644 src/Testcontainers.Sftp/SftpContainer.cs create mode 100644 src/Testcontainers.Sftp/Testcontainers.Sftp.csproj create mode 100644 src/Testcontainers.Sftp/Usings.cs create mode 100644 tests/Testcontainers.Sftp.Tests/.editorconfig create mode 100644 tests/Testcontainers.Sftp.Tests/SftpContainerTest.cs create mode 100644 tests/Testcontainers.Sftp.Tests/Testcontainers.Sftp.Tests.csproj create mode 100644 tests/Testcontainers.Sftp.Tests/Usings.cs diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 642ce1a33..80f7cbee3 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -79,6 +79,7 @@ jobs: { name: "Testcontainers.Redis", runs-on: "ubuntu-22.04" }, { name: "Testcontainers.Redpanda", runs-on: "ubuntu-22.04" }, { name: "Testcontainers.ServiceBus", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.Sftp", runs-on: "ubuntu-22.04" }, { name: "Testcontainers.Weaviate", runs-on: "ubuntu-22.04" }, { name: "Testcontainers.WebDriver", runs-on: "ubuntu-22.04" }, { name: "Testcontainers.Xunit", runs-on: "ubuntu-22.04" } diff --git a/Testcontainers.sln b/Testcontainers.sln index 444bdb1c3..4afa799e9 100644 --- a/Testcontainers.sln +++ b/Testcontainers.sln @@ -97,6 +97,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Redpanda", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.ServiceBus", "src\Testcontainers.ServiceBus\Testcontainers.ServiceBus.csproj", "{2E39E532-B81E-4B48-A004-FAE18EDF9E79}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Sftp", "src\Testcontainers.Sftp\Testcontainers.Sftp.csproj", "{7D5C6816-0DD2-4E13-A585-033B5D3C80D5}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Weaviate", "src\Testcontainers.Weaviate\Testcontainers.Weaviate.csproj", "{68F8600D-24E9-4E03-9E25-5F6EB338EAC1}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.WebDriver", "src\Testcontainers.WebDriver\Testcontainers.WebDriver.csproj", "{64A87DE5-29B0-4A54-9E74-560484D8C7C0}" @@ -201,6 +203,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.ResourceReap EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.ServiceBus.Tests", "tests\Testcontainers.ServiceBus.Tests\Testcontainers.ServiceBus.Tests.csproj", "{232DD918-46ED-4BA8-B383-1A9146D83064}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Sftp.Tests", "tests\Testcontainers.Sftp.Tests\Testcontainers.Sftp.Tests.csproj", "{B73C3CC0-9F16-4B34-92BE-6EC0853912C5}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Tests", "tests\Testcontainers.Tests\Testcontainers.Tests.csproj", "{27CDB869-A150-4593-958F-6F26E5391E7C}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Weaviate.Tests", "tests\Testcontainers.Weaviate.Tests\Testcontainers.Weaviate.Tests.csproj", "{DDB41BC8-5826-4D97-9C5F-001151E3FFD6}" @@ -386,6 +390,10 @@ Global {2E39E532-B81E-4B48-A004-FAE18EDF9E79}.Debug|Any CPU.Build.0 = Debug|Any CPU {2E39E532-B81E-4B48-A004-FAE18EDF9E79}.Release|Any CPU.ActiveCfg = Release|Any CPU {2E39E532-B81E-4B48-A004-FAE18EDF9E79}.Release|Any CPU.Build.0 = Release|Any CPU + {7D5C6816-0DD2-4E13-A585-033B5D3C80D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7D5C6816-0DD2-4E13-A585-033B5D3C80D5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7D5C6816-0DD2-4E13-A585-033B5D3C80D5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7D5C6816-0DD2-4E13-A585-033B5D3C80D5}.Release|Any CPU.Build.0 = Release|Any CPU {68F8600D-24E9-4E03-9E25-5F6EB338EAC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {68F8600D-24E9-4E03-9E25-5F6EB338EAC1}.Debug|Any CPU.Build.0 = Debug|Any CPU {68F8600D-24E9-4E03-9E25-5F6EB338EAC1}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -594,6 +602,10 @@ Global {232DD918-46ED-4BA8-B383-1A9146D83064}.Debug|Any CPU.Build.0 = Debug|Any CPU {232DD918-46ED-4BA8-B383-1A9146D83064}.Release|Any CPU.ActiveCfg = Release|Any CPU {232DD918-46ED-4BA8-B383-1A9146D83064}.Release|Any CPU.Build.0 = Release|Any CPU + {B73C3CC0-9F16-4B34-92BE-6EC0853912C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B73C3CC0-9F16-4B34-92BE-6EC0853912C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B73C3CC0-9F16-4B34-92BE-6EC0853912C5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B73C3CC0-9F16-4B34-92BE-6EC0853912C5}.Release|Any CPU.Build.0 = Release|Any CPU {27CDB869-A150-4593-958F-6F26E5391E7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {27CDB869-A150-4593-958F-6F26E5391E7C}.Debug|Any CPU.Build.0 = Debug|Any CPU {27CDB869-A150-4593-958F-6F26E5391E7C}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -654,6 +666,7 @@ Global {BFDA179A-40EB-4CEB-B8E9-0DF32C65E2C5} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {45D6F69C-4D87-4130-AA90-0DB2F7460DAE} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {2E39E532-B81E-4B48-A004-FAE18EDF9E79} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} + {7D5C6816-0DD2-4E13-A585-033B5D3C80D5} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {68F8600D-24E9-4E03-9E25-5F6EB338EAC1} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {64A87DE5-29B0-4A54-9E74-560484D8C7C0} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {380BB29B-F556-404D-B13B-CA250599C565} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} @@ -706,6 +719,7 @@ Global {867BD04E-4670-4FBA-98D5-9F83220E6DFB} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {9E8E6AA5-65D1-498F-BEAB-BA34723A0050} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {232DD918-46ED-4BA8-B383-1A9146D83064} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} + {B73C3CC0-9F16-4B34-92BE-6EC0853912C5} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {27CDB869-A150-4593-958F-6F26E5391E7C} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {DDB41BC8-5826-4D97-9C5F-001151E3FFD6} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} diff --git a/src/Testcontainers.Sftp/.editorconfig b/src/Testcontainers.Sftp/.editorconfig new file mode 100644 index 000000000..6f066619d --- /dev/null +++ b/src/Testcontainers.Sftp/.editorconfig @@ -0,0 +1 @@ +root = true \ No newline at end of file diff --git a/src/Testcontainers.Sftp/SftpBuilder.cs b/src/Testcontainers.Sftp/SftpBuilder.cs new file mode 100644 index 000000000..f294c55a2 --- /dev/null +++ b/src/Testcontainers.Sftp/SftpBuilder.cs @@ -0,0 +1,132 @@ +namespace Testcontainers.Sftp; + +/// +[PublicAPI] +public sealed class SftpBuilder : ContainerBuilder +{ + public const string SftpImage = "atmoz/sftp:alpine"; + + public const ushort SftpPort = 22; + + public const string DefaultUsername = "sftp"; + + public const string DefaultPassword = "sftp"; + + public const string DefaultUploadDirectory = "upload"; + + /// + /// Initializes a new instance of the class. + /// + public SftpBuilder() + : this(new SftpConfiguration()) + { + DockerResourceConfiguration = Init().DockerResourceConfiguration; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + private SftpBuilder(SftpConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + DockerResourceConfiguration = resourceConfiguration; + } + + /// + protected override SftpConfiguration DockerResourceConfiguration { get; } + + /// + /// Sets the Sftp username. + /// + /// The Sftp username. + /// A configured instance of . + public SftpBuilder WithUsername(string username) + { + return Merge(DockerResourceConfiguration, new SftpConfiguration(username: username)); + } + + /// + /// Sets the Sftp password. + /// + /// The Sftp password. + /// A configured instance of . + public SftpBuilder WithPassword(string password) + { + return Merge(DockerResourceConfiguration, new SftpConfiguration(password: password)); + } + + /// + /// Sets the directory to which files are uploaded. + /// + /// The upload directory. + /// A configured instance of . + public SftpBuilder WithUploadDirectory(string uploadDirectory) + { + return Merge(DockerResourceConfiguration, new SftpConfiguration(uploadDirectory: uploadDirectory)); + } + + /// + public override SftpContainer Build() + { + Validate(); + + var sftpContainer = WithCommand(string.Join( + ":", + DockerResourceConfiguration.Username, + DockerResourceConfiguration.Password, + string.Empty, + string.Empty, + DockerResourceConfiguration.UploadDirectory)); + + return new SftpContainer(sftpContainer.DockerResourceConfiguration); + } + + /// + protected override SftpBuilder Init() + { + return base.Init() + .WithImage(SftpImage) + .WithPortBinding(SftpPort, true) + .WithUsername(DefaultUsername) + .WithPassword(DefaultPassword) + .WithUploadDirectory(DefaultUploadDirectory) + .WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged("Server listening on .+")); + } + + /// + protected override void Validate() + { + base.Validate(); + + _ = Guard.Argument(DockerResourceConfiguration.Username, nameof(DockerResourceConfiguration.Username)) + .NotNull() + .NotEmpty(); + + _ = Guard.Argument(DockerResourceConfiguration.Password, nameof(DockerResourceConfiguration.Password)) + .NotNull() + .NotEmpty(); + + _ = Guard.Argument(DockerResourceConfiguration.UploadDirectory, nameof(DockerResourceConfiguration.UploadDirectory)) + .NotNull() + .NotEmpty(); + } + + /// + protected override SftpBuilder Clone(IResourceConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new SftpConfiguration(resourceConfiguration)); + } + + /// + protected override SftpBuilder Clone(IContainerConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new SftpConfiguration(resourceConfiguration)); + } + + /// + protected override SftpBuilder Merge(SftpConfiguration oldValue, SftpConfiguration newValue) + { + return new SftpBuilder(new SftpConfiguration(oldValue, newValue)); + } +} \ No newline at end of file diff --git a/src/Testcontainers.Sftp/SftpConfiguration.cs b/src/Testcontainers.Sftp/SftpConfiguration.cs new file mode 100644 index 000000000..9dfb00c44 --- /dev/null +++ b/src/Testcontainers.Sftp/SftpConfiguration.cs @@ -0,0 +1,80 @@ +namespace Testcontainers.Sftp; + +/// +[PublicAPI] +public sealed class SftpConfiguration : ContainerConfiguration +{ + /// + /// Initializes a new instance of the class. + /// + /// The Sftp username. + /// The Sftp password. + /// The directory to which files are uploaded. + public SftpConfiguration( + string username = null, + string password = null, + string uploadDirectory = null) + { + Username = username; + Password = password; + UploadDirectory = uploadDirectory; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public SftpConfiguration(IResourceConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + // Passes the configuration upwards to the base implementations to create an updated immutable copy. + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public SftpConfiguration(IContainerConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + // Passes the configuration upwards to the base implementations to create an updated immutable copy. + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public SftpConfiguration(SftpConfiguration resourceConfiguration) + : this(new SftpConfiguration(), resourceConfiguration) + { + // Passes the configuration upwards to the base implementations to create an updated immutable copy. + } + + /// + /// Initializes a new instance of the class. + /// + /// The old Docker resource configuration. + /// The new Docker resource configuration. + public SftpConfiguration(SftpConfiguration oldValue, SftpConfiguration newValue) + : base(oldValue, newValue) + { + Username = BuildConfiguration.Combine(oldValue.Username, newValue.Username); + Password = BuildConfiguration.Combine(oldValue.Password, newValue.Password); + UploadDirectory = BuildConfiguration.Combine(oldValue.UploadDirectory, newValue.UploadDirectory); + } + + /// + /// Gets the Sftp username. + /// + public string Username { get; } + + /// + /// Gets the Sftp password. + /// + public string Password { get; } + + /// + /// Gets the directory to which files are uploaded. + /// + public string UploadDirectory { get; } +} \ No newline at end of file diff --git a/src/Testcontainers.Sftp/SftpContainer.cs b/src/Testcontainers.Sftp/SftpContainer.cs new file mode 100644 index 000000000..d93956f73 --- /dev/null +++ b/src/Testcontainers.Sftp/SftpContainer.cs @@ -0,0 +1,15 @@ +namespace Testcontainers.Sftp; + +/// +[PublicAPI] +public sealed class SftpContainer : DockerContainer +{ + /// + /// Initializes a new instance of the class. + /// + /// The container configuration. + public SftpContainer(SftpConfiguration configuration) + : base(configuration) + { + } +} \ No newline at end of file diff --git a/src/Testcontainers.Sftp/Testcontainers.Sftp.csproj b/src/Testcontainers.Sftp/Testcontainers.Sftp.csproj new file mode 100644 index 000000000..9a25b9c4d --- /dev/null +++ b/src/Testcontainers.Sftp/Testcontainers.Sftp.csproj @@ -0,0 +1,12 @@ + + + net8.0;net9.0;netstandard2.0;netstandard2.1 + latest + + + + + + + + \ No newline at end of file diff --git a/src/Testcontainers.Sftp/Usings.cs b/src/Testcontainers.Sftp/Usings.cs new file mode 100644 index 000000000..fa3a104a1 --- /dev/null +++ b/src/Testcontainers.Sftp/Usings.cs @@ -0,0 +1,6 @@ +global using Docker.DotNet.Models; +global using DotNet.Testcontainers; +global using DotNet.Testcontainers.Builders; +global using DotNet.Testcontainers.Configurations; +global using DotNet.Testcontainers.Containers; +global using JetBrains.Annotations; \ No newline at end of file diff --git a/tests/Testcontainers.Sftp.Tests/.editorconfig b/tests/Testcontainers.Sftp.Tests/.editorconfig new file mode 100644 index 000000000..6f066619d --- /dev/null +++ b/tests/Testcontainers.Sftp.Tests/.editorconfig @@ -0,0 +1 @@ +root = true \ No newline at end of file diff --git a/tests/Testcontainers.Sftp.Tests/SftpContainerTest.cs b/tests/Testcontainers.Sftp.Tests/SftpContainerTest.cs new file mode 100644 index 000000000..e064280f8 --- /dev/null +++ b/tests/Testcontainers.Sftp.Tests/SftpContainerTest.cs @@ -0,0 +1,35 @@ +namespace Testcontainers.Sftp; + +public sealed class SftpContainerTest : IAsyncLifetime +{ + private readonly SftpContainer _sftpContainer = new SftpBuilder().Build(); + + public Task InitializeAsync() + { + return _sftpContainer.StartAsync(); + } + + public Task DisposeAsync() + { + return _sftpContainer.DisposeAsync().AsTask(); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task IsConnectedReturnsTrue() + { + // Given + var host = _sftpContainer.Hostname; + + var port = _sftpContainer.GetMappedPublicPort(SftpBuilder.SftpPort); + + using var sftpClient = new SftpClient(host, port, SftpBuilder.DefaultUsername, SftpBuilder.DefaultPassword); + + // When + await sftpClient.ConnectAsync(CancellationToken.None) + .ConfigureAwait(true); + + // Then + Assert.True(sftpClient.IsConnected); + } +} \ No newline at end of file diff --git a/tests/Testcontainers.Sftp.Tests/Testcontainers.Sftp.Tests.csproj b/tests/Testcontainers.Sftp.Tests/Testcontainers.Sftp.Tests.csproj new file mode 100644 index 000000000..f9cfbbac6 --- /dev/null +++ b/tests/Testcontainers.Sftp.Tests/Testcontainers.Sftp.Tests.csproj @@ -0,0 +1,17 @@ + + + net9.0 + false + false + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Testcontainers.Sftp.Tests/Usings.cs b/tests/Testcontainers.Sftp.Tests/Usings.cs new file mode 100644 index 000000000..9e93730ce --- /dev/null +++ b/tests/Testcontainers.Sftp.Tests/Usings.cs @@ -0,0 +1,5 @@ +global using System.Threading; +global using System.Threading.Tasks; +global using DotNet.Testcontainers.Commons; +global using Renci.SshNet; +global using Xunit; \ No newline at end of file