diff --git a/servicebus/spring-cloud-azure-stream-binder-servicebus/servicebus-queue-binder/pom.xml b/servicebus/spring-cloud-azure-stream-binder-servicebus/servicebus-queue-binder/pom.xml index b5fb365be..82adf58f8 100644 --- a/servicebus/spring-cloud-azure-stream-binder-servicebus/servicebus-queue-binder/pom.xml +++ b/servicebus/spring-cloud-azure-stream-binder-servicebus/servicebus-queue-binder/pom.xml @@ -21,7 +21,7 @@ 17 17 - 7.0.0 + 7.1.0-beta.1 @@ -60,6 +60,46 @@ org.hibernate.validator hibernate-validator + + 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 + + + org.springframework.boot + spring-boot-testcontainers + test + + + org.testcontainers + testcontainers-azure + test + + + org.testcontainers + testcontainers-junit-jupiter + test + + + com.azure.spring + spring-cloud-azure-testcontainers + test + + + com.microsoft.sqlserver + mssql-jdbc + test + diff --git a/servicebus/spring-cloud-azure-stream-binder-servicebus/servicebus-queue-binder/src/test/java/com/azure/spring/sample/servicebus/queue/binder/ServiceBusDockerComposeTest.java b/servicebus/spring-cloud-azure-stream-binder-servicebus/servicebus-queue-binder/src/test/java/com/azure/spring/sample/servicebus/queue/binder/ServiceBusDockerComposeTest.java new file mode 100644 index 000000000..66e9c6771 --- /dev/null +++ b/servicebus/spring-cloud-azure-stream-binder-servicebus/servicebus-queue-binder/src/test/java/com/azure/spring/sample/servicebus/queue/binder/ServiceBusDockerComposeTest.java @@ -0,0 +1,94 @@ +package com.azure.spring.sample.servicebus.queue.binder; + +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 messageSequence = 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 = messageSequence.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(); + } + }; + } + } +} + + diff --git a/servicebus/spring-cloud-azure-stream-binder-servicebus/servicebus-queue-binder/src/test/java/com/azure/spring/sample/servicebus/queue/binder/ServiceBusTestContainerTest.java b/servicebus/spring-cloud-azure-stream-binder-servicebus/servicebus-queue-binder/src/test/java/com/azure/spring/sample/servicebus/queue/binder/ServiceBusTestContainerTest.java new file mode 100644 index 000000000..47c489d99 --- /dev/null +++ b/servicebus/spring-cloud-azure-stream-binder-servicebus/servicebus-queue-binder/src/test/java/com/azure/spring/sample/servicebus/queue/binder/ServiceBusTestContainerTest.java @@ -0,0 +1,117 @@ +package com.azure.spring.sample.servicebus.queue.binder; + +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(ServiceBusDockerComposeTest.class); + private static final Set RECEIVED_MESSAGES = ConcurrentHashMap.newKeySet(); + private static final AtomicInteger messageSequence = 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 = messageSequence.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(); + } + }; + } + } +} + diff --git a/servicebus/spring-cloud-azure-stream-binder-servicebus/servicebus-queue-binder/src/test/resources/Config.json b/servicebus/spring-cloud-azure-stream-binder-servicebus/servicebus-queue-binder/src/test/resources/Config.json new file mode 100644 index 000000000..45c368034 --- /dev/null +++ b/servicebus/spring-cloud-azure-stream-binder-servicebus/servicebus-queue-binder/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/servicebus/spring-cloud-azure-stream-binder-servicebus/servicebus-queue-binder/src/test/resources/application.yaml b/servicebus/spring-cloud-azure-stream-binder-servicebus/servicebus-queue-binder/src/test/resources/application.yaml new file mode 100644 index 000000000..2ab1a8377 --- /dev/null +++ b/servicebus/spring-cloud-azure-stream-binder-servicebus/servicebus-queue-binder/src/test/resources/application.yaml @@ -0,0 +1,24 @@ +spring: + cloud: + azure: + servicebus: + namespace: + function: + definition: consume;supply + stream: + bindings: + consume-in-0: + destination: + supply-out-0: + destination: + servicebus: + bindings: + consume-in-0: + consumer: + auto-complete: false + supply-out-0: + producer: + entity-type: queue + poller: + fixed-delay: 1000 + initial-delay: 0 diff --git a/servicebus/spring-cloud-azure-stream-binder-servicebus/servicebus-queue-binder/src/test/resources/servicebus-compose.yaml b/servicebus/spring-cloud-azure-stream-binder-servicebus/servicebus-queue-binder/src/test/resources/servicebus-compose.yaml new file mode 100644 index 000000000..bda36d124 --- /dev/null +++ b/servicebus/spring-cloud-azure-stream-binder-servicebus/servicebus-queue-binder/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/servicebus/spring-messaging-azure-servicebus/servicebus-spring-messaging/pom.xml b/servicebus/spring-messaging-azure-servicebus/servicebus-spring-messaging/pom.xml index e4195999b..f6072430a 100644 --- a/servicebus/spring-messaging-azure-servicebus/servicebus-spring-messaging/pom.xml +++ b/servicebus/spring-messaging-azure-servicebus/servicebus-spring-messaging/pom.xml @@ -22,7 +22,7 @@ 17 17 - 7.0.0 + 7.1.0-beta.1 @@ -46,6 +46,43 @@ com.azure.spring spring-cloud-azure-starter + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-testcontainers + test + + + org.testcontainers + azure + 1.21.3 + test + + + org.testcontainers + junit-jupiter + 1.21.3 + test + + + com.azure.spring + spring-cloud-azure-docker-compose + test + + + com.azure.spring + spring-cloud-azure-testcontainers + test + + + com.microsoft.sqlserver + mssql-jdbc + test + diff --git a/servicebus/spring-messaging-azure-servicebus/servicebus-spring-messaging/src/test/java/com/azure/spring/sample/servicebus/messaging/ServiceBusDockerComposeTest.java b/servicebus/spring-messaging-azure-servicebus/servicebus-spring-messaging/src/test/java/com/azure/spring/sample/servicebus/messaging/ServiceBusDockerComposeTest.java new file mode 100644 index 000000000..d274ff9fe --- /dev/null +++ b/servicebus/spring-messaging-azure-servicebus/servicebus-spring-messaging/src/test/java/com/azure/spring/sample/servicebus/messaging/ServiceBusDockerComposeTest.java @@ -0,0 +1,109 @@ +package com.azure.spring.sample.servicebus.messaging; + + +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(); + }); + + 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) -> { + }; + } + } +} diff --git a/servicebus/spring-messaging-azure-servicebus/servicebus-spring-messaging/src/test/java/com/azure/spring/sample/servicebus/messaging/ServiceBusTestContainerTest.java b/servicebus/spring-messaging-azure-servicebus/servicebus-spring-messaging/src/test/java/com/azure/spring/sample/servicebus/messaging/ServiceBusTestContainerTest.java new file mode 100644 index 000000000..65c8ea871 --- /dev/null +++ b/servicebus/spring-messaging-azure-servicebus/servicebus-spring-messaging/src/test/java/com/azure/spring/sample/servicebus/messaging/ServiceBusTestContainerTest.java @@ -0,0 +1,125 @@ +package com.azure.spring.sample.servicebus.messaging; + +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(); + }); + + 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) -> { + }; + } + + } +} + diff --git a/servicebus/spring-messaging-azure-servicebus/servicebus-spring-messaging/src/test/resources/Config.json b/servicebus/spring-messaging-azure-servicebus/servicebus-spring-messaging/src/test/resources/Config.json new file mode 100644 index 000000000..256f4fbc4 --- /dev/null +++ b/servicebus/spring-messaging-azure-servicebus/servicebus-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" + } + } +} diff --git a/servicebus/spring-messaging-azure-servicebus/servicebus-spring-messaging/src/test/resources/servicebus-compose.yaml b/servicebus/spring-messaging-azure-servicebus/servicebus-spring-messaging/src/test/resources/servicebus-compose.yaml new file mode 100644 index 000000000..54c765b69 --- /dev/null +++ b/servicebus/spring-messaging-azure-servicebus/servicebus-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: diff --git a/storage/spring-cloud-azure-starter-storage-blob/storage-blob-sample/pom.xml b/storage/spring-cloud-azure-starter-storage-blob/storage-blob-sample/pom.xml index 5412de0bf..29805b40e 100644 --- a/storage/spring-cloud-azure-starter-storage-blob/storage-blob-sample/pom.xml +++ b/storage/spring-cloud-azure-starter-storage-blob/storage-blob-sample/pom.xml @@ -21,7 +21,7 @@ 17 17 - 7.0.0 + 7.1.0-beta.1 @@ -45,6 +45,38 @@ org.springframework.boot spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-testcontainers + test + + + com.azure.spring + spring-cloud-azure-docker-compose + test + + + org.testcontainers + azure + 1.21.3 + test + + + org.testcontainers + junit-jupiter + 1.21.3 + test + + + com.azure.spring + spring-cloud-azure-testcontainers + test + diff --git a/storage/spring-cloud-azure-starter-storage-blob/storage-blob-sample/src/test/java/com/azure/spring/sample/storage/resource/AzureBlobResourceDockerComposeTest.java b/storage/spring-cloud-azure-starter-storage-blob/storage-blob-sample/src/test/java/com/azure/spring/sample/storage/resource/AzureBlobResourceDockerComposeTest.java new file mode 100644 index 000000000..7a487a6ab --- /dev/null +++ b/storage/spring-cloud-azure-starter-storage-blob/storage-blob-sample/src/test/java/com/azure/spring/sample/storage/resource/AzureBlobResourceDockerComposeTest.java @@ -0,0 +1,48 @@ +package com.azure.spring.sample.storage.resource; + +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 test() 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 { + } +} diff --git a/storage/spring-cloud-azure-starter-storage-blob/storage-blob-sample/src/test/java/com/azure/spring/sample/storage/resource/AzureBlobResourceTestContainerTest.java b/storage/spring-cloud-azure-starter-storage-blob/storage-blob-sample/src/test/java/com/azure/spring/sample/storage/resource/AzureBlobResourceTestContainerTest.java new file mode 100644 index 000000000..6cecc73df --- /dev/null +++ b/storage/spring-cloud-azure-starter-storage-blob/storage-blob-sample/src/test/java/com/azure/spring/sample/storage/resource/AzureBlobResourceTestContainerTest.java @@ -0,0 +1,52 @@ +package com.azure.spring.sample.storage.resource; + +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.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.SpringJUnitConfig; +import org.springframework.util.StreamUtils; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringJUnitConfig +@Testcontainers +class AzureBlobResourceTestContainerTest { + @Container + @ServiceConnection + private static final GenericContainer AZURITE_CONTAINER = new GenericContainer<>( + "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; + + @Test + void test() 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 { + } +} diff --git a/storage/spring-cloud-azure-starter-storage-blob/storage-blob-sample/src/test/resources/application.yml b/storage/spring-cloud-azure-starter-storage-blob/storage-blob-sample/src/test/resources/application.yml new file mode 100644 index 000000000..854afc1c9 --- /dev/null +++ b/storage/spring-cloud-azure-starter-storage-blob/storage-blob-sample/src/test/resources/application.yml @@ -0,0 +1,10 @@ +# Storage account name length should be between 3 and 24 and use numbers and lower-case letters only +# Configuration +spring: + cloud: + azure: + storage: + blob: + account-name: + container-name: + diff --git a/storage/spring-cloud-azure-starter-storage-blob/storage-blob-sample/src/test/resources/storage-compose.yaml b/storage/spring-cloud-azure-starter-storage-blob/storage-blob-sample/src/test/resources/storage-compose.yaml new file mode 100644 index 000000000..7a44d5069 --- /dev/null +++ b/storage/spring-cloud-azure-starter-storage-blob/storage-blob-sample/src/test/resources/storage-compose.yaml @@ -0,0 +1,7 @@ +services: + storage: + image: mcr.microsoft.com/azure-storage/azurite:latest + ports: + - '10000' + - '10001' + 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