Skip to content

Commit a21b6e1

Browse files
0xcedHofmeisterAn
andauthoredFeb 17, 2024··
feat: Add URL-based resource mapping container builder API (#1118)
Co-authored-by: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com>
1 parent 5600106 commit a21b6e1

File tree

4 files changed

+153
-12
lines changed

4 files changed

+153
-12
lines changed
 

‎src/Testcontainers/Builders/ContainerBuilder`3.cs

+18
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,11 @@ public TBuilderEntity WithResourceMapping(byte[] resourceContent, string filePat
197197
/// <inheritdoc />
198198
public TBuilderEntity WithResourceMapping(string source, string target, UnixFileModes fileMode = Unix.FileMode644)
199199
{
200+
if (Uri.IsWellFormedUriString(source, UriKind.Absolute) && Uri.TryCreate(source, UriKind.Absolute, out var uri) && new[] { Uri.UriSchemeHttp, Uri.UriSchemeHttps, Uri.UriSchemeFile }.Contains(uri.Scheme))
201+
{
202+
return WithResourceMapping(uri, target, fileMode);
203+
}
204+
200205
var fileAttributes = File.GetAttributes(source);
201206

202207
if ((fileAttributes & FileAttributes.Directory) == FileAttributes.Directory)
@@ -234,6 +239,19 @@ public TBuilderEntity WithResourceMapping(FileInfo source, FileInfo target, Unix
234239
}
235240
}
236241

242+
/// <inheritdoc />
243+
public TBuilderEntity WithResourceMapping(Uri source, string target, UnixFileModes fileMode = Unix.FileMode644)
244+
{
245+
if (source.IsFile)
246+
{
247+
return WithResourceMapping(new FileResourceMapping(source.AbsolutePath, target, fileMode));
248+
}
249+
else
250+
{
251+
return WithResourceMapping(new UriResourceMapping(source, target, fileMode));
252+
}
253+
}
254+
237255
/// <inheritdoc />
238256
public TBuilderEntity WithMount(IMount mount)
239257
{

‎src/Testcontainers/Builders/IContainerBuilder`2.cs

+30-3
Original file line numberDiff line numberDiff line change
@@ -219,10 +219,18 @@ public interface IContainerBuilder<out TBuilderEntity, out TContainerEntity> : I
219219
TBuilderEntity WithResourceMapping(byte[] resourceContent, string filePath, UnixFileModes fileMode = Unix.FileMode644);
220220

221221
/// <summary>
222-
/// Copies a test host directory or file to the container before it starts.
222+
/// Copies the contents of a URL, a test host directory or file to the container before it starts.
223223
/// </summary>
224-
/// <param name="source">The source directory or file to be copied.</param>
225-
/// <param name="target">The target directory path to copy the files to.</param>
224+
/// <remarks>
225+
/// If the source corresponds to a file or the Uri scheme corresponds to a file,
226+
/// the content is copied to the target directory path. If the Uri scheme
227+
/// corresponds to HTTP or HTTPS, the content is copied to the target file path.
228+
///
229+
/// If you prefer to copy a file to a specific target file path instead of a
230+
/// directory, use: <see cref="WithResourceMapping(FileInfo, FileInfo, UnixFileModes)" />.
231+
/// </remarks>
232+
/// <param name="source">The source URL, directory or file to be copied.</param>
233+
/// <param name="target">The target directory or file path to copy the file to.</param>
226234
/// <param name="fileMode">The POSIX file mode permission.</param>
227235
/// <returns>A configured instance of <typeparamref name="TBuilderEntity" />.</returns>
228236
[PublicAPI]
@@ -258,6 +266,25 @@ public interface IContainerBuilder<out TBuilderEntity, out TContainerEntity> : I
258266
[PublicAPI]
259267
TBuilderEntity WithResourceMapping(FileInfo source, FileInfo target, UnixFileModes fileMode = Unix.FileMode644);
260268

269+
/// <summary>
270+
/// Copies a file from a URL to the container before it starts.
271+
/// </summary>
272+
/// <remarks>
273+
/// If the Uri scheme corresponds to a file, the content is copied to the target
274+
/// directory path. If the Uri scheme corresponds to HTTP or HTTPS, the content is
275+
/// copied to the target file path.
276+
///
277+
/// The Uri scheme must be either <c>http</c>, <c>https</c> or <c>file</c>.
278+
///
279+
/// If you prefer to copy a file to a specific target file path instead of a
280+
/// directory, use: <see cref="WithResourceMapping(FileInfo, FileInfo, UnixFileModes)" />.
281+
/// </remarks>
282+
/// <param name="source">The source URL of the file to be copied.</param>
283+
/// <param name="target">The target directory or file path to copy the file to.</param>
284+
/// <param name="fileMode">The POSIX file mode permission.</param>
285+
/// <returns>A configured instance of <typeparamref name="TBuilderEntity" />.</returns>
286+
TBuilderEntity WithResourceMapping(Uri source, string target, UnixFileModes fileMode = Unix.FileMode644);
287+
261288
/// <summary>
262289
/// Assigns the mount configuration to manage data in the container.
263290
/// </summary>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
namespace DotNet.Testcontainers.Configurations
2+
{
3+
using System;
4+
using System.Net.Http;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
8+
/// <inheritdoc cref="IResourceMapping" />
9+
internal sealed class UriResourceMapping : IResourceMapping
10+
{
11+
private readonly Uri _uri;
12+
13+
/// <summary>
14+
/// Initializes a new instance of the <see cref="UriResourceMapping" /> class.
15+
/// </summary>
16+
/// <param name="uri">The URL of the file to download.</param>
17+
/// <param name="containerPath">The absolute path of the file to map in the container.</param>
18+
/// <param name="fileMode">The POSIX file mode permission.</param>
19+
public UriResourceMapping(Uri uri, string containerPath, UnixFileModes fileMode)
20+
{
21+
_uri = uri;
22+
Type = MountType.Bind;
23+
Source = uri.AbsoluteUri;
24+
Target = containerPath;
25+
FileMode = fileMode;
26+
AccessMode = AccessMode.ReadOnly;
27+
}
28+
29+
/// <inheritdoc />
30+
public MountType Type { get; }
31+
32+
/// <inheritdoc />
33+
public AccessMode AccessMode { get; }
34+
35+
/// <inheritdoc />
36+
public string Source { get; }
37+
38+
/// <inheritdoc />
39+
public string Target { get; }
40+
41+
/// <inheritdoc />
42+
public UnixFileModes FileMode { get; }
43+
44+
/// <inheritdoc />
45+
public Task CreateAsync(CancellationToken ct = default) => Task.CompletedTask;
46+
47+
/// <inheritdoc />
48+
public Task DeleteAsync(CancellationToken ct = default) => Task.CompletedTask;
49+
50+
/// <inheritdoc />
51+
public async Task<byte[]> GetAllBytesAsync(CancellationToken ct = default)
52+
{
53+
using (var httpClient = new HttpClient())
54+
{
55+
return await httpClient.GetByteArrayAsync(_uri)
56+
.ConfigureAwait(false);
57+
}
58+
}
59+
}
60+
}

‎tests/Testcontainers.Platform.Linux.Tests/TarOutputMemoryStreamTest.cs

+45-9
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,18 @@ public void TestFileExistsInTarFile()
3333
}
3434

