From e6a288c86418707006d47b73e4305ac734033cc0 Mon Sep 17 00:00:00 2001 From: SebastienDegodez Date: Tue, 3 Dec 2024 23:16:02 +0100 Subject: [PATCH] feat: add Kafka functionality test with TestContainers Signed-off-by: SebastienDegodez --- .../Connection/KafkaConnection.cs | 28 ++ .../MicrocksAsyncMinionBuilder.cs | 102 +++++++ .../MicrocksAsyncMinionConfiguration.cs | 67 +++++ .../MicrocksAsyncMinionContainer.cs | 44 ++++ .../MicrocksContainerEnsemble.cs | 141 ++++++++++ .../MicrocksAsyncKafkaFunctionalityTest.cs | 248 ++++++++++++++++++ .../Async/MicrocksAsyncFeatureTest.cs | 52 ++++ .../Microcks.Testcontainers.Tests.csproj | 5 + .../pastry-orders-asyncapi.yml | 91 +++++++ 9 files changed, 778 insertions(+) create mode 100644 src/Microcks.Testcontainers/Connection/KafkaConnection.cs create mode 100644 src/Microcks.Testcontainers/MicrocksAsyncMinionBuilder.cs create mode 100644 src/Microcks.Testcontainers/MicrocksAsyncMinionConfiguration.cs create mode 100644 src/Microcks.Testcontainers/MicrocksAsyncMinionContainer.cs create mode 100644 src/Microcks.Testcontainers/MicrocksContainerEnsemble.cs create mode 100644 tests/Microcks.Testcontainers.Tests/Async/Kafka/MicrocksAsyncKafkaFunctionalityTest.cs create mode 100644 tests/Microcks.Testcontainers.Tests/Async/MicrocksAsyncFeatureTest.cs create mode 100644 tests/Microcks.Testcontainers.Tests/pastry-orders-asyncapi.yml diff --git a/src/Microcks.Testcontainers/Connection/KafkaConnection.cs b/src/Microcks.Testcontainers/Connection/KafkaConnection.cs new file mode 100644 index 0000000..67a7d79 --- /dev/null +++ b/src/Microcks.Testcontainers/Connection/KafkaConnection.cs @@ -0,0 +1,28 @@ +// +// Copyright The Microcks Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// + +namespace Microcks.Testcontainers.Connection; + +public class KafkaConnection +{ + public KafkaConnection(string bootstrapServers) + { + this.BootstrapServers = bootstrapServers; + } + + public string BootstrapServers { get; } +} diff --git a/src/Microcks.Testcontainers/MicrocksAsyncMinionBuilder.cs b/src/Microcks.Testcontainers/MicrocksAsyncMinionBuilder.cs new file mode 100644 index 0000000..f190596 --- /dev/null +++ b/src/Microcks.Testcontainers/MicrocksAsyncMinionBuilder.cs @@ -0,0 +1,102 @@ +// +// Copyright The Microcks Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// + + +using DotNet.Testcontainers.Networks; +using Microcks.Testcontainers.Connection; + +namespace Microcks.Testcontainers; + +/// +public sealed class MicrocksAsyncMinionBuilder + : ContainerBuilder +{ + public const int MicrocksAsyncMinionHttpPort = 8081; + private const string MicrocksAsyncMinionFullImageName = "quay.io/microcks/microcks-uber-async-minion"; + + private HashSet extraProtocols = []; + private INetwork _network; + + public MicrocksAsyncMinionBuilder(INetwork network) + : this(new MicrocksAsyncMinionConfiguration()) + { + this._network = network; + DockerResourceConfiguration = Init().DockerResourceConfiguration; + } + + private MicrocksAsyncMinionBuilder(MicrocksAsyncMinionConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + DockerResourceConfiguration = resourceConfiguration; + } + + protected override MicrocksAsyncMinionConfiguration DockerResourceConfiguration { get; } + + public override MicrocksAsyncMinionContainer Build() + { + Validate(); + + return new MicrocksAsyncMinionContainer(DockerResourceConfiguration); + } + + protected override MicrocksAsyncMinionBuilder Init() + { + return base.Init() + .WithImage(MicrocksAsyncMinionFullImageName) + .WithNetwork(this._network) + .WithNetworkAliases("microcks-async-minion") + .WithEnvironment("MICROCKS_HOST_PORT", "microcks:" + MicrocksBuilder.MicrocksHttpPort) + .WithExposedPort(MicrocksAsyncMinionHttpPort) + .WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged(".*Profile prod activated\\..*")); + } + + /// + protected override MicrocksAsyncMinionBuilder Clone(IResourceConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new MicrocksAsyncMinionConfiguration(resourceConfiguration)); + } + + /// + protected override MicrocksAsyncMinionBuilder Clone(IContainerConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new MicrocksAsyncMinionConfiguration(resourceConfiguration)); + } + + /// + protected override MicrocksAsyncMinionBuilder Merge(MicrocksAsyncMinionConfiguration oldValue, MicrocksAsyncMinionConfiguration newValue) + { + return new MicrocksAsyncMinionBuilder(new MicrocksAsyncMinionConfiguration(oldValue, newValue)); + } + + + /// + /// Configures the MicrocksAsyncMinionBuilder to use a Kafka connection. + /// + /// The Kafka connection details. + /// The updated MicrocksAsyncMinionBuilder instance. + public MicrocksAsyncMinionBuilder WithKafkaConnection(KafkaConnection kafkaConnection) + { + extraProtocols.Add("KAFKA"); + var environments = new Dictionary + { + { "ASYNC_PROTOCOLS", $",{string.Join(",", extraProtocols)}" }, + { "KAFKA_BOOTSTRAP_SERVER", kafkaConnection.BootstrapServers }, + }; + + return Merge(DockerResourceConfiguration, new MicrocksAsyncMinionConfiguration(new ContainerConfiguration(environments: environments))); + } +} diff --git a/src/Microcks.Testcontainers/MicrocksAsyncMinionConfiguration.cs b/src/Microcks.Testcontainers/MicrocksAsyncMinionConfiguration.cs new file mode 100644 index 0000000..b5c78c3 --- /dev/null +++ b/src/Microcks.Testcontainers/MicrocksAsyncMinionConfiguration.cs @@ -0,0 +1,67 @@ +// +// Copyright The Microcks Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// + +namespace Microcks.Testcontainers; + +/// +public sealed class MicrocksAsyncMinionConfiguration : ContainerConfiguration +{ + /// + /// Initializes a new instance of the class. + /// + /// The Microcks config. + public MicrocksAsyncMinionConfiguration(object config = null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public MicrocksAsyncMinionConfiguration(IResourceConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public MicrocksAsyncMinionConfiguration(IContainerConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public MicrocksAsyncMinionConfiguration(MicrocksAsyncMinionConfiguration resourceConfiguration) + : this(new MicrocksAsyncMinionConfiguration(), resourceConfiguration) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The old Docker resource configuration. + /// The new Docker resource configuration. + public MicrocksAsyncMinionConfiguration(MicrocksAsyncMinionConfiguration oldValue, MicrocksAsyncMinionConfiguration newValue) + : base(oldValue, newValue) + { + } +} diff --git a/src/Microcks.Testcontainers/MicrocksAsyncMinionContainer.cs b/src/Microcks.Testcontainers/MicrocksAsyncMinionContainer.cs new file mode 100644 index 0000000..bfb4195 --- /dev/null +++ b/src/Microcks.Testcontainers/MicrocksAsyncMinionContainer.cs @@ -0,0 +1,44 @@ +// +// Copyright The Microcks Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// + + +namespace Microcks.Testcontainers; + +/// +public sealed class MicrocksAsyncMinionContainer : DockerContainer +{ + private const string DESTINATION_PATTERN = "{0}-{1}-{2}"; + + public MicrocksAsyncMinionContainer(MicrocksAsyncMinionConfiguration configuration) + : base(configuration) + { + } + + + public string GetKafkaMockTopic(string service, string version, string operationName) + { + // operationName may start with SUBSCRIBE or PUBLISH. + if (operationName.Contains(" ")) + { + operationName = operationName.Split(' ')[1]; + } + return String.Format(DESTINATION_PATTERN, + service.Replace(" ", "").Replace("-", ""), + version, + operationName.Replace("/", "-")); + } +} diff --git a/src/Microcks.Testcontainers/MicrocksContainerEnsemble.cs b/src/Microcks.Testcontainers/MicrocksContainerEnsemble.cs new file mode 100644 index 0000000..87263cf --- /dev/null +++ b/src/Microcks.Testcontainers/MicrocksContainerEnsemble.cs @@ -0,0 +1,141 @@ +// +// Copyright The Microcks Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// + +using System.Threading.Tasks; +using DotNet.Testcontainers.Networks; +using Microcks.Testcontainers.Connection; + +namespace Microcks.Testcontainers; + +/// +public class MicrocksContainerEnsemble : IAsyncDisposable +{ + private readonly MicrocksBuilder _microcksBuilder; + + private MicrocksAsyncMinionBuilder _asyncMinionBuilder; + + /// + /// Gets the Microcks asynchronous minion container. + /// + public MicrocksAsyncMinionContainer AsyncMinionContainer { get; private set; } + + /// + /// Gets the Microcks container. + /// + public MicrocksContainer MicrocksContainer { get; private set; } + + private INetwork _network; + + private readonly string _microcksImage; + + public MicrocksContainerEnsemble(string microcksImage) + : this(new NetworkBuilder().Build(), microcksImage) + { + } + + public MicrocksContainerEnsemble(INetwork network, string microcksImage) + { + this._microcksImage = microcksImage; + this._network = network; + + this._microcksBuilder = new MicrocksBuilder() + .WithNetwork(this._network) + .WithNetworkAliases("microcks") + .WithExposedPort(MicrocksBuilder.MicrocksHttpPort) + .WithExposedPort(MicrocksBuilder.MicrocksGrpcPort) + .WithImage(this._microcksImage) + .WithEnvironment("TEST_CALLBACK_URL", "http://microcks:" + MicrocksBuilder.MicrocksHttpPort) + .WithEnvironment("ASYNC_MINION_URL", "http://microcks-async-minion:" + MicrocksAsyncMinionBuilder.MicrocksAsyncMinionHttpPort); + } + + public MicrocksContainerEnsemble WithMainArtifacts(params string[] mainArtifacts) + { + this._microcksBuilder.WithMainArtifacts(mainArtifacts); + return this; + } + + /// + /// Configures the Microcks container ensemble to use the asynchronous feature. + /// + /// + /// The instance with the asynchronous feature configured. + /// + /// + /// This method modifies the Microcks image to use the asynchronous version by replacing "microcks-uber" with "microcks-uber-async". + /// If the image name ends with "-native", it removes the "-native" suffix. + /// It also sets up the asynchronous minion builder with the modified image, network, and network aliases. + /// + public MicrocksContainerEnsemble WithAsyncFeature() + { + var image = this._microcksImage.Replace("microcks-uber", "microcks-uber-async-minion"); + if (image.EndsWith("-native")) + { + image = image.Replace("-native", ""); + } + + this._asyncMinionBuilder = new MicrocksAsyncMinionBuilder(this._network) + .WithImage(image); + + return this; + } + + /// + /// Configures the Microcks container ensemble with a Kafka connection. + /// + /// The Kafka connection details. + /// The updated instance. + public MicrocksContainerEnsemble WithKafkaConnection(KafkaConnection kafkaConnection) + { + if (this._asyncMinionBuilder == null) + { + this.WithAsyncFeature(); + } + + this._asyncMinionBuilder = (_asyncMinionBuilder ?? throw new NullReferenceException("MicrocksAsyncMinionBuilder is null")) + .WithKafkaConnection(kafkaConnection); + + return this; + } + + public async Task StartAsync() + { + this.MicrocksContainer = this._microcksBuilder.Build(); + await this.MicrocksContainer.StartAsync().ConfigureAwait(false); + + if (this._asyncMinionBuilder != null) + { + this.AsyncMinionContainer = this._asyncMinionBuilder + .DependsOn(this.MicrocksContainer) + .Build(); + + await this.AsyncMinionContainer.StartAsync().ConfigureAwait(false); + } + } + + public async ValueTask DisposeAsync() + { + if (this.AsyncMinionContainer != null) + { + await this.AsyncMinionContainer.DisposeAsync(); + } + + if (this.MicrocksContainer != null) + { + await this.MicrocksContainer.DisposeAsync(); + } + } +} diff --git a/tests/Microcks.Testcontainers.Tests/Async/Kafka/MicrocksAsyncKafkaFunctionalityTest.cs b/tests/Microcks.Testcontainers.Tests/Async/Kafka/MicrocksAsyncKafkaFunctionalityTest.cs new file mode 100644 index 0000000..c73d4a9 --- /dev/null +++ b/tests/Microcks.Testcontainers.Tests/Async/Kafka/MicrocksAsyncKafkaFunctionalityTest.cs @@ -0,0 +1,248 @@ +// +// Copyright The Microcks Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using Confluent.Kafka; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Configurations; +using DotNet.Testcontainers.Networks; +using FluentAssertions; +using Microcks.Testcontainers.Connection; +using Microcks.Testcontainers.Model; +using Testcontainers.Kafka; + +namespace Microcks.Testcontainers.Tests.Async.Kafka; + +public sealed class MicrocksAsyncKafkaFunctionalityTest : IAsyncLifetime +{ + /// + /// Image name for the Microcks container. + /// + private const string MicrocksImage = "quay.io/microcks/microcks-uber:1.10.0"; + + private MicrocksContainerEnsemble _microcksContainerEnsemble; + + private KafkaContainer _kafkaContainer; + + public async Task DisposeAsync() + { + await this._microcksContainerEnsemble.DisposeAsync(); + await this._kafkaContainer.DisposeAsync(); + } + + public async Task InitializeAsync() + { + var network = new NetworkBuilder().Build(); + + this._kafkaContainer = CreateKafkaContainer(network); + + // Start the Kafka container + await this._kafkaContainer.StartAsync().ConfigureAwait(false); + + this._microcksContainerEnsemble = new MicrocksContainerEnsemble(network, MicrocksImage) + .WithMainArtifacts("pastry-orders-asyncapi.yml") + .WithKafkaConnection(new KafkaConnection($"kafka:19092")); + + await this._microcksContainerEnsemble.StartAsync(); + } + + // TODO: Simplify this code after PR is merged https://github.com/testcontainers/testcontainers-dotnet/pull/1316 + private KafkaContainer CreateKafkaContainer(INetwork network) + { + return new KafkaBuilder() + .WithImage("confluentinc/cp-kafka:7.8.0") + .WithNetwork(network) + .WithNetworkAliases("kafka") + .WithEnvironment(new Dictionary + { + { + "KAFKA_LISTENERS", + $"PLAINTEXT://0.0.0.0:{KafkaBuilder.KafkaPort},BROKER://0.0.0.0:{KafkaBuilder.BrokerPort},CONTROLLER://0.0.0.0:9094,TC-0://kafka:19092" + }, + { + "KAFKA_LISTENER_SECURITY_PROTOCOL_MAP", + "PLAINTEXT:PLAINTEXT,BROKER:PLAINTEXT,CONTROLLER:PLAINTEXT,TC-0:PLAINTEXT" + } + }) + .WithStartupCallback((container, ct) => + { + const string advertisedListener = ",TC-0://kafka:19092"; + + const char lf = '\n'; + var startupScript = new StringBuilder(); + + startupScript.Append("#!/bin/bash"); + startupScript.Append(lf); + startupScript.Append("echo 'clientPort=" + KafkaBuilder.ZookeeperPort + "' > zookeeper.properties"); + startupScript.Append(lf); + startupScript.Append("echo 'dataDir=/var/lib/zookeeper/data' >> zookeeper.properties"); + startupScript.Append(lf); + startupScript.Append("echo 'dataLogDir=/var/lib/zookeeper/log' >> zookeeper.properties"); + startupScript.Append(lf); + startupScript.Append("zookeeper-server-start zookeeper.properties &"); + startupScript.Append(lf); + startupScript.Append("export KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://" + container.Hostname + ":" + container.GetMappedPublicPort(KafkaBuilder.KafkaPort) + ",BROKER://" + container.IpAddress + ":" + KafkaBuilder.BrokerPort + advertisedListener); + startupScript.Append(lf); + startupScript.Append("echo '' > /etc/confluent/docker/ensure"); + startupScript.Append(lf); + startupScript.Append("exec /etc/confluent/docker/run"); + return container.CopyAsync(Encoding.Default.GetBytes(startupScript.ToString()), + KafkaBuilder.StartupScriptFilePath, Unix.FileMode755, ct); + }) + .Build(); + } + + [Fact] + public void ShouldReceivedKafkaMessageWhenMessageIsEmitted() + { + const string expectedMessage = "{\"id\":\"4dab240d-7847-4e25-8ef3-1530687650c8\",\"customerId\":\"fe1088b3-9f30-4dc1-a93d-7b74f0a072b9\",\"status\":\"VALIDATED\",\"productQuantities\":[{\"quantity\":2,\"pastryName\":\"Croissant\"},{\"quantity\":1,\"pastryName\":\"Millefeuille\"}]}"; + var kafkaTopic = this._microcksContainerEnsemble.AsyncMinionContainer + .GetKafkaMockTopic("Pastry orders API", "0.1.0", "SUBSCRIBE pastry/orders"); + + var bootstrapServers = this._kafkaContainer.GetBootstrapAddress() + .Replace("PLAINTEXT://", "", StringComparison.OrdinalIgnoreCase); + + // Initialize Kafka consumer to receive message + var consumerConfig = new ConsumerConfig + { + BootstrapServers = bootstrapServers, + GroupId = $"test-group-{DateTime.Now.Ticks}", + ClientId = $"test-client-{DateTime.Now.Ticks}", + + AutoOffsetReset = AutoOffsetReset.Earliest, + EnableAutoCommit = false, + }; + + using var consumer = new ConsumerBuilder(consumerConfig) + .SetKeyDeserializer(Deserializers.Utf8) + .SetValueDeserializer(Deserializers.Utf8) + .SetErrorHandler((_, e) => + { + Debug.WriteLine($"Error: {e.Reason}"); + }) + .Build(); + + consumer.Subscribe(kafkaTopic); + string message = null; + // Consume message from Kafka 4000 milliseconds attempt + var consumeResult = consumer.Consume(TimeSpan.FromMilliseconds(5000)); + + if (consumeResult != null) + { + message = consumeResult.Message.Value; + } + + message.Should().Be(expectedMessage); + } + + [Theory] + [MemberData(nameof(ContractData))] + public async Task ShouldReturnsCorrectStatusContractWhenMessageIsEmitted( + string message, + bool result, + string? expectedMessage) + { + var testRequest = new TestRequest + { + ServiceId = "Pastry orders API:0.1.0", + RunnerType = TestRunnerType.ASYNC_API_SCHEMA, + TestEndpoint = "kafka://kafka:19092/pastry-orders", + Timeout = TimeSpan.FromMilliseconds(20001) + }; + + // Init Kafka producer to send a message + var producerConfig = new ProducerConfig + { + BootstrapServers = this._kafkaContainer.GetBootstrapAddress() + .Replace("PLAINTEXT://", "", StringComparison.OrdinalIgnoreCase), + ClientId = $"test-client-{DateTime.Now.Ticks}", + }; + + using var producer = new ProducerBuilder(producerConfig) + .SetKeySerializer(Serializers.Utf8) + .SetValueSerializer(Serializers.Utf8) + .SetErrorHandler((_, e) => + { + Debug.WriteLine($"Error: {e.Reason}"); + }) + .SetLogHandler((_, logMessage) => + { + Debug.WriteLine($"{logMessage.Name} sending {logMessage.Message}"); + }) + .Build(); + + var taskTestResult = Task.Run(() => this._microcksContainerEnsemble + .MicrocksContainer + .TestEndpointAsync(testRequest)); + + // Act + for (var i = 0; i < 5; i++) + { + producer.Produce("pastry-orders", new Message + { + Key = Guid.NewGuid().ToString(), + Value = message + }); + // producer.Flush(); + await Task.Delay(500); + } + + // Wait for a test result + var testResult = await taskTestResult; + + // Assert + testResult.Success.Should().Be(result); + testResult.TestedEndpoint.Should().Be(testRequest.TestEndpoint); + + var testCaseResult = testResult.TestCaseResults.First(); + var testStepResults = testCaseResult.TestStepResults; + + testStepResults.Should().NotBeEmpty(); + + if( expectedMessage == null ) + { + testStepResults.First().Message.Should().BeNull(); + } + else + { + testStepResults.First().Message.Should().Contain(expectedMessage); + } + } + + public static IEnumerable ContractData() + { + // Contract data + // good message + yield return + [ + "{\"id\":\"abcd\",\"customerId\":\"efgh\",\"status\":\"CREATED\",\"productQuantities\":[{\"quantity\":2,\"pastryName\":\"Croissant\"},{\"quantity\":1,\"pastryName\":\"Millefeuille\"}]}", + true, + null + ]; + // bad message has no status + yield return + [ + "{\"id\":\"abcd\",\"customerId\":\"efgh\",\"productQuantities\":[{\"quantity\":2,\"pastryName\":\"Croissant\"},{\"quantity\":1,\"pastryName\":\"Millefeuille\"}]}", + false, + "object has missing required properties ([\"status\"]" + ]; + } +} diff --git a/tests/Microcks.Testcontainers.Tests/Async/MicrocksAsyncFeatureTest.cs b/tests/Microcks.Testcontainers.Tests/Async/MicrocksAsyncFeatureTest.cs new file mode 100644 index 0000000..c7f847f --- /dev/null +++ b/tests/Microcks.Testcontainers.Tests/Async/MicrocksAsyncFeatureTest.cs @@ -0,0 +1,52 @@ +// +// Copyright The Microcks Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// + +using FluentAssertions; + +namespace Microcks.Testcontainers.Tests.Async; + +public sealed class MicrocksAsyncFeatureTest : IAsyncLifetime +{ + /// + /// Image name for the Microcks container. + /// + public const string MicrocksImage = "quay.io/microcks/microcks-uber:nightly-native"; + + private MicrocksContainerEnsemble _microcksContainerEnsemble; + + public async Task DisposeAsync() + { + await this._microcksContainerEnsemble.DisposeAsync(); + } + + public async Task InitializeAsync() + { + this._microcksContainerEnsemble = new MicrocksContainerEnsemble(MicrocksImage) + .WithAsyncFeature(); + + await this._microcksContainerEnsemble.StartAsync(); + } + + [Fact] + public void ShouldDetermineCorrectImageMessage() + { + this._microcksContainerEnsemble.AsyncMinionContainer.Image.FullName + .Should() + .Be("quay.io/microcks/microcks-uber-async-minion:nightly"); + } + +} diff --git a/tests/Microcks.Testcontainers.Tests/Microcks.Testcontainers.Tests.csproj b/tests/Microcks.Testcontainers.Tests/Microcks.Testcontainers.Tests.csproj index fe6b88e..ecaaf9d 100644 --- a/tests/Microcks.Testcontainers.Tests/Microcks.Testcontainers.Tests.csproj +++ b/tests/Microcks.Testcontainers.Tests/Microcks.Testcontainers.Tests.csproj @@ -10,6 +10,8 @@ + + @@ -20,6 +22,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/tests/Microcks.Testcontainers.Tests/pastry-orders-asyncapi.yml b/tests/Microcks.Testcontainers.Tests/pastry-orders-asyncapi.yml new file mode 100644 index 0000000..cc90a85 --- /dev/null +++ b/tests/Microcks.Testcontainers.Tests/pastry-orders-asyncapi.yml @@ -0,0 +1,91 @@ +asyncapi: '2.6.0' +id: 'urn:io.microcks.example.pastry-orders' +info: + title: Pastry orders API + version: 0.1.0 + description: Sample AsyncAPI for Pastry order events +defaultContentType: application/json +channels: + pastry/orders: + description: The topic on which pastry orders events may be consumed + subscribe: + summary: Receive informations about pastry orders + operationId: receivedPastryOrder + message: + $ref: '#/components/messages/PastryOrder' + bindings: + sqs: + queue: + name: pastry-orders + mqtt: + qos: 0 + retain: false + bindings: + ws: + method: POST + amqp: + is: routingKey + exchange: + type: topic + durable: true + autoDelete: false +components: + messages: + PastryOrder: + bindings: + kafka: + key: + type: string + payload: + type: object + additionalProperties: false + required: + - id + - customerId + - status + - productQuantities + properties: + id: + description: Unique identifier of order (guid) + type: string + customerId: + description: Identifier of customer of this order (guid) + type: string + status: + description: Status of Order + enum: + - CREATED + - VALIDATED + - CANCELED + - FAILED + type: string + productQuantities: + description: Desired products and quantities for this order + type: array + items: + $ref: '#/components/schemas/ProductQuantity' + examples: + - Validated order: + payload: + id: 4dab240d-7847-4e25-8ef3-1530687650c8 + customerId: fe1088b3-9f30-4dc1-a93d-7b74f0a072b9 + status: VALIDATED + productQuantities: + - quantity: 2 + pastryName: Croissant + - quantity: 1 + pastryName: Millefeuille + schemas: + ProductQuantity: + type: object + additionalProperties: false + required: + - quantity + - pastryName + properties: + quantity: + description: Desired quantity + type: integer + pastryName: + description: Desired pastry name + type: string \ No newline at end of file