diff --git a/Directory.Packages.props b/Directory.Packages.props index 9fb067dcf..2e5189342 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -71,6 +71,7 @@ + diff --git a/Testcontainers.dic b/Testcontainers.dic index df1f16915..a8eab215e 100644 --- a/Testcontainers.dic +++ b/Testcontainers.dic @@ -17,6 +17,7 @@ lipsum ltsc memopt mongosh +mosquitto mycounter mydatabase myregistry diff --git a/Testcontainers.sln b/Testcontainers.sln index 6a10627b8..98dabc193 100644 --- a/Testcontainers.sln +++ b/Testcontainers.sln @@ -88,6 +88,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Minio", "src EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.MongoDb", "src\Testcontainers.MongoDb\Testcontainers.MongoDb.csproj", "{2613F146-6C66-4059-9D37-D48BA6B61515}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Mosquitto", "src\Testcontainers.Mosquitto\Testcontainers.Mosquitto.csproj", "{3A64B210-645C-4229-B089-5BB2AAFCF535}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.MsSql", "src\Testcontainers.MsSql\Testcontainers.MsSql.csproj", "{121FB123-40D9-44D4-9AB7-AD57ED34F466}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.MySql", "src\Testcontainers.MySql\Testcontainers.MySql.csproj", "{9FDCFAEA-AE42-4C69-89EF-F1FF75E88CCC}" @@ -210,6 +212,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Minio.Tests" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.MongoDb.Tests", "tests\Testcontainers.MongoDb.Tests\Testcontainers.MongoDb.Tests.csproj", "{82A7E7B8-3187-4CAE-845B-0BF43409B38A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Mosquitto.Tests", "tests\Testcontainers.Mosquitto.Tests\Testcontainers.Mosquitto.Tests.csproj", "{6314B57A-EE0C-4C3B-A9A9-64D68A47312A}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.MsSql.Tests", "tests\Testcontainers.MsSql.Tests\Testcontainers.MsSql.Tests.csproj", "{25DBED78-99F4-433F-BBF5-1B4E9DEAE437}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.MySql.Tests", "tests\Testcontainers.MySql.Tests\Testcontainers.MySql.Tests.csproj", "{E42DA1CE-698F-4E45-8D1F-5D5895893840}" @@ -418,6 +422,10 @@ Global {2613F146-6C66-4059-9D37-D48BA6B61515}.Debug|Any CPU.Build.0 = Debug|Any CPU {2613F146-6C66-4059-9D37-D48BA6B61515}.Release|Any CPU.ActiveCfg = Release|Any CPU {2613F146-6C66-4059-9D37-D48BA6B61515}.Release|Any CPU.Build.0 = Release|Any CPU + {3A64B210-645C-4229-B089-5BB2AAFCF535}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3A64B210-645C-4229-B089-5BB2AAFCF535}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3A64B210-645C-4229-B089-5BB2AAFCF535}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3A64B210-645C-4229-B089-5BB2AAFCF535}.Release|Any CPU.Build.0 = Release|Any CPU {121FB123-40D9-44D4-9AB7-AD57ED34F466}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {121FB123-40D9-44D4-9AB7-AD57ED34F466}.Debug|Any CPU.Build.0 = Debug|Any CPU {121FB123-40D9-44D4-9AB7-AD57ED34F466}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -662,6 +670,10 @@ Global {82A7E7B8-3187-4CAE-845B-0BF43409B38A}.Debug|Any CPU.Build.0 = Debug|Any CPU {82A7E7B8-3187-4CAE-845B-0BF43409B38A}.Release|Any CPU.ActiveCfg = Release|Any CPU {82A7E7B8-3187-4CAE-845B-0BF43409B38A}.Release|Any CPU.Build.0 = Release|Any CPU + {6314B57A-EE0C-4C3B-A9A9-64D68A47312A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6314B57A-EE0C-4C3B-A9A9-64D68A47312A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6314B57A-EE0C-4C3B-A9A9-64D68A47312A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6314B57A-EE0C-4C3B-A9A9-64D68A47312A}.Release|Any CPU.Build.0 = Release|Any CPU {25DBED78-99F4-433F-BBF5-1B4E9DEAE437}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {25DBED78-99F4-433F-BBF5-1B4E9DEAE437}.Debug|Any CPU.Build.0 = Debug|Any CPU {25DBED78-99F4-433F-BBF5-1B4E9DEAE437}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -834,6 +846,7 @@ Global {B024E315-831F-429D-92AA-44B839AC10F4} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {1266E1E6-5CEF-4161-8B45-83282455746E} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {2613F146-6C66-4059-9D37-D48BA6B61515} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} + {3A64B210-645C-4229-B089-5BB2AAFCF535} = {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} @@ -895,6 +908,7 @@ Global {5247DF94-32F3-4ED6-AE71-6AB4F4078E6D} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {5DB1F35F-B714-4B62-84BE-16A33084D3E1} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {82A7E7B8-3187-4CAE-845B-0BF43409B38A} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} + {6314B57A-EE0C-4C3B-A9A9-64D68A47312A} = {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} diff --git a/Testcontainers.sln.DotSettings b/Testcontainers.sln.DotSettings index ff93162f2..71a67e934 100644 --- a/Testcontainers.sln.DotSettings +++ b/Testcontainers.sln.DotSettings @@ -26,6 +26,7 @@ True True True + True True True True diff --git a/docs/modules/index.md b/docs/modules/index.md index 95dac0163..d3e7dbffe 100644 --- a/docs/modules/index.md +++ b/docs/modules/index.md @@ -55,6 +55,7 @@ await moduleNameContainer.StartAsync(); | 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) | | 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) | +| 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) | | 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) | diff --git a/src/Testcontainers.Mosquitto/.editorconfig b/src/Testcontainers.Mosquitto/.editorconfig new file mode 100644 index 000000000..6f066619d --- /dev/null +++ b/src/Testcontainers.Mosquitto/.editorconfig @@ -0,0 +1 @@ +root = true \ No newline at end of file diff --git a/src/Testcontainers.Mosquitto/MosquittoBuilder.cs b/src/Testcontainers.Mosquitto/MosquittoBuilder.cs new file mode 100644 index 000000000..ff0bb3b38 --- /dev/null +++ b/src/Testcontainers.Mosquitto/MosquittoBuilder.cs @@ -0,0 +1,139 @@ +namespace Testcontainers.Mosquitto; + +/// +[PublicAPI] +public sealed class MosquittoBuilder : ContainerBuilder +{ + public const string MosquittoImage = "eclipse-mosquitto:2.0"; + + public const ushort MqttPort = 1883; + + public const ushort MqttTlsPort = 8883; + + public const ushort MqttWsPort = 8080; + + public const ushort MqttWssPort = 8081; + + public const string CertificateFilePath = "/etc/mosquitto/certs/server.crt"; + + public const string CertificateKeyFilePath = "/etc/mosquitto/certs/server.key"; + + /// + /// Initializes a new instance of the class. + /// + public MosquittoBuilder() + : this(new MosquittoConfiguration()) + { + DockerResourceConfiguration = Init().DockerResourceConfiguration; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public MosquittoBuilder(MosquittoConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + DockerResourceConfiguration = resourceConfiguration; + } + + /// + protected override MosquittoConfiguration DockerResourceConfiguration { get; } + + /// + /// Sets the public certificate and private key to enable TLS. + /// + /// The public certificate in PEM format. + /// The private key associated with the certificate in PEM format. + /// A configured instance of . + public MosquittoBuilder WithCertificate(string certificate, string certificateKey) + { + return Merge(DockerResourceConfiguration, new MosquittoConfiguration(certificate: certificate, certificateKey: certificateKey)) + .WithPortBinding(MqttTlsPort, true) + .WithPortBinding(MqttWssPort, true) + .WithResourceMapping(Encoding.Default.GetBytes(certificate), CertificateFilePath) + .WithResourceMapping(Encoding.Default.GetBytes(certificateKey), CertificateKeyFilePath); + } + + /// + public override MosquittoContainer Build() + { + Validate(); + + // Maybe we should move this into the startup callback. + var mosquittoConfig = new StringWriter(); + mosquittoConfig.NewLine = "\n"; + + mosquittoConfig.WriteLine("per_listener_settings true"); + mosquittoConfig.WriteLine("log_dest stdout"); + mosquittoConfig.WriteLine("log_type information"); + + mosquittoConfig.WriteLine(); + mosquittoConfig.WriteLine("persistence false"); + mosquittoConfig.WriteLine("persistence_location /mosquitto/data/"); + + mosquittoConfig.WriteLine(); + mosquittoConfig.WriteLine("# MQTT, unencrypted, unauthenticated"); + mosquittoConfig.WriteLine($"listener {MqttPort} 0.0.0.0"); + mosquittoConfig.WriteLine("protocol mqtt"); + mosquittoConfig.WriteLine("allow_anonymous true"); + + mosquittoConfig.WriteLine(); + mosquittoConfig.WriteLine("# MQTT over WebSockets, unencrypted, unauthenticated"); + mosquittoConfig.WriteLine($"listener {MqttWsPort} 0.0.0.0"); + mosquittoConfig.WriteLine("protocol websockets"); + mosquittoConfig.WriteLine("allow_anonymous true"); + + if (DockerResourceConfiguration.TlsEnabled) + { + mosquittoConfig.WriteLine(); + mosquittoConfig.WriteLine("# MQTT, encrypted, unauthenticated"); + mosquittoConfig.WriteLine($"listener {MqttTlsPort} 0.0.0.0"); + mosquittoConfig.WriteLine("protocol mqtt"); + mosquittoConfig.WriteLine("allow_anonymous true"); + mosquittoConfig.WriteLine("tls_version tlsv1.2"); + mosquittoConfig.WriteLine($"certfile {CertificateFilePath}"); + mosquittoConfig.WriteLine($"keyfile {CertificateKeyFilePath}"); + + mosquittoConfig.WriteLine(); + mosquittoConfig.WriteLine("# MQTT over WebSockets, encrypted, unauthenticated"); + mosquittoConfig.WriteLine($"listener {MqttWssPort} 0.0.0.0"); + mosquittoConfig.WriteLine("protocol websockets"); + mosquittoConfig.WriteLine("allow_anonymous true"); + mosquittoConfig.WriteLine("tls_version tlsv1.2"); + mosquittoConfig.WriteLine($"certfile {CertificateFilePath}"); + mosquittoConfig.WriteLine($"keyfile {CertificateKeyFilePath}"); + } + + var mosquittoBuilder = WithResourceMapping(Encoding.Default.GetBytes(mosquittoConfig.ToString()), "/mosquitto/config/mosquitto.conf"); + return new MosquittoContainer(mosquittoBuilder.DockerResourceConfiguration); + } + + /// + protected override MosquittoBuilder Init() + { + return base.Init() + .WithImage(MosquittoImage) + .WithPortBinding(MqttPort, true) + .WithPortBinding(MqttWsPort, true) + .WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged("mosquitto.*running")); + } + + /// + protected override MosquittoBuilder Clone(IResourceConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new MosquittoConfiguration(resourceConfiguration)); + } + + /// + protected override MosquittoBuilder Clone(IContainerConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new MosquittoConfiguration(resourceConfiguration)); + } + + /// + protected override MosquittoBuilder Merge(MosquittoConfiguration oldValue, MosquittoConfiguration newValue) + { + return new MosquittoBuilder(new MosquittoConfiguration(oldValue, newValue)); + } +} \ No newline at end of file diff --git a/src/Testcontainers.Mosquitto/MosquittoConfiguration.cs b/src/Testcontainers.Mosquitto/MosquittoConfiguration.cs new file mode 100644 index 000000000..c2128bb3f --- /dev/null +++ b/src/Testcontainers.Mosquitto/MosquittoConfiguration.cs @@ -0,0 +1,76 @@ +namespace Testcontainers.Mosquitto; + +/// +[PublicAPI] +public sealed class MosquittoConfiguration : ContainerConfiguration +{ + /// + /// Initializes a new instance of the class. + /// + /// The public certificate in PEM format. + /// The private key associated with the certificate in PEM format. + public MosquittoConfiguration( + string certificate = null, + string certificateKey = null) + { + Certificate = certificate; + CertificateKey = certificateKey; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public MosquittoConfiguration(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 MosquittoConfiguration(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 MosquittoConfiguration(MosquittoConfiguration resourceConfiguration) + : this(new MosquittoConfiguration(), 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 MosquittoConfiguration(MosquittoConfiguration oldValue, MosquittoConfiguration newValue) + : base(oldValue, newValue) + { + Certificate = BuildConfiguration.Combine(oldValue.Certificate, newValue.Certificate); + CertificateKey = BuildConfiguration.Combine(oldValue.CertificateKey, newValue.CertificateKey); + } + + /// + /// Gets a value indicating whether TLS is enabled or not. + /// + public bool TlsEnabled => Certificate != null && CertificateKey != null; + + /// + /// Gets the public certificate in PEM format. + /// + public string Certificate { get; } + + /// + /// Gets the private key associated with the certificate in PEM format. + /// + public string CertificateKey { get; } +} \ No newline at end of file diff --git a/src/Testcontainers.Mosquitto/MosquittoContainer.cs b/src/Testcontainers.Mosquitto/MosquittoContainer.cs new file mode 100644 index 000000000..2a40596df --- /dev/null +++ b/src/Testcontainers.Mosquitto/MosquittoContainer.cs @@ -0,0 +1,80 @@ +namespace Testcontainers.Mosquitto; + +/// +[PublicAPI] +public sealed class MosquittoContainer : DockerContainer +{ + private readonly MosquittoConfiguration _configuration; + + /// + /// Initializes a new instance of the class. + /// + /// The container configuration. + public MosquittoContainer(MosquittoConfiguration configuration) + : base(configuration) + { + _configuration = configuration; + } + + /// + /// Gets the MQTT port. + /// + public ushort MqttPort => GetMappedPublicPort(MosquittoBuilder.MqttPort); + + /// + /// Gets the secure MQTT port. + /// + public ushort MqttTlsPort => GetMappedPublicPort(MosquittoBuilder.MqttTlsPort); + + /// + /// Gets the MQTT connection string. + /// + /// An MQTT connection string in the format: mqtt://hostname:port. + public string GetConnectionString() + { + return new UriBuilder("mqtt", Hostname, MqttPort).ToString(); + } + + /// + /// Gets the secure MQTT connection string. + /// + /// A secure MQTT connection string in the format: mqtts://hostname:port. + /// TLS/SSL support is not enabled in the container configuration. + public string GetSecureConnectionString() + { + ThrowIfTlsNotEnabled(); + return new UriBuilder("mqtts", Hostname, MqttTlsPort).ToString(); + } + + /// + /// Gets the MQTT over WebSocket connection string. + /// + /// A WebSocket connection string in the format: ws://hostname:port. + public string GetWsConnectionString() + { + return new UriBuilder("ws", Hostname, GetMappedPublicPort(MosquittoBuilder.MqttWsPort)).ToString(); + } + + /// + /// Gets the secure MQTT over WebSocket connection string. + /// + /// A secure WebSocket connection string in the format: wss://hostname:port. + /// TLS/SSL support is not enabled in the container configuration. + public string GetWssConnectionString() + { + ThrowIfTlsNotEnabled(); + return new UriBuilder("wss", Hostname, GetMappedPublicPort(MosquittoBuilder.MqttWssPort)).ToString(); + } + + /// + /// Throws when TLS/SSL support is not enabled in the container configuration. + /// + /// TLS/SSL support is not enabled in the container configuration. + private void ThrowIfTlsNotEnabled() + { + if (!_configuration.TlsEnabled) + { + throw new InvalidOperationException("TLS/SSL support is not enabled in the container configuration."); + } + } +} \ No newline at end of file diff --git a/src/Testcontainers.Mosquitto/Testcontainers.Mosquitto.csproj b/src/Testcontainers.Mosquitto/Testcontainers.Mosquitto.csproj new file mode 100644 index 000000000..058c1e82b --- /dev/null +++ b/src/Testcontainers.Mosquitto/Testcontainers.Mosquitto.csproj @@ -0,0 +1,12 @@ + + + net8.0;net9.0;netstandard2.0;netstandard2.1 + latest + + + + + + + + \ No newline at end of file diff --git a/src/Testcontainers.Mosquitto/Usings.cs b/src/Testcontainers.Mosquitto/Usings.cs new file mode 100644 index 000000000..5cd85391a --- /dev/null +++ b/src/Testcontainers.Mosquitto/Usings.cs @@ -0,0 +1,8 @@ +global using System; +global using System.IO; +global using System.Text; +global using Docker.DotNet.Models; +global using DotNet.Testcontainers.Builders; +global using DotNet.Testcontainers.Configurations; +global using DotNet.Testcontainers.Containers; +global using JetBrains.Annotations; \ No newline at end of file diff --git a/src/Testcontainers.Qdrant/QdrantConfiguration.cs b/src/Testcontainers.Qdrant/QdrantConfiguration.cs index 99a4288d0..c465cfa2f 100644 --- a/src/Testcontainers.Qdrant/QdrantConfiguration.cs +++ b/src/Testcontainers.Qdrant/QdrantConfiguration.cs @@ -66,7 +66,7 @@ public QdrantConfiguration(QdrantConfiguration oldValue, QdrantConfiguration new /// /// Gets a value indicating whether TLS is enabled or not. /// - public bool TlsEnabled => Certificate != null; + public bool TlsEnabled => Certificate != null && CertificateKey != null; /// /// Gets the API key that secures the instance. diff --git a/tests/Testcontainers.Mosquitto.Tests/.editorconfig b/tests/Testcontainers.Mosquitto.Tests/.editorconfig new file mode 100644 index 000000000..6f066619d --- /dev/null +++ b/tests/Testcontainers.Mosquitto.Tests/.editorconfig @@ -0,0 +1 @@ +root = true \ No newline at end of file diff --git a/tests/Testcontainers.Mosquitto.Tests/.runs-on b/tests/Testcontainers.Mosquitto.Tests/.runs-on new file mode 100644 index 000000000..d0395e498 --- /dev/null +++ b/tests/Testcontainers.Mosquitto.Tests/.runs-on @@ -0,0 +1 @@ +ubuntu-24.04 \ No newline at end of file diff --git a/tests/Testcontainers.Mosquitto.Tests/MosquittoContainerTest.cs b/tests/Testcontainers.Mosquitto.Tests/MosquittoContainerTest.cs new file mode 100644 index 000000000..a62b24700 --- /dev/null +++ b/tests/Testcontainers.Mosquitto.Tests/MosquittoContainerTest.cs @@ -0,0 +1,135 @@ +namespace Testcontainers.Mosquitto; + +public abstract class MosquittoContainerTest : ContainerTest +{ + private static readonly string Certificate = File.ReadAllText(Certificates.Instance.GetFilePath("server", "server.crt")); + + private static readonly string CertificateKey = File.ReadAllText(Certificates.Instance.GetFilePath("server", "server.key")); + + private readonly MqttClientFactory _clientFactory = new MqttClientFactory(); + + private MosquittoContainerTest(ITestOutputHelper testOutputHelper) + : base(testOutputHelper) + { + } + + protected abstract MqttClientOptions GetClientOptions(); + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task EstablishesConnection() + { + // Given + using var client = _clientFactory.CreateMqttClient(); + + // When + var result = await client.ConnectAsync(GetClientOptions(), TestContext.Current.CancellationToken) + .ConfigureAwait(true); + + // Then + Assert.Equal(MqttClientConnectResultCode.Success, result.ResultCode); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task SubTopicReturnsPubMessage() + { + // Given + const string helloMosquitto = "Hello, Mosquitto!"; + + const string topicId = "hello-topic"; + + var messageReceived = new TaskCompletionSource(); + + using var client = _clientFactory.CreateMqttClient(); + + // When + _ = await client.ConnectAsync(GetClientOptions(), TestContext.Current.CancellationToken) + .ConfigureAwait(true); + + client.ApplicationMessageReceivedAsync += e => + { + messageReceived.SetResult(e.ApplicationMessage.ConvertPayloadToString()); + return Task.CompletedTask; + }; + + _ = await client.SubscribeAsync(topicId, cancellationToken: TestContext.Current.CancellationToken) + .ConfigureAwait(true); + + _ = await client.PublishStringAsync(topicId, helloMosquitto, cancellationToken: TestContext.Current.CancellationToken) + .ConfigureAwait(true); + + var completedTask = await Task.WhenAny(messageReceived.Task, Task.Delay(TimeSpan.FromSeconds(1), TestContext.Current.CancellationToken)) + .ConfigureAwait(true); + + // Then + Assert.Equal(messageReceived.Task, completedTask); + + var message = await messageReceived.Task + .ConfigureAwait(true); + + Assert.Equal(helloMosquitto, message); + } + + [UsedImplicitly] + public sealed class TcpUnencryptedUnauthenticatedConfiguration(ITestOutputHelper testOutputHelper) + : MosquittoContainerTest(testOutputHelper) + { + protected override MqttClientOptions GetClientOptions() + { + return new MqttClientOptionsBuilder() + .WithTcpServer(Container.Hostname, Container.MqttPort) + .Build(); + } + } + + [UsedImplicitly] + public sealed class TcpEncryptedUnauthenticatedConfiguration(ITestOutputHelper testOutputHelper) + : MosquittoContainerTest(testOutputHelper) + { + protected override MosquittoBuilder Configure(MosquittoBuilder builder) + { + return builder.WithCertificate(Certificate, CertificateKey); + } + + protected override MqttClientOptions GetClientOptions() + { + return new MqttClientOptionsBuilder() + .WithTcpServer(Container.Hostname, Container.MqttTlsPort) + .WithTlsOptions(options => options.WithCertificateValidationHandler(e => + "CN=Test CA".Equals(e.Certificate.Issuer, StringComparison.Ordinal))) + .Build(); + } + } + + [UsedImplicitly] + public sealed class WebSocketUnencryptedUnauthenticatedConfiguration(ITestOutputHelper testOutputHelper) + : MosquittoContainerTest(testOutputHelper) + { + protected override MqttClientOptions GetClientOptions() + { + return new MqttClientOptionsBuilder() + .WithWebSocketServer(options => options.WithUri(Container.GetWsConnectionString())) + .Build(); + } + } + + [UsedImplicitly] + public sealed class WebSocketEncryptedUnauthenticatedConfiguration(ITestOutputHelper testOutputHelper) + : MosquittoContainerTest(testOutputHelper) + { + protected override MosquittoBuilder Configure(MosquittoBuilder builder) + { + return builder.WithCertificate(Certificate, CertificateKey); + } + + protected override MqttClientOptions GetClientOptions() + { + return new MqttClientOptionsBuilder() + .WithWebSocketServer(options => options.WithUri(Container.GetWssConnectionString())) + .WithTlsOptions(options => options.WithCertificateValidationHandler(e => + "CN=Test CA".Equals(e.Certificate.Issuer, StringComparison.Ordinal))) + .Build(); + } + } +} \ No newline at end of file diff --git a/tests/Testcontainers.Mosquitto.Tests/Testcontainers.Mosquitto.Tests.csproj b/tests/Testcontainers.Mosquitto.Tests/Testcontainers.Mosquitto.Tests.csproj new file mode 100644 index 000000000..dd152d79e --- /dev/null +++ b/tests/Testcontainers.Mosquitto.Tests/Testcontainers.Mosquitto.Tests.csproj @@ -0,0 +1,20 @@ + + + net9.0 + false + false + Exe + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Testcontainers.Mosquitto.Tests/Usings.cs b/tests/Testcontainers.Mosquitto.Tests/Usings.cs new file mode 100644 index 000000000..f129b6023 --- /dev/null +++ b/tests/Testcontainers.Mosquitto.Tests/Usings.cs @@ -0,0 +1,8 @@ +global using System; +global using System.IO; +global using System.Threading.Tasks; +global using DotNet.Testcontainers.Commons; +global using JetBrains.Annotations; +global using MQTTnet; +global using Testcontainers.Xunit; +global using Xunit; \ No newline at end of file