3535
[UsedImplicitly]
36-
public sealed class FromResourceMapping : TarOutputMemoryStreamTest, IResourceMapping, IAsyncLifetime, IDisposable
36+
public sealed class FromResourceMapping : TarOutputMemoryStreamTest, IResourceMapping, IClassFixture<FromResourceMapping.HttpFixture>, IAsyncLifetime, IDisposable
3737
{
38+
private readonly string _testHttpUri;
39+
40+
private readonly string _testFileUri;
41+
42+
public FromResourceMapping(FromResourceMapping.HttpFixture httpFixture)
43+
{
44+
_testHttpUri = httpFixture.BaseAddress;
45+
_testFileUri = new Uri(_testFile.FullName).ToString();
46+
}
47+
3848
public MountType Type
3949
=> MountType.Bind;
4050

@@ -86,31 +96,32 @@ public async Task TestFileExistsInContainer()
8696
{
8797
// Given
8898
var targetFilePath1 = string.Join("/", string.Empty, "tmp", Guid.NewGuid(), _testFile.Name);
89-
9099
var targetFilePath2 = string.Join("/", string.Empty, "tmp", Guid.NewGuid(), _testFile.Name);
91-
100+
var targetFilePath3 = string.Join("/", string.Empty, "tmp", Guid.NewGuid(), _testFile.Name);
92101
var targetDirectoryPath1 = string.Join("/", string.Empty, "tmp", Guid.NewGuid());
93-
94102
var targetDirectoryPath2 = string.Join("/", string.Empty, "tmp", Guid.NewGuid());
95-
96103
var targetDirectoryPath3 = string.Join("/", string.Empty, "tmp", Guid.NewGuid());
97-
98104
var targetDirectoryPath4 = string.Join("/", string.Empty, "tmp", Guid.NewGuid());
105+
var targetDirectoryPath5 = string.Join("/", string.Empty, "tmp", Guid.NewGuid());
99106

100107
var targetFilePaths = new List<string>();
101108
targetFilePaths.Add(targetFilePath1);
102109
targetFilePaths.Add(targetFilePath2);
110+
targetFilePaths.Add(targetFilePath3);
103111
targetFilePaths.Add(string.Join("/", targetDirectoryPath1, _testFile.Name));
104112
targetFilePaths.Add(string.Join("/", targetDirectoryPath2, _testFile.Name));
105113
targetFilePaths.Add(string.Join("/", targetDirectoryPath3, _testFile.Name));
106114
targetFilePaths.Add(string.Join("/", targetDirectoryPath4, _testFile.Name));
115+
targetFilePaths.Add(string.Join("/", targetDirectoryPath5, _testFile.Name));
107116

108117
await using var container = new ContainerBuilder()
109118
.WithImage(CommonImages.Alpine)
110119
.WithEntrypoint(CommonCommands.SleepInfinity)
111120
.WithResourceMapping(_testFile, new FileInfo(targetFilePath1))
112121
.WithResourceMapping(_testFile.FullName, targetDirectoryPath1)
113122
.WithResourceMapping(_testFile.Directory.FullName, targetDirectoryPath2)
123+
.WithResourceMapping(_testHttpUri, targetFilePath2)
124+
.WithResourceMapping(_testFileUri, targetDirectoryPath3)
114125
.Build();
115126

116127
// When
@@ -120,13 +131,13 @@ public async Task TestFileExistsInContainer()
120131
await container.StartAsync()
121132
.ConfigureAwait(true);
122133

123-
await container.CopyAsync(fileContent, targetFilePath2)
134+
await container.CopyAsync(fileContent, targetFilePath3)
124135
.ConfigureAwait(true);
125136

126-
await container.CopyAsync(_testFile.FullName, targetDirectoryPath3)
137+
await container.CopyAsync(_testFile.FullName, targetDirectoryPath4)
127138
.ConfigureAwait(true);
128139

129-
await container.CopyAsync(_testFile.Directory.FullName, targetDirectoryPath4)
140+
await container.CopyAsync(_testFile.Directory.FullName, targetDirectoryPath5)
130141
.ConfigureAwait(true);
131142

132143
// Then
@@ -135,6 +146,31 @@ await container.CopyAsync(_testFile.Directory.FullName, targetDirectoryPath4)
135146

136147
Assert.All(execResults, result => Assert.Equal(0, result.ExitCode));
137148
}
149+
150+
public sealed class HttpFixture : IAsyncLifetime
151+
{
152+
private const ushort HttpPort = 80;
153+
154+
private readonly IContainer _container = new ContainerBuilder()
155+
.WithImage(CommonImages.Alpine)
156+
.WithEntrypoint("/bin/sh", "-c")
157+
.WithCommand($"while true; do echo \"HTTP/1.1 200 OK\r\n\" | nc -l -p {HttpPort}; done")
158+
.WithPortBinding(HttpPort, true)
159+
.Build();
160+
161+
public string BaseAddress
162+
=> new UriBuilder(Uri.UriSchemeHttp, _container.Hostname, _container.GetMappedPublicPort(HttpPort)).ToString();
163+
164+
public Task InitializeAsync()
165+
{
166+
return _container.StartAsync();
167+
}
168+
169+
public Task DisposeAsync()
170+
{
171+
return _container.DisposeAsync().AsTask();
172+
}
173+
}
138174
}
139175

140176
[UsedImplicitly]

0 commit comments

Comments
 (0)
Please sign in to comment.