From 0b7d624c127afdfeb26abcabc397cf9889e393e9 Mon Sep 17 00:00:00 2001 From: Niklas Petersen Date: Sun, 17 Sep 2023 14:21:37 +0200 Subject: [PATCH 1/8] Add Nats Server Container --- Testcontainers.sln | 14 +++ src/Testcontainers.Nats/.editorconfig | 1 + src/Testcontainers.Nats/NatsBuilder.cs | 108 ++++++++++++++++++ src/Testcontainers.Nats/NatsConfiguration.cs | 72 ++++++++++++ src/Testcontainers.Nats/NatsContainer.cs | 44 +++++++ .../Testcontainers.Nats.csproj | 13 +++ src/Testcontainers.Nats/Usings.cs | 7 ++ tests/Testcontainers.Nats.Tests/.editorconfig | 1 + .../NatsContainerTest.cs | 75 ++++++++++++ .../Testcontainers.Nats.Tests.csproj | 18 +++ tests/Testcontainers.Nats.Tests/Usings.cs | 7 ++ 11 files changed, 360 insertions(+) create mode 100644 src/Testcontainers.Nats/.editorconfig create mode 100644 src/Testcontainers.Nats/NatsBuilder.cs create mode 100644 src/Testcontainers.Nats/NatsConfiguration.cs create mode 100644 src/Testcontainers.Nats/NatsContainer.cs create mode 100644 src/Testcontainers.Nats/Testcontainers.Nats.csproj create mode 100644 src/Testcontainers.Nats/Usings.cs create mode 100644 tests/Testcontainers.Nats.Tests/.editorconfig create mode 100644 tests/Testcontainers.Nats.Tests/NatsContainerTest.cs create mode 100644 tests/Testcontainers.Nats.Tests/Testcontainers.Nats.Tests.csproj create mode 100644 tests/Testcontainers.Nats.Tests/Usings.cs diff --git a/Testcontainers.sln b/Testcontainers.sln index 4a415068d..5ba34f2bb 100644 --- a/Testcontainers.sln +++ b/Testcontainers.sln @@ -137,6 +137,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Tests", "tes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.WebDriver.Tests", "tests\Testcontainers.WebDriver.Tests\Testcontainers.WebDriver.Tests.csproj", "{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Nats", "src\Testcontainers.Nats\Testcontainers.Nats.csproj", "{BF37BEA1-0816-4326-B1E0-E82290F8FCE0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Nats.Tests", "tests\Testcontainers.Nats.Tests\Testcontainers.Nats.Tests.csproj", "{87A3F137-6DC3-4CE5-91E6-01797D076086}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -394,6 +398,14 @@ Global {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Debug|Any CPU.Build.0 = Debug|Any CPU {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Release|Any CPU.ActiveCfg = Release|Any CPU {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Release|Any CPU.Build.0 = Release|Any CPU + {BF37BEA1-0816-4326-B1E0-E82290F8FCE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BF37BEA1-0816-4326-B1E0-E82290F8FCE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BF37BEA1-0816-4326-B1E0-E82290F8FCE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BF37BEA1-0816-4326-B1E0-E82290F8FCE0}.Release|Any CPU.Build.0 = Release|Any CPU + {87A3F137-6DC3-4CE5-91E6-01797D076086}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {87A3F137-6DC3-4CE5-91E6-01797D076086}.Debug|Any CPU.Build.0 = Debug|Any CPU + {87A3F137-6DC3-4CE5-91E6-01797D076086}.Release|Any CPU.ActiveCfg = Release|Any CPU + {87A3F137-6DC3-4CE5-91E6-01797D076086}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {3F2E254F-C203-43FD-A078-DC3E2CBC0F9F} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} @@ -458,5 +470,7 @@ Global {1A1983E6-5297-435F-B467-E8E1F11277D6} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {27CDB869-A150-4593-958F-6F26E5391E7C} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} + {BF37BEA1-0816-4326-B1E0-E82290F8FCE0} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} + {87A3F137-6DC3-4CE5-91E6-01797D076086} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} EndGlobalSection EndGlobal diff --git a/src/Testcontainers.Nats/.editorconfig b/src/Testcontainers.Nats/.editorconfig new file mode 100644 index 000000000..6f066619d --- /dev/null +++ b/src/Testcontainers.Nats/.editorconfig @@ -0,0 +1 @@ +root = true \ No newline at end of file diff --git a/src/Testcontainers.Nats/NatsBuilder.cs b/src/Testcontainers.Nats/NatsBuilder.cs new file mode 100644 index 000000000..535d0682e --- /dev/null +++ b/src/Testcontainers.Nats/NatsBuilder.cs @@ -0,0 +1,108 @@ +namespace Testcontainers.Nats; + +/// +[PublicAPI] +public sealed class NatsBuilder : ContainerBuilder +{ + public const string NatsImage = "nats:2.9"; + + public const ushort ClientPort = 4222; + public const ushort RoutingPort = 6222; + public const ushort MonitoringPort = 8222; + + /// + /// Initializes a new instance of the class. + /// + public NatsBuilder() + : this(new NatsConfiguration()) + { + DockerResourceConfiguration = Init().DockerResourceConfiguration; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + private NatsBuilder(NatsConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + DockerResourceConfiguration = resourceConfiguration; + } + + /// + protected override NatsConfiguration DockerResourceConfiguration { get; } + + /// + /// Sets the Nats Server password. + /// + /// The Nats Server password. + /// A configured instance of . + public NatsBuilder WithPassword(string password) + { + return Merge(DockerResourceConfiguration, new NatsConfiguration(password: password)) + .WithCommand("-pass", password); + } + + /// + /// Sets the Nats Server username. + /// + /// The Nats Server username. + /// A configured instance of . + public NatsBuilder WithUsername(string username) + { + return Merge(DockerResourceConfiguration, new NatsConfiguration(username: username)) + .WithCommand("-user", username); + } + + /// + /// Sets the Nats config. + /// + /// The Nats config. + /// A configured instance of . + public NatsBuilder WithNatsConfig(NatsConfiguration config) + { + // Extends the ContainerBuilder capabilities and holds a custom configuration in NatsConfiguration. + // In case of a module requires other properties to represent itself, extend ContainerConfiguration. + return Merge(DockerResourceConfiguration, new NatsConfiguration(config)); + } + + /// + public override NatsContainer Build() + { + Validate(); + return new NatsContainer(DockerResourceConfiguration, TestcontainersSettings.Logger); + } + + /// + protected override NatsBuilder Init() + { + return base.Init() + .WithImage(NatsImage) + .WithPortBinding(ClientPort, true) + .WithPortBinding(MonitoringPort, true) + .WithPortBinding(RoutingPort, true) + .WithCommand("-m", MonitoringPort.ToString()) // Enable monitoring endpoint. + .WithCommand("-js") // Enable JetStream functionality. + .WithCommand("-DV") // Enable both debug and protocol trace messages + .WithWaitStrategy(Wait.ForUnixContainer() + .UntilMessageIsLogged("Listening for client connections on 0.0.0.0:4222")); + } + + /// + protected override NatsBuilder Clone(IResourceConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new NatsConfiguration(resourceConfiguration)); + } + + /// + protected override NatsBuilder Clone(IContainerConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new NatsConfiguration(resourceConfiguration)); + } + + /// + protected override NatsBuilder Merge(NatsConfiguration oldValue, NatsConfiguration newValue) + { + return new NatsBuilder(new NatsConfiguration(oldValue, newValue)); + } +} \ No newline at end of file diff --git a/src/Testcontainers.Nats/NatsConfiguration.cs b/src/Testcontainers.Nats/NatsConfiguration.cs new file mode 100644 index 000000000..4f87c389a --- /dev/null +++ b/src/Testcontainers.Nats/NatsConfiguration.cs @@ -0,0 +1,72 @@ +namespace Testcontainers.Nats; + +/// +[PublicAPI] +public sealed class NatsConfiguration : ContainerConfiguration +{ + /// + /// Initializes a new instance of the class. + /// + /// The nats server user name. + /// The nats server password. + public NatsConfiguration( + string username = null, + string password = null) + { + Username = username; + Password = password; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public NatsConfiguration(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 NatsConfiguration(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 NatsConfiguration(NatsConfiguration resourceConfiguration) + : this(new NatsConfiguration(), 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 NatsConfiguration(NatsConfiguration oldValue, NatsConfiguration newValue) + : base(oldValue, newValue) + { + // // Create an updated immutable copy of the module configuration. + Username = BuildConfiguration.Combine(oldValue.Username, newValue.Username); + Password = BuildConfiguration.Combine(oldValue.Password, newValue.Password); + } + + /// + /// The nats server user name. + /// + public string Username { get; } + + /// + /// The nats server password. + /// + public string Password { get; } +} \ No newline at end of file diff --git a/src/Testcontainers.Nats/NatsContainer.cs b/src/Testcontainers.Nats/NatsContainer.cs new file mode 100644 index 000000000..59f667570 --- /dev/null +++ b/src/Testcontainers.Nats/NatsContainer.cs @@ -0,0 +1,44 @@ +namespace Testcontainers.Nats; + +/// +[PublicAPI] +public sealed class NatsContainer : DockerContainer +{ + private readonly NatsConfiguration _natsConfig; + + /// + /// Initializes a new instance of the class. + /// + /// The container configuration. + /// The logger. + public NatsContainer(NatsConfiguration configuration, ILogger logger) + : base(configuration, logger) + { + _natsConfig = configuration; + } + + /// + /// Gets the nats connection string + /// + /// A nats connection string in the form: nats://hostname:mappedPort/>. + /// + /// If either username or password is set, the connection string will contain the credentials. + /// + public string GetConnectionString() + { + return new UriBuilder("nats", Hostname, GetMappedPublicPort(NatsBuilder.ClientPort)) + { + UserName = _natsConfig.Username, + Password = _natsConfig.Password, + }.ToString(); + } + + /// + /// Gets the nats monitor url + /// + /// A url in the form: http://hostname:mappedPort/>. + public string GetMonitorUrl() + { + return new UriBuilder("http", Hostname, GetMappedPublicPort(NatsBuilder.MonitoringPort)).ToString(); + } +} \ No newline at end of file diff --git a/src/Testcontainers.Nats/Testcontainers.Nats.csproj b/src/Testcontainers.Nats/Testcontainers.Nats.csproj new file mode 100644 index 000000000..4c05d521f --- /dev/null +++ b/src/Testcontainers.Nats/Testcontainers.Nats.csproj @@ -0,0 +1,13 @@ + + + netstandard2.0;netstandard2.1 + latest + + + + + + + + + \ No newline at end of file diff --git a/src/Testcontainers.Nats/Usings.cs b/src/Testcontainers.Nats/Usings.cs new file mode 100644 index 000000000..bf2829a65 --- /dev/null +++ b/src/Testcontainers.Nats/Usings.cs @@ -0,0 +1,7 @@ +global using System; +global using Docker.DotNet.Models; +global using DotNet.Testcontainers.Builders; +global using DotNet.Testcontainers.Configurations; +global using DotNet.Testcontainers.Containers; +global using JetBrains.Annotations; +global using Microsoft.Extensions.Logging; \ No newline at end of file diff --git a/tests/Testcontainers.Nats.Tests/.editorconfig b/tests/Testcontainers.Nats.Tests/.editorconfig new file mode 100644 index 000000000..6f066619d --- /dev/null +++ b/tests/Testcontainers.Nats.Tests/.editorconfig @@ -0,0 +1 @@ +root = true \ No newline at end of file diff --git a/tests/Testcontainers.Nats.Tests/NatsContainerTest.cs b/tests/Testcontainers.Nats.Tests/NatsContainerTest.cs new file mode 100644 index 000000000..9aac0bd93 --- /dev/null +++ b/tests/Testcontainers.Nats.Tests/NatsContainerTest.cs @@ -0,0 +1,75 @@ +namespace Testcontainers.Nats; + +public sealed class NatsContainerTest : IAsyncLifetime +{ + private readonly NatsContainer _natsContainer = new NatsBuilder().Build(); + + public Task InitializeAsync() + { + return _natsContainer.StartAsync(); + } + + public Task DisposeAsync() + { + return _natsContainer.DisposeAsync().AsTask(); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task ContainerIsStartedWithCorrectParameters() + { + using var client = new ConnectionFactory() + .CreateConnection(_natsContainer.GetConnectionString()); + + Assert.Equal(ConnState.CONNECTED, client.State); + Assert.True(client.ServerInfo.JetStreamAvailable); + + using var monitorClient = new HttpClient() + { + BaseAddress = new Uri(_natsContainer.GetMonitorUrl()), + }; + + using var response = await monitorClient.GetAsync("/healthz"); + var s = await response.Content.ReadAsStringAsync(); + Assert.True(response.IsSuccessStatusCode); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task PubSubSendsAndReturnsMessages() + { + using var client = new ConnectionFactory() + .CreateConnection(_natsContainer.GetConnectionString()); + + using ISyncSubscription subSync = client.SubscribeSync("greet.pam"); + client.Publish("greet.pam", Encoding.UTF8.GetBytes("hello pam 1")); + + var msg = subSync.NextMessage(1000); + var text = Encoding.UTF8.GetString(msg.Data); + + + Assert.Equal("hello pam 1", text); + await client.DrainAsync(); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task BuilderShouldBuildWithUserNameAndPassword() + { + var builder = new NatsBuilder() + .WithUsername("test") + .WithPassword("testpass"); + await using var container = builder.Build(); + + await container.StartAsync(); + + var uri = new Uri(container.GetConnectionString()); + + Assert.Equal("test:testpass", uri.UserInfo); + + using var client = new ConnectionFactory() + .CreateConnection(_natsContainer.GetConnectionString()); + + Assert.Equal(ConnState.CONNECTED, client.State); + } +} \ No newline at end of file diff --git a/tests/Testcontainers.Nats.Tests/Testcontainers.Nats.Tests.csproj b/tests/Testcontainers.Nats.Tests/Testcontainers.Nats.Tests.csproj new file mode 100644 index 000000000..ad254a3d3 --- /dev/null +++ b/tests/Testcontainers.Nats.Tests/Testcontainers.Nats.Tests.csproj @@ -0,0 +1,18 @@ + + + net6.0 + false + false + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Testcontainers.Nats.Tests/Usings.cs b/tests/Testcontainers.Nats.Tests/Usings.cs new file mode 100644 index 000000000..9cf5647c7 --- /dev/null +++ b/tests/Testcontainers.Nats.Tests/Usings.cs @@ -0,0 +1,7 @@ +global using System; +global using System.Net.Http; +global using System.Text; +global using System.Threading.Tasks; +global using DotNet.Testcontainers.Commons; +global using NATS.Client; +global using Xunit; \ No newline at end of file From da700e782d21dfe319b30ced6adb5d7ec0051331 Mon Sep 17 00:00:00 2001 From: Niklas Petersen Date: Sun, 17 Sep 2023 16:46:20 +0200 Subject: [PATCH 2/8] Fix bit to agressive using which could end up in a ObjectDisposed exception. Caused by a copy waste while doing the initial implementation. --- tests/Testcontainers.Nats.Tests/NatsContainerTest.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/Testcontainers.Nats.Tests/NatsContainerTest.cs b/tests/Testcontainers.Nats.Tests/NatsContainerTest.cs index 9aac0bd93..4a961f4cd 100644 --- a/tests/Testcontainers.Nats.Tests/NatsContainerTest.cs +++ b/tests/Testcontainers.Nats.Tests/NatsContainerTest.cs @@ -24,10 +24,8 @@ public async Task ContainerIsStartedWithCorrectParameters() Assert.Equal(ConnState.CONNECTED, client.State); Assert.True(client.ServerInfo.JetStreamAvailable); - using var monitorClient = new HttpClient() - { - BaseAddress = new Uri(_natsContainer.GetMonitorUrl()), - }; + using var monitorClient = new HttpClient(); + monitorClient.BaseAddress = new Uri(_natsContainer.GetMonitorUrl()); using var response = await monitorClient.GetAsync("/healthz"); var s = await response.Content.ReadAsStringAsync(); From 48d80b85aa8a5ce904a11fe142415dad35be8794 Mon Sep 17 00:00:00 2001 From: Niklas Petersen <7766483+niklasfp@users.noreply.github.com> Date: Sun, 17 Sep 2023 17:27:55 +0200 Subject: [PATCH 3/8] Update src/Testcontainers.Nats/NatsBuilder.cs Co-authored-by: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> --- src/Testcontainers.Nats/NatsBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Testcontainers.Nats/NatsBuilder.cs b/src/Testcontainers.Nats/NatsBuilder.cs index 535d0682e..f8123fb92 100644 --- a/src/Testcontainers.Nats/NatsBuilder.cs +++ b/src/Testcontainers.Nats/NatsBuilder.cs @@ -51,7 +51,7 @@ public NatsBuilder WithPassword(string password) public NatsBuilder WithUsername(string username) { return Merge(DockerResourceConfiguration, new NatsConfiguration(username: username)) - .WithCommand("-user", username); + .WithCommand("--user", username); } /// From 2dc3b5fb022aea862ac9c113db25eee35e7872be Mon Sep 17 00:00:00 2001 From: Niklas Petersen Date: Sun, 17 Sep 2023 18:03:31 +0200 Subject: [PATCH 4/8] Remove .WithNatsConfig(... Fix -user & -pass were missing a -, so --user and --pass is now used. Add long params instead, e.g --jetstream instead of -js Add Validatation, if either password or username is set, then both are required. Fix EscapeDataString on username and password. --- src/Testcontainers.Nats/NatsBuilder.cs | 41 +++++++++++-------- src/Testcontainers.Nats/NatsContainer.cs | 13 ++++-- .../NatsContainerTest.cs | 20 +++++++-- 3 files changed, 50 insertions(+), 24 deletions(-) diff --git a/src/Testcontainers.Nats/NatsBuilder.cs b/src/Testcontainers.Nats/NatsBuilder.cs index 535d0682e..ee6b19026 100644 --- a/src/Testcontainers.Nats/NatsBuilder.cs +++ b/src/Testcontainers.Nats/NatsBuilder.cs @@ -1,3 +1,5 @@ +using DotNet.Testcontainers; + namespace Testcontainers.Nats; /// @@ -40,7 +42,7 @@ private NatsBuilder(NatsConfiguration resourceConfiguration) public NatsBuilder WithPassword(string password) { return Merge(DockerResourceConfiguration, new NatsConfiguration(password: password)) - .WithCommand("-pass", password); + .WithCommand("--pass", password); } /// @@ -51,19 +53,7 @@ public NatsBuilder WithPassword(string password) public NatsBuilder WithUsername(string username) { return Merge(DockerResourceConfiguration, new NatsConfiguration(username: username)) - .WithCommand("-user", username); - } - - /// - /// Sets the Nats config. - /// - /// The Nats config. - /// A configured instance of . - public NatsBuilder WithNatsConfig(NatsConfiguration config) - { - // Extends the ContainerBuilder capabilities and holds a custom configuration in NatsConfiguration. - // In case of a module requires other properties to represent itself, extend ContainerConfiguration. - return Merge(DockerResourceConfiguration, new NatsConfiguration(config)); + .WithCommand("--user", username); } /// @@ -73,6 +63,22 @@ public override NatsContainer Build() return new NatsContainer(DockerResourceConfiguration, TestcontainersSettings.Logger); } + + /// + protected override void Validate() + { + base.Validate(); + + if (DockerResourceConfiguration.Password != null || DockerResourceConfiguration.Username != null) + { + _ = Guard.Argument(DockerResourceConfiguration.Username, nameof(DockerResourceConfiguration.Username)) + .NotNull(); + + _ = Guard.Argument(DockerResourceConfiguration.Password, nameof(DockerResourceConfiguration.Password)) + .NotNull(); + } + } + /// protected override NatsBuilder Init() { @@ -81,9 +87,10 @@ protected override NatsBuilder Init() .WithPortBinding(ClientPort, true) .WithPortBinding(MonitoringPort, true) .WithPortBinding(RoutingPort, true) - .WithCommand("-m", MonitoringPort.ToString()) // Enable monitoring endpoint. - .WithCommand("-js") // Enable JetStream functionality. - .WithCommand("-DV") // Enable both debug and protocol trace messages + .WithCommand("--http_port", MonitoringPort.ToString()) // Enable monitoring endpoint. + .WithCommand("--jetstream") // Enable JetStream functionality. + .WithCommand("--debug") // Enable both debug + .WithCommand("--trace") // Enable protocol trace messages .WithWaitStrategy(Wait.ForUnixContainer() .UntilMessageIsLogged("Listening for client connections on 0.0.0.0:4222")); } diff --git a/src/Testcontainers.Nats/NatsContainer.cs b/src/Testcontainers.Nats/NatsContainer.cs index 59f667570..d32368738 100644 --- a/src/Testcontainers.Nats/NatsContainer.cs +++ b/src/Testcontainers.Nats/NatsContainer.cs @@ -26,11 +26,16 @@ public NatsContainer(NatsConfiguration configuration, ILogger logger) /// public string GetConnectionString() { - return new UriBuilder("nats", Hostname, GetMappedPublicPort(NatsBuilder.ClientPort)) + var endpoint = new UriBuilder("nats", Hostname, GetMappedPublicPort(NatsBuilder.ClientPort)); + + // Both should be set, or neither, this is validated in the builder. + if (_natsConfig.Password != null && _natsConfig.Username != null) { - UserName = _natsConfig.Username, - Password = _natsConfig.Password, - }.ToString(); + endpoint.UserName = Uri.EscapeDataString(_natsConfig.Username); + endpoint.Password = Uri.EscapeDataString(_natsConfig.Password); + } + + return endpoint.ToString(); } /// diff --git a/tests/Testcontainers.Nats.Tests/NatsContainerTest.cs b/tests/Testcontainers.Nats.Tests/NatsContainerTest.cs index 4a961f4cd..f404f1d82 100644 --- a/tests/Testcontainers.Nats.Tests/NatsContainerTest.cs +++ b/tests/Testcontainers.Nats.Tests/NatsContainerTest.cs @@ -54,20 +54,34 @@ public async Task PubSubSendsAndReturnsMessages() [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] public async Task BuilderShouldBuildWithUserNameAndPassword() { + var encodedPassword = Uri.EscapeDataString("??&&testpass"); + var encodedUsername = Uri.EscapeDataString("??&&test"); + var builder = new NatsBuilder() - .WithUsername("test") - .WithPassword("testpass"); + .WithUsername("??&&test") + .WithPassword("??&&testpass"); await using var container = builder.Build(); await container.StartAsync(); var uri = new Uri(container.GetConnectionString()); - Assert.Equal("test:testpass", uri.UserInfo); + Assert.Equal($"{encodedUsername}:{encodedPassword}", uri.UserInfo); using var client = new ConnectionFactory() .CreateConnection(_natsContainer.GetConnectionString()); Assert.Equal(ConnState.CONNECTED, client.State); } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public void BuilderShouldFailWithOnlyUserNameOrPassword() + { + var builder = new NatsBuilder().WithUsername("??&&test"); + Assert.Throws(() => builder.Build()); + + builder = new NatsBuilder().WithPassword("??&&test"); + Assert.Throws(() => builder.Build()); + } } \ No newline at end of file From bc1980b618b2119ecdb886907b667489950bd7ab Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Mon, 18 Sep 2023 21:31:14 +0200 Subject: [PATCH 5/8] chore: Applied minor changes to keep the module consistent across other modules --- Testcontainers.sln | 28 ++++---- src/Testcontainers.Nats/NatsBuilder.cs | 65 +++++++++---------- src/Testcontainers.Nats/NatsConfiguration.cs | 11 ++-- src/Testcontainers.Nats/NatsContainer.cs | 26 ++++---- src/Testcontainers.Nats/Usings.cs | 1 + .../NatsContainerTest.cs | 4 +- .../Testcontainers.Nats.Tests.csproj | 2 +- 7 files changed, 68 insertions(+), 69 deletions(-) diff --git a/Testcontainers.sln b/Testcontainers.sln index 5ba34f2bb..2c80c4b60 100644 --- a/Testcontainers.sln +++ b/Testcontainers.sln @@ -51,6 +51,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.MsSql", "src EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.MySql", "src\Testcontainers.MySql\Testcontainers.MySql.csproj", "{9FDCFAEA-AE42-4C69-89EF-F1FF75E88CCC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Nats", "src\Testcontainers.Nats\Testcontainers.Nats.csproj", "{BF37BEA1-0816-4326-B1E0-E82290F8FCE0}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Neo4j", "src\Testcontainers.Neo4j\Testcontainers.Neo4j.csproj", "{ADC2372B-6FE0-421D-8277-BB628E8EFC22}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Oracle", "src\Testcontainers.Oracle\Testcontainers.Oracle.csproj", "{596EAFC1-0496-495C-B382-D57415FA456A}" @@ -111,6 +113,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.MsSql.Tests" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.MySql.Tests", "tests\Testcontainers.MySql.Tests\Testcontainers.MySql.Tests.csproj", "{E42DA1CE-698F-4E45-8D1F-5D5895893840}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Nats.Tests", "tests\Testcontainers.Nats.Tests\Testcontainers.Nats.Tests.csproj", "{87A3F137-6DC3-4CE5-91E6-01797D076086}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Neo4j.Tests", "tests\Testcontainers.Neo4j.Tests\Testcontainers.Neo4j.Tests.csproj", "{D3F63405-C0FA-4F83-8B79-E30BFF5FF5BF}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Oracle.Tests", "tests\Testcontainers.Oracle.Tests\Testcontainers.Oracle.Tests.csproj", "{4AC1088B-9965-4497-AC8E-570F1AD5631F}" @@ -137,10 +141,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Tests", "tes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.WebDriver.Tests", "tests\Testcontainers.WebDriver.Tests\Testcontainers.WebDriver.Tests.csproj", "{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Nats", "src\Testcontainers.Nats\Testcontainers.Nats.csproj", "{BF37BEA1-0816-4326-B1E0-E82290F8FCE0}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Nats.Tests", "tests\Testcontainers.Nats.Tests\Testcontainers.Nats.Tests.csproj", "{87A3F137-6DC3-4CE5-91E6-01797D076086}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -226,6 +226,10 @@ Global {9FDCFAEA-AE42-4C69-89EF-F1FF75E88CCC}.Debug|Any CPU.Build.0 = Debug|Any CPU {9FDCFAEA-AE42-4C69-89EF-F1FF75E88CCC}.Release|Any CPU.ActiveCfg = Release|Any CPU {9FDCFAEA-AE42-4C69-89EF-F1FF75E88CCC}.Release|Any CPU.Build.0 = Release|Any CPU + {BF37BEA1-0816-4326-B1E0-E82290F8FCE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BF37BEA1-0816-4326-B1E0-E82290F8FCE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BF37BEA1-0816-4326-B1E0-E82290F8FCE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BF37BEA1-0816-4326-B1E0-E82290F8FCE0}.Release|Any CPU.Build.0 = Release|Any CPU {ADC2372B-6FE0-421D-8277-BB628E8EFC22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {ADC2372B-6FE0-421D-8277-BB628E8EFC22}.Debug|Any CPU.Build.0 = Debug|Any CPU {ADC2372B-6FE0-421D-8277-BB628E8EFC22}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -346,6 +350,10 @@ Global {E42DA1CE-698F-4E45-8D1F-5D5895893840}.Debug|Any CPU.Build.0 = Debug|Any CPU {E42DA1CE-698F-4E45-8D1F-5D5895893840}.Release|Any CPU.ActiveCfg = Release|Any CPU {E42DA1CE-698F-4E45-8D1F-5D5895893840}.Release|Any CPU.Build.0 = Release|Any CPU + {87A3F137-6DC3-4CE5-91E6-01797D076086}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {87A3F137-6DC3-4CE5-91E6-01797D076086}.Debug|Any CPU.Build.0 = Debug|Any CPU + {87A3F137-6DC3-4CE5-91E6-01797D076086}.Release|Any CPU.ActiveCfg = Release|Any CPU + {87A3F137-6DC3-4CE5-91E6-01797D076086}.Release|Any CPU.Build.0 = Release|Any CPU {D3F63405-C0FA-4F83-8B79-E30BFF5FF5BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D3F63405-C0FA-4F83-8B79-E30BFF5FF5BF}.Debug|Any CPU.Build.0 = Debug|Any CPU {D3F63405-C0FA-4F83-8B79-E30BFF5FF5BF}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -398,14 +406,6 @@ Global {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Debug|Any CPU.Build.0 = Debug|Any CPU {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Release|Any CPU.ActiveCfg = Release|Any CPU {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Release|Any CPU.Build.0 = Release|Any CPU - {BF37BEA1-0816-4326-B1E0-E82290F8FCE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BF37BEA1-0816-4326-B1E0-E82290F8FCE0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BF37BEA1-0816-4326-B1E0-E82290F8FCE0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BF37BEA1-0816-4326-B1E0-E82290F8FCE0}.Release|Any CPU.Build.0 = Release|Any CPU - {87A3F137-6DC3-4CE5-91E6-01797D076086}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {87A3F137-6DC3-4CE5-91E6-01797D076086}.Debug|Any CPU.Build.0 = Debug|Any CPU - {87A3F137-6DC3-4CE5-91E6-01797D076086}.Release|Any CPU.ActiveCfg = Release|Any CPU - {87A3F137-6DC3-4CE5-91E6-01797D076086}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {3F2E254F-C203-43FD-A078-DC3E2CBC0F9F} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} @@ -427,6 +427,7 @@ Global {2613F146-6C66-4059-9D37-D48BA6B61515} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {121FB123-40D9-44D4-9AB7-AD57ED34F466} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {9FDCFAEA-AE42-4C69-89EF-F1FF75E88CCC} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} + {BF37BEA1-0816-4326-B1E0-E82290F8FCE0} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {ADC2372B-6FE0-421D-8277-BB628E8EFC22} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {596EAFC1-0496-495C-B382-D57415FA456A} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {8AB91636-9055-4900-A72A-7CFFACDFDBF0} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} @@ -457,6 +458,7 @@ Global {82A7E7B8-3187-4CAE-845B-0BF43409B38A} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {25DBED78-99F4-433F-BBF5-1B4E9DEAE437} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {E42DA1CE-698F-4E45-8D1F-5D5895893840} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} + {87A3F137-6DC3-4CE5-91E6-01797D076086} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {D3F63405-C0FA-4F83-8B79-E30BFF5FF5BF} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {4AC1088B-9965-4497-AC8E-570F1AD5631F} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {DA1D7ADE-452C-4369-83CC-56289176EACD} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} @@ -470,7 +472,5 @@ Global {1A1983E6-5297-435F-B467-E8E1F11277D6} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {27CDB869-A150-4593-958F-6F26E5391E7C} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} - {BF37BEA1-0816-4326-B1E0-E82290F8FCE0} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} - {87A3F137-6DC3-4CE5-91E6-01797D076086} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} EndGlobalSection EndGlobal diff --git a/src/Testcontainers.Nats/NatsBuilder.cs b/src/Testcontainers.Nats/NatsBuilder.cs index ee6b19026..4a6568c12 100644 --- a/src/Testcontainers.Nats/NatsBuilder.cs +++ b/src/Testcontainers.Nats/NatsBuilder.cs @@ -1,5 +1,3 @@ -using DotNet.Testcontainers; - namespace Testcontainers.Nats; /// @@ -8,9 +6,11 @@ public sealed class NatsBuilder : ContainerBuilder /// Initializes a new instance of the class. @@ -35,25 +35,25 @@ private NatsBuilder(NatsConfiguration resourceConfiguration) protected override NatsConfiguration DockerResourceConfiguration { get; } /// - /// Sets the Nats Server password. + /// Sets the Nats username. /// - /// The Nats Server password. + /// The Nats username. /// A configured instance of . - public NatsBuilder WithPassword(string password) + public NatsBuilder WithUsername(string username) { - return Merge(DockerResourceConfiguration, new NatsConfiguration(password: password)) - .WithCommand("--pass", password); + return Merge(DockerResourceConfiguration, new NatsConfiguration(username: username)) + .WithCommand("--user", username); } /// - /// Sets the Nats Server username. + /// Sets the Nats password. /// - /// The Nats Server username. + /// The Nats password. /// A configured instance of . - public NatsBuilder WithUsername(string username) + public NatsBuilder WithPassword(string password) { - return Merge(DockerResourceConfiguration, new NatsConfiguration(username: username)) - .WithCommand("--user", username); + return Merge(DockerResourceConfiguration, new NatsConfiguration(password: password)) + .WithCommand("--pass", password); } /// @@ -63,12 +63,27 @@ public override NatsContainer Build() return new NatsContainer(DockerResourceConfiguration, TestcontainersSettings.Logger); } - + /// + protected override NatsBuilder Init() + { + return base.Init() + .WithImage(NatsImage) + .WithPortBinding(NatsClientPort, true) + .WithPortBinding(NatsMonitoringPort, true) + .WithPortBinding(NatsRoutingPort, true) + .WithCommand("--http_port", NatsMonitoringPort.ToString()) + .WithCommand("--jetstream") + .WithCommand("--debug") + .WithCommand("--trace") + .WithWaitStrategy(Wait.ForUnixContainer() + .UntilMessageIsLogged("Listening for client connections on 0.0.0.0:4222")); + } + /// protected override void Validate() { base.Validate(); - + if (DockerResourceConfiguration.Password != null || DockerResourceConfiguration.Username != null) { _ = Guard.Argument(DockerResourceConfiguration.Username, nameof(DockerResourceConfiguration.Username)) @@ -79,22 +94,6 @@ protected override void Validate() } } - /// - protected override NatsBuilder Init() - { - return base.Init() - .WithImage(NatsImage) - .WithPortBinding(ClientPort, true) - .WithPortBinding(MonitoringPort, true) - .WithPortBinding(RoutingPort, true) - .WithCommand("--http_port", MonitoringPort.ToString()) // Enable monitoring endpoint. - .WithCommand("--jetstream") // Enable JetStream functionality. - .WithCommand("--debug") // Enable both debug - .WithCommand("--trace") // Enable protocol trace messages - .WithWaitStrategy(Wait.ForUnixContainer() - .UntilMessageIsLogged("Listening for client connections on 0.0.0.0:4222")); - } - /// protected override NatsBuilder Clone(IResourceConfiguration resourceConfiguration) { diff --git a/src/Testcontainers.Nats/NatsConfiguration.cs b/src/Testcontainers.Nats/NatsConfiguration.cs index 4f87c389a..4c410b773 100644 --- a/src/Testcontainers.Nats/NatsConfiguration.cs +++ b/src/Testcontainers.Nats/NatsConfiguration.cs @@ -7,8 +7,8 @@ public sealed class NatsConfiguration : ContainerConfiguration /// /// Initializes a new instance of the class. /// - /// The nats server user name. - /// The nats server password. + /// The Nats username. + /// The Nats password. public NatsConfiguration( string username = null, string password = null) @@ -55,18 +55,17 @@ public NatsConfiguration(NatsConfiguration resourceConfiguration) public NatsConfiguration(NatsConfiguration oldValue, NatsConfiguration newValue) : base(oldValue, newValue) { - // // Create an updated immutable copy of the module configuration. Username = BuildConfiguration.Combine(oldValue.Username, newValue.Username); Password = BuildConfiguration.Combine(oldValue.Password, newValue.Password); } /// - /// The nats server user name. + /// The Nats username. /// public string Username { get; } - + /// - /// The nats server password. + /// The Nats password. /// public string Password { get; } } \ No newline at end of file diff --git a/src/Testcontainers.Nats/NatsContainer.cs b/src/Testcontainers.Nats/NatsContainer.cs index d32368738..36ada8f66 100644 --- a/src/Testcontainers.Nats/NatsContainer.cs +++ b/src/Testcontainers.Nats/NatsContainer.cs @@ -4,7 +4,7 @@ namespace Testcontainers.Nats; [PublicAPI] public sealed class NatsContainer : DockerContainer { - private readonly NatsConfiguration _natsConfig; + private readonly NatsConfiguration _configuration; /// /// Initializes a new instance of the class. @@ -14,36 +14,36 @@ public sealed class NatsContainer : DockerContainer public NatsContainer(NatsConfiguration configuration, ILogger logger) : base(configuration, logger) { - _natsConfig = configuration; + _configuration = configuration; } /// - /// Gets the nats connection string + /// Gets the Nats connection string. /// - /// A nats connection string in the form: nats://hostname:mappedPort/>. /// - /// If either username or password is set, the connection string will contain the credentials. + /// If both username and password are set in the builder configuration, they will be included in the connection string. /// + /// A Nats connection string in the format: nats://hostname:port. public string GetConnectionString() { - var endpoint = new UriBuilder("nats", Hostname, GetMappedPublicPort(NatsBuilder.ClientPort)); + var endpoint = new UriBuilder("nats://", Hostname, GetMappedPublicPort(NatsBuilder.NatsClientPort)); // Both should be set, or neither, this is validated in the builder. - if (_natsConfig.Password != null && _natsConfig.Username != null) + if (_configuration.Password != null && _configuration.Username != null) { - endpoint.UserName = Uri.EscapeDataString(_natsConfig.Username); - endpoint.Password = Uri.EscapeDataString(_natsConfig.Password); + endpoint.UserName = Uri.EscapeDataString(_configuration.Username); + endpoint.Password = Uri.EscapeDataString(_configuration.Password); } return endpoint.ToString(); } /// - /// Gets the nats monitor url + /// Gets the Nats monitoring endpoint. /// - /// A url in the form: http://hostname:mappedPort/>. - public string GetMonitorUrl() + /// An HTTP address in the format: http://hostname:port. + public string GetMonitoringEndpoint() { - return new UriBuilder("http", Hostname, GetMappedPublicPort(NatsBuilder.MonitoringPort)).ToString(); + return new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(NatsBuilder.NatsMonitoringPort)).ToString(); } } \ No newline at end of file diff --git a/src/Testcontainers.Nats/Usings.cs b/src/Testcontainers.Nats/Usings.cs index bf2829a65..c6ee33b57 100644 --- a/src/Testcontainers.Nats/Usings.cs +++ b/src/Testcontainers.Nats/Usings.cs @@ -1,5 +1,6 @@ global using System; global using Docker.DotNet.Models; +global using DotNet.Testcontainers; global using DotNet.Testcontainers.Builders; global using DotNet.Testcontainers.Configurations; global using DotNet.Testcontainers.Containers; diff --git a/tests/Testcontainers.Nats.Tests/NatsContainerTest.cs b/tests/Testcontainers.Nats.Tests/NatsContainerTest.cs index f404f1d82..1ff4d4ca7 100644 --- a/tests/Testcontainers.Nats.Tests/NatsContainerTest.cs +++ b/tests/Testcontainers.Nats.Tests/NatsContainerTest.cs @@ -25,7 +25,7 @@ public async Task ContainerIsStartedWithCorrectParameters() Assert.True(client.ServerInfo.JetStreamAvailable); using var monitorClient = new HttpClient(); - monitorClient.BaseAddress = new Uri(_natsContainer.GetMonitorUrl()); + monitorClient.BaseAddress = new Uri(_natsContainer.GetMonitoringEndpoint()); using var response = await monitorClient.GetAsync("/healthz"); var s = await response.Content.ReadAsStringAsync(); @@ -80,7 +80,7 @@ public void BuilderShouldFailWithOnlyUserNameOrPassword() { var builder = new NatsBuilder().WithUsername("??&&test"); Assert.Throws(() => builder.Build()); - + builder = new NatsBuilder().WithPassword("??&&test"); Assert.Throws(() => builder.Build()); } diff --git a/tests/Testcontainers.Nats.Tests/Testcontainers.Nats.Tests.csproj b/tests/Testcontainers.Nats.Tests/Testcontainers.Nats.Tests.csproj index ad254a3d3..4958edbaa 100644 --- a/tests/Testcontainers.Nats.Tests/Testcontainers.Nats.Tests.csproj +++ b/tests/Testcontainers.Nats.Tests/Testcontainers.Nats.Tests.csproj @@ -7,9 +7,9 @@ - + From d23b2e50c7c37d64cb8141ad5f7fc5b498cd840a Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Tue, 19 Sep 2023 18:59:24 +0200 Subject: [PATCH 6/8] chore: Configure credentials not nullable, align with node module --- src/Testcontainers.MongoDb/MongoDbBuilder.cs | 2 +- src/Testcontainers.Nats/NatsBuilder.cs | 31 ++++++++++--------- src/Testcontainers.Nats/NatsContainer.cs | 14 +++------ src/Testcontainers.Nats/Usings.cs | 1 + .../NatsContainerTest.cs | 2 +- 5 files changed, 24 insertions(+), 26 deletions(-) diff --git a/src/Testcontainers.MongoDb/MongoDbBuilder.cs b/src/Testcontainers.MongoDb/MongoDbBuilder.cs index ea0808826..e34666b95 100644 --- a/src/Testcontainers.MongoDb/MongoDbBuilder.cs +++ b/src/Testcontainers.MongoDb/MongoDbBuilder.cs @@ -96,7 +96,7 @@ protected override void Validate() .NotNull(); _ = Guard.Argument(DockerResourceConfiguration, "Credentials") - .ThrowIf(argument => 1.Equals(new[] { argument.Value.Username, argument.Value.Password }.Count(string.IsNullOrEmpty)), argument => new ArgumentException(message, argument.Name)); + .ThrowIf(argument => 1.Equals(new[] { argument.Value.Username, argument.Value.Password }.Count(string.IsNullOrWhiteSpace)), argument => new ArgumentException(message, argument.Name)); } /// diff --git a/src/Testcontainers.Nats/NatsBuilder.cs b/src/Testcontainers.Nats/NatsBuilder.cs index 4a6568c12..8de672756 100644 --- a/src/Testcontainers.Nats/NatsBuilder.cs +++ b/src/Testcontainers.Nats/NatsBuilder.cs @@ -8,9 +8,9 @@ public sealed class NatsBuilder : ContainerBuilder /// Initializes a new instance of the class. @@ -69,29 +69,32 @@ protected override NatsBuilder Init() return base.Init() .WithImage(NatsImage) .WithPortBinding(NatsClientPort, true) - .WithPortBinding(NatsMonitoringPort, true) - .WithPortBinding(NatsRoutingPort, true) - .WithCommand("--http_port", NatsMonitoringPort.ToString()) + .WithPortBinding(NatsHttpManagementPort, true) + .WithPortBinding(NatsClusterRoutingPort, true) + .WithUsername(string.Empty) + .WithPassword(string.Empty) + .WithCommand("--http_port", NatsHttpManagementPort.ToString()) .WithCommand("--jetstream") .WithCommand("--debug") .WithCommand("--trace") - .WithWaitStrategy(Wait.ForUnixContainer() - .UntilMessageIsLogged("Listening for client connections on 0.0.0.0:4222")); + .WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged("Server is ready")); } /// protected override void Validate() { + const string message = "Missing username or password. Both must be specified."; + base.Validate(); - if (DockerResourceConfiguration.Password != null || DockerResourceConfiguration.Username != null) - { - _ = Guard.Argument(DockerResourceConfiguration.Username, nameof(DockerResourceConfiguration.Username)) - .NotNull(); + _ = Guard.Argument(DockerResourceConfiguration.Username, nameof(DockerResourceConfiguration.Username)) + .NotNull(); + + _ = Guard.Argument(DockerResourceConfiguration.Password, nameof(DockerResourceConfiguration.Password)) + .NotNull(); - _ = Guard.Argument(DockerResourceConfiguration.Password, nameof(DockerResourceConfiguration.Password)) - .NotNull(); - } + _ = Guard.Argument(DockerResourceConfiguration, "Credentials") + .ThrowIf(argument => 1.Equals(new[] { argument.Value.Username, argument.Value.Password }.Count(string.IsNullOrWhiteSpace)), argument => new ArgumentException(message, argument.Name)); } /// diff --git a/src/Testcontainers.Nats/NatsContainer.cs b/src/Testcontainers.Nats/NatsContainer.cs index 36ada8f66..0800fe0b5 100644 --- a/src/Testcontainers.Nats/NatsContainer.cs +++ b/src/Testcontainers.Nats/NatsContainer.cs @@ -27,14 +27,8 @@ public NatsContainer(NatsConfiguration configuration, ILogger logger) public string GetConnectionString() { var endpoint = new UriBuilder("nats://", Hostname, GetMappedPublicPort(NatsBuilder.NatsClientPort)); - - // Both should be set, or neither, this is validated in the builder. - if (_configuration.Password != null && _configuration.Username != null) - { - endpoint.UserName = Uri.EscapeDataString(_configuration.Username); - endpoint.Password = Uri.EscapeDataString(_configuration.Password); - } - + endpoint.UserName = Uri.EscapeDataString(_configuration.Username); + endpoint.Password = Uri.EscapeDataString(_configuration.Password); return endpoint.ToString(); } @@ -42,8 +36,8 @@ public string GetConnectionString() /// Gets the Nats monitoring endpoint. /// /// An HTTP address in the format: http://hostname:port. - public string GetMonitoringEndpoint() + public string GetManagementEndpoint() { - return new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(NatsBuilder.NatsMonitoringPort)).ToString(); + return new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(NatsBuilder.NatsHttpManagementPort)).ToString(); } } \ No newline at end of file diff --git a/src/Testcontainers.Nats/Usings.cs b/src/Testcontainers.Nats/Usings.cs index c6ee33b57..346e012e3 100644 --- a/src/Testcontainers.Nats/Usings.cs +++ b/src/Testcontainers.Nats/Usings.cs @@ -1,4 +1,5 @@ global using System; +global using System.Linq; global using Docker.DotNet.Models; global using DotNet.Testcontainers; global using DotNet.Testcontainers.Builders; diff --git a/tests/Testcontainers.Nats.Tests/NatsContainerTest.cs b/tests/Testcontainers.Nats.Tests/NatsContainerTest.cs index 1ff4d4ca7..376abed62 100644 --- a/tests/Testcontainers.Nats.Tests/NatsContainerTest.cs +++ b/tests/Testcontainers.Nats.Tests/NatsContainerTest.cs @@ -25,7 +25,7 @@ public async Task ContainerIsStartedWithCorrectParameters() Assert.True(client.ServerInfo.JetStreamAvailable); using var monitorClient = new HttpClient(); - monitorClient.BaseAddress = new Uri(_natsContainer.GetMonitoringEndpoint()); + monitorClient.BaseAddress = new Uri(_natsContainer.GetManagementEndpoint()); using var response = await monitorClient.GetAsync("/healthz"); var s = await response.Content.ReadAsStringAsync(); From 71a700bbfc05d920751da5b310f809c43ede3354 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Tue, 19 Sep 2023 19:49:20 +0200 Subject: [PATCH 7/8] chore: Test Nats configuration w/wo auth config --- .../NatsContainerTest.cs | 106 +++++++++--------- tests/Testcontainers.Nats.Tests/Usings.cs | 2 + 2 files changed, 58 insertions(+), 50 deletions(-) diff --git a/tests/Testcontainers.Nats.Tests/NatsContainerTest.cs b/tests/Testcontainers.Nats.Tests/NatsContainerTest.cs index 376abed62..0251f6b2a 100644 --- a/tests/Testcontainers.Nats.Tests/NatsContainerTest.cs +++ b/tests/Testcontainers.Nats.Tests/NatsContainerTest.cs @@ -1,8 +1,13 @@ namespace Testcontainers.Nats; -public sealed class NatsContainerTest : IAsyncLifetime +public abstract class NatsContainerTest : IAsyncLifetime { - private readonly NatsContainer _natsContainer = new NatsBuilder().Build(); + private readonly NatsContainer _natsContainer; + + private NatsContainerTest(NatsContainer natsContainer) + { + _natsContainer = natsContainer; + } public Task InitializeAsync() { @@ -16,72 +21,73 @@ public Task DisposeAsync() [Fact] [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] - public async Task ContainerIsStartedWithCorrectParameters() - { - using var client = new ConnectionFactory() - .CreateConnection(_natsContainer.GetConnectionString()); - - Assert.Equal(ConnState.CONNECTED, client.State); - Assert.True(client.ServerInfo.JetStreamAvailable); - - using var monitorClient = new HttpClient(); - monitorClient.BaseAddress = new Uri(_natsContainer.GetManagementEndpoint()); - - using var response = await monitorClient.GetAsync("/healthz"); - var s = await response.Content.ReadAsStringAsync(); - Assert.True(response.IsSuccessStatusCode); - } - - [Fact] - [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] - public async Task PubSubSendsAndReturnsMessages() + public async Task HealthcheckReturnsHttpStatusCodeOk() { - using var client = new ConnectionFactory() - .CreateConnection(_natsContainer.GetConnectionString()); - - using ISyncSubscription subSync = client.SubscribeSync("greet.pam"); - client.Publish("greet.pam", Encoding.UTF8.GetBytes("hello pam 1")); + // Given + using var client = new HttpClient(); + client.BaseAddress = new Uri(_natsContainer.GetManagementEndpoint()); - var msg = subSync.NextMessage(1000); - var text = Encoding.UTF8.GetString(msg.Data); + // When + using var response = await client.GetAsync("/healthz") + .ConfigureAwait(false); + var jsonStatusString = await response.Content.ReadAsStringAsync() + .ConfigureAwait(false); - Assert.Equal("hello pam 1", text); - await client.DrainAsync(); + // Then + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("{\"status\":\"ok\"}", jsonStatusString); } [Fact] [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] - public async Task BuilderShouldBuildWithUserNameAndPassword() + public void GetStringReturnsPublishString() { - var encodedPassword = Uri.EscapeDataString("??&&testpass"); - var encodedUsername = Uri.EscapeDataString("??&&test"); + // Given + var subject = Guid.NewGuid().ToString("D"); - var builder = new NatsBuilder() - .WithUsername("??&&test") - .WithPassword("??&&testpass"); - await using var container = builder.Build(); + var message = Guid.NewGuid().ToString("D"); - await container.StartAsync(); + using var client = new ConnectionFactory().CreateConnection(_natsContainer.GetConnectionString()); - var uri = new Uri(container.GetConnectionString()); + using var subscription = client.SubscribeSync(subject); - Assert.Equal($"{encodedUsername}:{encodedPassword}", uri.UserInfo); + // When + client.Publish(subject, Encoding.Default.GetBytes(message)); - using var client = new ConnectionFactory() - .CreateConnection(_natsContainer.GetConnectionString()); + var actualMessage = Encoding.Default.GetString(subscription.NextMessage().Data); - Assert.Equal(ConnState.CONNECTED, client.State); + // Then + Assert.Equal(message, actualMessage); } - [Fact] - [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] - public void BuilderShouldFailWithOnlyUserNameOrPassword() + [UsedImplicitly] + public sealed class NatsDefaultConfiguration : NatsContainerTest { - var builder = new NatsBuilder().WithUsername("??&&test"); - Assert.Throws(() => builder.Build()); + public NatsDefaultConfiguration() + : base(new NatsBuilder().Build()) + { + } + } - builder = new NatsBuilder().WithPassword("??&&test"); - Assert.Throws(() => builder.Build()); + [UsedImplicitly] + public sealed class NatsAuthConfiguration : NatsContainerTest + { + public NatsAuthConfiguration() + : base(new NatsBuilder().WithUsername("%username!").WithPassword("?password&").Build()) + { + } + + [Fact] + public void ThrowsExceptionIfUsernameIsMissing() + { + Assert.Throws(() => new NatsBuilder().WithPassword("password").Build()); + } + + [Fact] + public void ThrowsExceptionIfPasswordIsMissing() + { + Assert.Throws(() => new NatsBuilder().WithUsername("username").Build()); + } } } \ No newline at end of file diff --git a/tests/Testcontainers.Nats.Tests/Usings.cs b/tests/Testcontainers.Nats.Tests/Usings.cs index 9cf5647c7..49a70954e 100644 --- a/tests/Testcontainers.Nats.Tests/Usings.cs +++ b/tests/Testcontainers.Nats.Tests/Usings.cs @@ -1,7 +1,9 @@ global using System; +global using System.Net; global using System.Net.Http; global using System.Text; global using System.Threading.Tasks; global using DotNet.Testcontainers.Commons; +global using JetBrains.Annotations; global using NATS.Client; global using Xunit; \ No newline at end of file From d04863d50068fad7c744e20dbe3a71cf06ab2395 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Tue, 19 Sep 2023 19:51:15 +0200 Subject: [PATCH 8/8] chore: Update module docs (add nats) --- docs/modules/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/modules/index.md b/docs/modules/index.md index 187d76268..9110942b7 100644 --- a/docs/modules/index.md +++ b/docs/modules/index.md @@ -40,6 +40,7 @@ await moduleNameContainer.StartAsync(); | MinIO | `minio/minio:RELEASE.2023-01-31T02-24-19Z` | [NuGet](https://www.nuget.org/packages/Testcontainers.Minio) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Minio) | | MongoDB | `mongo:6.0` | [NuGet](https://www.nuget.org/packages/Testcontainers.MongoDb) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.MongoDb) | | MySQL | `mysql:8.0` | [NuGet](https://www.nuget.org/packages/Testcontainers.MySql) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.MySql) | +| NATS | `nats:2.9` | [NuGet](https://www.nuget.org/packages/Testcontainers.Nats) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Nats) | | Neo4j | `neo4j:5.4` | [NuGet](https://www.nuget.org/packages/Testcontainers.Neo4j) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Neo4j) | | Oracle | `gvenzl/oracle-xe:21.3.0-slim-faststart` | [NuGet](https://www.nuget.org/packages/Testcontainers.Oracle) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Oracle) | | PostgreSQL | `postgres:15.1` | [NuGet](https://www.nuget.org/packages/Testcontainers.PostgreSql) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.PostgreSql) |