Skip to content

Commit

Permalink
Add Nats Server Container
Browse files Browse the repository at this point in the history
  • Loading branch information
niklasfp committed Sep 17, 2023
1 parent 5317240 commit 0b7d624
Show file tree
Hide file tree
Showing 11 changed files with 360 additions and 0 deletions.
14 changes: 14 additions & 0 deletions Testcontainers.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions src/Testcontainers.Nats/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
root = true
108 changes: 108 additions & 0 deletions src/Testcontainers.Nats/NatsBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
namespace Testcontainers.Nats;

/// <inheritdoc cref="ContainerBuilder{TBuilderEntity, TContainerEntity, TConfigurationEntity}" />
[PublicAPI]
public sealed class NatsBuilder : ContainerBuilder<NatsBuilder, NatsContainer, NatsConfiguration>
{
public const string NatsImage = "nats:2.9";

public const ushort ClientPort = 4222;
public const ushort RoutingPort = 6222;
public const ushort MonitoringPort = 8222;

/// <summary>
/// Initializes a new instance of the <see cref="NatsBuilder" /> class.
/// </summary>
public NatsBuilder()
: this(new NatsConfiguration())
{
DockerResourceConfiguration = Init().DockerResourceConfiguration;
}

/// <summary>
/// Initializes a new instance of the <see cref="NatsBuilder" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
private NatsBuilder(NatsConfiguration resourceConfiguration)
: base(resourceConfiguration)
{
DockerResourceConfiguration = resourceConfiguration;
}

/// <inheritdoc />
protected override NatsConfiguration DockerResourceConfiguration { get; }

/// <summary>
/// Sets the Nats Server password.
/// </summary>
/// <param name="password">The Nats Server password.</param>
/// <returns>A configured instance of <see cref="NatsBuilder" />.</returns>
public NatsBuilder WithPassword(string password)
{
return Merge(DockerResourceConfiguration, new NatsConfiguration(password: password))
.WithCommand("-pass", password);
}

/// <summary>
/// Sets the Nats Server username.
/// </summary>
/// <param name="username">The Nats Server username.</param>
/// <returns>A configured instance of <see cref="NatsBuilder" />.</returns>
public NatsBuilder WithUsername(string username)
{
return Merge(DockerResourceConfiguration, new NatsConfiguration(username: username))
.WithCommand("-user", username);
}

/// <summary>
/// Sets the Nats config.
/// </summary>
/// <param name="config">The Nats config.</param>
/// <returns>A configured instance of <see cref="NatsBuilder" />.</returns>
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));
}

/// <inheritdoc />
public override NatsContainer Build()
{
Validate();
return new NatsContainer(DockerResourceConfiguration, TestcontainersSettings.Logger);
}

/// <inheritdoc />
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"));
}

/// <inheritdoc />
protected override NatsBuilder Clone(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)
{
return Merge(DockerResourceConfiguration, new NatsConfiguration(resourceConfiguration));
}

/// <inheritdoc />
protected override NatsBuilder Clone(IContainerConfiguration resourceConfiguration)
{
return Merge(DockerResourceConfiguration, new NatsConfiguration(resourceConfiguration));
}

/// <inheritdoc />
protected override NatsBuilder Merge(NatsConfiguration oldValue, NatsConfiguration newValue)
{
return new NatsBuilder(new NatsConfiguration(oldValue, newValue));
}
}
72 changes: 72 additions & 0 deletions src/Testcontainers.Nats/NatsConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
namespace Testcontainers.Nats;

