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