Skip to content

Commit 0faae49

Browse files
committed
TestContainers.Mosquitto
1 parent b0b0cbc commit 0faae49

19 files changed

+603
-23
lines changed

.github/workflows/cicd.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ jobs:
6969
{ name: "Testcontainers.Milvus", runs-on: "ubuntu-22.04" },
7070
{ name: "Testcontainers.Minio", runs-on: "ubuntu-22.04" },
7171
{ name: "Testcontainers.MongoDb", runs-on: "ubuntu-22.04" },
72+
{ name: "TestContainers.Mosquitto", runs-on: "ubuntu-22.04" },
7273
{ name: "Testcontainers.MsSql", runs-on: "ubuntu-22.04" },
7374
{ name: "Testcontainers.MySql", runs-on: "ubuntu-22.04" },
7475
{ name: "Testcontainers.Nats", runs-on: "ubuntu-22.04" },

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
<PackageVersion Include="MongoDB.Driver" Version="3.2.0"/>
7373
<PackageVersion Include="MyCouch" Version="7.6.0"/>
7474
<PackageVersion Include="MySqlConnector" Version="2.2.5"/>
75+
<PackageVersion Include="MQTTnet" Version="5.0.1.1416"/>
7576
<PackageVersion Include="NATS.Client" Version="1.0.8"/>
7677
<PackageVersion Include="Neo4j.Driver" Version="5.5.0"/>
7778
<PackageVersion Include="Net.IBM.Data.Db2-lnx" Version="9.0.0.100"/>

Testcontainers.dic

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ lipsum
1717
ltsc
1818
memopt
1919
mongosh
20+
mosquitto
2021
mycounter
2122
mydatabase
2223
myregistry

Testcontainers.lutconfig

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<LUTConfig Version="1.0">
2+
<Repository />
3+
<ParallelBuilds>true</ParallelBuilds>
4+
<ParallelTestRuns>true</ParallelTestRuns>
5+
<TestCaseTimeout>180000</TestCaseTimeout>
6+
</LUTConfig>

