Skip to content

Commit c56c495

Browse files
feat: Add SFTP module (#1362)
Co-authored-by: Andre Hofmeister <[email protected]>
1 parent b877ebb commit c56c495

File tree

12 files changed

+319
-0
lines changed

12 files changed

+319
-0
lines changed

.github/workflows/cicd.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ jobs:
7979
{ name: "Testcontainers.Redis", runs-on: "ubuntu-22.04" },
8080
{ name: "Testcontainers.Redpanda", runs-on: "ubuntu-22.04" },
8181
{ name: "Testcontainers.ServiceBus", runs-on: "ubuntu-22.04" },
82+
{ name: "Testcontainers.Sftp", runs-on: "ubuntu-22.04" },
8283
{ name: "Testcontainers.Weaviate", runs-on: "ubuntu-22.04" },
8384
{ name: "Testcontainers.WebDriver", runs-on: "ubuntu-22.04" },
8485
{ name: "Testcontainers.Xunit", runs-on: "ubuntu-22.04" }

Testcontainers.sln

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Redpanda", "
9797
EndProject
9898
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.ServiceBus", "src\Testcontainers.ServiceBus\Testcontainers.ServiceBus.csproj", "{2E39E532-B81E-4B48-A004-FAE18EDF9E79}"
9999
EndProject
100+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Sftp", "src\Testcontainers.Sftp\Testcontainers.Sftp.csproj", "{7D5C6816-0DD2-4E13-A585-033B5D3C80D5}"
101+
EndProject
100102
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Weaviate", "src\Testcontainers.Weaviate\Testcontainers.Weaviate.csproj", "{68F8600D-24E9-4E03-9E25-5F6EB338EAC1}"
101103
EndProject
102104
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
201203
EndProject
202204
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.ServiceBus.Tests", "tests\Testcontainers.ServiceBus.Tests\Testcontainers.ServiceBus.Tests.csproj", "{232DD918-46ED-4BA8-B383-1A9146D83064}"
203205
EndProject
206+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Sftp.Tests", "tests\Testcontainers.Sftp.Tests\Testcontainers.Sftp.Tests.csproj", "{B73C3CC0-9F16-4B34-92BE-6EC0853912C5}"
207+
EndProject
204208
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Tests", "tests\Testcontainers.Tests\Testcontainers.Tests.csproj", "{27CDB869-A150-4593-958F-6F26E5391E7C}"
205209
EndProject
206210
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
386390
{2E39E532-B81E-4B48-A004-FAE18EDF9E79}.Debug|Any CPU.Build.0 = Debug|Any CPU
387391
{2E39E532-B81E-4B48-A004-FAE18EDF9E79}.Release|Any CPU.ActiveCfg = Release|Any CPU
388392
{2E39E532-B81E-4B48-A004-FAE18EDF9E79}.Release|Any CPU.Build.0 = Release|Any CPU
393+
{7D5C6816-0DD2-4E13-A585-033B5D3C80D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
394+
{7D5C6816-0DD2-4E13-A585-033B5D3C80D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
395+
{7D5C6816-0DD2-4E13-A585-033B5D3C80D5}.Release|Any CPU.ActiveCfg = Release|Any CPU
396+
{7D5C6816-0DD2-4E13-A585-033B5D3C80D5}.Release|Any CPU.Build.0 = Release|Any CPU
389397
{68F8600D-24E9-4E03-9E25-5F6EB338EAC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
390398
{68F8600D-24E9-4E03-9E25-5F6EB338EAC1}.Debug|Any CPU.Build.0 = Debug|Any CPU
391399
{68F8600D-24E9-4E03-9E25-5F6EB338EAC1}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -594,6 +602,10 @@ Global
594602
{232DD918-46ED-4BA8-B383-1A9146D83064}.Debug|Any CPU.Build.0 = Debug|Any CPU
595603
{232DD918-46ED-4BA8-B383-1A9146D83064}.Release|Any CPU.ActiveCfg = Release|Any CPU
596604
{232DD918-46ED-4BA8-B383-1A9146D83064}.Release|Any CPU.Build.0 = Release|Any CPU
605+
{B73C3CC0-9F16-4B34-92BE-6EC0853912C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
606+
{B73C3CC0-9F16-4B34-92BE-6EC0853912C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
607+
{B73C3CC0-9F16-4B34-92BE-6EC0853912C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
608+
{B73C3CC0-9F16-4B34-92BE-6EC0853912C5}.Release|Any CPU.Build.0 = Release|Any CPU
597609
{27CDB869-A150-4593-958F-6F26E5391E7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
598610
{27CDB869-A150-4593-958F-6F26E5391E7C}.Debug|Any CPU.Build.0 = Debug|Any CPU
599611
{27CDB869-A150-4593-958F-6F26E5391E7C}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -654,6 +666,7 @@ Global
654666
{BFDA179A-40EB-4CEB-B8E9-0DF32C65E2C5} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
655667
{45D6F69C-4D87-4130-AA90-0DB2F7460DAE} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
656668
{2E39E532-B81E-4B48-A004-FAE18EDF9E79} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
669+
{7D5C6816-0DD2-4E13-A585-033B5D3C80D5} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
657670
{68F8600D-24E9-4E03-9E25-5F6EB338EAC1} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
658671
{64A87DE5-29B0-4A54-9E74-560484D8C7C0} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
659672
{380BB29B-F556-404D-B13B-CA250599C565} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
@@ -706,6 +719,7 @@ Global
706719
{867BD04E-4670-4FBA-98D5-9F83220E6DFB} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
707720
{9E8E6AA5-65D1-498F-BEAB-BA34723A0050} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
708721
{232DD918-46ED-4BA8-B383-1A9146D83064} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
722+
{B73C3CC0-9F16-4B34-92BE-6EC0853912C5} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
709723
{27CDB869-A150-4593-958F-6F26E5391E7C} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
710724
{DDB41BC8-5826-4D97-9C5F-001151E3FFD6} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
711725
{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}

src/Testcontainers.Sftp/.editorconfig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
root = true
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
namespace Testcontainers.Sftp;
2+
3+
/// <inheritdoc cref="ContainerBuilder{TBuilderEntity, TContainerEntity, TConfigurationEntity}" />
4+
[PublicAPI]
5+
public sealed class SftpBuilder : ContainerBuilder<SftpBuilder, SftpContainer, SftpConfiguration>
6+
{
7+
public const string SftpImage = "atmoz/sftp:alpine";
8+
9+
public const ushort SftpPort = 22;
10+
11+
public const string DefaultUsername = "sftp";
12+
13+
public const string DefaultPassword = "sftp";
14+
15+
public const string DefaultUploadDirectory = "upload";
16+
17+
/// <summary>
18+
/// Initializes a new instance of the <see cref="SftpBuilder" /> class.
19+
/// </summary>
20+
public SftpBuilder()
21+
: this(new SftpConfiguration())
22+
{
23+
DockerResourceConfiguration = Init().DockerResourceConfiguration;
24+
}
25+
26+
/// <summary>
27+
/// Initializes a new instance of the <see cref="SftpBuilder" /> class.
28+
/// </summary>
29+
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
30+
private SftpBuilder(SftpConfiguration resourceConfiguration)
31+
: base(resourceConfiguration)
32+
{
33+
DockerResourceConfiguration = resourceConfiguration;
34+
}
35+
36+
/// <inheritdoc />
37+
protected override SftpConfiguration DockerResourceConfiguration { get; }
38+
39+
/// <summary>
40+
/// Sets the Sftp username.
41+
/// </summary>
42+
/// <param name="username">The Sftp username.</param>
43+
/// <returns>A configured instance of <see cref="SftpBuilder" />.</returns>
44+
public SftpBuilder WithUsername(string username)
45+
{
46+
return Merge(DockerResourceConfiguration, new SftpConfiguration(username: username));
47+
}
48+
49+
/// <summary>
50+
/// Sets the Sftp password.
51+
/// </summary>
52+
/// <param name="password">The Sftp password.</param>
53+
/// <returns>A configured instance of <see cref="SftpBuilder" />.</returns>
54+
public SftpBuilder WithPassword(string password)
55+
{
56+
return Merge(DockerResourceConfiguration, new SftpConfiguration(password: password));
57+
}
58+
59+
/// <summary>
60+
/// Sets the directory to which files are uploaded.
61+
/// </summary>
62+
/// <param name="uploadDirectory">The upload directory.</param>
63+
/// <returns>A configured instance of <see cref="SftpBuilder" />.</returns>
64+
public SftpBuilder WithUploadDirectory(string uploadDirectory)
65+
{
66+
return Merge(DockerResourceConfiguration, new SftpConfiguration(uploadDirectory: uploadDirectory));
67+
}
68+
69+
/// <inheritdoc />
70+
public override SftpContainer Build()
71+
{
72+
Validate();
73+
74+
var sftpContainer = WithCommand(string.Join(
75+
":",
76+
DockerResourceConfiguration.Username,
77+
DockerResourceConfiguration.Password,
78+
string.Empty,
79+
string.Empty,
80+
DockerResourceConfiguration.UploadDirectory));
81+
82+
return new SftpContainer(sftpContainer.DockerResourceConfiguration);
83+
}
84+
85+
/// <inheritdoc />
86+
protected override SftpBuilder Init()
87+
{
88+
return base.Init()
89+
.WithImage(SftpImage)
90+
.WithPortBinding(SftpPort, true)
91+
.WithUsername(DefaultUsername)
92+
.WithPassword(DefaultPassword)
93+
.WithUploadDirectory(DefaultUploadDirectory)
94+
.WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged("Server listening on .+"));
95+
}
96+
97+
/// <inheritdoc />
98+
protected override void Validate()
99+
{
100+
base.Validate();
101+
102+
_ = Guard.Argument(DockerResourceConfiguration.Username, nameof(DockerResourceConfiguration.Username))
103+
.NotNull()
104+
.NotEmpty();
105+
106+
_ = Guard.Argument(DockerResourceConfiguration.Password, nameof(DockerResourceConfiguration.Password))
107+
.NotNull()
108+
.NotEmpty();
109+
110+
_ = Guard.Argument(DockerResourceConfiguration.UploadDirectory, nameof(DockerResourceConfiguration.UploadDirectory))
111+
.NotNull()
112+
.NotEmpty();
113+
}
114+
115+
/// <inheritdoc />
116+
protected override SftpBuilder Clone(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)
117+
{
118+
return Merge(DockerResourceConfiguration, new SftpConfiguration(resourceConfiguration));
119+
}
120+
121+
/// <inheritdoc />
122+
protected override SftpBuilder Clone(IContainerConfiguration resourceConfiguration)
123+
{
124+
return Merge(DockerResourceConfiguration, new SftpConfiguration(resourceConfiguration));
125+
}
126+
127+
/// <inheritdoc />
128+
protected override SftpBuilder Merge(SftpConfiguration oldValue, SftpConfiguration newValue)
129+
{
130+
return new SftpBuilder(new SftpConfiguration(oldValue, newValue));
131+
}
132+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
namespace Testcontainers.Sftp;
2+
3+
/// <inheritdoc cref="ContainerConfiguration" />
4+
[PublicAPI]
5+
public sealed class SftpConfiguration : ContainerConfiguration
6+
{
7+
/// <summary>
8+
/// Initializes a new instance of the <see cref="SftpConfiguration" /> class.
9+
/// </summary>
10+
/// <param name="username">The Sftp username.</param>
11+
/// <param name="password">The Sftp password.</param>
12+
/// <param name="uploadDirectory">The directory to which files are uploaded.</param>
13+
public SftpConfiguration(
14+
string username = null,
15+
string password = null,
16+
string uploadDirectory = null)
17+
{
18+
Username = username;
19+
Password = password;
20+
UploadDirectory = uploadDirectory;
21+
}
22+
23+
/// <summary>
24+
/// Initializes a new instance of the <see cref="SftpConfiguration" /> class.
25+
/// </summary>
26+
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
27+
public SftpConfiguration(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)
28+
: base(resourceConfiguration)
29+
{
30+
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
31+
}
32+
33+
/// <summary>
34+
/// Initializes a new instance of the <see cref="SftpConfiguration" /> class.
35+
/// </summary>
36+
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
37+
public SftpConfiguration(IContainerConfiguration resourceConfiguration)
38+
: base(resourceConfiguration)
39+
{
40+
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
41+
}
42+
43+
/// <summary>
44+
/// Initializes a new instance of the <see cref="SftpConfiguration" /> class.
45+
/// </summary>
46+
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
47+
public SftpConfiguration(SftpConfiguration resourceConfiguration)
48+
: this(new SftpConfiguration(), resourceConfiguration)
49+
{
50+
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
51+
}
52+
53+
/// <summary>
54+
/// Initializes a new instance of the <see cref="SftpConfiguration" /> class.
55+
/// </summary>
56+
/// <param name="oldValue">The old Docker resource configuration.</param>
57+
/// <param name="newValue">The new Docker resource configuration.</param>
58+
public SftpConfiguration(SftpConfiguration oldValue, SftpConfiguration newValue)
59+
: base(oldValue, newValue)
60+
{
61+
Username = BuildConfiguration.Combine(oldValue.Username, newValue.Username);
62+
Password = BuildConfiguration.Combine(oldValue.Password, newValue.Password);
63+
UploadDirectory = BuildConfiguration.Combine(oldValue.UploadDirectory, newValue.UploadDirectory);
64+
}
65+
66+
/// <summary>
67+
/// Gets the Sftp username.
68+
/// </summary>
69+
public string Username { get; }
70+
71+
/// <summary>
72+
/// Gets the Sftp password.
73+
/// </summary>
74+
public string Password { get; }
75+
76+
/// <summary>
77+
/// Gets the directory to which files are uploaded.
78+
/// </summary>
79+
public string UploadDirectory { get; }
80+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace Testcontainers.Sftp;
2+
3+
/// <inheritdoc cref="DockerContainer" />
4+
[PublicAPI]
5+
public sealed class SftpContainer : DockerContainer
6+
{
7+
/// <summary>
8+
/// Initializes a new instance of the <see cref="SftpContainer" /> class.
9+
/// </summary>
10+
/// <param name="configuration">The container configuration.</param>
11+
public SftpContainer(SftpConfiguration configuration)
12+
: base(configuration)
13+
{
14+
}
15+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFrameworks>net8.0;net9.0;netstandard2.0;netstandard2.1</TargetFrameworks>
4+
<LangVersion>latest</LangVersion>
5+
</PropertyGroup>
6+
<ItemGroup>
7+
<PackageReference Include="JetBrains.Annotations" VersionOverride="2023.3.0" PrivateAssets="All"/>
8+
</ItemGroup>
9+
<ItemGroup>
10+
<ProjectReference Include="../Testcontainers/Testcontainers.csproj"/>
11+
</ItemGroup>
12+
</Project>

src/Testcontainers.Sftp/Usings.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
global using Docker.DotNet.Models;
2+
global using DotNet.Testcontainers;
3+
global using DotNet.Testcontainers.Builders;
4+
global using DotNet.Testcontainers.Configurations;
5+
global using DotNet.Testcontainers.Containers;
6+
global using JetBrains.Annotations;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
root = true
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
namespace Testcontainers.Sftp;
2+
3+
public sealed class SftpContainerTest : IAsyncLifetime
4+
{
5+
private readonly SftpContainer _sftpContainer = new SftpBuilder().Build();
6+
7+
public Task InitializeAsync()
8+
{
9+
return _sftpContainer.StartAsync();
10+
}
11+
12+
public Task DisposeAsync()
13+
{
14+
return _sftpContainer.DisposeAsync().AsTask();
15+
}
16+
17+
[Fact]
18+
[Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
19+
public async Task IsConnectedReturnsTrue()
20+
{
21+
// Given
22+
var host = _sftpContainer.Hostname;
23+
24+
var port = _sftpContainer.GetMappedPublicPort(SftpBuilder.SftpPort);
25+
26+
using var sftpClient = new SftpClient(host, port, SftpBuilder.DefaultUsername, SftpBuilder.DefaultPassword);
27+
28+
// When
29+
await sftpClient.ConnectAsync(CancellationToken.None)
30+
.ConfigureAwait(true);
31+
32+
// Then
33+
Assert.True(sftpClient.IsConnected);
34+
}
35+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFrameworks>net9.0</TargetFrameworks>
4+
<IsPackable>false</IsPackable>
5+
<IsPublishable>false</IsPublishable>
6+
</PropertyGroup>
7+
<ItemGroup>
8+
<PackageReference Include="Microsoft.NET.Test.Sdk"/>
9+
<PackageReference Include="coverlet.collector"/>
10+
<PackageReference Include="xunit.runner.visualstudio"/>
11+
<PackageReference Include="xunit"/>
12+
</ItemGroup>
13+
<ItemGroup>
14+
<ProjectReference Include="../../src/Testcontainers.Sftp/Testcontainers.Sftp.csproj"/>
15+
<ProjectReference Include="../Testcontainers.Commons/Testcontainers.Commons.csproj"/>
16+
</ItemGroup>
17+
</Project>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
global using System.Threading;
2+
global using System.Threading.Tasks;
3+
global using DotNet.Testcontainers.Commons;
4+
global using Renci.SshNet;
5+
global using Xunit;

0 commit comments

Comments
 (0)