Skip to content

Commit

Permalink
feat: Add SFTP module (#1362)
Browse files Browse the repository at this point in the history
Co-authored-by: Andre Hofmeister <[email protected]>
  • Loading branch information
wim07101993 and HofmeisterAn authored Feb 10, 2025
1 parent b877ebb commit c56c495
Show file tree
Hide file tree
Showing 12 changed files with 319 additions and 0 deletions.
1 change: 1 addition & 0 deletions .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
14 changes: 14 additions & 0 deletions Testcontainers.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down Expand Up @@ -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}"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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}
Expand Down
1 change: 1 addition & 0 deletions src/Testcontainers.Sftp/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
root = true
132 changes: 132 additions & 0 deletions src/Testcontainers.Sftp/SftpBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
namespace Testcontainers.Sftp;

/// <inheritdoc cref="ContainerBuilder{TBuilderEntity, TContainerEntity, TConfigurationEntity}" />
[PublicAPI]
public sealed class SftpBuilder : ContainerBuilder<SftpBuilder, SftpContainer, SftpConfiguration>
{
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";

/// <summary>
/// Initializes a new instance of the <see cref="SftpBuilder" /> class.
/// </summary>
public SftpBuilder()
: this(new SftpConfiguration())
{
DockerResourceConfiguration = Init().DockerResourceConfiguration;
}

/// <summary>
/// Initializes a new instance of the <see cref="SftpBuilder" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
private SftpBuilder(SftpConfiguration resourceConfiguration)
: base(resourceConfiguration)
{
DockerResourceConfiguration = resourceConfiguration;
}

/// <inheritdoc />
protected override SftpConfiguration DockerResourceConfiguration { get; }

/// <summary>
/// Sets the Sftp username.
/// </summary>
/// <param name="username">The Sftp username.</param>
/// <returns>A configured instance of <see cref="SftpBuilder" />.</returns>
public SftpBuilder WithUsername(string username)
{
return Merge(DockerResourceConfiguration, new SftpConfiguration(username: username));
}

/// <summary>
/// Sets the Sftp password.
/// </summary>
/// <param name="password">The Sftp password.</param>
/// <returns>A configured instance of <see cref="SftpBuilder" />.</returns>
public SftpBuilder WithPassword(string password)
{
return Merge(DockerResourceConfiguration, new SftpConfiguration(password: password));
}

/// <summary>
/// Sets the directory to which files are uploaded.
/// </summary>
/// <param name="uploadDirectory">The upload directory.</param>
/// <returns>A configured instance of <see cref="SftpBuilder" />.</returns>
public SftpBuilder WithUploadDirectory(string uploadDirectory)
{
return Merge(DockerResourceConfiguration, new SftpConfiguration(uploadDirectory: uploadDirectory));
}

/// <inheritdoc />
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);
}

/// <inheritdoc />
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 .+"));
}

/// <inheritdoc />
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();
}

/// <inheritdoc />
protected override SftpBuilder Clone(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)
{
return Merge(DockerResourceConfiguration, new SftpConfiguration(resourceConfiguration));
}

/// <inheritdoc />
protected override SftpBuilder Clone(IContainerConfiguration resourceConfiguration)
{
return Merge(DockerResourceConfiguration, new SftpConfiguration(resourceConfiguration));
}

/// <inheritdoc />
protected override SftpBuilder Merge(SftpConfiguration oldValue, SftpConfiguration newValue)
{
return new SftpBuilder(new SftpConfiguration(oldValue, newValue));
}
}
80 changes: 80 additions & 0 deletions src/Testcontainers.Sftp/SftpConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
namespace Testcontainers.Sftp;

/// <inheritdoc cref="ContainerConfiguration" />
[PublicAPI]
public sealed class SftpConfiguration : ContainerConfiguration
{
/// <summary>
/// Initializes a new instance of the <see cref="SftpConfiguration" /> class.
/// </summary>
/// <param name="username">The Sftp username.</param>
/// <param name="password">The Sftp password.</param>
/// <param name="uploadDirectory">The directory to which files are uploaded.</param>
public SftpConfiguration(
string username = null,
string password = null,
string uploadDirectory = null)
{
Username = username;
Password = password;
UploadDirectory = uploadDirectory;
}

/// <summary>
/// Initializes a new instance of the <see cref="SftpConfiguration" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
public SftpConfiguration(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)
: base(resourceConfiguration)
{
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
}

/// <summary>
/// Initializes a new instance of the <see cref="SftpConfiguration" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
public SftpConfiguration(IContainerConfiguration resourceConfiguration)
: base(resourceConfiguration)
{
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
}

/// <summary>
/// Initializes a new instance of the <see cref="SftpConfiguration" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
public SftpConfiguration(SftpConfiguration resourceConfiguration)
: this(new SftpConfiguration(), resourceConfiguration)
{
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
}

/// <summary>
/// Initializes a new instance of the <see cref="SftpConfiguration" /> class.
/// </summary>
/// <param name="oldValue">The old Docker resource configuration.</param>
/// <param name="newValue">The new Docker resource configuration.</param>
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);
}

/// <summary>
/// Gets the Sftp username.
/// </summary>
public string Username { get; }

/// <summary>
/// Gets the Sftp password.
/// </summary>
public string Password { get; }

/// <summary>
/// Gets the directory to which files are uploaded.
/// </summary>
public string UploadDirectory { get; }
}
15 changes: 15 additions & 0 deletions src/Testcontainers.Sftp/SftpContainer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Testcontainers.Sftp;

/// <inheritdoc cref="DockerContainer" />
[PublicAPI]
public sealed class SftpContainer : DockerContainer
{
/// <summary>
/// Initializes a new instance of the <see cref="SftpContainer" /> class.
/// </summary>
/// <param name="configuration">The container configuration.</param>
public SftpContainer(SftpConfiguration configuration)
: base(configuration)
{
}
}
12 changes: 12 additions & 0 deletions src/Testcontainers.Sftp/Testcontainers.Sftp.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;netstandard2.0;netstandard2.1</TargetFrameworks>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" VersionOverride="2023.3.0" PrivateAssets="All"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../Testcontainers/Testcontainers.csproj"/>
</ItemGroup>
</Project>
6 changes: 6 additions & 0 deletions src/Testcontainers.Sftp/Usings.cs
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions tests/Testcontainers.Sftp.Tests/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
root = true
35 changes: 35 additions & 0 deletions tests/Testcontainers.Sftp.Tests/SftpContainerTest.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
17 changes: 17 additions & 0 deletions tests/Testcontainers.Sftp.Tests/Testcontainers.Sftp.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net9.0</TargetFrameworks>
<IsPackable>false</IsPackable>
<IsPublishable>false</IsPublishable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk"/>
<PackageReference Include="coverlet.collector"/>
<PackageReference Include="xunit.runner.visualstudio"/>
<PackageReference Include="xunit"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../src/Testcontainers.Sftp/Testcontainers.Sftp.csproj"/>
<ProjectReference Include="../Testcontainers.Commons/Testcontainers.Commons.csproj"/>
</ItemGroup>
</Project>
5 changes: 5 additions & 0 deletions tests/Testcontainers.Sftp.Tests/Usings.cs
Original file line number Diff line number Diff line change
@@ -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;

0 comments on commit c56c495

Please sign in to comment.