Testcontainers.sln

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Xunit.Tests"
257257
EndProject
258258
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.XunitV3.Tests", "tests\Testcontainers.XunitV3.Tests\Testcontainers.XunitV3.Tests.csproj", "{B2E8B7FB-7D1E-4DD3-A25E-34DE4386B1EB}"
259259
EndProject
260+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestContainers.Mosquitto", "src\TestContainers.Mosquitto\TestContainers.Mosquitto.csproj", "{3A64B210-645C-4229-B089-5BB2AAFCF535}"
261+
EndProject
262+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestContainers.Mosquitto.Tests", "tests\TestContainers.Mosquitto.Tests\TestContainers.Mosquitto.Tests.csproj", "{6314B57A-EE0C-4C3B-A9A9-64D68A47312A}"
263+
EndProject
260264
Global
261265
GlobalSection(SolutionConfigurationPlatforms) = preSolution
262266
Debug|Any CPU = Debug|Any CPU
@@ -751,6 +755,14 @@ Global
751755
{B2E8B7FB-7D1E-4DD3-A25E-34DE4386B1EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
752756
{B2E8B7FB-7D1E-4DD3-A25E-34DE4386B1EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
753757
{B2E8B7FB-7D1E-4DD3-A25E-34DE4386B1EB}.Release|Any CPU.Build.0 = Release|Any CPU
758+
{3A64B210-645C-4229-B089-5BB2AAFCF535}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
759+
{3A64B210-645C-4229-B089-5BB2AAFCF535}.Debug|Any CPU.Build.0 = Debug|Any CPU
760+
{3A64B210-645C-4229-B089-5BB2AAFCF535}.Release|Any CPU.ActiveCfg = Release|Any CPU
761+
{3A64B210-645C-4229-B089-5BB2AAFCF535}.Release|Any CPU.Build.0 = Release|Any CPU
762+
{6314B57A-EE0C-4C3B-A9A9-64D68A47312A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
763+
{6314B57A-EE0C-4C3B-A9A9-64D68A47312A}.Debug|Any CPU.Build.0 = Debug|Any CPU
764+
{6314B57A-EE0C-4C3B-A9A9-64D68A47312A}.Release|Any CPU.ActiveCfg = Release|Any CPU
765+
{6314B57A-EE0C-4C3B-A9A9-64D68A47312A}.Release|Any CPU.Build.0 = Release|Any CPU
754766
EndGlobalSection
755767
GlobalSection(SolutionProperties) = preSolution
756768
HideSolutionNode = FALSE
@@ -878,5 +890,10 @@ Global
878890
{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
879891
{E901DF14-6F05-4FC2-825A-3055FAD33561} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
880892
{B2E8B7FB-7D1E-4DD3-A25E-34DE4386B1EB} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
893+
{3A64B210-645C-4229-B089-5BB2AAFCF535} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
894+
{6314B57A-EE0C-4C3B-A9A9-64D68A47312A} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
895+
EndGlobalSection
896+
GlobalSection(ExtensibilityGlobals) = postSolution
897+
SolutionGuid = {06AF4E8B-EB32-4C33-B1DD-923580E132D5}
881898
EndGlobalSection
882899
EndGlobal

docs/modules/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ await moduleNameContainer.StartAsync();
5454
| Milvus | `milvusdb/milvus:v2.3.10` | [NuGet](https://www.nuget.org/packages/Testcontainers.Milvus) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Milvus) |
5555
| 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) |
5656
| MongoDB | `mongo:6.0` | [NuGet](https://www.nuget.org/packages/Testcontainers.MongoDb) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.MongoDb) |
57+
| Mosquitto | `eclipse-mosquitto:2.0` | [NuGet](https://www.nuget.org/packages/Testcontainers.Mosquitto) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Mosquitto) |
5758
| MySQL | `mysql:8.0` | [NuGet](https://www.nuget.org/packages/Testcontainers.MySql) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.MySql) |
5859
| NATS | `nats:2.9` | [NuGet](https://www.nuget.org/packages/Testcontainers.Nats) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Nats) |
5960
| Neo4j | `neo4j:5.4` | [NuGet](https://www.nuget.org/packages/Testcontainers.Neo4j) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Neo4j) |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
root = true
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
namespace TestContainers.Mosquitto;
2+
3+
/// <inheritdoc cref="ContainerBuilder{TBuilderEntity, TContainerEntity, TConfigurationEntity}" />
4+
[PublicAPI]
5+
public class MosquittoBuilder : ContainerBuilder<MosquittoBuilder, MosquittoContainer, MosquittoConfiguration>
6+
{
7+
public const string MosquittoImage = "eclipse-mosquitto:2.0";
8+
9+
public const int TcpPort = 1883;
10+
public const int TlsPort = 8883;
11+
public const int WsPort = 80;
12+
public const int WssPort = 443;
13+
public const string CertificateFilePath = "/mosquitto/certs/server.pem";
14+
public const string CertificateKeyFilePath = "/mosquitto/certs/server-key.pem";
15+
16+
/// <summary>
17+
/// Initializes a new instance of the <see cref="MosquittoBuilder" /> class.
18+
/// </summary>
19+
public MosquittoBuilder()
20+
: this(new MosquittoConfiguration())
21+
{
22+
DockerResourceConfiguration = Init().DockerResourceConfiguration;
23+
}
24+
25+
public MosquittoBuilder(MosquittoConfiguration resourceConfiguration)
26+
: base(resourceConfiguration)
27+
{
28+
DockerResourceConfiguration = resourceConfiguration;
29+
}
30+
31+
/// <inheritdoc />
32+
protected override MosquittoConfiguration DockerResourceConfiguration { get; }
33+
34+
/// <inheritdoc />
35+
public override MosquittoContainer Build()
36+
{
37+
Validate();
38+
39+
var sb = new StringBuilder();
40+
sb.AppendUnixLine("per_listener_settings true");
41+
42+
sb.AppendUnixLine();
43+
sb.AppendUnixLine("# MQTT listener");
44+
sb.AppendUnixLine($"listener {TcpPort}");
45+
sb.AppendUnixLine("protocol mqtt");
46+
sb.AppendUnixLine("allow_anonymous true");
47+
48+
sb.AppendUnixLine();
49+
sb.AppendUnixLine("# WebSocket listener");
50+
sb.AppendUnixLine($"listener {WsPort}");
51+
sb.AppendUnixLine("protocol websockets");
52+
sb.AppendUnixLine("allow_anonymous true");
53+
54+
if (DockerResourceConfiguration.HasCertificate)
55+
{
56+
sb.AppendUnixLine();
57+
sb.AppendUnixLine("# MQTT listener (encrypted)");
58+
sb.AppendUnixLine($"listener {TlsPort}");
59+
sb.AppendUnixLine("protocol mqtt");
60+
sb.AppendUnixLine("allow_anonymous true");
61+
sb.AppendUnixLine($"certfile {CertificateFilePath}");
62+
sb.AppendUnixLine($"keyfile {CertificateKeyFilePath}");
63+
64+
sb.AppendUnixLine();
65+
sb.AppendUnixLine("# WebSocket listener (encrypted)");
66+
sb.AppendUnixLine($"listener {WssPort}");
67+
sb.AppendUnixLine("protocol websockets");
68+
sb.AppendUnixLine("allow_anonymous true");
69+
sb.AppendUnixLine($"certfile {CertificateFilePath}");
70+
sb.AppendUnixLine($"keyfile {CertificateKeyFilePath}");
71+
}
72+
73+
var config = Clone(DockerResourceConfiguration)
74+
.WithResourceMapping(Encoding.UTF8.GetBytes(sb.ToString()), "/mosquitto/config/mosquitto.conf");
75+
76+
return new MosquittoContainer(config.DockerResourceConfiguration);
77+
}
78+
79+
80+
public MosquittoBuilder WithCertificate(string certificate, string certificateKey)
81+
{
82+
return Merge(DockerResourceConfiguration, new MosquittoConfiguration(certificate: certificate, certificateKey: certificateKey))
83+
.WithPortBinding(TlsPort, true)
84+
.WithPortBinding(WssPort, true)
85+
.WithResourceMapping(Encoding.UTF8.GetBytes(certificate), CertificateFilePath)
86+
.WithResourceMapping(Encoding.UTF8.GetBytes(certificateKey), CertificateKeyFilePath);
87+
}
88+
89+
/// <inheritdoc />
90+
protected override MosquittoBuilder Init()
91+
{
92+
var builder = base.Init()
93+
.WithImage(MosquittoImage)
94+
.WithPortBinding(TcpPort, true)
95+
.WithPortBinding(WsPort, true)
96+
.WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged(@"mosquitto.*running"));
97+
98+
return builder;
99+
}
100+
101+
/// <inheritdoc />
102+
protected override void Validate()
103+
{
104+
base.Validate();
105+
106+
_ = Guard.Argument(DockerResourceConfiguration, "Certificate")
107+
.ThrowIf(argument => 1.Equals(new[] { argument.Value.Certificate, argument.Value.CertificateKey }.Count(string.IsNullOrWhiteSpace)), argument => new ArgumentException($"Both {nameof(argument.Value.Certificate)} and {nameof(argument.Value.CertificateKey)} must be supplied if one is.", argument.Name));
108+
}
109+
110+
/// <inheritdoc />
111+
protected override MosquittoBuilder Clone(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)
112+
{
113+
return Merge(DockerResourceConfiguration, new MosquittoConfiguration(resourceConfiguration));
114+
}
115+
116+
/// <inheritdoc />
117+
protected override MosquittoBuilder Clone(IContainerConfiguration resourceConfiguration)
118+
{
119+
return Merge(DockerResourceConfiguration, new MosquittoConfiguration(resourceConfiguration));
120+
}
121+
122+
/// <inheritdoc />
123+
protected override MosquittoBuilder Merge(MosquittoConfiguration oldValue, MosquittoConfiguration newValue)
124+
{
125+
return new MosquittoBuilder(new MosquittoConfiguration(oldValue, newValue));
126+
}
127+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
namespace TestContainers.Mosquitto;
2+
3+
/// <inheritdoc cref="ContainerConfiguration" />
4+
[PublicAPI]
5+
public sealed class MosquittoConfiguration : ContainerConfiguration
6+
{
7+
/// <summary>
8+
/// Initializes a new instance of the <see cref="MosquittoConfiguration" /> class.
9+
/// </summary>
10+
public MosquittoConfiguration(
11+
string certificate = null,
12+
string certificateKey = null)
13+
{
14+
Certificate = certificate;
15+
CertificateKey = certificateKey;
16+
}
17+
18+
/// <summary>
19+
/// Initializes a new instance of the <see cref="MosquittoConfiguration" /> class.
20+
/// </summary>
21+
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
22+
public MosquittoConfiguration(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)
23+
: base(resourceConfiguration)
24+
{
25+
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
26+
}
27+
28+
/// <summary>
29+
/// Initializes a new instance of the <see cref="MosquittoConfiguration" /> class.
30+
/// </summary>
31+
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
32+
public MosquittoConfiguration(IContainerConfiguration resourceConfiguration)
33+
: base(resourceConfiguration)
34+
{
35+
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
36+
}
37+
38+
/// <summary>
39+
/// Initializes a new instance of the <see cref="MosquittoConfiguration" /> class.
40+
/// </summary>
41+
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
42+
public MosquittoConfiguration(MosquittoConfiguration resourceConfiguration)
43+
: this(new MosquittoConfiguration(), resourceConfiguration)
44+
{
45+
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
46+
}
47+
48+
/// <summary>
49+
/// Initializes a new instance of the <see cref="MosquittoConfiguration" /> class.
50+
/// </summary>
51+
/// <param name="oldValue">The old Docker resource configuration.</param>
52+
/// <param name="newValue">The new Docker resource configuration.</param>
53+
public MosquittoConfiguration(MosquittoConfiguration oldValue, MosquittoConfiguration newValue)
54+
: base(oldValue, newValue)
55+
{
56+
Certificate = BuildConfiguration.Combine(oldValue.Certificate, newValue.Certificate);
57+
CertificateKey = BuildConfiguration.Combine(oldValue.CertificateKey, newValue.CertificateKey);
58+
59+
}
60+
61+
/// <summary>
62+
/// Gets the public certificate in PEM format.
63+
/// </summary>
64+
public string Certificate { get; }
65+
66+
/// <summary>
67+
/// Gets the private key associated with the certificate in PEM format.
68+
/// </summary>
69+
public string CertificateKey { get; }
70+
71+
/// <summary>
72+
/// Gets a value indicating whether both the certificate and the certificate key are provided.
73+
/// </summary>
74+
public bool HasCertificate => !string.IsNullOrWhiteSpace(Certificate) && !string.IsNullOrWhiteSpace(CertificateKey);
75+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
namespace TestContainers.Mosquitto;
2+
3+
/// <inheritdoc cref="DockerContainer" />
4+
[PublicAPI]
5+
public sealed class MosquittoContainer : DockerContainer
6+
{
7+
private readonly bool _isSecure;
8+
9+
/// <summary>
10+
/// Initializes a new instance of the <see cref="MosquittoContainer" /> class.
11+
/// </summary>
12+
/// <param name="configuration">The container configuration.</param>
13+
public MosquittoContainer(MosquittoConfiguration configuration)
14+
: base(configuration)
15+
{
16+
_isSecure = configuration.HasCertificate;
17+
}
18+
19+
/// <summary>
20+
/// Gets the MQTT endpoint.
21+
/// </summary>
22+
/// <returns>A TCP address in the format: <c>tcp://hostname:port</c>.</returns>
23+
public string GetEndpoint()
24+
{
25+
return new UriBuilder(Uri.UriSchemeNetTcp, Hostname, GetPort()).ToString();
26+
}
27+
28+
/// <summary>
29+
/// Gets the MQTT endpoint port.
30+
/// </summary>
31+
/// <returns>Exposed port for insecure MQQT connections.</returns>
32+
public ushort GetPort()
33+
{
34+
return GetMappedPublicPort(MosquittoBuilder.TcpPort);
35+
}
36+
37+
/// <summary>
38+
/// Gets the secure MQTT endpoint.
39+
/// </summary>
40+
/// <returns>A TCP address in the format: <c>tcp://hostname:port</c>.</returns>
41+
public string GetSecureEndpoint()
42+
{
43+
ThrowIfNotSecure();
44+
return new UriBuilder(Uri.UriSchemeNetTcp, Hostname, GetMappedPublicPort(MosquittoBuilder.TlsPort)).ToString();
45+
}
46+
47+
/// <summary>
48+
/// Gets the secure MQTT endpoint port.
49+
/// </summary>
50+
/// <returns>Exposed port for secure MQTT connections.</returns>
51+
public ushort GetSecurePort()
52+
{
53+
return GetMappedPublicPort(MosquittoBuilder.TlsPort);
54+
}
55+
56+
/// <summary>
57+
/// Gets the WebSocket endpoint.
58+
/// </summary>
59+
/// <returns>A WS address in the format: <c>ws://hostname:port</c>.</returns>
60+
public string GetWsEndpoint()
61+
{
62+
return new UriBuilder("ws", Hostname, GetMappedPublicPort(MosquittoBuilder.WsPort)).ToString();
63+
}
64+
65+
/// <summary>
66+
/// Gets the secure WebSocket endpoint.
67+
/// </summary>
68+
/// <returns>A WS address in the format: <c>ws://hostname:port</c>.</returns>
69+
public string GetWssEndpoint()
70+
{
71+
ThrowIfNotSecure();
72+
return new UriBuilder("wss", Hostname, GetMappedPublicPort(MosquittoBuilder.WssPort)).ToString();
73+
}
74+
75+
private void ThrowIfNotSecure()
76+
{
77+
if (_isSecure)
78+
{
79+
return;
80+
}
81+
82+
throw new InvalidOperationException("The container was not configured with TLS/SSL support.");
83+
}
84+
}

0 commit comments

Comments
 (0)