Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add NATS module #1003

Merged
merged 10 commits into from
Sep 19, 2023
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);
HofmeisterAn marked this conversation as resolved.
Show resolved Hide resolved
}

/// <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);
niklasfp marked this conversation as resolved.
Show resolved Hide resolved
}

/// <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));
}
HofmeisterAn marked this conversation as resolved.
Show resolved Hide resolved

/// <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
HofmeisterAn marked this conversation as resolved.
Show resolved Hide resolved
.WithWaitStrategy(Wait.ForUnixContainer()
.UntilMessageIsLogged("Listening for client connections on 0.0.0.0:4222"));
HofmeisterAn marked this conversation as resolved.
Show resolved Hide resolved
}

/// <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();
HofmeisterAn marked this conversation as resolved.
Show resolved Hide resolved
}
}
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;