diff --git a/pom.xml b/pom.xml index 13e9641d5..893b3c0f3 100644 --- a/pom.xml +++ b/pom.xml @@ -79,9 +79,19 @@ storage/spring-cloud-azure-starter-storage-file-share/storage-file-sample storage/spring-cloud-azure-starter-storage-queue/storage-queue-client storage/spring-messaging-azure-storage-queue/storage-queue-spring-messaging - testcontainers/spring-cloud-azure-testcontainers-for-cosmos-sample - testcontainers/spring-cloud-azure-testcontainers-for-storage-blob-sample - testcontainers/spring-cloud-azure-testcontainers-for-storage-queue-sample + spring-cloud-azure-docker-compose/event-hubs/client + spring-cloud-azure-docker-compose/event-hubs/spring-cloud-stream-binders + spring-cloud-azure-docker-compose/service-bus/spring-cloud-stream-binders + spring-cloud-azure-docker-compose/service-bus/spring-messaging + spring-cloud-azure-docker-compose/storage-blob + spring-cloud-azure-docker-compose/storage-queue + spring-cloud-azure-testcontainers/cosmos + spring-cloud-azure-testcontainers/event-hubs/client + spring-cloud-azure-testcontainers/event-hubs/spring-cloud-stream-binders + spring-cloud-azure-testcontainers/service-bus/spring-cloud-stream-binders + spring-cloud-azure-testcontainers/service-bus/spring-messaging + spring-cloud-azure-testcontainers/storage-blob + spring-cloud-azure-testcontainers/storage-queue diff --git a/spring-cloud-azure-docker-compose/event-hubs/client/pom.xml b/spring-cloud-azure-docker-compose/event-hubs/client/pom.xml new file mode 100644 index 000000000..bef4424bd --- /dev/null +++ b/spring-cloud-azure-docker-compose/event-hubs/client/pom.xml @@ -0,0 +1,55 @@ + + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 4.0.3 + + + + com.azure.spring + spring-cloud-azure-docker-compose-for-event-hubs-client-sample + 1.0.0 + jar + + + 17 + 17 + 7.1.0 + + + + + + com.azure.spring + spring-cloud-azure-dependencies + ${version.spring.cloud.azure} + pom + import + + + + + + + com.azure.spring + spring-cloud-azure-starter-eventhubs + + + org.springframework.boot + spring-boot-starter-test + test + + + com.azure.spring + spring-cloud-azure-docker-compose + test + + + + \ No newline at end of file diff --git a/spring-cloud-azure-docker-compose/event-hubs/client/src/test/java/EventHubsDockerComposeTest.java b/spring-cloud-azure-docker-compose/event-hubs/client/src/test/java/EventHubsDockerComposeTest.java new file mode 100644 index 000000000..943974b82 --- /dev/null +++ b/spring-cloud-azure-docker-compose/event-hubs/client/src/test/java/EventHubsDockerComposeTest.java @@ -0,0 +1,57 @@ +import com.azure.messaging.eventhubs.EventData; +import com.azure.messaging.eventhubs.EventHubProducerClient; +import com.azure.spring.cloud.autoconfigure.implementation.context.AzureGlobalPropertiesAutoConfiguration; +import com.azure.spring.cloud.autoconfigure.implementation.eventhubs.AzureEventHubsAutoConfiguration; +import com.azure.spring.cloud.autoconfigure.implementation.eventhubs.properties.AzureEventHubsConnectionDetails; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Configuration; + +import java.time.Duration; +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.waitAtMost; + +@SpringBootTest(properties = { + "spring.docker.compose.skip.in-tests=false", + "spring.docker.compose.file=classpath:eventhubs-compose.yaml", + "spring.docker.compose.stop.command=down", + "spring.docker.compose.readiness.timeout=PT5M", + "spring.cloud.azure.eventhubs.event-hub-name=eh1", + "spring.cloud.azure.eventhubs.producer.event-hub-name=eh1" +}) +class EventHubsDockerComposeTest { + + @Autowired + private AzureEventHubsConnectionDetails connectionDetails; + + @Autowired + private EventHubProducerClient producerClient; + + @Test + void connectionDetailsShouldBeProvidedByFactory() { + assertThat(connectionDetails).isNotNull(); + assertThat(connectionDetails.getConnectionString()) + .isNotBlank() + .startsWith("Endpoint=sb://"); + } + + @Test + void producerClientCanSendMessage() { + // Wait for Event Hubs emulator to be fully ready and event hub entity to be available + waitAtMost(Duration.ofSeconds(120)).pollInterval(Duration.ofSeconds(2)).untilAsserted(() -> { + EventData event = new EventData("Hello World!"); + this.producerClient.send(Collections.singletonList(event)); + }); + } + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration(classes = { + AzureGlobalPropertiesAutoConfiguration.class, + AzureEventHubsAutoConfiguration.class}) + static class Config { + } +} \ No newline at end of file diff --git a/spring-cloud-azure-docker-compose/event-hubs/client/src/test/resources/Config.json b/spring-cloud-azure-docker-compose/event-hubs/client/src/test/resources/Config.json new file mode 100644 index 000000000..4610f9a23 --- /dev/null +++ b/spring-cloud-azure-docker-compose/event-hubs/client/src/test/resources/Config.json @@ -0,0 +1,20 @@ +{ + "UserConfig": { + "NamespaceConfig": [ + { + "Type": "EventHub", + "Name": "emulatorns1", + "Entities": [ + { + "Name": "eh1", + "PartitionCount": "2", + "ConsumerGroups": [] + } + ] + } + ], + "LoggingConfig": { + "Type": "File" + } + } +} \ No newline at end of file diff --git a/spring-cloud-azure-docker-compose/event-hubs/client/src/test/resources/eventhubs-compose.yaml b/spring-cloud-azure-docker-compose/event-hubs/client/src/test/resources/eventhubs-compose.yaml new file mode 100644 index 000000000..afd48b157 --- /dev/null +++ b/spring-cloud-azure-docker-compose/event-hubs/client/src/test/resources/eventhubs-compose.yaml @@ -0,0 +1,34 @@ +services: + eventhubs: + image: mcr.microsoft.com/azure-messaging/eventhubs-emulator:latest + pull_policy: always + volumes: + # Mount the emulator configuration to the path expected by the emulator image + - "./Config.json:/Eventhubs_Emulator/ConfigFiles/Config.json" + ports: + - "5672" + environment: + # Event Hubs emulator requires external blob/metadata storage provided by azurite + BLOB_SERVER: azurite + METADATA_SERVER: azurite + ACCEPT_EULA: Y + depends_on: + - azurite + networks: + eh-emulator: + aliases: + - "eh-emulator" + + azurite: + image: "mcr.microsoft.com/azure-storage/azurite:latest" + ports: + - "10000" + - "10001" + - "10002" + networks: + eh-emulator: + aliases: + - "azurite" + +networks: + eh-emulator: \ No newline at end of file diff --git a/spring-cloud-azure-docker-compose/event-hubs/spring-cloud-stream-binders/pom.xml b/spring-cloud-azure-docker-compose/event-hubs/spring-cloud-stream-binders/pom.xml new file mode 100644 index 000000000..4eb0f3c2c --- /dev/null +++ b/spring-cloud-azure-docker-compose/event-hubs/spring-cloud-stream-binders/pom.xml @@ -0,0 +1,59 @@ + + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 4.0.3 + + + + com.azure.spring + spring-cloud-azure-docker-compose-for-event-hubs-spring-cloud-stream-sample + 1.0.0 + jar + + + 17 + 17 + 7.1.0 + + + + + + com.azure.spring + spring-cloud-azure-dependencies + ${version.spring.cloud.azure} + pom + import + + + + + + + com.azure + azure-messaging-eventhubs + + + com.azure.spring + spring-cloud-azure-stream-binder-eventhubs + + + org.springframework.boot + spring-boot-starter-test + test + + + com.azure.spring + spring-cloud-azure-docker-compose + test + + + + \ No newline at end of file diff --git a/spring-cloud-azure-docker-compose/event-hubs/spring-cloud-stream-binders/src/test/java/EventHubsDockerComposeTest.java b/spring-cloud-azure-docker-compose/event-hubs/spring-cloud-stream-binders/src/test/java/EventHubsDockerComposeTest.java new file mode 100644 index 000000000..9bc5289dc --- /dev/null +++ b/spring-cloud-azure-docker-compose/event-hubs/spring-cloud-stream-binders/src/test/java/EventHubsDockerComposeTest.java @@ -0,0 +1,112 @@ +import com.azure.messaging.eventhubs.checkpointstore.blob.BlobCheckpointStore; +import com.azure.spring.cloud.autoconfigure.implementation.context.AzureGlobalPropertiesAutoConfiguration; +import com.azure.spring.cloud.autoconfigure.implementation.eventhubs.AzureEventHubsAutoConfiguration; +import com.azure.spring.cloud.autoconfigure.implementation.eventhubs.AzureEventHubsMessagingAutoConfiguration; +import com.azure.spring.cloud.autoconfigure.implementation.storage.blob.properties.AzureStorageBlobConnectionDetails; +import com.azure.spring.messaging.checkpoint.Checkpointer; +import com.azure.storage.blob.BlobContainerAsyncClient; +import com.azure.storage.blob.BlobServiceAsyncClient; +import com.azure.storage.blob.BlobServiceClientBuilder; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; + +import java.time.Duration; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import static com.azure.spring.messaging.AzureHeaders.CHECKPOINTER; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.waitAtMost; + +@SpringBootTest(properties = { + "spring.docker.compose.skip.in-tests=false", + "spring.docker.compose.file=classpath:eventhubs-compose.yaml", + "spring.docker.compose.stop.command=down", + "spring.docker.compose.readiness.timeout=PT5M", + "spring.cloud.function.definition=consume;supply", + "spring.cloud.stream.bindings.consume-in-0.destination=eh1", + "spring.cloud.stream.bindings.consume-in-0.group=$Default", + "spring.cloud.stream.bindings.supply-out-0.destination=eh1", + "spring.cloud.stream.eventhubs.bindings.consume-in-0.consumer.checkpoint.mode=MANUAL", + "spring.cloud.stream.poller.fixed-delay=1000", + "spring.cloud.stream.poller.initial-delay=0" +}) +class EventHubsDockerComposeTest { + + private static final Logger LOGGER = LoggerFactory.getLogger(EventHubsDockerComposeTest.class); + private static final Set RECEIVED_MESSAGES = ConcurrentHashMap.newKeySet(); + private static final AtomicInteger MESSAGE_SEQUENCE = new AtomicInteger(0); + + @Test + void supplierAndConsumerShouldWorkThroughEventHub() { + waitAtMost(Duration.ofSeconds(120)) + .pollDelay(Duration.ofSeconds(2)) + .pollInterval(Duration.ofSeconds(2)) + .untilAsserted(() -> { + assertThat(RECEIVED_MESSAGES).isNotEmpty(); + LOGGER.info("✓ Test passed - Consumer received {} message(s)", RECEIVED_MESSAGES.size()); + }); + } + + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration + @ImportAutoConfiguration(classes = { + AzureGlobalPropertiesAutoConfiguration.class, + AzureEventHubsAutoConfiguration.class, + AzureEventHubsMessagingAutoConfiguration.class}) + static class Config { + + private static final String CHECKPOINT_CONTAINER_NAME = "eventhubs-checkpoint"; + + @Bean + public BlobCheckpointStore blobCheckpointStore(AzureStorageBlobConnectionDetails connectionDetails) { + BlobServiceAsyncClient blobServiceAsyncClient = new BlobServiceClientBuilder() + .connectionString(connectionDetails.getConnectionString()) + .buildAsyncClient(); + BlobContainerAsyncClient containerAsyncClient = blobServiceAsyncClient + .getBlobContainerAsyncClient(CHECKPOINT_CONTAINER_NAME); + if (Boolean.FALSE.equals(containerAsyncClient.exists().block(Duration.ofSeconds(3)))) { + containerAsyncClient.create().block(Duration.ofSeconds(3)); + } + return new BlobCheckpointStore(containerAsyncClient); + } + + @Bean + public Supplier> supply() { + return () -> { + int sequence = MESSAGE_SEQUENCE.getAndIncrement(); + String payload = "Hello world, " + sequence; + LOGGER.info("[Supplier] Invoked - message sequence: {}", sequence); + return MessageBuilder.withPayload(payload).build(); + }; + } + + @Bean + public Consumer> consume() { + return message -> { + String payload = message.getPayload(); + RECEIVED_MESSAGES.add(payload); + LOGGER.info("[Consumer] Received message: {}", payload); + + Checkpointer checkpointer = (Checkpointer) message.getHeaders().get(CHECKPOINTER); + if (checkpointer != null) { + checkpointer.success() + .doOnSuccess(s -> LOGGER.info("[Consumer] Message checkpointed")) + .doOnError(e -> LOGGER.error("[Consumer] Checkpoint failed", e)) + .block(); + } + }; + } + } +} \ No newline at end of file diff --git a/spring-cloud-azure-docker-compose/event-hubs/spring-cloud-stream-binders/src/test/resources/Config.json b/spring-cloud-azure-docker-compose/event-hubs/spring-cloud-stream-binders/src/test/resources/Config.json new file mode 100644 index 000000000..4610f9a23 --- /dev/null +++ b/spring-cloud-azure-docker-compose/event-hubs/spring-cloud-stream-binders/src/test/resources/Config.json @@ -0,0 +1,20 @@ +{ + "UserConfig": { + "NamespaceConfig": [ + { + "Type": "EventHub", + "Name": "emulatorns1", + "Entities": [ + { + "Name": "eh1", + "PartitionCount": "2", + "ConsumerGroups": [] + } + ] + } + ], + "LoggingConfig": { + "Type": "File" + } + } +} \ No newline at end of file diff --git a/spring-cloud-azure-docker-compose/event-hubs/spring-cloud-stream-binders/src/test/resources/eventhubs-compose.yaml b/spring-cloud-azure-docker-compose/event-hubs/spring-cloud-stream-binders/src/test/resources/eventhubs-compose.yaml new file mode 100644 index 000000000..021991832 --- /dev/null +++ b/spring-cloud-azure-docker-compose/event-hubs/spring-cloud-stream-binders/src/test/resources/eventhubs-compose.yaml @@ -0,0 +1,35 @@ +services: + eventhubs: + image: mcr.microsoft.com/azure-messaging/eventhubs-emulator:latest + pull_policy: always + volumes: + # Mount the emulator configuration to the path expected by the emulator image + - "./Config.json:/Eventhubs_Emulator/ConfigFiles/Config.json" + ports: + - "5672" + environment: + # Event Hubs emulator requires external blob/metadata storage provided by azurite + BLOB_SERVER: azurite + METADATA_SERVER: azurite + ACCEPT_EULA: Y + depends_on: + - azurite + networks: + eh-emulator: + aliases: + - "eh-emulator" + + azurite: + image: "mcr.microsoft.com/azure-storage/azurite:latest" + command: "azurite -l /data --blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0 --skipApiVersionCheck" + ports: + - "10000" + - "10001" + - "10002" + networks: + eh-emulator: + aliases: + - "azurite" + +networks: + eh-emulator: \ No newline at end of file diff --git a/spring-cloud-azure-docker-compose/service-bus/spring-cloud-stream-binders/pom.xml b/spring-cloud-azure-docker-compose/service-bus/spring-cloud-stream-binders/pom.xml new file mode 100644 index 000000000..514ba167d --- /dev/null +++ b/spring-cloud-azure-docker-compose/service-bus/spring-cloud-stream-binders/pom.xml @@ -0,0 +1,60 @@ + + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 4.0.3 + + + + com.azure.spring + spring-cloud-azure-docker-compose-for-service-bus-spring-cloud-stream-sample + 1.0.0 + jar + + + 17 + 17 + 7.1.0 + + + + + + com.azure.spring + spring-cloud-azure-dependencies + ${version.spring.cloud.azure} + pom + import + + + + + + + com.azure.spring + spring-cloud-azure-stream-binder-servicebus + + + org.springframework.boot + spring-boot-starter-test + test + + + com.azure.spring + spring-cloud-azure-autoconfigure + test + + + com.azure.spring + spring-cloud-azure-docker-compose + test + + + + \ No newline at end of file diff --git a/spring-cloud-azure-docker-compose/service-bus/spring-cloud-stream-binders/src/test/java/ServiceBusDockerComposeTest.java b/spring-cloud-azure-docker-compose/service-bus/spring-cloud-stream-binders/src/test/java/ServiceBusDockerComposeTest.java new file mode 100644 index 000000000..48b46c84c --- /dev/null +++ b/spring-cloud-azure-docker-compose/service-bus/spring-cloud-stream-binders/src/test/java/ServiceBusDockerComposeTest.java @@ -0,0 +1,90 @@ +import com.azure.spring.cloud.autoconfigure.implementation.context.AzureGlobalPropertiesAutoConfiguration; +import com.azure.spring.cloud.autoconfigure.implementation.servicebus.AzureServiceBusAutoConfiguration; +import com.azure.spring.cloud.autoconfigure.implementation.servicebus.AzureServiceBusMessagingAutoConfiguration; +import com.azure.spring.messaging.checkpoint.Checkpointer; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; + +import java.time.Duration; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import static com.azure.spring.messaging.AzureHeaders.CHECKPOINTER; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.waitAtMost; + +@SpringBootTest(properties = { + "spring.docker.compose.skip.in-tests=false", + "spring.docker.compose.file=classpath:servicebus-compose.yaml", + "spring.docker.compose.stop.command=down", + "spring.cloud.function.definition=consume;supply", + "spring.cloud.stream.bindings.consume-in-0.destination=queue.1", + "spring.cloud.stream.bindings.supply-out-0.destination=queue.1", + "spring.cloud.stream.servicebus.bindings.consume-in-0.consumer.auto-complete=false", + "spring.cloud.stream.servicebus.bindings.supply-out-0.producer.entity-type=queue", + "spring.cloud.stream.poller.fixed-delay=1000", + "spring.cloud.stream.poller.initial-delay=0" +}) +class ServiceBusDockerComposeTest { + + private static final Logger LOGGER = LoggerFactory.getLogger(ServiceBusDockerComposeTest.class); + private static final Set RECEIVED_MESSAGES = ConcurrentHashMap.newKeySet(); + private static final AtomicInteger MESSAGE_SEQUENCE = new AtomicInteger(0); + + @Test + void supplierAndConsumerShouldWorkThroughServiceBusQueue() { + waitAtMost(Duration.ofSeconds(60)) + .pollDelay(Duration.ofSeconds(2)) + .untilAsserted(() -> { + assertThat(RECEIVED_MESSAGES).isNotEmpty(); + LOGGER.info("✓ Test passed - Consumer received {} message(s)", RECEIVED_MESSAGES.size()); + }); + } + + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration + @ImportAutoConfiguration(classes = { + AzureGlobalPropertiesAutoConfiguration.class, + AzureServiceBusAutoConfiguration.class, + AzureServiceBusMessagingAutoConfiguration.class}) + static class Config { + + @Bean + public Supplier> supply() { + return () -> { + int sequence = MESSAGE_SEQUENCE.getAndIncrement(); + String payload = "Hello world, " + sequence; + LOGGER.info("[Supplier] Invoked - message sequence: {}", sequence); + return MessageBuilder.withPayload(payload).build(); + }; + } + + @Bean + public Consumer> consume() { + return message -> { + String payload = message.getPayload(); + RECEIVED_MESSAGES.add(payload); + LOGGER.info("[Consumer] Received message: {}", payload); + + Checkpointer checkpointer = (Checkpointer) message.getHeaders().get(CHECKPOINTER); + if (checkpointer != null) { + checkpointer.success() + .doOnSuccess(s -> LOGGER.info("[Consumer] Message checkpointed")) + .doOnError(e -> LOGGER.error("[Consumer] Checkpoint failed", e)) + .block(); + } + }; + } + } +} \ No newline at end of file diff --git a/spring-cloud-azure-docker-compose/service-bus/spring-cloud-stream-binders/src/test/resources/Config.json b/spring-cloud-azure-docker-compose/service-bus/spring-cloud-stream-binders/src/test/resources/Config.json new file mode 100644 index 000000000..45c368034 --- /dev/null +++ b/spring-cloud-azure-docker-compose/service-bus/spring-cloud-stream-binders/src/test/resources/Config.json @@ -0,0 +1,108 @@ +{ + "UserConfig": { + "Namespaces": [ + { + "Name": "sbemulatorns", + "Queues": [ + { + "Name": "queue.1", + "Properties": { + "DeadLetteringOnMessageExpiration": false, + "DefaultMessageTimeToLive": "PT1H", + "DuplicateDetectionHistoryTimeWindow": "PT20S", + "ForwardDeadLetteredMessagesTo": "", + "ForwardTo": "", + "LockDuration": "PT1M", + "MaxDeliveryCount": 10, + "RequiresDuplicateDetection": false, + "RequiresSession": false + } + } + ], + + "Topics": [ + { + "Name": "topic.1", + "Properties": { + "DefaultMessageTimeToLive": "PT1H", + "DuplicateDetectionHistoryTimeWindow": "PT20S", + "RequiresDuplicateDetection": false + }, + "Subscriptions": [ + { + "Name": "subscription.1", + "Properties": { + "DeadLetteringOnMessageExpiration": false, + "DefaultMessageTimeToLive": "PT1H", + "LockDuration": "PT1M", + "MaxDeliveryCount": 10, + "ForwardDeadLetteredMessagesTo": "", + "ForwardTo": "", + "RequiresSession": false + }, + "Rules": [ + { + "Name": "app-prop-filter-1", + "Properties": { + "FilterType": "Correlation", + "CorrelationFilter": { + "ContentType": "application/text", + "CorrelationId": "id1", + "Label": "subject1", + "MessageId": "msgid1", + "ReplyTo": "someQueue", + "ReplyToSessionId": "sessionId", + "SessionId": "session1", + "To": "xyz" + } + } + } + ] + }, + { + "Name": "subscription.2", + "Properties": { + "DeadLetteringOnMessageExpiration": false, + "DefaultMessageTimeToLive": "PT1H", + "LockDuration": "PT1M", + "MaxDeliveryCount": 10, + "ForwardDeadLetteredMessagesTo": "", + "ForwardTo": "", + "RequiresSession": false + }, + "Rules": [ + { + "Name": "user-prop-filter-1", + "Properties": { + "FilterType": "Correlation", + "CorrelationFilter": { + "Properties": { + "prop3": "value3" + } + } + } + } + ] + }, + { + "Name": "subscription.3", + "Properties": { + "DeadLetteringOnMessageExpiration": false, + "DefaultMessageTimeToLive": "PT1H", + "LockDuration": "PT1M", + "MaxDeliveryCount": 10, + "ForwardDeadLetteredMessagesTo": "", + "ForwardTo": "", + "RequiresSession": false + } + } + ] + } + ] + } + ], + "Logging": { + "Type": "File" + } + } +} \ No newline at end of file diff --git a/spring-cloud-azure-docker-compose/service-bus/spring-cloud-stream-binders/src/test/resources/servicebus-compose.yaml b/spring-cloud-azure-docker-compose/service-bus/spring-cloud-stream-binders/src/test/resources/servicebus-compose.yaml new file mode 100644 index 000000000..bda36d124 --- /dev/null +++ b/spring-cloud-azure-docker-compose/service-bus/spring-cloud-stream-binders/src/test/resources/servicebus-compose.yaml @@ -0,0 +1,30 @@ +services: + servicebus: + image: mcr.microsoft.com/azure-messaging/servicebus-emulator:latest + pull_policy: always + volumes: + - "./Config.json:/ServiceBus_Emulator/ConfigFiles/Config.json" + ports: + - "5672" + environment: + SQL_SERVER: sqledge + MSSQL_SA_PASSWORD: A_Str0ng_Required_Password + ACCEPT_EULA: Y + depends_on: + - sqledge + networks: + sb-emulator: + aliases: + - "sb-emulator" + sqledge: + image: "mcr.microsoft.com/azure-sql-edge:latest" + networks: + sb-emulator: + aliases: + - "sqledge" + environment: + ACCEPT_EULA: Y + MSSQL_SA_PASSWORD: A_Str0ng_Required_Password + +networks: + sb-emulator: \ No newline at end of file diff --git a/spring-cloud-azure-docker-compose/service-bus/spring-messaging/pom.xml b/spring-cloud-azure-docker-compose/service-bus/spring-messaging/pom.xml new file mode 100644 index 000000000..7a09085d8 --- /dev/null +++ b/spring-cloud-azure-docker-compose/service-bus/spring-messaging/pom.xml @@ -0,0 +1,59 @@ + + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 4.0.3 + + + + com.azure.spring + spring-cloud-azure-docker-compose-for-service-bus-spring-messaging-sample + 1.0.0 + jar + + + 17 + 17 + 7.1.0 + + + + + + com.azure.spring + spring-cloud-azure-dependencies + ${version.spring.cloud.azure} + pom + import + + + + + + + com.azure.spring + spring-messaging-azure-servicebus + + + com.azure.spring + spring-cloud-azure-starter + + + org.springframework.boot + spring-boot-starter-test + test + + + com.azure.spring + spring-cloud-azure-docker-compose + test + + + + \ No newline at end of file diff --git a/spring-cloud-azure-docker-compose/service-bus/spring-messaging/src/test/java/ServiceBusDockerComposeTest.java b/spring-cloud-azure-docker-compose/service-bus/spring-messaging/src/test/java/ServiceBusDockerComposeTest.java new file mode 100644 index 000000000..1e035d1ce --- /dev/null +++ b/spring-cloud-azure-docker-compose/service-bus/spring-messaging/src/test/java/ServiceBusDockerComposeTest.java @@ -0,0 +1,107 @@ +import com.azure.messaging.servicebus.ServiceBusMessage; +import com.azure.messaging.servicebus.ServiceBusSenderClient; +import com.azure.spring.cloud.autoconfigure.implementation.context.AzureGlobalPropertiesAutoConfiguration; +import com.azure.spring.cloud.autoconfigure.implementation.servicebus.AzureServiceBusAutoConfiguration; +import com.azure.spring.cloud.autoconfigure.implementation.servicebus.AzureServiceBusMessagingAutoConfiguration; +import com.azure.spring.cloud.autoconfigure.implementation.servicebus.properties.AzureServiceBusConnectionDetails; +import com.azure.spring.cloud.service.servicebus.consumer.ServiceBusErrorHandler; +import com.azure.spring.cloud.service.servicebus.consumer.ServiceBusRecordMessageListener; +import com.azure.spring.messaging.servicebus.core.ServiceBusTemplate; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.support.MessageBuilder; + +import java.time.Duration; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.waitAtMost; + +@SpringBootTest(properties = { + "spring.docker.compose.skip.in-tests=false", + "spring.docker.compose.file=classpath:servicebus-compose.yaml", + "spring.docker.compose.stop.command=down", + "spring.docker.compose.readiness.timeout=PT5M", + "spring.cloud.azure.servicebus.namespace=sbemulatorns", + "spring.cloud.azure.servicebus.entity-name=queue.1", + "spring.cloud.azure.servicebus.entity-type=queue", + "spring.cloud.azure.servicebus.producer.entity-name=queue.1", + "spring.cloud.azure.servicebus.producer.entity-type=queue", + "spring.cloud.azure.servicebus.processor.entity-name=queue.1", + "spring.cloud.azure.servicebus.processor.entity-type=queue" +}) +class ServiceBusDockerComposeTest { + + @Autowired + private AzureServiceBusConnectionDetails connectionDetails; + + @Autowired + private ServiceBusSenderClient senderClient; + + @Autowired + private ServiceBusTemplate serviceBusTemplate; + + @Test + void connectionDetailsShouldBeProvidedByFactory() { + assertThat(connectionDetails).isNotNull(); + assertThat(connectionDetails.getConnectionString()) + .isNotBlank() + .startsWith("Endpoint=sb://"); + } + + @Test + void senderClientCanSendMessage() { + // Wait for Service Bus emulator to be fully ready and queue entity to be available + // The emulator depends on SQL Edge and needs time to initialize the messaging entities + waitAtMost(Duration.ofSeconds(120)).pollInterval(Duration.ofSeconds(2)).untilAsserted(() -> { + this.senderClient.sendMessage(new ServiceBusMessage("Hello World!")); + }); + + waitAtMost(Duration.ofSeconds(30)).pollDelay(Duration.ofSeconds(5)).untilAsserted(() -> { + assertThat(Config.MESSAGES).contains("Hello World!"); + }); + } + + @Test + void serviceBusTemplateCanSendMessage() { + // Wait for Service Bus emulator to be fully ready and queue entity to be available + // The emulator depends on SQL Edge and needs time to initialize the messaging entities + waitAtMost(Duration.ofSeconds(120)).pollInterval(Duration.ofSeconds(2)).untilAsserted(() -> { + this.serviceBusTemplate.sendAsync("queue.1", + MessageBuilder.withPayload("Hello from ServiceBusTemplate!").build()).block(Duration.ofSeconds(10)); + }); + + waitAtMost(Duration.ofSeconds(30)).pollDelay(Duration.ofSeconds(5)).untilAsserted(() -> { + assertThat(Config.MESSAGES).contains("Hello from ServiceBusTemplate!"); + }); + } + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration(classes = { + AzureGlobalPropertiesAutoConfiguration.class, + AzureServiceBusAutoConfiguration.class, + AzureServiceBusMessagingAutoConfiguration.class}) + static class Config { + + private static final Set MESSAGES = ConcurrentHashMap.newKeySet(); + + @Bean + ServiceBusRecordMessageListener processMessage() { + return context -> { + MESSAGES.add(context.getMessage().getBody().toString()); + }; + } + + @Bean + ServiceBusErrorHandler errorHandler() { + // No-op error handler for tests: acknowledge errors without affecting test execution. + return (context) -> { + }; + } + } +} \ No newline at end of file diff --git a/spring-cloud-azure-docker-compose/service-bus/spring-messaging/src/test/resources/Config.json b/spring-cloud-azure-docker-compose/service-bus/spring-messaging/src/test/resources/Config.json new file mode 100644 index 000000000..45c368034 --- /dev/null +++ b/spring-cloud-azure-docker-compose/service-bus/spring-messaging/src/test/resources/Config.json @@ -0,0 +1,108 @@ +{ + "UserConfig": { + "Namespaces": [ + { + "Name": "sbemulatorns", + "Queues": [ + { + "Name": "queue.1", + "Properties": { + "DeadLetteringOnMessageExpiration": false, + "DefaultMessageTimeToLive": "PT1H", + "DuplicateDetectionHistoryTimeWindow": "PT20S", + "ForwardDeadLetteredMessagesTo": "", + "ForwardTo": "", + "LockDuration": "PT1M", + "MaxDeliveryCount": 10, + "RequiresDuplicateDetection": false, + "RequiresSession": false + } + } + ], + + "Topics": [ + { + "Name": "topic.1", + "Properties": { + "DefaultMessageTimeToLive": "PT1H", + "DuplicateDetectionHistoryTimeWindow": "PT20S", + "RequiresDuplicateDetection": false + }, + "Subscriptions": [ + { + "Name": "subscription.1", + "Properties": { + "DeadLetteringOnMessageExpiration": false, + "DefaultMessageTimeToLive": "PT1H", + "LockDuration": "PT1M", + "MaxDeliveryCount": 10, + "ForwardDeadLetteredMessagesTo": "", + "ForwardTo": "", + "RequiresSession": false + }, + "Rules": [ + { + "Name": "app-prop-filter-1", + "Properties": { + "FilterType": "Correlation", + "CorrelationFilter": { + "ContentType": "application/text", + "CorrelationId": "id1", + "Label": "subject1", + "MessageId": "msgid1", + "ReplyTo": "someQueue", + "ReplyToSessionId": "sessionId", + "SessionId": "session1", + "To": "xyz" + } + } + } + ] + }, + { + "Name": "subscription.2", + "Properties": { + "DeadLetteringOnMessageExpiration": false, + "DefaultMessageTimeToLive": "PT1H", + "LockDuration": "PT1M", + "MaxDeliveryCount": 10, + "ForwardDeadLetteredMessagesTo": "", + "ForwardTo": "", + "RequiresSession": false + }, + "Rules": [ + { + "Name": "user-prop-filter-1", + "Properties": { + "FilterType": "Correlation", + "CorrelationFilter": { + "Properties": { + "prop3": "value3" + } + } + } + } + ] + }, + { + "Name": "subscription.3", + "Properties": { + "DeadLetteringOnMessageExpiration": false, + "DefaultMessageTimeToLive": "PT1H", + "LockDuration": "PT1M", + "MaxDeliveryCount": 10, + "ForwardDeadLetteredMessagesTo": "", + "ForwardTo": "", + "RequiresSession": false + } + } + ] + } + ] + } + ], + "Logging": { + "Type": "File" + } + } +} \ No newline at end of file diff --git a/spring-cloud-azure-docker-compose/service-bus/spring-messaging/src/test/resources/servicebus-compose.yaml b/spring-cloud-azure-docker-compose/service-bus/spring-messaging/src/test/resources/servicebus-compose.yaml new file mode 100644 index 000000000..bda36d124 --- /dev/null +++ b/spring-cloud-azure-docker-compose/service-bus/spring-messaging/src/test/resources/servicebus-compose.yaml @@ -0,0 +1,30 @@ +services: + servicebus: + image: mcr.microsoft.com/azure-messaging/servicebus-emulator:latest + pull_policy: always + volumes: + - "./Config.json:/ServiceBus_Emulator/ConfigFiles/Config.json" + ports: + - "5672" + environment: + SQL_SERVER: sqledge + MSSQL_SA_PASSWORD: A_Str0ng_Required_Password + ACCEPT_EULA: Y + depends_on: + - sqledge + networks: + sb-emulator: + aliases: + - "sb-emulator" + sqledge: + image: "mcr.microsoft.com/azure-sql-edge:latest" + networks: + sb-emulator: + aliases: + - "sqledge" + environment: + ACCEPT_EULA: Y + MSSQL_SA_PASSWORD: A_Str0ng_Required_Password + +networks: + sb-emulator: \ No newline at end of file diff --git a/spring-cloud-azure-docker-compose/storage-blob/pom.xml b/spring-cloud-azure-docker-compose/storage-blob/pom.xml new file mode 100644 index 000000000..f26c82524 --- /dev/null +++ b/spring-cloud-azure-docker-compose/storage-blob/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 4.0.3 + + + + com.azure.spring + spring-cloud-azure-docker-compose-for-storage-blob-sample + 1.0.0 + jar + + + 17 + 17 + 7.1.0 + + + + + + com.azure.spring + spring-cloud-azure-dependencies + ${version.spring.cloud.azure} + pom + import + + + + + + + com.azure.spring + spring-cloud-azure-starter-storage-blob + + + org.springframework.boot + spring-boot-starter-test + test + + + com.azure.spring + spring-cloud-azure-docker-compose + test + + + + diff --git a/spring-cloud-azure-docker-compose/storage-blob/src/test/java/AzureBlobResourceDockerComposeTest.java b/spring-cloud-azure-docker-compose/storage-blob/src/test/java/AzureBlobResourceDockerComposeTest.java new file mode 100644 index 000000000..8bfe2f3de --- /dev/null +++ b/spring-cloud-azure-docker-compose/storage-blob/src/test/java/AzureBlobResourceDockerComposeTest.java @@ -0,0 +1,46 @@ +import com.azure.spring.cloud.autoconfigure.implementation.context.AzureGlobalPropertiesAutoConfiguration; +import com.azure.spring.cloud.autoconfigure.implementation.storage.blob.AzureStorageBlobAutoConfiguration; +import com.azure.spring.cloud.autoconfigure.implementation.storage.blob.AzureStorageBlobResourceAutoConfiguration; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; +import org.springframework.core.io.WritableResource; +import org.springframework.util.StreamUtils; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(properties = { + "spring.docker.compose.skip.in-tests=false", + "spring.docker.compose.file=classpath:storage-compose.yaml", + "spring.docker.compose.stop.command=down" +}) +public class AzureBlobResourceDockerComposeTest { + + @Value("azure-blob://testcontainers/message.txt") + private Resource blobFile; + + @Test + void blobResourceShouldWriteAndReadContent() throws IOException { + String originalContent = "Hello World!"; + try (OutputStream os = ((WritableResource) this.blobFile).getOutputStream()) { + os.write(originalContent.getBytes()); + } + String resultContent = StreamUtils.copyToString(this.blobFile.getInputStream(), Charset.defaultCharset()); + assertThat(resultContent).isEqualTo(originalContent); + } + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration(classes = { + AzureGlobalPropertiesAutoConfiguration.class, + AzureStorageBlobAutoConfiguration.class, + AzureStorageBlobResourceAutoConfiguration.class}) + static class Config { + } +} \ No newline at end of file diff --git a/spring-cloud-azure-docker-compose/storage-blob/src/test/resources/storage-compose.yaml b/spring-cloud-azure-docker-compose/storage-blob/src/test/resources/storage-compose.yaml new file mode 100644 index 000000000..ed3907da5 --- /dev/null +++ b/spring-cloud-azure-docker-compose/storage-blob/src/test/resources/storage-compose.yaml @@ -0,0 +1,8 @@ +services: + storage: + image: mcr.microsoft.com/azure-storage/azurite:latest + ports: + - '10000' + - '10001' + - '10002' + command: azurite -l /data --blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0 --skipApiVersionCheck \ No newline at end of file diff --git a/spring-cloud-azure-docker-compose/storage-queue/pom.xml b/spring-cloud-azure-docker-compose/storage-queue/pom.xml new file mode 100644 index 000000000..48e27d8c8 --- /dev/null +++ b/spring-cloud-azure-docker-compose/storage-queue/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 4.0.3 + + + + com.azure.spring + spring-cloud-azure-docker-compose-for-storage-queue-sample + 1.0.0 + jar + + + 17 + 17 + 7.1.0 + + + + + + com.azure.spring + spring-cloud-azure-dependencies + ${version.spring.cloud.azure} + pom + import + + + + + + + com.azure.spring + spring-cloud-azure-starter-storage-queue + test + + + org.springframework.boot + spring-boot-starter-test + test + + + com.azure.spring + spring-cloud-azure-docker-compose + test + + + + diff --git a/spring-cloud-azure-docker-compose/storage-queue/src/test/java/StorageQueueDockerComposeTest.java b/spring-cloud-azure-docker-compose/storage-queue/src/test/java/StorageQueueDockerComposeTest.java new file mode 100644 index 000000000..188ce5249 --- /dev/null +++ b/spring-cloud-azure-docker-compose/storage-queue/src/test/java/StorageQueueDockerComposeTest.java @@ -0,0 +1,39 @@ +import com.azure.spring.cloud.autoconfigure.implementation.context.AzureGlobalPropertiesAutoConfiguration; +import com.azure.spring.cloud.autoconfigure.implementation.storage.queue.AzureStorageQueueAutoConfiguration; +import com.azure.storage.queue.QueueClient; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(properties = { + "spring.docker.compose.skip.in-tests=false", + "spring.docker.compose.file=classpath:storage-compose.yaml", + "spring.docker.compose.stop.command=down", + "spring.cloud.azure.storage.queue.queue-name=devstoreaccount1/tc-queue" +}) +class StorageQueueDockerComposeTest { + + @Autowired + private QueueClient queueClient; + + @Test + void queueClientShouldSendAndReceiveMessage() { + String message = "Hello World!"; + this.queueClient.create(); + this.queueClient.sendMessage(message); + var messageItem = this.queueClient.receiveMessage(); + assertThat(messageItem.getBody().toString()).isEqualTo(message); + } + + @Configuration + @ImportAutoConfiguration(classes = { + AzureGlobalPropertiesAutoConfiguration.class, + AzureStorageQueueAutoConfiguration.class}) + static class Config { + } + +} diff --git a/spring-cloud-azure-docker-compose/storage-queue/src/test/resources/storage-compose.yaml b/spring-cloud-azure-docker-compose/storage-queue/src/test/resources/storage-compose.yaml new file mode 100644 index 000000000..f84556f7d --- /dev/null +++ b/spring-cloud-azure-docker-compose/storage-queue/src/test/resources/storage-compose.yaml @@ -0,0 +1,8 @@ +services: + storage: + image: mcr.microsoft.com/azure-storage/azurite:latest + ports: + - '10000' + - '10001' + - '10002' + command: azurite -l /data --blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0 --skipApiVersionCheck diff --git a/testcontainers/README.md b/spring-cloud-azure-testcontainers/README.md similarity index 100% rename from testcontainers/README.md rename to spring-cloud-azure-testcontainers/README.md diff --git a/testcontainers/spring-cloud-azure-testcontainers-for-cosmos-sample/pom.xml b/spring-cloud-azure-testcontainers/cosmos/pom.xml similarity index 75% rename from testcontainers/spring-cloud-azure-testcontainers-for-cosmos-sample/pom.xml rename to spring-cloud-azure-testcontainers/cosmos/pom.xml index 7b23492b2..4921f336f 100644 --- a/testcontainers/spring-cloud-azure-testcontainers-for-cosmos-sample/pom.xml +++ b/spring-cloud-azure-testcontainers/cosmos/pom.xml @@ -15,12 +15,9 @@ 1.0.0 jar - TestContainers for Azure Cosmos DB - 17 17 - 3.4.0 7.1.0 @@ -38,13 +35,8 @@ - com.azure.spring - spring-cloud-azure-starter-cosmos - test - - - com.azure.spring - spring-cloud-azure-testcontainers + org.springframework.boot + spring-boot-starter-test test @@ -53,23 +45,13 @@ test - org.testcontainers - testcontainers-azure - test - - - org.springframework.boot - spring-boot-test - test - - - org.junit.jupiter - junit-jupiter-api + com.azure.spring + spring-cloud-azure-testcontainers test - org.assertj - assertj-core + com.azure.spring + spring-cloud-azure-starter-cosmos test diff --git a/testcontainers/spring-cloud-azure-testcontainers-for-cosmos-sample/src/test/java/CosmosTestcontainersTest.java b/spring-cloud-azure-testcontainers/cosmos/src/test/java/CosmosTestcontainersTest.java similarity index 100% rename from testcontainers/spring-cloud-azure-testcontainers-for-cosmos-sample/src/test/java/CosmosTestcontainersTest.java rename to spring-cloud-azure-testcontainers/cosmos/src/test/java/CosmosTestcontainersTest.java diff --git a/spring-cloud-azure-testcontainers/event-hubs/client/pom.xml b/spring-cloud-azure-testcontainers/event-hubs/client/pom.xml new file mode 100644 index 000000000..83edd2f79 --- /dev/null +++ b/spring-cloud-azure-testcontainers/event-hubs/client/pom.xml @@ -0,0 +1,61 @@ + + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 4.0.3 + + + + com.azure.spring + spring-cloud-azure-testcontainers-for-event-hubs-client-sample + 1.0.0 + jar + + + 17 + 17 + 7.1.0 + + + + + + com.azure.spring + spring-cloud-azure-dependencies + ${version.spring.cloud.azure} + pom + import + + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.testcontainers + testcontainers-junit-jupiter + test + + + com.azure.spring + spring-cloud-azure-testcontainers + test + + + com.azure.spring + spring-cloud-azure-starter-eventhubs + test + + + + \ No newline at end of file diff --git a/spring-cloud-azure-testcontainers/event-hubs/client/src/test/java/EventHubsTestContainerTest.java b/spring-cloud-azure-testcontainers/event-hubs/client/src/test/java/EventHubsTestContainerTest.java new file mode 100644 index 000000000..e68b2ee3d --- /dev/null +++ b/spring-cloud-azure-testcontainers/event-hubs/client/src/test/java/EventHubsTestContainerTest.java @@ -0,0 +1,78 @@ +import com.azure.messaging.eventhubs.EventData; +import com.azure.messaging.eventhubs.EventHubProducerClient; +import com.azure.spring.cloud.autoconfigure.implementation.context.AzureGlobalPropertiesAutoConfiguration; +import com.azure.spring.cloud.autoconfigure.implementation.eventhubs.AzureEventHubsAutoConfiguration; +import com.azure.spring.cloud.autoconfigure.implementation.eventhubs.properties.AzureEventHubsConnectionDetails; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.testcontainers.azure.AzuriteContainer; +import org.testcontainers.azure.EventHubsEmulatorContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.MountableFile; + +import java.time.Duration; +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.waitAtMost; + +@SpringJUnitConfig +@TestPropertySource(properties = { "spring.cloud.azure.eventhubs.event-hub-name=eh1", + "spring.cloud.azure.eventhubs.producer.event-hub-name=eh1" }) +@Testcontainers +class EventHubsTestContainerTest { + + private static final Network NETWORK = Network.newNetwork(); + + private static final AzuriteContainer AZURITE = new AzuriteContainer( + "mcr.microsoft.com/azure-storage/azurite:latest") + .withNetwork(NETWORK) + .withNetworkAliases("azurite"); + + @Container + @ServiceConnection + private static final EventHubsEmulatorContainer EVENT_HUBS = new EventHubsEmulatorContainer( + "mcr.microsoft.com/azure-messaging/eventhubs-emulator:latest") + .acceptLicense() + .withCopyFileToContainer(MountableFile.forClasspathResource("Config.json"), + "/Eventhubs_Emulator/ConfigFiles/Config.json") + .withNetwork(NETWORK) + .withAzuriteContainer(AZURITE); + + @Autowired + private AzureEventHubsConnectionDetails connectionDetails; + + @Autowired + private EventHubProducerClient producerClient; + + @Test + void connectionDetailsShouldBeProvidedByFactory() { + assertThat(connectionDetails).isNotNull(); + assertThat(connectionDetails.getConnectionString()) + .isNotBlank() + .startsWith("Endpoint=sb://"); + } + + @Test + void producerClientCanSendMessage() { + // Wait for Event Hubs emulator to be fully ready and event hub entity to be available + waitAtMost(Duration.ofSeconds(120)).pollInterval(Duration.ofSeconds(2)).untilAsserted(() -> { + EventData event = new EventData("Hello World!"); + this.producerClient.send(Collections.singletonList(event)); + }); + } + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration(classes = {AzureGlobalPropertiesAutoConfiguration.class, + AzureEventHubsAutoConfiguration.class}) + static class Config { + + } +} \ No newline at end of file diff --git a/spring-cloud-azure-testcontainers/event-hubs/client/src/test/resources/Config.json b/spring-cloud-azure-testcontainers/event-hubs/client/src/test/resources/Config.json new file mode 100644 index 000000000..4610f9a23 --- /dev/null +++ b/spring-cloud-azure-testcontainers/event-hubs/client/src/test/resources/Config.json @@ -0,0 +1,20 @@ +{ + "UserConfig": { + "NamespaceConfig": [ + { + "Type": "EventHub", + "Name": "emulatorns1", + "Entities": [ + { + "Name": "eh1", + "PartitionCount": "2", + "ConsumerGroups": [] + } + ] + } + ], + "LoggingConfig": { + "Type": "File" + } + } +} \ No newline at end of file diff --git a/spring-cloud-azure-testcontainers/event-hubs/spring-cloud-stream-binders/pom.xml b/spring-cloud-azure-testcontainers/event-hubs/spring-cloud-stream-binders/pom.xml new file mode 100644 index 000000000..e1505bd62 --- /dev/null +++ b/spring-cloud-azure-testcontainers/event-hubs/spring-cloud-stream-binders/pom.xml @@ -0,0 +1,61 @@ + + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 4.0.3 + + + + com.azure.spring + spring-cloud-azure-testcontainers-for-event-hubs-spring-cloud-stream-sample + 1.0.0 + jar + + + 17 + 17 + 7.1.0 + + + + + + com.azure.spring + spring-cloud-azure-dependencies + ${version.spring.cloud.azure} + pom + import + + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.testcontainers + testcontainers-junit-jupiter + test + + + com.azure.spring + spring-cloud-azure-testcontainers + test + + + com.azure.spring + spring-cloud-azure-stream-binder-eventhubs + test + + + + \ No newline at end of file diff --git a/spring-cloud-azure-testcontainers/event-hubs/spring-cloud-stream-binders/src/test/java/EventHubsTestContainerTest.java b/spring-cloud-azure-testcontainers/event-hubs/spring-cloud-stream-binders/src/test/java/EventHubsTestContainerTest.java new file mode 100644 index 000000000..f9d54e858 --- /dev/null +++ b/spring-cloud-azure-testcontainers/event-hubs/spring-cloud-stream-binders/src/test/java/EventHubsTestContainerTest.java @@ -0,0 +1,137 @@ +import com.azure.messaging.eventhubs.checkpointstore.blob.BlobCheckpointStore; +import com.azure.spring.cloud.autoconfigure.implementation.context.AzureGlobalPropertiesAutoConfiguration; +import com.azure.spring.cloud.autoconfigure.implementation.eventhubs.AzureEventHubsAutoConfiguration; +import com.azure.spring.cloud.autoconfigure.implementation.eventhubs.AzureEventHubsMessagingAutoConfiguration; +import com.azure.spring.messaging.checkpoint.Checkpointer; +import com.azure.storage.blob.BlobContainerAsyncClient; +import com.azure.storage.blob.BlobServiceAsyncClient; +import com.azure.storage.blob.BlobServiceClientBuilder; +import com.azure.storage.blob.BlobServiceVersion; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.testcontainers.azure.AzuriteContainer; +import org.testcontainers.azure.EventHubsEmulatorContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.MountableFile; + +import java.time.Duration; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import static com.azure.spring.messaging.AzureHeaders.CHECKPOINTER; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.waitAtMost; + +@SpringJUnitConfig +@TestPropertySource(properties = { + "spring.cloud.function.definition=consume;supply", + "spring.cloud.stream.bindings.consume-in-0.destination=eh1", + "spring.cloud.stream.bindings.consume-in-0.group=$Default", + "spring.cloud.stream.bindings.supply-out-0.destination=eh1", + "spring.cloud.stream.eventhubs.bindings.consume-in-0.consumer.checkpoint.mode=MANUAL", + "spring.cloud.stream.poller.fixed-delay=1000", + "spring.cloud.stream.poller.initial-delay=0"}) +@Testcontainers +class EventHubsTestContainerTest { + + private static final Network NETWORK = Network.newNetwork(); + + private static final AzuriteContainer AZURITE = new AzuriteContainer( + "mcr.microsoft.com/azure-storage/azurite:latest") + .withCommand("azurite", "--blobHost", "0.0.0.0", "--queueHost", "0.0.0.0", "--tableHost", "0.0.0.0", + "--skipApiVersionCheck") + .withNetwork(NETWORK) + .withNetworkAliases("azurite"); + + @Container + @ServiceConnection + private static final EventHubsEmulatorContainer EVENT_HUBS = new EventHubsEmulatorContainer( + "mcr.microsoft.com/azure-messaging/eventhubs-emulator:latest") + .acceptLicense() + .withCopyFileToContainer(MountableFile.forClasspathResource("Config.json"), + "/Eventhubs_Emulator/ConfigFiles/Config.json") + .withNetwork(NETWORK) + .withAzuriteContainer(AZURITE); + + private static final Logger LOGGER = LoggerFactory.getLogger(EventHubsTestContainerTest.class); + private static final Set RECEIVED_MESSAGES = ConcurrentHashMap.newKeySet(); + private static final AtomicInteger MESSAGE_SEQUENCE = new AtomicInteger(0); + + @Test + void supplierAndConsumerShouldWorkThroughEventHubs() { + waitAtMost(Duration.ofSeconds(120)) + .pollDelay(Duration.ofSeconds(2)) + .pollInterval(Duration.ofSeconds(2)) + .untilAsserted(() -> { + assertThat(RECEIVED_MESSAGES).isNotEmpty(); + LOGGER.info("✓ Test passed - Consumer received {} message(s)", RECEIVED_MESSAGES.size()); + }); + } + + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration + @ImportAutoConfiguration(classes = { + AzureGlobalPropertiesAutoConfiguration.class, + AzureEventHubsAutoConfiguration.class, + AzureEventHubsMessagingAutoConfiguration.class}) + static class Config { + + private static final String CHECKPOINT_CONTAINER_NAME = "eventhubs-checkpoint"; + + @Bean + public BlobCheckpointStore blobCheckpointStore() { + BlobServiceAsyncClient blobServiceAsyncClient = new BlobServiceClientBuilder() + .connectionString(AZURITE.getConnectionString()) + .serviceVersion(BlobServiceVersion.V2025_01_05) + .buildAsyncClient(); + BlobContainerAsyncClient containerAsyncClient = blobServiceAsyncClient + .getBlobContainerAsyncClient(CHECKPOINT_CONTAINER_NAME); + if (Boolean.FALSE.equals(containerAsyncClient.exists().block(Duration.ofSeconds(3)))) { + containerAsyncClient.create().block(Duration.ofSeconds(3)); + } + return new BlobCheckpointStore(containerAsyncClient); + } + + @Bean + public Supplier> supply() { + return () -> { + int sequence = MESSAGE_SEQUENCE.getAndIncrement(); + String payload = "Hello world, " + sequence; + LOGGER.info("[Supplier] Invoked - message sequence: {}", sequence); + return MessageBuilder.withPayload(payload).build(); + }; + } + + @Bean + public Consumer> consume() { + return message -> { + String payload = message.getPayload(); + RECEIVED_MESSAGES.add(payload); + LOGGER.info("[Consumer] Received message: {}", payload); + + Checkpointer checkpointer = (Checkpointer) message.getHeaders().get(CHECKPOINTER); + if (checkpointer != null) { + checkpointer.success() + .doOnSuccess(s -> LOGGER.info("[Consumer] Message checkpointed")) + .doOnError(e -> LOGGER.error("[Consumer] Checkpoint failed", e)) + .block(); + } + }; + } + } +} \ No newline at end of file diff --git a/spring-cloud-azure-testcontainers/event-hubs/spring-cloud-stream-binders/src/test/resources/Config.json b/spring-cloud-azure-testcontainers/event-hubs/spring-cloud-stream-binders/src/test/resources/Config.json new file mode 100644 index 000000000..4610f9a23 --- /dev/null +++ b/spring-cloud-azure-testcontainers/event-hubs/spring-cloud-stream-binders/src/test/resources/Config.json @@ -0,0 +1,20 @@ +{ + "UserConfig": { + "NamespaceConfig": [ + { + "Type": "EventHub", + "Name": "emulatorns1", + "Entities": [ + { + "Name": "eh1", + "PartitionCount": "2", + "ConsumerGroups": [] + } + ] + } + ], + "LoggingConfig": { + "Type": "File" + } + } +} \ No newline at end of file diff --git a/spring-cloud-azure-testcontainers/service-bus/spring-cloud-stream-binders/pom.xml b/spring-cloud-azure-testcontainers/service-bus/spring-cloud-stream-binders/pom.xml new file mode 100644 index 000000000..6ff966caf --- /dev/null +++ b/spring-cloud-azure-testcontainers/service-bus/spring-cloud-stream-binders/pom.xml @@ -0,0 +1,66 @@ + + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 4.0.3 + + + + com.azure.spring + spring-cloud-azure-testcontainers-for-service-bus-spring-cloud-stream-sample + 1.0.0 + jar + + + 17 + 17 + 7.1.0 + + + + + + com.azure.spring + spring-cloud-azure-dependencies + ${version.spring.cloud.azure} + pom + import + + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.testcontainers + testcontainers-junit-jupiter + test + + + com.azure.spring + spring-cloud-azure-testcontainers + test + + + com.azure.spring + spring-cloud-azure-stream-binder-servicebus + test + + + com.microsoft.sqlserver + mssql-jdbc + test + + + + \ No newline at end of file diff --git a/spring-cloud-azure-testcontainers/service-bus/spring-cloud-stream-binders/src/test/java/ServiceBusTestContainerTest.java b/spring-cloud-azure-testcontainers/service-bus/spring-cloud-stream-binders/src/test/java/ServiceBusTestContainerTest.java new file mode 100644 index 000000000..f7e28dcce --- /dev/null +++ b/spring-cloud-azure-testcontainers/service-bus/spring-cloud-stream-binders/src/test/java/ServiceBusTestContainerTest.java @@ -0,0 +1,114 @@ +import com.azure.spring.cloud.autoconfigure.implementation.context.AzureGlobalPropertiesAutoConfiguration; +import com.azure.spring.cloud.autoconfigure.implementation.servicebus.AzureServiceBusAutoConfiguration; +import com.azure.spring.cloud.autoconfigure.implementation.servicebus.AzureServiceBusMessagingAutoConfiguration; +import com.azure.spring.messaging.checkpoint.Checkpointer; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.testcontainers.azure.ServiceBusEmulatorContainer; +import org.testcontainers.containers.MSSQLServerContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.MountableFile; + +import java.time.Duration; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import static com.azure.spring.messaging.AzureHeaders.CHECKPOINTER; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.waitAtMost; + +@SpringJUnitConfig +@TestPropertySource(properties = { + "spring.cloud.function.definition=consume;supply", + "spring.cloud.stream.bindings.consume-in-0.destination=queue.1", + "spring.cloud.stream.bindings.supply-out-0.destination=queue.1", + "spring.cloud.stream.servicebus.bindings.consume-in-0.consumer.auto-complete=false", + "spring.cloud.stream.servicebus.bindings.supply-out-0.producer.entity-type=queue", + "spring.cloud.stream.poller.fixed-delay=1000", + "spring.cloud.stream.poller.initial-delay=0"}) +@Testcontainers +class ServiceBusTestContainerTest { + + private static final Network NETWORK = Network.newNetwork(); + + private static final MSSQLServerContainer SQLSERVER = new MSSQLServerContainer<>( + "mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04") + .acceptLicense() + .withNetwork(NETWORK) + .withNetworkAliases("sqlserver"); + + @Container + @ServiceConnection + private static final ServiceBusEmulatorContainer SERVICE_BUS = new ServiceBusEmulatorContainer( + "mcr.microsoft.com/azure-messaging/servicebus-emulator:latest") + .acceptLicense() + .withCopyFileToContainer(MountableFile.forClasspathResource("Config.json"), + "/ServiceBus_Emulator/ConfigFiles/Config.json") + .withNetwork(NETWORK) + .withMsSqlServerContainer(SQLSERVER); + + private static final Logger LOGGER = LoggerFactory.getLogger(ServiceBusTestContainerTest.class); + private static final Set RECEIVED_MESSAGES = ConcurrentHashMap.newKeySet(); + private static final AtomicInteger MESSAGE_SEQUENCE = new AtomicInteger(0); + + @Test + void supplierAndConsumerShouldWorkThroughServiceBusQueue() { + waitAtMost(Duration.ofSeconds(60)) + .pollDelay(Duration.ofSeconds(2)) + .untilAsserted(() -> { + assertThat(RECEIVED_MESSAGES).isNotEmpty(); + LOGGER.info("✓ Test passed - Consumer received {} message(s)", RECEIVED_MESSAGES.size()); + }); + } + + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration + @ImportAutoConfiguration(classes = { + AzureGlobalPropertiesAutoConfiguration.class, + AzureServiceBusAutoConfiguration.class, + AzureServiceBusMessagingAutoConfiguration.class}) + static class Config { + + @Bean + public Supplier> supply() { + return () -> { + int sequence = MESSAGE_SEQUENCE.getAndIncrement(); + String payload = "Hello world, " + sequence; + LOGGER.info("[Supplier] Invoked - message sequence: {}", sequence); + return MessageBuilder.withPayload(payload).build(); + }; + } + + @Bean + public Consumer> consume() { + return message -> { + String payload = message.getPayload(); + RECEIVED_MESSAGES.add(payload); + LOGGER.info("[Consumer] Received message: {}", payload); + + Checkpointer checkpointer = (Checkpointer) message.getHeaders().get(CHECKPOINTER); + if (checkpointer != null) { + checkpointer.success() + .doOnSuccess(s -> LOGGER.info("[Consumer] Message checkpointed")) + .doOnError(e -> LOGGER.error("[Consumer] Checkpoint failed", e)) + .block(); + } + }; + } + } +} \ No newline at end of file diff --git a/spring-cloud-azure-testcontainers/service-bus/spring-cloud-stream-binders/src/test/resources/Config.json b/spring-cloud-azure-testcontainers/service-bus/spring-cloud-stream-binders/src/test/resources/Config.json new file mode 100644 index 000000000..45c368034 --- /dev/null +++ b/spring-cloud-azure-testcontainers/service-bus/spring-cloud-stream-binders/src/test/resources/Config.json @@ -0,0 +1,108 @@ +{ + "UserConfig": { + "Namespaces": [ + { + "Name": "sbemulatorns", + "Queues": [ + { + "Name": "queue.1", + "Properties": { + "DeadLetteringOnMessageExpiration": false, + "DefaultMessageTimeToLive": "PT1H", + "DuplicateDetectionHistoryTimeWindow": "PT20S", + "ForwardDeadLetteredMessagesTo": "", + "ForwardTo": "", + "LockDuration": "PT1M", + "MaxDeliveryCount": 10, + "RequiresDuplicateDetection": false, + "RequiresSession": false + } + } + ], + + "Topics": [ + { + "Name": "topic.1", + "Properties": { + "DefaultMessageTimeToLive": "PT1H", + "DuplicateDetectionHistoryTimeWindow": "PT20S", + "RequiresDuplicateDetection": false + }, + "Subscriptions": [ + { + "Name": "subscription.1", + "Properties": { + "DeadLetteringOnMessageExpiration": false, + "DefaultMessageTimeToLive": "PT1H", + "LockDuration": "PT1M", + "MaxDeliveryCount": 10, + "ForwardDeadLetteredMessagesTo": "", + "ForwardTo": "", + "RequiresSession": false + }, + "Rules": [ + { + "Name": "app-prop-filter-1", + "Properties": { + "FilterType": "Correlation", + "CorrelationFilter": { + "ContentType": "application/text", + "CorrelationId": "id1", + "Label": "subject1", + "MessageId": "msgid1", + "ReplyTo": "someQueue", + "ReplyToSessionId": "sessionId", + "SessionId": "session1", + "To": "xyz" + } + } + } + ] + }, + { + "Name": "subscription.2", + "Properties": { + "DeadLetteringOnMessageExpiration": false, + "DefaultMessageTimeToLive": "PT1H", + "LockDuration": "PT1M", + "MaxDeliveryCount": 10, + "ForwardDeadLetteredMessagesTo": "", + "ForwardTo": "", + "RequiresSession": false + }, + "Rules": [ + { + "Name": "user-prop-filter-1", + "Properties": { + "FilterType": "Correlation", + "CorrelationFilter": { + "Properties": { + "prop3": "value3" + } + } + } + } + ] + }, + { + "Name": "subscription.3", + "Properties": { + "DeadLetteringOnMessageExpiration": false, + "DefaultMessageTimeToLive": "PT1H", + "LockDuration": "PT1M", + "MaxDeliveryCount": 10, + "ForwardDeadLetteredMessagesTo": "", + "ForwardTo": "", + "RequiresSession": false + } + } + ] + } + ] + } + ], + "Logging": { + "Type": "File" + } + } +} \ No newline at end of file diff --git a/spring-cloud-azure-testcontainers/service-bus/spring-messaging/pom.xml b/spring-cloud-azure-testcontainers/service-bus/spring-messaging/pom.xml new file mode 100644 index 000000000..194ef92fe --- /dev/null +++ b/spring-cloud-azure-testcontainers/service-bus/spring-messaging/pom.xml @@ -0,0 +1,71 @@ + + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 4.0.3 + + + + com.azure.spring + spring-cloud-azure-testcontainers-for-service-bus-spring-messaging-sample + 1.0.0 + jar + + + 17 + 17 + 7.1.0 + + + + + + com.azure.spring + spring-cloud-azure-dependencies + ${version.spring.cloud.azure} + pom + import + + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.testcontainers + testcontainers-junit-jupiter + test + + + com.azure.spring + spring-cloud-azure-testcontainers + test + + + com.azure.spring + spring-cloud-azure-starter + test + + + com.azure.spring + spring-messaging-azure-servicebus + test + + + com.microsoft.sqlserver + mssql-jdbc + test + + + + \ No newline at end of file diff --git a/spring-cloud-azure-testcontainers/service-bus/spring-messaging/src/test/java/ServiceBusTestContainerTest.java b/spring-cloud-azure-testcontainers/service-bus/spring-messaging/src/test/java/ServiceBusTestContainerTest.java new file mode 100644 index 000000000..275334ca8 --- /dev/null +++ b/spring-cloud-azure-testcontainers/service-bus/spring-messaging/src/test/java/ServiceBusTestContainerTest.java @@ -0,0 +1,123 @@ +import com.azure.messaging.servicebus.ServiceBusMessage; +import com.azure.messaging.servicebus.ServiceBusSenderClient; +import com.azure.spring.cloud.autoconfigure.implementation.context.AzureGlobalPropertiesAutoConfiguration; +import com.azure.spring.cloud.autoconfigure.implementation.servicebus.AzureServiceBusAutoConfiguration; +import com.azure.spring.cloud.autoconfigure.implementation.servicebus.AzureServiceBusMessagingAutoConfiguration; +import com.azure.spring.cloud.autoconfigure.implementation.servicebus.properties.AzureServiceBusConnectionDetails; +import com.azure.spring.cloud.service.servicebus.consumer.ServiceBusErrorHandler; +import com.azure.spring.cloud.service.servicebus.consumer.ServiceBusRecordMessageListener; +import com.azure.spring.messaging.servicebus.core.ServiceBusTemplate; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.testcontainers.azure.ServiceBusEmulatorContainer; +import org.testcontainers.containers.MSSQLServerContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.MountableFile; + +import java.time.Duration; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.waitAtMost; + +@SpringJUnitConfig +@TestPropertySource(properties = { "spring.cloud.azure.servicebus.entity-name=queue.1", + "spring.cloud.azure.servicebus.entity-type=queue" }) +@Testcontainers +class ServiceBusTestContainerTest { + + private static final Network NETWORK = Network.newNetwork(); + + private static final MSSQLServerContainer SQLSERVER = new MSSQLServerContainer<>( + "mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04") + .acceptLicense() + .withNetwork(NETWORK) + .withNetworkAliases("sqlserver"); + + @Container + @ServiceConnection + private static final ServiceBusEmulatorContainer SERVICE_BUS = new ServiceBusEmulatorContainer( + "mcr.microsoft.com/azure-messaging/servicebus-emulator:latest") + .acceptLicense() + .withCopyFileToContainer(MountableFile.forClasspathResource("Config.json"), + "/ServiceBus_Emulator/ConfigFiles/Config.json") + .withNetwork(NETWORK) + .withMsSqlServerContainer(SQLSERVER); + + @Autowired + private AzureServiceBusConnectionDetails connectionDetails; + + @Autowired + private ServiceBusSenderClient senderClient; + + @Autowired + private ServiceBusTemplate serviceBusTemplate; + + @Test + void connectionDetailsShouldBeProvidedByFactory() { + assertThat(connectionDetails).isNotNull(); + assertThat(connectionDetails.getConnectionString()) + .isNotBlank() + .startsWith("Endpoint=sb://"); + } + + @Test + void senderClientCanSendMessage() { + // Wait for Service Bus emulator to be fully ready and queue entity to be available + waitAtMost(Duration.ofSeconds(120)).pollInterval(Duration.ofSeconds(2)).untilAsserted(() -> { + this.senderClient.sendMessage(new ServiceBusMessage("Hello World!")); + }); + + waitAtMost(Duration.ofSeconds(30)).untilAsserted(() -> { + assertThat(Config.MESSAGES).contains("Hello World!"); + }); + } + + @Test + void serviceBusTemplateCanSendMessage() { + // Wait for Service Bus emulator to be fully ready and queue entity to be available + waitAtMost(Duration.ofSeconds(120)).pollInterval(Duration.ofSeconds(2)).untilAsserted(() -> { + this.serviceBusTemplate.sendAsync("queue.1", + MessageBuilder.withPayload("Hello from ServiceBusTemplate!").build()).block(Duration.ofSeconds(10)); + }); + + waitAtMost(Duration.ofSeconds(30)).untilAsserted(() -> { + assertThat(Config.MESSAGES).contains("Hello from ServiceBusTemplate!"); + }); + } + + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration(classes = {AzureGlobalPropertiesAutoConfiguration.class, + AzureServiceBusAutoConfiguration.class, + AzureServiceBusMessagingAutoConfiguration.class}) + static class Config { + + private static final Set MESSAGES = ConcurrentHashMap.newKeySet(); + + @Bean + ServiceBusRecordMessageListener processMessage() { + return context -> { + MESSAGES.add(context.getMessage().getBody().toString()); + }; + } + + @Bean + ServiceBusErrorHandler errorHandler() { + // No-op error handler for tests: acknowledge errors without affecting test execution. + return (context) -> { + }; + } + + } +} \ No newline at end of file diff --git a/spring-cloud-azure-testcontainers/service-bus/spring-messaging/src/test/resources/Config.json b/spring-cloud-azure-testcontainers/service-bus/spring-messaging/src/test/resources/Config.json new file mode 100644 index 000000000..45c368034 --- /dev/null +++ b/spring-cloud-azure-testcontainers/service-bus/spring-messaging/src/test/resources/Config.json @@ -0,0 +1,108 @@ +{ + "UserConfig": { + "Namespaces": [ + { + "Name": "sbemulatorns", + "Queues": [ + { + "Name": "queue.1", + "Properties": { + "DeadLetteringOnMessageExpiration": false, + "DefaultMessageTimeToLive": "PT1H", + "DuplicateDetectionHistoryTimeWindow": "PT20S", + "ForwardDeadLetteredMessagesTo": "", + "ForwardTo": "", + "LockDuration": "PT1M", + "MaxDeliveryCount": 10, + "RequiresDuplicateDetection": false, + "RequiresSession": false + } + } + ], + + "Topics": [ + { + "Name": "topic.1", + "Properties": { + "DefaultMessageTimeToLive": "PT1H", + "DuplicateDetectionHistoryTimeWindow": "PT20S", + "RequiresDuplicateDetection": false + }, + "Subscriptions": [ + { + "Name": "subscription.1", + "Properties": { + "DeadLetteringOnMessageExpiration": false, + "DefaultMessageTimeToLive": "PT1H", + "LockDuration": "PT1M", + "MaxDeliveryCount": 10, + "ForwardDeadLetteredMessagesTo": "", + "ForwardTo": "", + "RequiresSession": false + }, + "Rules": [ + { + "Name": "app-prop-filter-1", + "Properties": { + "FilterType": "Correlation", + "CorrelationFilter": { + "ContentType": "application/text", + "CorrelationId": "id1", + "Label": "subject1", + "MessageId": "msgid1", + "ReplyTo": "someQueue", + "ReplyToSessionId": "sessionId", + "SessionId": "session1", + "To": "xyz" + } + } + } + ] + }, + { + "Name": "subscription.2", + "Properties": { + "DeadLetteringOnMessageExpiration": false, + "DefaultMessageTimeToLive": "PT1H", + "LockDuration": "PT1M", + "MaxDeliveryCount": 10, + "ForwardDeadLetteredMessagesTo": "", + "ForwardTo": "", + "RequiresSession": false + }, + "Rules": [ + { + "Name": "user-prop-filter-1", + "Properties": { + "FilterType": "Correlation", + "CorrelationFilter": { + "Properties": { + "prop3": "value3" + } + } + } + } + ] + }, + { + "Name": "subscription.3", + "Properties": { + "DeadLetteringOnMessageExpiration": false, + "DefaultMessageTimeToLive": "PT1H", + "LockDuration": "PT1M", + "MaxDeliveryCount": 10, + "ForwardDeadLetteredMessagesTo": "", + "ForwardTo": "", + "RequiresSession": false + } + } + ] + } + ] + } + ], + "Logging": { + "Type": "File" + } + } +} \ No newline at end of file diff --git a/testcontainers/spring-cloud-azure-testcontainers-for-storage-blob-sample/pom.xml b/spring-cloud-azure-testcontainers/storage-blob/pom.xml similarity index 82% rename from testcontainers/spring-cloud-azure-testcontainers-for-storage-blob-sample/pom.xml rename to spring-cloud-azure-testcontainers/storage-blob/pom.xml index ad5d25528..aba8de2df 100644 --- a/testcontainers/spring-cloud-azure-testcontainers-for-storage-blob-sample/pom.xml +++ b/spring-cloud-azure-testcontainers/storage-blob/pom.xml @@ -16,8 +16,6 @@ 1.0.0 jar - TestContainers for Azure Storage Blob - 17 17 @@ -38,33 +36,23 @@ - com.azure.spring - spring-cloud-azure-starter-storage-blob - test - - - com.azure.spring - spring-cloud-azure-testcontainers - test - - - org.junit.jupiter - junit-jupiter-api + org.springframework.boot + spring-boot-starter-test test - org.springframework.boot - spring-boot-test + org.testcontainers + testcontainers-junit-jupiter test - org.assertj - assertj-core + com.azure.spring + spring-cloud-azure-testcontainers test - org.testcontainers - testcontainers-junit-jupiter + com.azure.spring + spring-cloud-azure-starter-storage-blob test diff --git a/testcontainers/spring-cloud-azure-testcontainers-for-storage-blob-sample/src/test/java/StorageBlobTestcontainersTest.java b/spring-cloud-azure-testcontainers/storage-blob/src/test/java/StorageBlobTestcontainersTest.java similarity index 63% rename from testcontainers/spring-cloud-azure-testcontainers-for-storage-blob-sample/src/test/java/StorageBlobTestcontainersTest.java rename to spring-cloud-azure-testcontainers/storage-blob/src/test/java/StorageBlobTestcontainersTest.java index c1e56840d..e438310c1 100644 --- a/testcontainers/spring-cloud-azure-testcontainers-for-storage-blob-sample/src/test/java/StorageBlobTestcontainersTest.java +++ b/spring-cloud-azure-testcontainers/storage-blob/src/test/java/StorageBlobTestcontainersTest.java @@ -1,19 +1,14 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. import com.azure.spring.cloud.autoconfigure.implementation.context.AzureGlobalPropertiesAutoConfiguration; import com.azure.spring.cloud.autoconfigure.implementation.storage.blob.AzureStorageBlobAutoConfiguration; import com.azure.spring.cloud.autoconfigure.implementation.storage.blob.AzureStorageBlobResourceAutoConfiguration; - -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.context.annotation.Configuration; import org.springframework.core.io.Resource; import org.springframework.core.io.WritableResource; -import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.util.StreamUtils; import org.testcontainers.containers.GenericContainer; import org.testcontainers.junit.jupiter.Container; @@ -22,30 +17,24 @@ import java.io.IOException; import java.io.OutputStream; import java.nio.charset.Charset; + import static org.assertj.core.api.Assertions.assertThat; -@SpringBootTest(classes = StorageBlobTestcontainersTest.class) +@SpringJUnitConfig @Testcontainers -@ExtendWith(SpringExtension.class) -@ImportAutoConfiguration(classes = { AzureGlobalPropertiesAutoConfiguration.class, AzureStorageBlobAutoConfiguration.class, AzureStorageBlobResourceAutoConfiguration.class}) -public class StorageBlobTestcontainersTest { +class StorageBlobTestcontainersTest { @Container @ServiceConnection private static final GenericContainer AZURITE_CONTAINER = new GenericContainer<>( - "mcr.microsoft.com/azure-storage/azurite:latest") - .withExposedPorts(10000) - .withCommand("azurite-blob", "--blobHost", "0.0.0.0", "--skipApiVersionCheck"); + "mcr.microsoft.com/azure-storage/azurite:latest") + .withExposedPorts(10000) + .withCommand("azurite --skipApiVersionCheck && azurite -l /data --blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0"); @Value("azure-blob://testcontainers/message.txt") private Resource blobFile; - @BeforeAll - public static void setup() { - AZURITE_CONTAINER.start(); - } - @Test - public void test() throws IOException { + void test() throws IOException { String originalContent = "Hello World!"; try (OutputStream os = ((WritableResource) this.blobFile).getOutputStream()) { os.write(originalContent.getBytes()); @@ -54,4 +43,8 @@ public void test() throws IOException { assertThat(resultContent).isEqualTo(originalContent); } -} + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration(classes = {AzureGlobalPropertiesAutoConfiguration.class, AzureStorageBlobAutoConfiguration.class, AzureStorageBlobResourceAutoConfiguration.class}) + static class Config { + } +} \ No newline at end of file diff --git a/testcontainers/spring-cloud-azure-testcontainers-for-storage-queue-sample/pom.xml b/spring-cloud-azure-testcontainers/storage-queue/pom.xml similarity index 83% rename from testcontainers/spring-cloud-azure-testcontainers-for-storage-queue-sample/pom.xml rename to spring-cloud-azure-testcontainers/storage-queue/pom.xml index 955d4c481..574876904 100644 --- a/testcontainers/spring-cloud-azure-testcontainers-for-storage-queue-sample/pom.xml +++ b/spring-cloud-azure-testcontainers/storage-queue/pom.xml @@ -15,7 +15,6 @@ spring-cloud-azure-testcontainers-for-storage-queue-sample 1.0.0 jar - TestContainers for Storage Queue 17 @@ -37,18 +36,8 @@ - com.azure.spring - spring-cloud-azure-starter-storage-queue - test - - - com.azure.spring - spring-cloud-azure-testcontainers - test - - - org.junit.jupiter - junit-jupiter-api + org.springframework.boot + spring-boot-starter-test test @@ -57,13 +46,13 @@ test - org.springframework.boot - spring-boot-test + com.azure.spring + spring-cloud-azure-testcontainers test - org.assertj - assertj-core + com.azure.spring + spring-cloud-azure-starter-storage-queue test diff --git a/testcontainers/spring-cloud-azure-testcontainers-for-storage-queue-sample/src/test/java/StorageQueueTestcontainersTest.java b/spring-cloud-azure-testcontainers/storage-queue/src/test/java/StorageQueueTestcontainersTest.java similarity index 100% rename from testcontainers/spring-cloud-azure-testcontainers-for-storage-queue-sample/src/test/java/StorageQueueTestcontainersTest.java rename to spring-cloud-azure-testcontainers/storage-queue/src/test/java/StorageQueueTestcontainersTest.java