/// <inheritdoc cref="ContainerConfiguration" />
[PublicAPI]
public sealed class NatsConfiguration : ContainerConfiguration
{
/// <summary>
/// Initializes a new instance of the <see cref="NatsConfiguration" /> class.
/// </summary>
/// <param name="username">The nats server user name.</param>
/// <param name="password">The nats server password.</param>
public NatsConfiguration(
string username = null,
string password = null)
{
Username = username;
Password = password;
}

/// <summary>
/// Initializes a new instance of the <see cref="NatsConfiguration" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
public NatsConfiguration(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)
: base(resourceConfiguration)
{
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
}

/// <summary>
/// Initializes a new instance of the <see cref="NatsConfiguration" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
public NatsConfiguration(IContainerConfiguration resourceConfiguration)
: base(resourceConfiguration)
{
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
}

/// <summary>
/// Initializes a new instance of the <see cref="NatsConfiguration" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
public NatsConfiguration(NatsConfiguration resourceConfiguration)
: this(new NatsConfiguration(), resourceConfiguration)
{
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
}

/// <summary>
/// Initializes a new instance of the <see cref="NatsConfiguration" /> class.
/// </summary>
/// <param name="oldValue">The old Docker resource configuration.</param>
/// <param name="newValue">The new Docker resource configuration.</param>
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);
}

/// <summary>
/// The nats server user name.
/// </summary>
public string Username { get; }

/// <summary>
/// The nats server password.
/// </summary>
public string Password { get; }
}
44 changes: 44 additions & 0 deletions src/Testcontainers.Nats/NatsContainer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
namespace Testcontainers.Nats;

/// <inheritdoc cref="DockerContainer" />
[PublicAPI]
public sealed class NatsContainer : DockerContainer
{
private readonly NatsConfiguration _natsConfig;

/// <summary>
/// Initializes a new instance of the <see cref="NatsContainer" /> class.
/// </summary>
/// <param name="configuration">The container configuration.</param>
/// <param name="logger">The logger.</param>
public NatsContainer(NatsConfiguration configuration, ILogger logger)
: base(configuration, logger)
{
_natsConfig = configuration;
}

/// <summary>
/// Gets the nats connection string
/// </summary>
/// <returns>A nats connection string in the form: nats://hostname:mappedPort/>.</returns>
/// <remarks>
/// If either username or password is set, the connection string will contain the credentials.
/// </remarks>
public string GetConnectionString()
{
return new UriBuilder("nats", Hostname, GetMappedPublicPort(NatsBuilder.ClientPort))
{
UserName = _natsConfig.Username,
Password = _natsConfig.Password,
}.ToString();
}

/// <summary>
/// Gets the nats monitor url
/// </summary>
/// <returns>A url in the form: http://hostname:mappedPort/>.</returns>
public string GetMonitorUrl()
{
return new UriBuilder("http", Hostname, GetMappedPublicPort(NatsBuilder.MonitoringPort)).ToString();
}
}
13 changes: 13 additions & 0 deletions src/Testcontainers.Nats/Testcontainers.Nats.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All"/>
<PackageReference Include="JetBrains.Annotations" Version="2022.3.1" PrivateAssets="All"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(SolutionDir)src/Testcontainers/Testcontainers.csproj"/>
</ItemGroup>
</Project>
7 changes: 7 additions & 0 deletions src/Testcontainers.Nats/Usings.cs
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions tests/Testcontainers.Nats.Tests/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
root = true
75 changes: 75 additions & 0 deletions tests/Testcontainers.Nats.Tests/NatsContainerTest.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
18 changes: 18 additions & 0 deletions tests/Testcontainers.Nats.Tests/Testcontainers.Nats.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0</TargetFrameworks>
<IsPackable>false</IsPackable>
<IsPublishable>false</IsPublishable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2"/>
<PackageReference Include="coverlet.collector" Version="6.0.0"/>
<PackageReference Include="NATS.Client" Version="1.0.8" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.0"/>
<PackageReference Include="xunit" Version="2.5.0"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(SolutionDir)src/Testcontainers.Nats/Testcontainers.Nats.csproj"/>
<ProjectReference Include="$(SolutionDir)tests/Testcontainers.Commons/Testcontainers.Commons.csproj"/>
</ItemGroup>
</Project>
7 changes: 7 additions & 0 deletions tests/Testcontainers.Nats.Tests/Usings.cs
Original file line number Diff line number Diff line change
@@ -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;

0 comments on commit 0b7d624

Please sign in to comment.