From c2e763adc410703ec41a82132570c726d739205e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= Date: Wed, 24 Jan 2024 15:24:55 +0100 Subject: [PATCH 1/3] feat: Introduce a new URL-based resource mapping --- .../Builders/ContainerBuilder`3.cs | 11 ++++ .../Builders/IContainerBuilder`2.cs | 15 ++++- .../Volumes/UriResourceMapping.cs | 60 +++++++++++++++++++ 3 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 src/Testcontainers/Configurations/Volumes/UriResourceMapping.cs diff --git a/src/Testcontainers/Builders/ContainerBuilder`3.cs b/src/Testcontainers/Builders/ContainerBuilder`3.cs index b2288d44c..02c13278a 100644 --- a/src/Testcontainers/Builders/ContainerBuilder`3.cs +++ b/src/Testcontainers/Builders/ContainerBuilder`3.cs @@ -197,6 +197,11 @@ public TBuilderEntity WithResourceMapping(byte[] resourceContent, string filePat /// public TBuilderEntity WithResourceMapping(string source, string target, UnixFileModes fileMode = Unix.FileMode644) { + if (Uri.TryCreate(source, UriKind.Absolute, out var uri)) + { + return WithResourceMapping(uri, target, fileMode); + } + var fileAttributes = File.GetAttributes(source); if ((fileAttributes & FileAttributes.Directory) == FileAttributes.Directory) @@ -234,6 +239,12 @@ public TBuilderEntity WithResourceMapping(FileInfo source, FileInfo target, Unix } } + /// + public TBuilderEntity WithResourceMapping(Uri source, string target, UnixFileModes fileMode = Unix.FileMode644) + { + return source.IsFile ? WithResourceMapping(new FileResourceMapping(source.AbsolutePath, target, fileMode)) : WithResourceMapping(new UriResourceMapping(source, target, fileMode)); + } + /// public TBuilderEntity WithMount(IMount mount) { diff --git a/src/Testcontainers/Builders/IContainerBuilder`2.cs b/src/Testcontainers/Builders/IContainerBuilder`2.cs index 6516c3c22..467fcb28d 100644 --- a/src/Testcontainers/Builders/IContainerBuilder`2.cs +++ b/src/Testcontainers/Builders/IContainerBuilder`2.cs @@ -219,10 +219,10 @@ public interface IContainerBuilder : I TBuilderEntity WithResourceMapping(byte[] resourceContent, string filePath, UnixFileModes fileMode = Unix.FileMode644); /// - /// Copies a test host directory or file to the container before it starts. + /// Copies the contents of a URL or a test host directory or file to the container before it starts. /// - /// The source directory or file to be copied. - /// The target directory path to copy the files to. + /// The source URL, directory or file to be copied. + /// The target directory path (if the source is a directory) or file path (if the source is a URL or file) to copy the file(s) to. /// The POSIX file mode permission. /// A configured instance of . [PublicAPI] @@ -258,6 +258,15 @@ public interface IContainerBuilder : I [PublicAPI] TBuilderEntity WithResourceMapping(FileInfo source, FileInfo target, UnixFileModes fileMode = Unix.FileMode644); + /// + /// Copies a file at a given URL to the container before it starts. + /// + /// The source URL of the file to be copied. Must be a file, http or https URL. + /// The target file path to copy the file to. + /// The POSIX file mode permission. + /// A configured instance of . + TBuilderEntity WithResourceMapping(Uri source, string target, UnixFileModes fileMode = Unix.FileMode644); + /// /// Assigns the mount configuration to manage data in the container. /// diff --git a/src/Testcontainers/Configurations/Volumes/UriResourceMapping.cs b/src/Testcontainers/Configurations/Volumes/UriResourceMapping.cs new file mode 100644 index 000000000..5145daaa4 --- /dev/null +++ b/src/Testcontainers/Configurations/Volumes/UriResourceMapping.cs @@ -0,0 +1,60 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace DotNet.Testcontainers.Configurations +{ + /// + internal class UriResourceMapping : IResourceMapping + { + private readonly Uri _uri; + + /// + /// Initializes a new instance of the class. + /// + /// The URL of the file to download. + /// The absolute path of the file to map in the container. + /// The POSIX file mode permission. + public UriResourceMapping(Uri uri, string containerPath, UnixFileModes fileMode) + { + _uri = uri; + Type = MountType.Bind; + Source = uri.AbsoluteUri; + Target = containerPath; + FileMode = fileMode; + AccessMode = AccessMode.ReadOnly; + } + + /// + public MountType Type { get; } + + /// + public AccessMode AccessMode { get; } + + /// + public string Source { get; } + + /// + public string Target { get; } + + /// + public UnixFileModes FileMode { get; } + + /// + public Task CreateAsync(CancellationToken ct = default) => Task.CompletedTask; + + /// + public Task DeleteAsync(CancellationToken ct = default) => Task.CompletedTask; + + /// + public virtual async Task GetAllBytesAsync(CancellationToken ct = default) + { + using (var httpClient = new HttpClient()) + { + return await httpClient.GetByteArrayAsync(_uri) + .ConfigureAwait(false); + } + } + } +} From edb84035810275ef43f8830d6afafbff0140b46a Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Sat, 17 Feb 2024 12:27:41 +0100 Subject: [PATCH 2/3] chore: Add tests for UriResourceMapping --- .../Builders/ContainerBuilder`3.cs | 11 +++- .../Builders/IContainerBuilder`2.cs | 26 +++++++-- .../Volumes/UriResourceMapping.cs | 14 ++--- .../TarOutputMemoryStreamTest.cs | 54 +++++++++++++++---- 4 files changed, 82 insertions(+), 23 deletions(-) diff --git a/src/Testcontainers/Builders/ContainerBuilder`3.cs b/src/Testcontainers/Builders/ContainerBuilder`3.cs index 02c13278a..9fed8de2d 100644 --- a/src/Testcontainers/Builders/ContainerBuilder`3.cs +++ b/src/Testcontainers/Builders/ContainerBuilder`3.cs @@ -197,7 +197,7 @@ public TBuilderEntity WithResourceMapping(byte[] resourceContent, string filePat /// public TBuilderEntity WithResourceMapping(string source, string target, UnixFileModes fileMode = Unix.FileMode644) { - if (Uri.TryCreate(source, UriKind.Absolute, out var uri)) + if (Uri.IsWellFormedUriString(source, UriKind.Absolute) && Uri.TryCreate(source, UriKind.Absolute, out var uri) && new[] { Uri.UriSchemeHttp, Uri.UriSchemeHttps, Uri.UriSchemeFile }.Contains(uri.Scheme)) { return WithResourceMapping(uri, target, fileMode); } @@ -242,7 +242,14 @@ public TBuilderEntity WithResourceMapping(FileInfo source, FileInfo target, Unix /// public TBuilderEntity WithResourceMapping(Uri source, string target, UnixFileModes fileMode = Unix.FileMode644) { - return source.IsFile ? WithResourceMapping(new FileResourceMapping(source.AbsolutePath, target, fileMode)) : WithResourceMapping(new UriResourceMapping(source, target, fileMode)); + if (source.IsFile) + { + return WithResourceMapping(new FileResourceMapping(source.AbsolutePath, target, fileMode)); + } + else + { + return WithResourceMapping(new UriResourceMapping(source, target, fileMode)); + } } /// diff --git a/src/Testcontainers/Builders/IContainerBuilder`2.cs b/src/Testcontainers/Builders/IContainerBuilder`2.cs index 467fcb28d..fa500e684 100644 --- a/src/Testcontainers/Builders/IContainerBuilder`2.cs +++ b/src/Testcontainers/Builders/IContainerBuilder`2.cs @@ -219,10 +219,18 @@ public interface IContainerBuilder : I TBuilderEntity WithResourceMapping(byte[] resourceContent, string filePath, UnixFileModes fileMode = Unix.FileMode644); /// - /// Copies the contents of a URL or a test host directory or file to the container before it starts. + /// Copies the contents of a URL, a test host directory or file to the container before it starts. /// + /// + /// If the source corresponds to a file or the Uri scheme corresponds to a file, + /// the content is copied to the target directory path. If the Uri scheme + /// corresponds to HTTP or HTTPS, the content is copied to the target file. + /// + /// If you prefer to copy a file to a specific target file path instead of a + /// directory, use: . + /// /// The source URL, directory or file to be copied. - /// The target directory path (if the source is a directory) or file path (if the source is a URL or file) to copy the file(s) to. + /// The target directory or file path to copy the file to. /// The POSIX file mode permission. /// A configured instance of . [PublicAPI] @@ -259,10 +267,18 @@ public interface IContainerBuilder : I TBuilderEntity WithResourceMapping(FileInfo source, FileInfo target, UnixFileModes fileMode = Unix.FileMode644); /// - /// Copies a file at a given URL to the container before it starts. + /// Copies a file from a URL to the container before it starts. /// - /// The source URL of the file to be copied. Must be a file, http or https URL. - /// The target file path to copy the file to. + /// + /// If the Uri scheme corresponds to a file, the content is copied to the target + /// directory path. If the Uri scheme corresponds to HTTP or HTTPS, the content is + /// copied to the target file. + /// + /// If you prefer to copy a file to a specific target file path instead of a + /// directory, use: . + /// + /// The source URL of the file to be copied. Must be a http, https or file URL. + /// The target directory or file path to copy the file to. /// The POSIX file mode permission. /// A configured instance of . TBuilderEntity WithResourceMapping(Uri source, string target, UnixFileModes fileMode = Unix.FileMode644); diff --git a/src/Testcontainers/Configurations/Volumes/UriResourceMapping.cs b/src/Testcontainers/Configurations/Volumes/UriResourceMapping.cs index 5145daaa4..132a56ed3 100644 --- a/src/Testcontainers/Configurations/Volumes/UriResourceMapping.cs +++ b/src/Testcontainers/Configurations/Volumes/UriResourceMapping.cs @@ -1,12 +1,12 @@ -using System; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; - namespace DotNet.Testcontainers.Configurations { + using System; + using System.Net.Http; + using System.Threading; + using System.Threading.Tasks; + /// - internal class UriResourceMapping : IResourceMapping + internal sealed class UriResourceMapping : IResourceMapping { private readonly Uri _uri; @@ -48,7 +48,7 @@ public UriResourceMapping(Uri uri, string containerPath, UnixFileModes fileMode) public Task DeleteAsync(CancellationToken ct = default) => Task.CompletedTask; /// - public virtual async Task GetAllBytesAsync(CancellationToken ct = default) + public async Task GetAllBytesAsync(CancellationToken ct = default) { using (var httpClient = new HttpClient()) { diff --git a/tests/Testcontainers.Platform.Linux.Tests/TarOutputMemoryStreamTest.cs b/tests/Testcontainers.Platform.Linux.Tests/TarOutputMemoryStreamTest.cs index d1c986644..45f2852a2 100644 --- a/tests/Testcontainers.Platform.Linux.Tests/TarOutputMemoryStreamTest.cs +++ b/tests/Testcontainers.Platform.Linux.Tests/TarOutputMemoryStreamTest.cs @@ -33,8 +33,18 @@ public void TestFileExistsInTarFile() } [UsedImplicitly] - public sealed class FromResourceMapping : TarOutputMemoryStreamTest, IResourceMapping, IAsyncLifetime, IDisposable + public sealed class FromResourceMapping : TarOutputMemoryStreamTest, IResourceMapping, IClassFixture, IAsyncLifetime, IDisposable { + private readonly string _testUriHttp; + + private readonly string _testUriFile; + + public FromResourceMapping(FromResourceMapping.HttpFixture httpFixture) + { + _testUriHttp = httpFixture.BaseAddress; + _testUriFile = new Uri(_testFile.FullName).ToString(); + } + public MountType Type => MountType.Bind; @@ -86,24 +96,23 @@ public async Task TestFileExistsInContainer() { // Given var targetFilePath1 = string.Join("/", string.Empty, "tmp", Guid.NewGuid(), _testFile.Name); - var targetFilePath2 = string.Join("/", string.Empty, "tmp", Guid.NewGuid(), _testFile.Name); - + var targetFilePath3 = string.Join("/", string.Empty, "tmp", Guid.NewGuid(), _testFile.Name); var targetDirectoryPath1 = string.Join("/", string.Empty, "tmp", Guid.NewGuid()); - var targetDirectoryPath2 = string.Join("/", string.Empty, "tmp", Guid.NewGuid()); - var targetDirectoryPath3 = string.Join("/", string.Empty, "tmp", Guid.NewGuid()); - var targetDirectoryPath4 = string.Join("/", string.Empty, "tmp", Guid.NewGuid()); + var targetDirectoryPath5 = string.Join("/", string.Empty, "tmp", Guid.NewGuid()); var targetFilePaths = new List(); targetFilePaths.Add(targetFilePath1); targetFilePaths.Add(targetFilePath2); + targetFilePaths.Add(targetFilePath3); targetFilePaths.Add(string.Join("/", targetDirectoryPath1, _testFile.Name)); targetFilePaths.Add(string.Join("/", targetDirectoryPath2, _testFile.Name)); targetFilePaths.Add(string.Join("/", targetDirectoryPath3, _testFile.Name)); targetFilePaths.Add(string.Join("/", targetDirectoryPath4, _testFile.Name)); + targetFilePaths.Add(string.Join("/", targetDirectoryPath5, _testFile.Name)); await using var container = new ContainerBuilder() .WithImage(CommonImages.Alpine) @@ -111,6 +120,8 @@ public async Task TestFileExistsInContainer() .WithResourceMapping(_testFile, new FileInfo(targetFilePath1)) .WithResourceMapping(_testFile.FullName, targetDirectoryPath1) .WithResourceMapping(_testFile.Directory.FullName, targetDirectoryPath2) + .WithResourceMapping(_testUriHttp, targetFilePath2) + .WithResourceMapping(_testUriFile, targetDirectoryPath3) .Build(); // When @@ -120,13 +131,13 @@ public async Task TestFileExistsInContainer() await container.StartAsync() .ConfigureAwait(true); - await container.CopyAsync(fileContent, targetFilePath2) + await container.CopyAsync(fileContent, targetFilePath3) .ConfigureAwait(true); - await container.CopyAsync(_testFile.FullName, targetDirectoryPath3) + await container.CopyAsync(_testFile.FullName, targetDirectoryPath4) .ConfigureAwait(true); - await container.CopyAsync(_testFile.Directory.FullName, targetDirectoryPath4) + await container.CopyAsync(_testFile.Directory.FullName, targetDirectoryPath5) .ConfigureAwait(true); // Then @@ -135,6 +146,31 @@ await container.CopyAsync(_testFile.Directory.FullName, targetDirectoryPath4) Assert.All(execResults, result => Assert.Equal(0, result.ExitCode)); } + + public sealed class HttpFixture : IAsyncLifetime + { + private const ushort HttpPort = 80; + + private readonly IContainer _container = new ContainerBuilder() + .WithImage(CommonImages.Alpine) + .WithEntrypoint("/bin/sh", "-c") + .WithCommand($"while true; do echo \"HTTP/1.1 200 OK\r\n\" | nc -l -p {HttpPort}; done") + .WithPortBinding(HttpPort, true) + .Build(); + + public string BaseAddress + => new UriBuilder(Uri.UriSchemeHttp, _container.Hostname, _container.GetMappedPublicPort(HttpPort)).ToString(); + + public Task InitializeAsync() + { + return _container.StartAsync(); + } + + public Task DisposeAsync() + { + return _container.DisposeAsync().AsTask(); + } + } } [UsedImplicitly] From 551608615f9c42d2b1160d9f8ea71c61af0cd151 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Sat, 17 Feb 2024 13:58:40 +0100 Subject: [PATCH 3/3] chore: Rename test fields --- src/Testcontainers/Builders/IContainerBuilder`2.cs | 8 +++++--- .../TarOutputMemoryStreamTest.cs | 12 ++++++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Testcontainers/Builders/IContainerBuilder`2.cs b/src/Testcontainers/Builders/IContainerBuilder`2.cs index fa500e684..6fade9c82 100644 --- a/src/Testcontainers/Builders/IContainerBuilder`2.cs +++ b/src/Testcontainers/Builders/IContainerBuilder`2.cs @@ -224,7 +224,7 @@ public interface IContainerBuilder : I /// /// If the source corresponds to a file or the Uri scheme corresponds to a file, /// the content is copied to the target directory path. If the Uri scheme - /// corresponds to HTTP or HTTPS, the content is copied to the target file. + /// corresponds to HTTP or HTTPS, the content is copied to the target file path. /// /// If you prefer to copy a file to a specific target file path instead of a /// directory, use: . @@ -272,12 +272,14 @@ public interface IContainerBuilder : I /// /// If the Uri scheme corresponds to a file, the content is copied to the target /// directory path. If the Uri scheme corresponds to HTTP or HTTPS, the content is - /// copied to the target file. + /// copied to the target file path. + /// + /// The Uri scheme must be either http, https or file. /// /// If you prefer to copy a file to a specific target file path instead of a /// directory, use: . /// - /// The source URL of the file to be copied. Must be a http, https or file URL. + /// The source URL of the file to be copied. /// The target directory or file path to copy the file to. /// The POSIX file mode permission. /// A configured instance of . diff --git a/tests/Testcontainers.Platform.Linux.Tests/TarOutputMemoryStreamTest.cs b/tests/Testcontainers.Platform.Linux.Tests/TarOutputMemoryStreamTest.cs index 45f2852a2..98ae58dd5 100644 --- a/tests/Testcontainers.Platform.Linux.Tests/TarOutputMemoryStreamTest.cs +++ b/tests/Testcontainers.Platform.Linux.Tests/TarOutputMemoryStreamTest.cs @@ -35,14 +35,14 @@ public void TestFileExistsInTarFile() [UsedImplicitly] public sealed class FromResourceMapping : TarOutputMemoryStreamTest, IResourceMapping, IClassFixture, IAsyncLifetime, IDisposable { - private readonly string _testUriHttp; + private readonly string _testHttpUri; - private readonly string _testUriFile; + private readonly string _testFileUri; public FromResourceMapping(FromResourceMapping.HttpFixture httpFixture) { - _testUriHttp = httpFixture.BaseAddress; - _testUriFile = new Uri(_testFile.FullName).ToString(); + _testHttpUri = httpFixture.BaseAddress; + _testFileUri = new Uri(_testFile.FullName).ToString(); } public MountType Type @@ -120,8 +120,8 @@ public async Task TestFileExistsInContainer() .WithResourceMapping(_testFile, new FileInfo(targetFilePath1)) .WithResourceMapping(_testFile.FullName, targetDirectoryPath1) .WithResourceMapping(_testFile.Directory.FullName, targetDirectoryPath2) - .WithResourceMapping(_testUriHttp, targetFilePath2) - .WithResourceMapping(_testUriFile, targetDirectoryPath3) + .WithResourceMapping(_testHttpUri, targetFilePath2) + .WithResourceMapping(_testFileUri, targetDirectoryPath3) .Build(); // When