From f9dc2f7e4d66411968a9bdd31a2fa3dd020727a7 Mon Sep 17 00:00:00 2001 From: Esta Nagy Date: Wed, 15 Jan 2025 22:08:48 +0100 Subject: [PATCH] Add Azure Service Bus Emulator container to Azure module - Implements new Service Bus Container - Adds new test case for Service Bus - Updates the Azure documentation Signed-off-by: Esta Nagy --- docs/modules/azure.md | 36 +++++- modules/azure/build.gradle | 3 + .../azure/AzureServiceBusContainer.java | 105 ++++++++++++++++++ .../azure/AzureServiceBusContainerTest.java | 102 +++++++++++++++++ .../test/resources/service-bus-config.json | 29 +++++ 5 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 modules/azure/src/main/java/org/testcontainers/azure/AzureServiceBusContainer.java create mode 100644 modules/azure/src/test/java/org/testcontainers/azure/AzureServiceBusContainerTest.java create mode 100644 modules/azure/src/test/resources/service-bus-config.json diff --git a/docs/modules/azure.md b/docs/modules/azure.md index e09634b09c4..507a7e3699b 100644 --- a/docs/modules/azure.md +++ b/docs/modules/azure.md @@ -5,12 +5,14 @@ This module is INCUBATING. While it is ready for use and operational in the curr Testcontainers module for the Microsoft Azure's [SDK](https://github.com/Azure/azure-sdk-for-java). -Currently, the module supports `Azurite`, `Azure Event Hubs` and `CosmosDB` emulators. In order to use them, you should use the following classes: +Currently, the module supports `Azurite`, `Azure Event Hubs`, `Azure Service Bus` and `CosmosDB` emulators. In order to use them, you should use the following classes: Class | Container Image -|- AzuriteContainer | [mcr.microsoft.com/azure-storage/azurite](https://github.com/microsoft/containerregistry) AzureEventHubsContainer | [mcr.microsoft.com/azure-messaging/eventhubs-emulator](https://github.com/microsoft/containerregistry) +AzureServiceBusEmulatorContainer | [mcr.microsoft.com/azure-messaging/servicebus-emulator](https://github.com/microsoft/containerregistry) +AzureServiceBusContainer | [mcr.microsoft.com/azure-messaging/servicebus-emulator](https://github.com/microsoft/containerregistry) CosmosDBEmulatorContainer | [mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator](https://github.com/microsoft/containerregistry) ## Usage example @@ -101,6 +103,38 @@ Configure the consumer and the producer clients: [Configuring the clients](../../modules/azure/src/test/java/org/testcontainers/azure/AzureEventHubsContainerTest.java) inside_block:createProducerAndConsumer +### Azure Service Bus Emulator + + +[Configuring the Azure Service Bus Emulator container](../../modules/azure/src/test/resources/service-bus-config.json) + + +Start Azure Service Bus Emulator during a test: + + +[Setting up a network](../../modules/azure/src/test/java/org/testcontainers/azure/AzureServiceBusContainerTest.java) inside_block:network + + + +[Starting a SQL Server container as dependency](../../modules/azure/src/test/java/org/testcontainers/azure/AzureServiceBusContainerTest.java) inside_block:sqlContainer + + + +[Starting a Service Bus Emulator container](../../modules/azure/src/test/java/org/testcontainers/azure/AzureServiceBusContainerTest.java) inside_block:emulatorContainer + + +#### Using Azure Service Bus clients + +Configure the sender and the processor clients: + + +[Configuring the sender client](../../modules/azure/src/test/java/org/testcontainers/azure/AzureServiceBusContainerTest.java) inside_block:senderClient + + + +[Configuring the processor client](../../modules/azure/src/test/java/org/testcontainers/azure/AzureServiceBusContainerTest.java) inside_block:processorClient + + ### CosmosDB Start Azure CosmosDB Emulator during a test: diff --git a/modules/azure/build.gradle b/modules/azure/build.gradle index 3dc97d03fce..1b4242b4eb6 100644 --- a/modules/azure/build.gradle +++ b/modules/azure/build.gradle @@ -2,6 +2,7 @@ description = "Testcontainers :: Azure" dependencies { api project(':testcontainers') + api project(':mssqlserver') // TODO use JDK's HTTP client and/or Apache HttpClient5 shaded 'com.squareup.okhttp3:okhttp:4.12.0' @@ -11,4 +12,6 @@ dependencies { testImplementation 'com.azure:azure-storage-queue:12.24.0' testImplementation 'com.azure:azure-data-tables:12.5.0' testImplementation 'com.azure:azure-messaging-eventhubs:5.19.2' + testImplementation 'com.azure:azure-messaging-servicebus:7.17.8' + testImplementation 'com.microsoft.sqlserver:mssql-jdbc:12.8.1.jre8' } diff --git a/modules/azure/src/main/java/org/testcontainers/azure/AzureServiceBusContainer.java b/modules/azure/src/main/java/org/testcontainers/azure/AzureServiceBusContainer.java new file mode 100644 index 00000000000..a2322915048 --- /dev/null +++ b/modules/azure/src/main/java/org/testcontainers/azure/AzureServiceBusContainer.java @@ -0,0 +1,105 @@ +package org.testcontainers.azure; + +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.MSSQLServerContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.images.builder.Transferable; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.LicenseAcceptance; + +/** + * Testcontainers implementation for Azure Service Bus Emulator. + *

+ * Supported image: {@code mcr.microsoft.com/azure-messaging/servicebus-emulator} + *

+ * Exposed port: 5672 + */ +public class AzureServiceBusContainer extends GenericContainer { + + private static final String CONNECTION_STRING_FORMAT = + "Endpoint=sb://%s:%d;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;"; + + private static final int DEFAULT_PORT = 5672; + + private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse( + "mcr.microsoft.com/azure-messaging/servicebus-emulator" + ); + + private MSSQLServerContainer msSqlServerContainer; + + /** + * @param dockerImageName The specified docker image name to run + */ + public AzureServiceBusContainer(final String dockerImageName) { + this(DockerImageName.parse(dockerImageName)); + } + + /** + * @param dockerImageName The specified docker image name to run + */ + public AzureServiceBusContainer(final DockerImageName dockerImageName) { + super(dockerImageName); + dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); + withExposedPorts(DEFAULT_PORT); + waitingFor(Wait.forLogMessage(".*Emulator Service is Successfully Up!.*", 1)); + } + + /** + * Sets the MS SQL Server dependency needed by the Service Bus Container, + * + * @param msSqlServerContainer The MS SQL Server container used by Service Bus as a dependency + * @return this + */ + public AzureServiceBusContainer withMsSqlServerContainer(final MSSQLServerContainer msSqlServerContainer) { + dependsOn(msSqlServerContainer); + this.msSqlServerContainer = msSqlServerContainer; + return this; + } + + /** + * Provide the Service Bus configuration JSON. + * + * @param config The configuration + * @return this + */ + public AzureServiceBusContainer withConfig(final Transferable config) { + withCopyToContainer(config, "/ServiceBus_Emulator/ConfigFiles/Config.json"); + return this; + } + + /** + * Accepts the EULA of the container. + * + * @return this + */ + public AzureServiceBusContainer acceptLicense() { + return withEnv("ACCEPT_EULA", "Y"); + } + + @Override + protected void configure() { + if (msSqlServerContainer == null) { + throw new IllegalStateException( + "The image " + + getDockerImageName() + + " requires a Microsoft SQL Server container. Please provide one with the withMsSqlServerContainer method!" + ); + } + withEnv("SQL_SERVER", msSqlServerContainer.getNetworkAliases().get(0)); + withEnv("MSSQL_SA_PASSWORD", msSqlServerContainer.getPassword()); + // If license was not accepted programmatically, check if it was accepted via resource file + if (!getEnvMap().containsKey("ACCEPT_EULA")) { + LicenseAcceptance.assertLicenseAccepted(this.getDockerImageName()); + acceptLicense(); + } + } + + /** + * Returns the connection string. + * + * @return connection string + */ + public String getConnectionString() { + return String.format(CONNECTION_STRING_FORMAT, getHost(), getMappedPort(DEFAULT_PORT)); + } +} diff --git a/modules/azure/src/test/java/org/testcontainers/azure/AzureServiceBusContainerTest.java b/modules/azure/src/test/java/org/testcontainers/azure/AzureServiceBusContainerTest.java new file mode 100644 index 00000000000..df0ada3e7d7 --- /dev/null +++ b/modules/azure/src/test/java/org/testcontainers/azure/AzureServiceBusContainerTest.java @@ -0,0 +1,102 @@ +package org.testcontainers.azure; + +import com.azure.messaging.servicebus.ServiceBusClientBuilder; +import com.azure.messaging.servicebus.ServiceBusErrorContext; +import com.azure.messaging.servicebus.ServiceBusException; +import com.azure.messaging.servicebus.ServiceBusMessage; +import com.azure.messaging.servicebus.ServiceBusProcessorClient; +import com.azure.messaging.servicebus.ServiceBusReceivedMessageContext; +import com.azure.messaging.servicebus.ServiceBusSenderClient; +import com.github.dockerjava.api.model.Capability; +import org.assertj.core.api.Assertions; +import org.junit.Rule; +import org.junit.Test; +import org.testcontainers.containers.MSSQLServerContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.utility.MountableFile; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +public class AzureServiceBusContainerTest { + + @Rule + // network { + public Network network = Network.newNetwork(); + // } + + @Rule + // sqlContainer { + public MSSQLServerContainer mssqlServerContainer = new MSSQLServerContainer<>( + "mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04" + ) + .acceptLicense() + .withPassword("yourStrong(!)Password") + .withCreateContainerCmdModifier(cmd -> { + cmd.getHostConfig().withCapAdd(Capability.SYS_PTRACE); + }) + .withNetwork(network); + // } + + @Rule + // emulatorContainer { + public AzureServiceBusContainer emulator = new AzureServiceBusContainer( + "mcr.microsoft.com/azure-messaging/servicebus-emulator:1.0.1" + ) + .acceptLicense() + .withConfig(MountableFile.forClasspathResource("/service-bus-config.json")) + .withNetwork(network) + .withMsSqlServerContainer(mssqlServerContainer); + // } + + @Test + public void testWithClient() { + assertThat(emulator.getConnectionString()).startsWith("Endpoint=sb://"); + + // senderClient { + ServiceBusSenderClient senderClient = new ServiceBusClientBuilder() + .connectionString(emulator.getConnectionString()) + .sender() + .queueName("queue.1") + .buildClient(); + // } + + await() + .atMost(20, TimeUnit.SECONDS) + .ignoreException(ServiceBusException.class) + .until(() -> { + senderClient.sendMessage(new ServiceBusMessage("Hello, Testcontainers!")); + return true; + }); + senderClient.close(); + + final List received = new CopyOnWriteArrayList<>(); + Consumer messageConsumer = m -> { + received.add(m.getMessage().getBody().toString()); + m.complete(); + }; + Consumer errorConsumer = e -> Assertions.fail("Unexpected error: " + e); + // processorClient { + ServiceBusProcessorClient processorClient = new ServiceBusClientBuilder() + .connectionString(emulator.getConnectionString()) + .processor() + .queueName("queue.1") + .processMessage(messageConsumer) + .processError(errorConsumer) + .buildProcessorClient(); + // } + processorClient.start(); + + await() + .atMost(20, TimeUnit.SECONDS) + .untilAsserted(() -> { + assertThat(received).hasSize(1).containsExactlyInAnyOrder("Hello, Testcontainers!"); + }); + processorClient.close(); + } +} diff --git a/modules/azure/src/test/resources/service-bus-config.json b/modules/azure/src/test/resources/service-bus-config.json new file mode 100644 index 00000000000..18ac2e69c7b --- /dev/null +++ b/modules/azure/src/test/resources/service-bus-config.json @@ -0,0 +1,29 @@ +{ + "UserConfig": { + "Namespaces": [ + { + "Name": "sbemulatorns", + "Queues": [ + { + "Name": "queue.1", + "Properties": { + "DeadLetteringOnMessageExpiration": false, + "DefaultMessageTimeToLive": "PT1H", + "DuplicateDetectionHistoryTimeWindow": "PT20S", + "ForwardDeadLetteredMessagesTo": "", + "ForwardTo": "", + "LockDuration": "PT1M", + "MaxDeliveryCount": 3, + "RequiresDuplicateDetection": false, + "RequiresSession": false + } + } + ], + "Topics": [] + } + ], + "Logging": { + "Type": "File" + } + } +}