diff --git a/src/Testcontainers/Builders/BuildConfiguration.cs b/src/Testcontainers/Builders/BuildConfiguration.cs index b96a71c48..824008646 100644 --- a/src/Testcontainers/Builders/BuildConfiguration.cs +++ b/src/Testcontainers/Builders/BuildConfiguration.cs @@ -27,7 +27,6 @@ public static T Combine(T oldValue, T newValue) /// Type of . /// An updated configuration. public static IEnumerable Combine(IEnumerable oldValue, IEnumerable newValue) - where T : class { if (newValue == null && oldValue == null) { @@ -51,7 +50,6 @@ public static IEnumerable Combine(IEnumerable oldValue, IEnumerable /// Type of . /// An updated configuration. public static IReadOnlyList Combine(IReadOnlyList oldValue, IReadOnlyList newValue) - where T : class { if (newValue == null && oldValue == null) { @@ -75,8 +73,6 @@ public static IReadOnlyList Combine(IReadOnlyList oldValue, IReadOnlyLi /// The type of values in the read-only dictionary. /// An updated configuration. public static IReadOnlyDictionary Combine(IReadOnlyDictionary oldValue, IReadOnlyDictionary newValue) - where TKey : class - where TValue : class { if (newValue == null && oldValue == null) { diff --git a/src/Testcontainers/Containers/SocatBuilder.cs b/src/Testcontainers/Containers/SocatBuilder.cs new file mode 100644 index 000000000..fef22f990 --- /dev/null +++ b/src/Testcontainers/Containers/SocatBuilder.cs @@ -0,0 +1,118 @@ +namespace DotNet.Testcontainers.Containers +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Docker.DotNet.Models; + using DotNet.Testcontainers.Builders; + using DotNet.Testcontainers.Configurations; + using JetBrains.Annotations; + + /// + [PublicAPI] + public sealed class SocatBuilder : ContainerBuilder + { + public const string SocatImage = "alpine/socat:1.7.4.3-r0"; + + /// + /// Initializes a new instance of the class. + /// + public SocatBuilder() + : this(new SocatConfiguration()) + { + DockerResourceConfiguration = Init().DockerResourceConfiguration; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + private SocatBuilder(SocatConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + DockerResourceConfiguration = resourceConfiguration; + } + + /// + protected override SocatConfiguration DockerResourceConfiguration { get; } + + /// + /// Sets the Socat target. + /// + /// The Socat exposed port. + /// The Socat target host. + /// A configured instance of . + public SocatBuilder WithTarget(int exposedPort, string host) + { + return WithTarget(exposedPort, host, exposedPort); + } + + /// + /// Sets the Socat target. + /// + /// The Socat exposed port. + /// The Socat target host. + /// The Socat target port. + /// A configured instance of . + public SocatBuilder WithTarget(int exposedPort, string host, int internalPort) + { + var targets = new Dictionary { { exposedPort, $"{host}:{internalPort}" } }; + return Merge(DockerResourceConfiguration, new SocatConfiguration(targets)) + .WithPortBinding(exposedPort, true); + } + + /// + public override SocatContainer Build() + { + Validate(); + + const string argument = "socat TCP-LISTEN:{0},fork,reuseaddr TCP:{1}"; + + var command = string.Join(" & ", DockerResourceConfiguration.Targets + .Select(item => string.Format(argument, item.Key, item.Value))); + + var waitStrategy = DockerResourceConfiguration.Targets + .Aggregate(Wait.ForUnixContainer(), (waitStrategy, item) => waitStrategy.UntilPortIsAvailable(item.Key)); + + var socatBuilder = WithCommand(command).WithWaitStrategy(waitStrategy); + return new SocatContainer(socatBuilder.DockerResourceConfiguration); + } + + /// + protected override SocatBuilder Init() + { + return base.Init() + .WithImage(SocatImage) + .WithEntrypoint("/bin/sh", "-c"); + } + + /// + protected override void Validate() + { + const string message = "Missing targets. One target must be specified to be created."; + + base.Validate(); + + _ = Guard.Argument(DockerResourceConfiguration.Targets, nameof(DockerResourceConfiguration.Targets)) + .ThrowIf(argument => argument.Value.Count == 0, argument => new ArgumentException(message, argument.Name)); + } + + /// + protected override SocatBuilder Clone(IResourceConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new SocatConfiguration(resourceConfiguration)); + } + + /// + protected override SocatBuilder Clone(IContainerConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new SocatConfiguration(resourceConfiguration)); + } + + /// + protected override SocatBuilder Merge(SocatConfiguration oldValue, SocatConfiguration newValue) + { + return new SocatBuilder(new SocatConfiguration(oldValue, newValue)); + } + } +} diff --git a/src/Testcontainers/Containers/SocatConfiguration.cs b/src/Testcontainers/Containers/SocatConfiguration.cs new file mode 100644 index 000000000..e1f9c0384 --- /dev/null +++ b/src/Testcontainers/Containers/SocatConfiguration.cs @@ -0,0 +1,69 @@ +namespace DotNet.Testcontainers.Containers +{ + using System.Collections.Generic; + using Docker.DotNet.Models; + using DotNet.Testcontainers.Builders; + using DotNet.Testcontainers.Configurations; + using JetBrains.Annotations; + + /// + [PublicAPI] + public sealed class SocatConfiguration : ContainerConfiguration + { + /// + /// Initializes a new instance of the class. + /// + /// A list of target addresses. + public SocatConfiguration( + IReadOnlyDictionary targets = null) + { + Targets = targets; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public SocatConfiguration(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 SocatConfiguration(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 SocatConfiguration(SocatConfiguration resourceConfiguration) + : this(new SocatConfiguration(), 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 SocatConfiguration(SocatConfiguration oldValue, SocatConfiguration newValue) + : base(oldValue, newValue) + { + Targets = BuildConfiguration.Combine(oldValue.Targets, newValue.Targets); + } + + /// + /// Gets a list of target addresses. + /// + public IReadOnlyDictionary Targets { get; } + } +} diff --git a/src/Testcontainers/Containers/SocatContainer.cs b/src/Testcontainers/Containers/SocatContainer.cs new file mode 100644 index 000000000..027672d49 --- /dev/null +++ b/src/Testcontainers/Containers/SocatContainer.cs @@ -0,0 +1,21 @@ +namespace DotNet.Testcontainers.Containers +{ + using JetBrains.Annotations; + + /// + [PublicAPI] + public sealed class SocatContainer : DockerContainer + { + private readonly SocatConfiguration _configuration; + + /// + /// Initializes a new instance of the class. + /// + /// The container configuration. + public SocatContainer(SocatConfiguration configuration) + : base(configuration) + { + _configuration = configuration; + } + } +} diff --git a/tests/Testcontainers.Commons/CommonImages.cs b/tests/Testcontainers.Commons/CommonImages.cs index 22a036285..c58e57283 100644 --- a/tests/Testcontainers.Commons/CommonImages.cs +++ b/tests/Testcontainers.Commons/CommonImages.cs @@ -5,6 +5,8 @@ public static class CommonImages { public static readonly IImage Ryuk = new DockerImage("testcontainers/ryuk:0.9.0"); + public static readonly IImage HelloWorld = new DockerImage("testcontainers/helloworld:1.2.0"); + public static readonly IImage Alpine = new DockerImage("alpine:3.17"); public static readonly IImage Socat = new DockerImage("alpine/socat:1.8.0.0"); diff --git a/tests/Testcontainers.Platform.Linux.Tests/SocatContainerTest.cs b/tests/Testcontainers.Platform.Linux.Tests/SocatContainerTest.cs new file mode 100644 index 000000000..879aced43 --- /dev/null +++ b/tests/Testcontainers.Platform.Linux.Tests/SocatContainerTest.cs @@ -0,0 +1,71 @@ +namespace Testcontainers.Tests; + +public sealed class SocatContainerTest : IAsyncLifetime +{ + private const string HelloWorldAlias = "hello-world-container"; + + private readonly INetwork _network; + + private readonly IContainer _helloWorldContainer; + + private readonly IContainer _socatContainer; + + public SocatContainerTest() + { + _network = new NetworkBuilder() + .Build(); + + _helloWorldContainer = new ContainerBuilder() + .WithImage(CommonImages.HelloWorld) + .WithNetwork(_network) + .WithNetworkAliases(HelloWorldAlias) + .Build(); + + _socatContainer = new SocatBuilder() + .WithNetwork(_network) + .WithTarget(8080, HelloWorldAlias) + .WithTarget(8081, HelloWorldAlias, 8080) + .Build(); + } + + public async Task InitializeAsync() + { + await _helloWorldContainer.StartAsync() + .ConfigureAwait(false); + + await _socatContainer.StartAsync() + .ConfigureAwait(false); + } + + public async Task DisposeAsync() + { + await _socatContainer.DisposeAsync() + .ConfigureAwait(false); + + await _helloWorldContainer.DisposeAsync() + .ConfigureAwait(false); + + await _network.DisposeAsync() + .ConfigureAwait(false); + } + + [Theory] + [InlineData(8080)] + [InlineData(8081)] + public async Task RequestTargetContainer(int containerPort) + { + // Given + using var httpClient = new HttpClient(); + httpClient.BaseAddress = new UriBuilder(Uri.UriSchemeHttp, _socatContainer.Hostname, _socatContainer.GetMappedPublicPort(containerPort)).Uri; + + // When + using var httpResponse = await httpClient.GetAsync("/ping") + .ConfigureAwait(true); + + var response = await httpResponse.Content.ReadAsStringAsync() + .ConfigureAwait(true); + + // Then + Assert.Equal("PONG", response); + } +} \ No newline at end of file diff --git a/tests/Testcontainers.Platform.Linux.Tests/Usings.cs b/tests/Testcontainers.Platform.Linux.Tests/Usings.cs index ea641a068..89d5592ac 100644 --- a/tests/Testcontainers.Platform.Linux.Tests/Usings.cs +++ b/tests/Testcontainers.Platform.Linux.Tests/Usings.cs @@ -4,6 +4,7 @@ global using System.IO; global using System.Linq; global using System.Net; +global using System.Net.Http; global using System.Net.Sockets; global using System.Text; global using System.Threading; @@ -16,6 +17,7 @@ global using DotNet.Testcontainers.Commons; global using DotNet.Testcontainers.Configurations; global using DotNet.Testcontainers.Containers; +global using DotNet.Testcontainers.Networks; global using ICSharpCode.SharpZipLib.Tar; global using JetBrains.Annotations; global using Microsoft.Extensions.Logging.Abstractions; diff --git a/tests/Testcontainers.WebDriver.Tests/WebDriverContainerTest.cs b/tests/Testcontainers.WebDriver.Tests/WebDriverContainerTest.cs index fa472ed8e..ecbe234b9 100644 --- a/tests/Testcontainers.WebDriver.Tests/WebDriverContainerTest.cs +++ b/tests/Testcontainers.WebDriver.Tests/WebDriverContainerTest.cs @@ -11,7 +11,7 @@ public abstract class WebDriverContainerTest : IAsyncLifetime private WebDriverContainerTest(WebDriverContainer webDriverContainer) { _helloWorldContainer = new ContainerBuilder() - .WithImage("testcontainers/helloworld:1.1.0") + .WithImage(CommonImages.HelloWorld) .WithNetwork(webDriverContainer.GetNetwork()) .WithNetworkAliases(_helloWorldBaseAddress.Host) .WithPortBinding(_helloWorldBaseAddress.Port, true)