diff --git a/spring-cloud-aws-sns/src/test/java/io/awspring/cloud/sns/LocalstackContainerTest.java b/spring-cloud-aws-sns/src/test/java/io/awspring/cloud/sns/LocalstackContainerTest.java new file mode 100644 index 000000000..1959400cd --- /dev/null +++ b/spring-cloud-aws-sns/src/test/java/io/awspring/cloud/sns/LocalstackContainerTest.java @@ -0,0 +1,72 @@ +/* + * Copyright 2013-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.sns; + +import org.junit.jupiter.api.BeforeAll; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.localstack.LocalStackContainer; +import org.testcontainers.utility.DockerImageName; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.awscore.client.builder.AwsClientBuilder; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.sns.SnsClient; +import software.amazon.awssdk.services.sqs.SqsClient; + +/** + * The base contract for JUnit tests based on the container for Localstack. The Testcontainers 'reuse' option must be + * disabled, so Ryuk container is started and will clean all the containers up from this test suite after JVM exit. + * Since the Localstack container instance is shared via static property, it is going to be started only once per JVM; + * therefore, the target Docker container is reused automatically. + * + * @author haroya01 + * + * @since 4.0 + */ +@Testcontainers(disabledWithoutDocker = true) +public interface LocalstackContainerTest { + + LocalStackContainer LOCAL_STACK_CONTAINER = new LocalStackContainer( + DockerImageName.parse("localstack/localstack:4.4.0")) + .withEnv("DEBUG", "1"); + + @BeforeAll + static void startContainer() { + LOCAL_STACK_CONTAINER.start(); + } + + static SnsClient snsClient() { + return applyAwsClientOptions(SnsClient.builder()); + } + + static SqsClient sqsClient() { + return applyAwsClientOptions(SqsClient.builder()); + } + + static AwsCredentialsProvider credentialsProvider() { + return StaticCredentialsProvider.create( + AwsBasicCredentials.create(LOCAL_STACK_CONTAINER.getAccessKey(), LOCAL_STACK_CONTAINER.getSecretKey())); + } + + private static , T> T applyAwsClientOptions(B clientBuilder) { + return clientBuilder.region(Region.of(LOCAL_STACK_CONTAINER.getRegion())) + .credentialsProvider(credentialsProvider()) + .endpointOverride(LOCAL_STACK_CONTAINER.getEndpoint()) + .build(); + } + +} diff --git a/spring-cloud-aws-sns/src/test/java/io/awspring/cloud/sns/core/batch/SnsBatchTemplateIntegrationTest.java b/spring-cloud-aws-sns/src/test/java/io/awspring/cloud/sns/core/batch/SnsBatchTemplateIntegrationTest.java index e541da752..ec65e86c0 100644 --- a/spring-cloud-aws-sns/src/test/java/io/awspring/cloud/sns/core/batch/SnsBatchTemplateIntegrationTest.java +++ b/spring-cloud-aws-sns/src/test/java/io/awspring/cloud/sns/core/batch/SnsBatchTemplateIntegrationTest.java @@ -15,6 +15,10 @@ */ package io.awspring.cloud.sns.core.batch; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import io.awspring.cloud.sns.LocalstackContainerTest; import io.awspring.cloud.sns.Person; import io.awspring.cloud.sns.core.CachingTopicArnResolver; import io.awspring.cloud.sns.core.DefaultTopicArnResolver; @@ -23,6 +27,12 @@ import io.awspring.cloud.sns.core.TopicArnResolver; import io.awspring.cloud.sns.core.batch.converter.DefaultSnsMessageConverter; import io.awspring.cloud.sns.core.batch.executor.SequentialBatchExecutionStrategy; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Nested; @@ -30,13 +40,6 @@ import org.springframework.messaging.Message; import org.springframework.messaging.converter.JacksonJsonMessageConverter; import org.springframework.messaging.support.MessageBuilder; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; -import org.testcontainers.localstack.LocalStackContainer; -import org.testcontainers.utility.DockerImageName; -import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; -import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; -import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.sns.SnsClient; import software.amazon.awssdk.services.sns.model.CreateTopicRequest; import software.amazon.awssdk.services.sqs.SqsClient; @@ -47,32 +50,19 @@ import tools.jackson.databind.JsonNode; import tools.jackson.databind.json.JsonMapper; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; - /** * Integration tests for {@link SnsBatchTemplate} * * @author Matej Nedic + * @author haroya01 */ -@Testcontainers -class SnsBatchTemplateIntegrationTest { +class SnsBatchTemplateIntegrationTest implements LocalstackContainerTest { public static final String BATCH_TEST_TOPIC = "batch-test-topic"; public static final String BATCH_TEST_QUEUE = "batch-test-queue"; public static final String BATCH_TEST_TOPIC_FIFO = "batch-test-topic.fifo"; public static final String BATCH_TEST_QUEUE_FIFO = "batch-test-queue.fifo"; private static final JsonMapper jsonMapper = JsonMapper.builder().build(); - @Container - static LocalStackContainer localstack = new LocalStackContainer( - DockerImageName.parse("localstack/localstack:4.4.0")); private static SqsClient sqsClient; private static SnsBatchTemplate snsBatchTemplate; private static String standardTopicArn; @@ -82,14 +72,8 @@ class SnsBatchTemplateIntegrationTest { @BeforeAll static void setUp() { - StaticCredentialsProvider credentialsProvider = StaticCredentialsProvider - .create(AwsBasicCredentials.create(localstack.getAccessKey(), localstack.getSecretKey())); - - SnsClient snsClient = SnsClient.builder().endpointOverride(localstack.getEndpoint()) - .credentialsProvider(credentialsProvider).region(Region.of(localstack.getRegion())).build(); - - sqsClient = SqsClient.builder().endpointOverride(localstack.getEndpoint()) - .credentialsProvider(credentialsProvider).region(Region.of(localstack.getRegion())).build(); + SnsClient snsClient = LocalstackContainerTest.snsClient(); + sqsClient = LocalstackContainerTest.sqsClient(); // Standard queue and Topic standardTopicArn = snsClient.createTopic(r -> r.name(BATCH_TEST_TOPIC)).topicArn(); diff --git a/spring-cloud-aws-sns/src/test/java/io/awspring/cloud/sns/integration/SnsTemplateIntegrationTest.java b/spring-cloud-aws-sns/src/test/java/io/awspring/cloud/sns/integration/SnsTemplateIntegrationTest.java index 4cd0366ac..5c879f032 100644 --- a/spring-cloud-aws-sns/src/test/java/io/awspring/cloud/sns/integration/SnsTemplateIntegrationTest.java +++ b/spring-cloud-aws-sns/src/test/java/io/awspring/cloud/sns/integration/SnsTemplateIntegrationTest.java @@ -20,6 +20,7 @@ import static org.assertj.core.api.Assertions.*; import static org.awaitility.Awaitility.await; +import io.awspring.cloud.sns.LocalstackContainerTest; import io.awspring.cloud.sns.Person; import io.awspring.cloud.sns.core.SnsTemplate; import io.awspring.cloud.sns.core.TopicNotFoundException; @@ -32,15 +33,8 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.messaging.converter.MappingJackson2MessageConverter; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; -import org.testcontainers.localstack.LocalStackContainer; import org.testcontainers.shaded.com.fasterxml.jackson.databind.JsonNode; import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper; -import org.testcontainers.utility.DockerImageName; -import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; -import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; -import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.sns.SnsClient; import software.amazon.awssdk.services.sns.model.CreateTopicRequest; import software.amazon.awssdk.services.sqs.SqsClient; @@ -53,29 +47,20 @@ * * @author Matej Nedic * @author Hardik Singh Behl + * @author haroya01 */ -@Testcontainers -class SnsTemplateIntegrationTest { +class SnsTemplateIntegrationTest implements LocalstackContainerTest { + private static final String TOPIC_NAME = "my_topic_name"; private static SnsTemplate snsTemplate; private static SnsClient snsClient; private static final ObjectMapper objectMapper = new ObjectMapper(); private static SqsClient sqsClient; - @Container - static LocalStackContainer localstack = new LocalStackContainer( - DockerImageName.parse("localstack/localstack:4.4.0")); - @BeforeAll public static void createSnsTemplate() { - snsClient = SnsClient.builder().endpointOverride(localstack.getEndpoint()) - .region(Region.of(localstack.getRegion())) - .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("noop", "noop"))) - .build(); - sqsClient = SqsClient.builder().endpointOverride(localstack.getEndpoint()) - .region(Region.of(localstack.getRegion())) - .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("noop", "noop"))) - .build(); + snsClient = LocalstackContainerTest.snsClient(); + sqsClient = LocalstackContainerTest.sqsClient(); MappingJackson2MessageConverter mappingJackson2MessageConverter = new MappingJackson2MessageConverter(); mappingJackson2MessageConverter.setSerializedPayloadClass(String.class); snsTemplate = new SnsTemplate(snsClient, mappingJackson2MessageConverter); diff --git a/spring-cloud-aws-sns/src/test/java/io/awspring/cloud/sns/sms/SnsSmsTemplateIntegrationTest.java b/spring-cloud-aws-sns/src/test/java/io/awspring/cloud/sns/sms/SnsSmsTemplateIntegrationTest.java index ea68b88d6..a2ea33df0 100644 --- a/spring-cloud-aws-sns/src/test/java/io/awspring/cloud/sns/sms/SnsSmsTemplateIntegrationTest.java +++ b/spring-cloud-aws-sns/src/test/java/io/awspring/cloud/sns/sms/SnsSmsTemplateIntegrationTest.java @@ -18,38 +18,26 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +import io.awspring.cloud.sns.LocalstackContainerTest; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.testcontainers.containers.output.OutputFrame; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; -import org.testcontainers.localstack.LocalStackContainer; -import org.testcontainers.utility.DockerImageName; -import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; -import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; -import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.sns.SnsClient; /** * Integration tests for {@link SnsSmsTemplate}. * * @author Matej Nedic + * @author haroya01 */ -@Testcontainers -class SnsSmsTemplateIntegrationTest { - private static SnsSmsTemplate snsSmsTemplate; +class SnsSmsTemplateIntegrationTest implements LocalstackContainerTest { - @Container - static LocalStackContainer localstack = new LocalStackContainer( - DockerImageName.parse("localstack/localstack:4.4.0")).withEnv("DEBUG", "1"); + private static SnsSmsTemplate snsSmsTemplate; @BeforeAll public static void createSnsTemplate() { - SnsClient snsClient = SnsClient.builder().endpointOverride(localstack.getEndpoint()) - .region(Region.of(localstack.getRegion())) - .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("noop", "noop"))) - .build(); + SnsClient snsClient = LocalstackContainerTest.snsClient(); snsSmsTemplate = new SnsSmsTemplate(snsClient); } @@ -58,7 +46,7 @@ void sendValidMessage_ToPhoneNumber() { Assertions.assertDoesNotThrow(() -> snsSmsTemplate.send("+385000000000", "Spring Cloud AWS got you covered!")); await().untilAsserted(() -> { - String logs = localstack.getLogs(OutputFrame.OutputType.STDOUT, OutputFrame.OutputType.STDERR); + String logs = LOCAL_STACK_CONTAINER.getLogs(OutputFrame.OutputType.STDOUT, OutputFrame.OutputType.STDERR); assertThat(logs).contains("Delivering SMS message to +385000000000: Spring Cloud AWS got you covered!"); }); } @@ -70,7 +58,7 @@ void sendValidMessage_ToPhoneNumber_WithAttributes() { .builder().smsType(SmsType.PROMOTIONAL).senderID("AWSPRING").maxPrice("1.00").build())); await().untilAsserted(() -> { - String logs = localstack.getLogs(OutputFrame.OutputType.STDOUT, OutputFrame.OutputType.STDERR); + String logs = LOCAL_STACK_CONTAINER.getLogs(OutputFrame.OutputType.STDOUT, OutputFrame.OutputType.STDERR); assertThat(logs).contains("Delivering SMS message to +385000000000: Spring Cloud AWS got you covered!"); }); }