Refactor testcontainers service connections

Update restcontainers service connections support so that
technology specific `@ServiceConnector` annotations are not longer
required.

A single `@ServiceConnector` annotation can now be used to create
all `ConnectionDetail` beans.

Closes gh-35017
pull/35031/head
Phillip Webb 2 years ago
parent 11dac5b5b7
commit 81a972af8d

@ -17,14 +17,17 @@
package org.springframework.boot.autoconfigure.service.connection;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
/**
* A registry of {@link ConnectionDetailsFactory} instances.
@ -49,30 +52,55 @@ public class ConnectionDetailsFactories {
registrations.filter(Objects::nonNull).forEach(this.registrations::add);
}
public <S> ConnectionDetails getConnectionDetails(S source) {
return getConnectionDetailsFactory(source).getConnectionDetails(source);
/**
* Return a {@link Map} of {@link ConnectionDetails} interface type to
* {@link ConnectionDetails} instance created from the factories associated with the
* given source.
* @param <S> the source type
* @param source the source
* @return a list of {@link ConnectionDetails} instances.
*/
public <S> Map<Class<?>, ConnectionDetails> getConnectionDetails(S source) {
List<Registration<S, ?>> registrations = getRegistrations(source);
Map<Class<?>, ConnectionDetails> result = new LinkedHashMap<>();
for (Registration<S, ?> registration : registrations) {
ConnectionDetails connectionDetails = registration.factory().getConnectionDetails(source);
if (connectionDetails != null) {
Class<?> connectionDetailsType = registration.connectionDetailsType();
ConnectionDetails previous = result.put(connectionDetailsType, connectionDetails);
Assert.state(previous == null, () -> "Duplicate connection details supplied for %s"
.formatted(connectionDetailsType.getName()));
}
}
return Map.copyOf(result);
}
@SuppressWarnings("unchecked")
public <S> ConnectionDetailsFactory<S, ConnectionDetails> getConnectionDetailsFactory(S source) {
<S> List<Registration<S, ?>> getRegistrations(S source) {
Class<S> sourceType = (Class<S>) source.getClass();
List<ConnectionDetailsFactory<S, ConnectionDetails>> result = new ArrayList<>();
List<Registration<S, ?>> result = new ArrayList<>();
for (Registration<?, ?> candidate : this.registrations) {
if (candidate.sourceType().isAssignableFrom(sourceType)) {
result.add((ConnectionDetailsFactory<S, ConnectionDetails>) candidate.factory());
result.add((Registration<S, ?>) candidate);
}
}
if (result.isEmpty()) {
throw new ConnectionDetailsFactoryNotFoundException(source);
}
AnnotationAwareOrderComparator.sort(result);
return (result.size() != 1) ? new CompositeConnectionDetailsFactory<>(result) : result.get(0);
result.sort(Comparator.comparing(Registration::factory, AnnotationAwareOrderComparator.INSTANCE));
return List.copyOf(result);
}
/**
* A {@link ConnectionDetailsFactory} registration.
*
* @param <S> the source type
* @param <D> the connection details type
* @param sourceType the source type
* @param connectionDetailsType the connection details type
* @param factory the factory
*/
private record Registration<S, D extends ConnectionDetails>(Class<S> sourceType, Class<D> connectionDetailsType,
record Registration<S, D extends ConnectionDetails>(Class<S> sourceType, Class<D> connectionDetailsType,
ConnectionDetailsFactory<S, D> factory) {
@SuppressWarnings("unchecked")
@ -87,37 +115,4 @@ public class ConnectionDetailsFactories {
}
/**
* Composite {@link ConnectionDetailsFactory} implementation.
*
* @param <S> the source type
*/
static class CompositeConnectionDetailsFactory<S> implements ConnectionDetailsFactory<S, ConnectionDetails> {
private final List<ConnectionDetailsFactory<S, ConnectionDetails>> delegates;
CompositeConnectionDetailsFactory(List<ConnectionDetailsFactory<S, ConnectionDetails>> delegates) {
this.delegates = delegates;
}
@Override
public ConnectionDetails getConnectionDetails(S source) {
return this.delegates.stream()
.map((delegate) -> delegate.getConnectionDetails(source))
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
}
List<ConnectionDetailsFactory<S, ConnectionDetails>> getDelegates() {
return this.delegates;
}
@Override
public String toString() {
return new ToStringCreator(this).append("delegates", this.delegates).toString();
}
}
}

@ -16,15 +16,18 @@
package org.springframework.boot.autoconfigure.service.connection;
import org.assertj.core.api.InstanceOfAssertFactories;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactories.CompositeConnectionDetailsFactory;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactories.Registration;
import org.springframework.core.Ordered;
import org.springframework.core.test.io.support.MockSpringFactoriesLoader;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link ConnectionDetailsFactories}.
@ -38,43 +41,50 @@ class ConnectionDetailsFactoriesTests {
private final MockSpringFactoriesLoader loader = new MockSpringFactoriesLoader();
@Test
void getConnectionDetailsFactoryShouldThrowWhenNoFactoryForSource() {
void getConnectionDetailsWhenNoFactoryForSourceThrowsException() {
ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader);
assertThatExceptionOfType(ConnectionDetailsFactoryNotFoundException.class)
.isThrownBy(() -> factories.getConnectionDetailsFactory("source"));
.isThrownBy(() -> factories.getConnectionDetails("source"));
}
@Test
void getConnectionDetailsFactoryShouldReturnSingleFactoryWhenSourceHasOneMatch() {
void getConnectionDetailsWhenSourceHasOneMatchReturnsSingleResult() {
this.loader.addInstance(ConnectionDetailsFactory.class, new TestConnectionDetailsFactory());
ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader);
ConnectionDetailsFactory<String, ConnectionDetails> factory = factories.getConnectionDetailsFactory("source");
assertThat(factory).isInstanceOf(TestConnectionDetailsFactory.class);
Map<Class<?>, ConnectionDetails> connectionDetails = factories.getConnectionDetails("source");
assertThat(connectionDetails).hasSize(1);
assertThat(connectionDetails.get(TestConnectionDetails.class)).isInstanceOf(TestConnectionDetailsImpl.class);
}
@Test
void getConnectionDetailsWhenSourceHasMultipleMatchesReturnsMultipleResults() {
this.loader.addInstance(ConnectionDetailsFactory.class, new TestConnectionDetailsFactory(),
new OtherConnectionDetailsFactory());
ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader);
Map<Class<?>, ConnectionDetails> connectionDetails = factories.getConnectionDetails("source");
assertThat(connectionDetails).hasSize(2);
}
@Test
@SuppressWarnings("unchecked")
void getConnectionDetailsFactoryShouldReturnCompositeFactoryWhenSourceHasMultipleMatches() {
void getConnectionDetailsWhenDuplicatesThrowsException() {
this.loader.addInstance(ConnectionDetailsFactory.class, new TestConnectionDetailsFactory(),
new TestConnectionDetailsFactory());
ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader);
ConnectionDetailsFactory<String, ConnectionDetails> factory = factories.getConnectionDetailsFactory("source");
assertThat(factory).asInstanceOf(InstanceOfAssertFactories.type(CompositeConnectionDetailsFactory.class))
.satisfies((composite) -> assertThat(composite.getDelegates()).hasSize(2));
assertThatIllegalStateException().isThrownBy(() -> factories.getConnectionDetails("source"))
.withMessage("Duplicate connection details supplied for " + TestConnectionDetails.class.getName());
}
@Test
@SuppressWarnings("unchecked")
void compositeFactoryShouldHaveOrderedDelegates() {
void getRegistrationsReturnsOrderedDelegates() {
TestConnectionDetailsFactory orderOne = new TestConnectionDetailsFactory(1);
TestConnectionDetailsFactory orderTwo = new TestConnectionDetailsFactory(2);
TestConnectionDetailsFactory orderThree = new TestConnectionDetailsFactory(3);
this.loader.addInstance(ConnectionDetailsFactory.class, orderOne, orderThree, orderTwo);
ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader);
ConnectionDetailsFactory<String, ConnectionDetails> factory = factories.getConnectionDetailsFactory("source");
assertThat(factory).asInstanceOf(InstanceOfAssertFactories.type(CompositeConnectionDetailsFactory.class))
.satisfies((composite) -> assertThat(composite.getDelegates()).containsExactly(orderOne, orderTwo,
orderThree));
List<Registration<String, ?>> registrations = factories.getRegistrations("source");
assertThat(registrations.get(0).factory()).isEqualTo(orderOne);
assertThat(registrations.get(1).factory()).isEqualTo(orderTwo);
assertThat(registrations.get(2).factory()).isEqualTo(orderThree);
}
private static final class TestConnectionDetailsFactory
@ -92,7 +102,7 @@ class ConnectionDetailsFactoriesTests {
@Override
public TestConnectionDetails getConnectionDetails(String source) {
return new TestConnectionDetails();
return new TestConnectionDetailsImpl();
}
@Override
@ -102,11 +112,30 @@ class ConnectionDetailsFactoriesTests {
}
private static final class TestConnectionDetails implements ConnectionDetails {
private static final class OtherConnectionDetailsFactory
implements ConnectionDetailsFactory<String, OtherConnectionDetails> {
private TestConnectionDetails() {
@Override
public OtherConnectionDetails getConnectionDetails(String source) {
return new OtherConnectionDetailsImpl();
}
}
private interface TestConnectionDetails extends ConnectionDetails {
}
private static final class TestConnectionDetailsImpl implements TestConnectionDetails {
}
private interface OtherConnectionDetails extends ConnectionDetails {
}
private static final class OtherConnectionDetailsImpl implements OtherConnectionDetails {
}
}

@ -18,12 +18,12 @@ For additional details on Spring Security's testing support, see Spring Security
[[howto.testing.testcontainers]]
=== Use Testcontainers for Integration Testing
The https://www.testcontainers.org/[Testcontainers] library provides a way to manage services running inside Docker containers.
It integrates with JUnit, allowing you to write a test class that can start up a container before any of the tests run.
Testcontainers is especially useful for writing integration tests that talk to a real backend service such as MySQL, MongoDB, Cassandra and others.
Testcontainers can be used in a Spring Boot test as follows:
include::code:vanilla/MyIntegrationTests[]
@ -32,6 +32,7 @@ This will start up a docker container running Neo4j (if Docker is running locall
In most cases, you will need to configure the application to connect to the service running in the container.
[[howto.testing.testcontainers.service-connections]]
==== Service Connections
A service connection is a connection to any remote service.
@ -42,24 +43,69 @@ When using Testcontainers, connection details can be automatically created for a
include::code:MyIntegrationTests[]
Thanks to `@Neo4jServiceConnection`, the above configuration allows Neo4j-related beans in the application to communicate with Neo4j running inside the Testcontainers-managed Docker container.
Thanks to `@ServiceConnection`, the above configuration allows Neo4j-related beans in the application to communicate with Neo4j running inside the Testcontainers-managed Docker container.
This is done by automatically defining a `Neo4jConnectionDetails` bean which is then used by the Neo4j auto-configuration, overriding any connection-related configuration properties.
The following service connection annotations are provided by `spring-boot-test-autoconfigure`:
- `@CassandraServiceConnection`
- `@CouchbaseServiceConnection`
- `@ElasticsearchServiceConnection`
- `@InfluxDbServiceConnection`
- `@JdbcServiceConnection`
- `@KafkaServiceConnection`
- `@MongoServiceConnection`
- `@Neo4jServiceConnection`
- `@R2dbcServiceConnection`
- `@RabbitServiceConnection`
- `@RedisServiceConnection`
As with the earlier `@Neo4jConnectionDetails` example, each can be used on a container field. Doing so will automatically configure the application to connect to the service running in the container.
Service connection annotations are processed by `ContainerConnectionDetailsFactory` classes registered with `spring.factories`.
A `ContainerConnectionDetailsFactory` can create a `ConnectionDetails` bean based on a specific `Container` subclass, or the Docker image name.
The following service connection factories are provided in the `spring-boot-testcontainers` jar:
|===
| Connection Details | Matched on
| `CassandraConnectionDetails`
| Containers of type `CassandraContainer`
| `CouchbaseConnectionDetails`
| Containers of type `CouchbaseContainer`
| `ElasticsearchConnectionDetails`
| Containers of type `ElasticsearchContainer`
| `FlywayConnectionDetails`
| Containers of type `JdbcDatabaseContainer`
| `InfluxDbConnectionDetails`
| Containers of type `InfluxDBContainer`
| `JdbcConnectionDetails`
| Containers of type `JdbcDatabaseContainer`
| `KafkaConnectionDetails`
| Containers of type `KafkaContainer`
| `LiquibaseConnectionDetails`
| Containers of type `JdbcDatabaseContainer`
| `MongoConnectionDetails`
| Containers of type `MongoDBContainer`
| `Neo4jConnectionDetails`
| Containers of type `Neo4jContainer`
| `R2dbcConnectionDetails`
| Containers of type `MariaDBContainer`, `MSSQLServerContainer`, `MySQLContainer` or `PostgreSQLContainer`
| `RabbitConnectionDetails`
| Containers of type `RabbitMQContainer`
| `RedisConnectionDetails`
| Containers named "redis"
|===
[TIP]
====
By default all applicable connection details beans will be created for a given `Container`.
For example, a `PostgreSQLContainer` will create both `JdbcConnectionDetails` and `R2dbcConnectionDetails`.
If you want to create only a subset of the applicable types, you can use the `type` attribute of `@ServiceConnection`.
====
By default `Container.getDockerImageName()` is used to obtain the name used to find connection details.
If you are using a custom docker image, you can use the `name` attribute of `@ServiceConnection` to override it.
For example, if you have a `GenericContainer` using a Docker image of `registry.mycompany.com/mirror/myredis`, you'd use `@ServiceConnection(name="redis")` to ensure `RedisConnectionDetails` are created.

@ -22,14 +22,14 @@ import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.neo4j.Neo4jServiceConnection;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
@SpringBootTest
@Testcontainers
class MyIntegrationTests {
@Container
@Neo4jServiceConnection
@ServiceConnection
static Neo4jContainer<?> neo4j = new Neo4jContainer<>("neo4j:5");
@Test

@ -17,14 +17,13 @@
package org.springframework.boot.docs.howto.testing.testcontainers.serviceconnection
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.testcontainers.service.connection.neo4j.Neo4jServiceConnection;
import org.springframework.test.context.DynamicPropertyRegistry
import org.springframework.test.context.DynamicPropertySource
import org.testcontainers.containers.Neo4jContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
@SpringBootTest
@Testcontainers
internal class MyIntegrationTests {
@ -37,7 +36,7 @@ internal class MyIntegrationTests {
companion object {
@Container
@Neo4jServiceConnection
@ServiceConnection
var neo4j: Neo4jContainer<*> = Neo4jContainer<Nothing>("neo4j:5")
}

@ -28,7 +28,7 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.redis.ExampleService;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.cassandra.CassandraServiceConnection;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.testcontainers.CassandraContainer;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
@ -52,7 +52,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
class DataCassandraTestIntegrationTests {
@Container
@CassandraServiceConnection
@ServiceConnection
static final CassandraContainer cassandra = new CassandraContainer();
@Autowired

@ -26,7 +26,7 @@ import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.cassandra.CassandraServiceConnection;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.testcontainers.CassandraContainer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan.Filter;
@ -51,7 +51,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class DataCassandraTestWithIncludeFilterIntegrationTests {
@Container
@CassandraServiceConnection
@ServiceConnection
static final CassandraContainer cassandra = new CassandraContainer();
@Autowired

@ -26,7 +26,7 @@ import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.testcontainers.service.connection.couchbase.CouchbaseServiceConnection;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.testcontainers.DockerImageNames;
import org.springframework.context.ApplicationContext;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
@ -50,7 +50,7 @@ class DataCouchbaseTestIntegrationTests {
private static final String BUCKET_NAME = "cbbucket";
@Container
@CouchbaseServiceConnection
@ServiceConnection
static final CouchbaseContainer couchbase = new CouchbaseContainer(DockerImageNames.couchbase())
.withStartupAttempts(5)
.withStartupTimeout(Duration.ofMinutes(10))

@ -25,7 +25,7 @@ import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.testcontainers.service.connection.couchbase.CouchbaseServiceConnection;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.testcontainers.DockerImageNames;
import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate;
@ -47,7 +47,7 @@ class DataCouchbaseTestReactiveIntegrationTests {
private static final String BUCKET_NAME = "cbbucket";
@Container
@CouchbaseServiceConnection
@ServiceConnection
static final CouchbaseContainer couchbase = new CouchbaseContainer(DockerImageNames.couchbase())
.withStartupAttempts(5)
.withStartupTimeout(Duration.ofMinutes(10))

@ -25,7 +25,7 @@ import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.testcontainers.service.connection.couchbase.CouchbaseServiceConnection;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.testcontainers.DockerImageNames;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.stereotype.Service;
@ -48,7 +48,7 @@ class DataCouchbaseTestWithIncludeFilterIntegrationTests {
private static final String BUCKET_NAME = "cbbucket";
@Container
@CouchbaseServiceConnection
@ServiceConnection
static final CouchbaseContainer couchbase = new CouchbaseContainer(DockerImageNames.couchbase())
.withStartupAttempts(5)
.withStartupTimeout(Duration.ofMinutes(10))

@ -26,7 +26,7 @@ import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.testcontainers.service.connection.elasticsearch.ElasticsearchServiceConnection;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.testcontainers.DockerImageNames;
import org.springframework.context.ApplicationContext;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchTemplate;
@ -47,7 +47,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
class DataElasticsearchTestIntegrationTests {
@Container
@ElasticsearchServiceConnection
@ServiceConnection
static final ElasticsearchContainer elasticsearch = new ElasticsearchContainer(DockerImageNames.elasticsearch())
.withStartupAttempts(5)
.withStartupTimeout(Duration.ofMinutes(10));

@ -25,7 +25,7 @@ import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.testcontainers.service.connection.elasticsearch.ElasticsearchServiceConnection;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.testcontainers.DockerImageNames;
import org.springframework.core.env.Environment;
@ -45,7 +45,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class DataElasticsearchTestPropertiesIntegrationTests {
@Container
@ElasticsearchServiceConnection
@ServiceConnection
static final ElasticsearchContainer elasticsearch = new ElasticsearchContainer(DockerImageNames.elasticsearch())
.withStartupAttempts(5)
.withStartupTimeout(Duration.ofMinutes(10));

@ -24,7 +24,7 @@ import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.testcontainers.service.connection.elasticsearch.ElasticsearchServiceConnection;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.testcontainers.DockerImageNames;
import org.springframework.data.elasticsearch.client.elc.ReactiveElasticsearchTemplate;
@ -44,7 +44,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class DataElasticsearchTestReactiveIntegrationTests {
@Container
@ElasticsearchServiceConnection
@ServiceConnection
static final ElasticsearchContainer elasticsearch = new ElasticsearchContainer(DockerImageNames.elasticsearch())
.withStartupAttempts(5)
.withStartupTimeout(Duration.ofMinutes(10));

@ -25,7 +25,7 @@ import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.testcontainers.service.connection.elasticsearch.ElasticsearchServiceConnection;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.testcontainers.DockerImageNames;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.stereotype.Service;
@ -46,7 +46,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class DataElasticsearchTestWithIncludeFilterIntegrationTests {
@Container
@ElasticsearchServiceConnection
@ServiceConnection
static final ElasticsearchContainer elasticsearch = new ElasticsearchContainer(DockerImageNames.elasticsearch())
.withStartupAttempts(5)
.withStartupTimeout(Duration.ofMinutes(10));

@ -25,7 +25,7 @@ import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.testcontainers.service.connection.mongo.MongoServiceConnection;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.testcontainers.DockerImageNames;
import org.springframework.context.ApplicationContext;
import org.springframework.data.mongodb.core.MongoTemplate;
@ -46,7 +46,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
class DataMongoTestIntegrationTests {
@Container
@MongoServiceConnection
@ServiceConnection
static final MongoDBContainer mongoDB = new MongoDBContainer(DockerImageNames.mongo()).withStartupAttempts(5)
.withStartupTimeout(Duration.ofMinutes(5));

@ -24,7 +24,7 @@ import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.testcontainers.service.connection.mongo.MongoServiceConnection;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.testcontainers.DockerImageNames;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
@ -43,7 +43,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class DataMongoTestReactiveIntegrationTests {
@Container
@MongoServiceConnection
@ServiceConnection
static final MongoDBContainer mongoDB = new MongoDBContainer(DockerImageNames.mongo()).withStartupAttempts(5)
.withStartupTimeout(Duration.ofMinutes(5));

@ -24,7 +24,7 @@ import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.testcontainers.service.connection.mongo.MongoServiceConnection;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.testcontainers.DockerImageNames;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.stereotype.Service;
@ -44,7 +44,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class DataMongoTestWithIncludeFilterIntegrationTests {
@Container
@MongoServiceConnection
@ServiceConnection
static final MongoDBContainer mongoDB = new MongoDBContainer(DockerImageNames.mongo()).withStartupAttempts(5)
.withStartupTimeout(Duration.ofMinutes(5));

@ -26,7 +26,7 @@ import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.mongo.MongoServiceConnection;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.testcontainers.DockerImageNames;
import org.springframework.context.annotation.Bean;
import org.springframework.data.mongodb.MongoDatabaseFactory;
@ -49,7 +49,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class TransactionalDataMongoTestIntegrationTests {
@Container
@MongoServiceConnection
@ServiceConnection
static final MongoDBContainer mongoDB = new MongoDBContainer(DockerImageNames.mongo()).withStartupAttempts(5)
.withStartupTimeout(Duration.ofMinutes(5));

@ -25,7 +25,7 @@ import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.testcontainers.service.connection.neo4j.Neo4jServiceConnection;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.testcontainers.DockerImageNames;
import org.springframework.context.ApplicationContext;
import org.springframework.data.neo4j.core.Neo4jTemplate;
@ -48,7 +48,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
class DataNeo4jTestIntegrationTests {
@Container
@Neo4jServiceConnection
@ServiceConnection
static final Neo4jContainer<?> neo4j = new Neo4jContainer<>(DockerImageNames.neo4j()).withStartupAttempts(5)
.withStartupTimeout(Duration.ofMinutes(10));

@ -25,7 +25,7 @@ import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.testcontainers.service.connection.neo4j.Neo4jServiceConnection;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.testcontainers.DockerImageNames;
import org.springframework.core.env.Environment;
@ -45,7 +45,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class DataNeo4jTestPropertiesIntegrationTests {
@Container
@Neo4jServiceConnection
@ServiceConnection
static final Neo4jContainer<?> neo4j = new Neo4jContainer<>(DockerImageNames.neo4j()).withoutAuthentication()
.withStartupAttempts(5)
.withStartupTimeout(Duration.ofMinutes(10));

@ -29,7 +29,7 @@ import reactor.test.StepVerifier;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.neo4j.Neo4jServiceConnection;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.testcontainers.DockerImageNames;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
@ -56,7 +56,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
class DataNeo4jTestReactiveIntegrationTests {
@Container
@Neo4jServiceConnection
@ServiceConnection
static final Neo4jContainer<?> neo4j = new Neo4jContainer<>(DockerImageNames.neo4j()).withoutAuthentication()
.withStartupAttempts(5)
.withStartupTimeout(Duration.ofMinutes(10));

@ -24,7 +24,7 @@ import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.testcontainers.service.connection.neo4j.Neo4jServiceConnection;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.testcontainers.DockerImageNames;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.stereotype.Service;
@ -45,7 +45,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class DataNeo4jTestWithIncludeFilterIntegrationTests {
@Container
@Neo4jServiceConnection
@ServiceConnection
static final Neo4jContainer<?> neo4j = new Neo4jContainer<>(DockerImageNames.neo4j()).withoutAuthentication()
.withStartupAttempts(5)
.withStartupTimeout(Duration.ofMinutes(10));

@ -25,7 +25,7 @@ import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.testcontainers.service.connection.redis.RedisServiceConnection;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.testcontainers.RedisContainer;
import org.springframework.context.ApplicationContext;
import org.springframework.data.redis.core.RedisOperations;
@ -48,7 +48,7 @@ class DataRedisTestIntegrationTests {
private static final Charset CHARSET = StandardCharsets.UTF_8;
@Container
@RedisServiceConnection
@ServiceConnection
static RedisContainer redis = new RedisContainer();
@Autowired

@ -22,7 +22,7 @@ import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.testcontainers.service.connection.redis.RedisServiceConnection;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.testcontainers.RedisContainer;
import org.springframework.core.env.Environment;
@ -42,7 +42,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class DataRedisTestPropertiesIntegrationTests {
@Container
@RedisServiceConnection
@ServiceConnection
static final RedisContainer redis = new RedisContainer();
@Autowired

@ -25,7 +25,7 @@ import reactor.test.StepVerifier;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.testcontainers.service.connection.redis.RedisServiceConnection;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.testcontainers.RedisContainer;
import org.springframework.context.ApplicationContext;
import org.springframework.data.redis.core.ReactiveRedisOperations;
@ -45,7 +45,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
class DataRedisTestReactiveIntegrationTests {
@Container
@RedisServiceConnection
@ServiceConnection
static RedisContainer redis = new RedisContainer();
@Autowired

@ -21,7 +21,7 @@ import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.testcontainers.service.connection.redis.RedisServiceConnection;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.testcontainers.RedisContainer;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.stereotype.Service;
@ -41,7 +41,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class DataRedisTestWithIncludeFilterIntegrationTests {
@Container
@RedisServiceConnection
@ServiceConnection
static final RedisContainer redis = new RedisContainer();
@Autowired

@ -1,5 +1,6 @@
plugins {
id "java-library"
id "org.springframework.boot.auto-configuration"
id "org.springframework.boot.conventions"
id "org.springframework.boot.deployed"
id "org.springframework.boot.optional-dependencies"
@ -28,6 +29,7 @@ dependencies {
optional("org.testcontainers:rabbitmq")
optional("org.testcontainers:r2dbc")
testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support"))
testImplementation("ch.qos.logback:logback-classic")
testImplementation("org.assertj:assertj-core")
testImplementation("org.awaitility:awaitility")

@ -16,9 +16,9 @@
package org.springframework.boot.testcontainers.service.connection;
import java.lang.annotation.Annotation;
import java.util.Arrays;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Container;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory;
@ -26,13 +26,13 @@ import org.springframework.boot.origin.Origin;
import org.springframework.boot.origin.OriginProvider;
import org.springframework.core.ResolvableType;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
/**
* Base class for {@link ConnectionDetailsFactory} implementations that provide
* {@link ConnectionDetails} from a {@link ContainerConnectionSource}.
*
* @param <A> the source annotation type. The annotation will be mergable to a
* {@link ServiceConnection @ServiceConnection}.
* @param <D> the connection details type
* @param <C> the generic container type
* @author Moritz Halbritter
@ -40,17 +40,58 @@ import org.springframework.util.Assert;
* @author Phillip Webb
* @since 3.1.0
*/
public abstract class ContainerConnectionDetailsFactory<A extends Annotation, D extends ConnectionDetails, C extends GenericContainer<?>>
implements ConnectionDetailsFactory<ContainerConnectionSource<A, D, C>, D> {
public abstract class ContainerConnectionDetailsFactory<D extends ConnectionDetails, C extends Container<?>>
implements ConnectionDetailsFactory<ContainerConnectionSource<D, C>, D> {
/**
* Constant passed to the constructor when any connection name is accepted.
*/
protected static final String ANY_CONNECTION_NAME = null;
private final String connectionName;
private final String[] requiredClassNames;
/**
* Create a new {@link ContainerConnectionDetailsFactory} instance that accepts
* {@link #ANY_CONNECTION_NAME any connection name}.
*/
protected ContainerConnectionDetailsFactory() {
this(ANY_CONNECTION_NAME);
}
/**
* Create a new {@link ContainerConnectionDetailsFactory} instance with the given
* connection name restriction.
* @param connectionName the required connection name or {@link #ANY_CONNECTION_NAME}
* @param requiredClassNames the names of classes that must be present
*/
protected ContainerConnectionDetailsFactory(String connectionName, String... requiredClassNames) {
this.connectionName = connectionName;
this.requiredClassNames = requiredClassNames;
}
@Override
public final D getConnectionDetails(ContainerConnectionSource<A, D, C> source) {
public final D getConnectionDetails(ContainerConnectionSource<D, C> source) {
if (!hasRequiredClasses()) {
return null;
}
try {
Class<?>[] generics = resolveGenerics();
Class<?> annotationType = generics[0];
Class<?> connectionDetailsType = generics[1];
Class<?> containerType = generics[2];
return (!source.accepts(annotationType, connectionDetailsType, containerType)) ? null
: getContainerConnectionDetails(source);
Class<?> connectionDetailsType = generics[0];
Class<?> containerType = generics[1];
if (source.accepts(this.connectionName, connectionDetailsType, containerType)) {
return getContainerConnectionDetails(source);
}
}
catch (NoClassDefFoundError ex) {
}
return null;
}
private boolean hasRequiredClasses() {
return ObjectUtils.isEmpty(this.requiredClassNames) || Arrays.stream(this.requiredClassNames)
.allMatch((requiredClassName) -> ClassUtils.isPresent(requiredClassName, null));
}
private Class<?>[] resolveGenerics() {
@ -64,7 +105,7 @@ public abstract class ContainerConnectionDetailsFactory<A extends Annotation, D
* @param source the source
* @return the service connection or {@code null}.
*/
protected abstract D getContainerConnectionDetails(ContainerConnectionSource<A, D, C> source);
protected abstract D getContainerConnectionDetails(ContainerConnectionSource<D, C> source);
/**
* Convenient base class for {@link ConnectionDetails} results that are backed by a
@ -78,7 +119,7 @@ public abstract class ContainerConnectionDetailsFactory<A extends Annotation, D
* Create a new {@link ContainerConnectionDetails} instance.
* @param source the source {@link ContainerConnectionSource}
*/
protected ContainerConnectionDetails(ContainerConnectionSource<?, ?, ?> source) {
protected ContainerConnectionDetails(ContainerConnectionSource<?, ?> source) {
Assert.notNull(source, "Source must not be null");
this.origin = source.getOrigin();
}

@ -16,23 +16,25 @@
package org.springframework.boot.testcontainers.service.connection;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.Set;
import org.testcontainers.containers.GenericContainer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.testcontainers.containers.Container;
import org.testcontainers.utility.DockerImageName;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
import org.springframework.boot.origin.Origin;
import org.springframework.boot.origin.OriginProvider;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.log.LogMessage;
import org.springframework.util.StringUtils;
/**
* Passed to {@link ContainerConnectionDetailsFactory} to provide details of the
* {@link ServiceConnection @ServiceConnection} annotation {@link GenericContainer} field
* that provides the service.
* {@link ServiceConnection @ServiceConnection} annotated {@link Container} that provides
* the service.
*
* @param <A> the source annotation type. The annotation will mergable to a
* {@link ServiceConnection @ServiceConnection}
* @param <D> the connection details type
* @param <C> the generic container type
* @author Moritz Halbritter
@ -41,58 +43,77 @@ import org.springframework.core.annotation.MergedAnnotation;
* @since 3.1.0
* @see ContainerConnectionDetailsFactory
*/
public final class ContainerConnectionSource<A extends Annotation, D extends ConnectionDetails, C extends GenericContainer<?>>
public final class ContainerConnectionSource<D extends ConnectionDetails, C extends Container<?>>
implements OriginProvider {
private final Class<D> connectionDetailsType;
private static final Log logger = LogFactory.getLog(ContainerConnectionSource.class);
private final Field field;
private final String beanNameSuffix;
private final A annotation;
private final Origin origin;
private final C container;
private final AnnotatedFieldOrigin origin;
private String acceptedConnectionName;
@SuppressWarnings("unchecked")
ContainerConnectionSource(Class<D> connectionDetailsType, Field field,
MergedAnnotation<ServiceConnection> annotation, C container) {
this(connectionDetailsType, field, (A) annotation.getRoot().synthesize(), container);
}
private Set<Class<?>> acceptedConnectionDetailsTypes;
ContainerConnectionSource(Class<D> connectionDetailsType, Field field, A annotation, C container) {
this.connectionDetailsType = connectionDetailsType;
this.field = field;
this.annotation = annotation;
ContainerConnectionSource(String beanNameSuffix, Origin origin, C container,
MergedAnnotation<ServiceConnection> annotation) {
this.beanNameSuffix = beanNameSuffix;
this.origin = origin;
this.container = container;
this.origin = new AnnotatedFieldOrigin(field, annotation);
this.acceptedConnectionName = getConnectionName(container, annotation.getString("name"));
this.acceptedConnectionDetailsTypes = Set.of(annotation.getClassArray("type"));
}
boolean accepts(Class<?> annotationType, Class<?> connectionDetailsType, Class<?> containerType) {
return annotationType.isInstance(this.annotation)
&& connectionDetailsType.isAssignableFrom(this.connectionDetailsType)
&& containerType.isInstance(this.container);
ContainerConnectionSource(String beanNameSuffix, Origin origin, C container, ServiceConnection annotation) {
this.beanNameSuffix = beanNameSuffix;
this.origin = origin;
this.container = container;
this.acceptedConnectionName = getConnectionName(container, annotation.name());
this.acceptedConnectionDetailsTypes = Set.of(annotation.type());
}
String getBeanName() {
return this.field.getName() + this.connectionDetailsType.getSimpleName() + "ConnectedContainer";
private static String getConnectionName(Container<?> container, String connectionName) {
if (StringUtils.hasLength(connectionName)) {
return connectionName;
}
try {
DockerImageName imageName = DockerImageName.parse(container.getDockerImageName());
imageName.assertValid();
return imageName.getRepository();
}
catch (IllegalArgumentException ex) {
return container.getDockerImageName();
}
}
/**
* Return the source annotation that provided the connection to the container. This
* annotation will be mergable to {@link ServiceConnection @ServiceConnection}.
* @return the source annotation
*/
public A getAnnotation() {
return this.annotation;
boolean accepts(String connectionName, Class<?> connectionDetailsType, Class<?> containerType) {
if (!containerType.isInstance(this.container)) {
logger.trace(LogMessage.of(() -> "%s not accepted as %s is not an instance of %s".formatted(this,
this.container.getClass().getName(), containerType.getName())));
return false;
}
if (StringUtils.hasLength(connectionName) && !connectionName.equalsIgnoreCase(this.acceptedConnectionName)) {
logger.trace(LogMessage.of(() -> "%s not accepted as connection names '%s' and '%s' do not match"
.formatted(this, connectionName, this.acceptedConnectionName)));
return false;
}
if (!this.acceptedConnectionDetailsTypes.isEmpty() && !this.acceptedConnectionDetailsTypes.stream()
.anyMatch((candidate) -> candidate.isAssignableFrom(connectionDetailsType))) {
logger.trace(LogMessage.of(() -> "%s not accepted as connection details type %s not in %s".formatted(this,
connectionDetailsType, this.acceptedConnectionDetailsTypes)));
return false;
}
logger.trace(LogMessage
.of(() -> "%s accepted for connection name '%s', connection details type %s, container type %s"
.formatted(this, connectionName, connectionDetailsType.getName(), containerType.getName())));
return true;
}
/**
* Return the {@link GenericContainer} that implements the service being connected to.
* @return the {@link GenericContainer} providing the service
*/
public C getContainer() {
return this.container;
String getBeanNameSuffix() {
return this.beanNameSuffix;
}
@Override
@ -100,9 +121,17 @@ public final class ContainerConnectionSource<A extends Annotation, D extends Con
return this.origin;
}
/**
* Return the {@link Container} that implements the service being connected to.
* @return the {@link Container} providing the service
*/
public C getContainer() {
return this.container;
}
@Override
public String toString() {
return "ServiceConnectedContainer for %s".formatted(this.origin);
return "@ServiceConnection source for %s".formatted(this.origin);
}
}

@ -0,0 +1,106 @@
/*
* Copyright 2012-2023 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 org.springframework.boot.testcontainers.service.connection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactories;
import org.springframework.core.log.LogMessage;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Class used to register bean definitions from a list of
* {@link ContainerConnectionSource} instances.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
*/
class ContainerConnectionSourcesRegistrar {
private static final Log logger = LogFactory.getLog(ContainerConnectionSourcesRegistrar.class);
private final ListableBeanFactory beanFactory;
private final ConnectionDetailsFactories connectionDetailsFactories;
private final List<ContainerConnectionSource<?, ?>> sources;
ContainerConnectionSourcesRegistrar(ListableBeanFactory beanFactory,
ConnectionDetailsFactories connectionDetailsFactories, List<ContainerConnectionSource<?, ?>> sources) {
this.beanFactory = beanFactory;
this.connectionDetailsFactories = connectionDetailsFactories;
this.sources = sources;
}
void registerBeanDefinitions(BeanDefinitionRegistry registry) {
this.sources.forEach((source) -> registerBeanDefinition(registry, source));
}
private void registerBeanDefinition(BeanDefinitionRegistry registry, ContainerConnectionSource<?, ?> source) {
getConnectionDetails(source)
.forEach((connectionDetailsType, connectionDetails) -> registerBeanDefinition(registry, source,
connectionDetailsType, connectionDetails));
}
private <S> Map<Class<?>, ConnectionDetails> getConnectionDetails(S source) {
Map<Class<?>, ConnectionDetails> connectionDetails = this.connectionDetailsFactories
.getConnectionDetails(source);
Assert.state(!connectionDetails.isEmpty(), () -> "No connection details created for %s".formatted(source));
return connectionDetails;
}
@SuppressWarnings("unchecked")
private <T> void registerBeanDefinition(BeanDefinitionRegistry registry, ContainerConnectionSource<?, ?> source,
Class<?> connectionDetailsType, ConnectionDetails connectionDetails) {
String[] existingBeans = this.beanFactory.getBeanNamesForType(connectionDetailsType);
if (!ObjectUtils.isEmpty(existingBeans)) {
logger.debug(LogMessage.of(() -> "Skipping registration of %s due to existing beans %s".formatted(source,
Arrays.asList(existingBeans))));
return;
}
String beanName = getBeanName(source, connectionDetails);
Class<T> beanType = (Class<T>) connectionDetails.getClass();
Supplier<T> beanSupplier = () -> (T) connectionDetails;
logger.debug(LogMessage.of(() -> "Registering '%s' for %s".formatted(beanName, source)));
registry.registerBeanDefinition(beanName, new RootBeanDefinition(beanType, beanSupplier));
}
private String getBeanName(ContainerConnectionSource<?, ?> source, ConnectionDetails connectionDetails) {
List<String> parts = new ArrayList<>();
parts.add(ClassUtils.getShortNameAsProperty(connectionDetails.getClass()));
parts.add("for");
parts.add(source.getBeanNameSuffix());
return StringUtils.uncapitalize(parts.stream().map(StringUtils::capitalize).collect(Collectors.joining()));
}
}

@ -16,31 +16,26 @@
package org.springframework.boot.testcontainers.service.connection;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.Objects;
import org.springframework.boot.origin.Origin;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* {@link Origin} backed by a {@link Field} and an {@link Annotation}.
* {@link Origin} backed by a {@link Field}.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
*/
class AnnotatedFieldOrigin implements Origin {
class FieldOrigin implements Origin {
private final Field field;
private final Annotation annotation;
AnnotatedFieldOrigin(Field field, Annotation annotation) {
FieldOrigin(Field field) {
Assert.notNull(field, "Field must not be null");
Assert.notNull(annotation, "Annotation must not be null");
this.field = field;
this.annotation = annotation;
}
@Override
@ -51,18 +46,18 @@ class AnnotatedFieldOrigin implements Origin {
if (obj == null || getClass() != obj.getClass()) {
return false;
}
AnnotatedFieldOrigin other = (AnnotatedFieldOrigin) obj;
return this.field.equals(other.field) && this.annotation.equals(other.annotation);
FieldOrigin other = (FieldOrigin) obj;
return this.field.equals(other.field);
}
@Override
public int hashCode() {
return Objects.hash(this.field, this.annotation);
return this.field.hashCode();
}
@Override
public String toString() {
return this.annotation + " " + this.field;
return ClassUtils.getShortName(this.field.getDeclaringClass()) + "." + this.field.getName();
}
}

@ -21,15 +21,14 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.testcontainers.containers.Container;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory;
import org.springframework.core.annotation.AliasFor;
/**
* Annotation used to indicate that a field provides a service that can be connected to.
* Typically used to meta-annotate a higher-level annotation.
* <p>
* When used, a {@link ConnectionDetailsFactory} must be registered in
* {@code spring.factories} to provide {@link ConnectionDetails} for the field value.
* Annotation used to indicate that a field or method is a
* {@link ContainerConnectionSource} which provides a service that can be connected to.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
@ -37,14 +36,35 @@ import org.springframework.boot.autoconfigure.service.connection.ConnectionDetai
* @since 3.1.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE })
public @interface ServiceConnection {
/**
* The type of {@link ConnectionDetails} that can describe how to connect to the
* service.
* @return the connection type
* The name of the service being connected to. If not specified, the image name will
* be used. Container names are used to determine the connection details that should
* be created when a technology-specific {@link Container} subclass is not available.
* This attribute is an alias for {@link #name()}.
* @return the name of the service
* @see #name()
*/
@AliasFor("name")
String value() default "";
/**
* The name of the service being connected to. If not specified, the image name will
* be used. Container names are used to determine the connection details that should
* be created when a technology-specific {@link Container} subclass is not available.
* @return the name of the service
* @see #value()
*/
@AliasFor("value")
String name() default "";
/**
* A restriction to types of {@link ConnectionDetails} that can be created from this
* connection. The default value does not restrict the types that can be created.
* @return the connection detail types that can be created to establish the connection
*/
Class<? extends ConnectionDetails> value();
Class<? extends ConnectionDetails>[] type() default {};
}

@ -17,21 +17,17 @@
package org.springframework.boot.testcontainers.service.connection;
import java.util.List;
import java.util.function.Supplier;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactories;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.util.Assert;
/**
* {@link ContextCustomizer} to support registering {@link ConnectionDetails}.
* Spring Test {@link ContextCustomizer} to support registering {@link ConnectionDetails}.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
@ -39,46 +35,30 @@ import org.springframework.util.Assert;
*/
class ServiceConnectionContextCustomizer implements ContextCustomizer {
private final ConnectionDetailsFactories factories = new ConnectionDetailsFactories();
private final List<ContainerConnectionSource<?, ?>> sources;
private final List<ContainerConnectionSource<?, ?, ?>> sources;
private final ConnectionDetailsFactories connectionDetailsFactories;
ServiceConnectionContextCustomizer(List<ContainerConnectionSource<?, ?, ?>> sources) {
ServiceConnectionContextCustomizer(List<ContainerConnectionSource<?, ?>> sources) {
this(sources, new ConnectionDetailsFactories());
}
ServiceConnectionContextCustomizer(List<ContainerConnectionSource<?, ?>> sources,
ConnectionDetailsFactories connectionDetailsFactories) {
this.sources = sources;
this.connectionDetailsFactories = connectionDetailsFactories;
}
@Override
public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
if (beanFactory instanceof BeanDefinitionRegistry registry) {
registerServiceConnections(registry);
}
}
private void registerServiceConnections(BeanDefinitionRegistry registry) {
this.sources.forEach((source) -> registerServiceConnection(registry, source));
new ContainerConnectionSourcesRegistrar(beanFactory, this.connectionDetailsFactories, this.sources)
.registerBeanDefinitions(registry);
}
private void registerServiceConnection(BeanDefinitionRegistry registry, ContainerConnectionSource<?, ?, ?> source) {
ConnectionDetails connectionDetails = getConnectionDetails(source);
register(connectionDetails, registry, source.getBeanName());
}
@SuppressWarnings("unchecked")
private <T> void register(ConnectionDetails connectionDetails, BeanDefinitionRegistry registry, String beanName) {
Class<T> beanType = (Class<T>) connectionDetails.getClass();
Supplier<T> beanSupplier = () -> (T) connectionDetails;
BeanDefinition beanDefinition = new RootBeanDefinition(beanType, beanSupplier);
registry.registerBeanDefinition(beanName, beanDefinition);
}
private <S> ConnectionDetails getConnectionDetails(S source) {
ConnectionDetails connectionDetails = this.factories.getConnectionDetails(source);
Assert.state(connectionDetails != null, () -> "No connection details created for %s".formatted(source));
return connectionDetails;
}
List<ContainerConnectionSource<?, ?, ?>> getSources() {
List<ContainerConnectionSource<?, ?>> getSources() {
return this.sources;
}

@ -21,9 +21,9 @@ import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Container;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
import org.springframework.boot.origin.Origin;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.test.context.ContextConfigurationAttributes;
@ -31,10 +31,14 @@ import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.ContextCustomizerFactory;
import org.springframework.test.context.TestContextAnnotationUtils;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
* {@link ContextCustomizerFactory} to support service connections in tests.
* Spring Test {@link ContextCustomizerFactory} to support
* {@link ServiceConnection @ServiceConnection} annotated {@link Container} fields in
* tests.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
@ -45,12 +49,12 @@ class ServiceConnectionContextCustomizerFactory implements ContextCustomizerFact
@Override
public ContextCustomizer createContextCustomizer(Class<?> testClass,
List<ContextConfigurationAttributes> configAttributes) {
List<ContainerConnectionSource<?, ?, ?>> sources = new ArrayList<>();
List<ContainerConnectionSource<?, ?>> sources = new ArrayList<>();
findSources(testClass, sources);
return (sources.isEmpty()) ? null : new ServiceConnectionContextCustomizer(sources);
}
private void findSources(Class<?> clazz, List<ContainerConnectionSource<?, ?, ?>> sources) {
private void findSources(Class<?> clazz, List<ContainerConnectionSource<?, ?>> sources) {
ReflectionUtils.doWithFields(clazz, (field) -> {
MergedAnnotations annotations = MergedAnnotations.from(field);
annotations.stream(ServiceConnection.class)
@ -61,23 +65,17 @@ class ServiceConnectionContextCustomizerFactory implements ContextCustomizerFact
}
}
private ContainerConnectionSource<?, ?, ?> createSource(Field field,
MergedAnnotation<ServiceConnection> annotation) {
if (!Modifier.isStatic(field.getModifiers())) {
throw new IllegalStateException("@ServiceConnection field '%s' must be static".formatted(field.getName()));
}
Class<? extends ConnectionDetails> connectionDetailsType = getConnectionDetailsType(annotation);
private ContainerConnectionSource<?, ?> createSource(Field field, MergedAnnotation<ServiceConnection> annotation) {
Assert.state(Modifier.isStatic(field.getModifiers()),
() -> "@ServiceConnection field '%s' must be static".formatted(field.getName()));
String beanNameSuffix = StringUtils.capitalize(ClassUtils.getShortNameAsProperty(field.getDeclaringClass()))
+ StringUtils.capitalize(field.getName());
Origin origin = new FieldOrigin(field);
Object fieldValue = getFieldValue(field);
Assert.isInstanceOf(GenericContainer.class, fieldValue,
"Field %s must be a %s".formatted(field.getName(), GenericContainer.class.getName()));
GenericContainer<?> container = (GenericContainer<?>) fieldValue;
return new ContainerConnectionSource<>(connectionDetailsType, field, annotation, container);
}
@SuppressWarnings("unchecked")
private Class<? extends ConnectionDetails> getConnectionDetailsType(
MergedAnnotation<ServiceConnection> annotation) {
return (Class<? extends ConnectionDetails>) annotation.getClass(MergedAnnotation.VALUE);
Assert.state(Container.class.isInstance(fieldValue), () -> "Field '%s' in %s must be a %s"
.formatted(field.getName(), field.getDeclaringClass().getName(), Container.class.getName()));
Container<?> container = (Container<?>) fieldValue;
return new ContainerConnectionSource<>(beanNameSuffix, origin, container, annotation);
}
private Object getFieldValue(Field field) {

@ -24,22 +24,23 @@ import org.testcontainers.containers.RabbitMQContainer;
import org.springframework.boot.autoconfigure.amqp.RabbitConnectionDetails;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/**
* {@link ContainerConnectionDetailsFactory} for
* {@link RabbitServiceConnection @RabbitServiceConnection}-annotated
* {@link RabbitMQContainer} fields.
* {@link ContainerConnectionDetailsFactory} to create {@link RabbitConnectionDetails}
* from a {@link ServiceConnection @ServiceConnection}-annotated
* {@link RabbitMQContainer}.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
*/
class RabbitContainerConnectionDetailsFactory
extends ContainerConnectionDetailsFactory<RabbitServiceConnection, RabbitConnectionDetails, RabbitMQContainer> {
extends ContainerConnectionDetailsFactory<RabbitConnectionDetails, RabbitMQContainer> {
@Override
protected RabbitConnectionDetails getContainerConnectionDetails(
ContainerConnectionSource<RabbitServiceConnection, RabbitConnectionDetails, RabbitMQContainer> source) {
ContainerConnectionSource<RabbitConnectionDetails, RabbitMQContainer> source) {
return new RabbitMqContainerConnectionDetails(source);
}
@ -52,7 +53,7 @@ class RabbitContainerConnectionDetailsFactory
private final RabbitMQContainer container;
private RabbitMqContainerConnectionDetails(
ContainerConnectionSource<RabbitServiceConnection, RabbitConnectionDetails, RabbitMQContainer> source) {
ContainerConnectionSource<RabbitConnectionDetails, RabbitMQContainer> source) {
super(source);
this.container = source.getContainer();
}

@ -1,44 +0,0 @@
/*
* Copyright 2012-2023 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 org.springframework.boot.testcontainers.service.connection.amqp;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.amqp.RabbitConnectionDetails;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/**
* Annotation that indicates that a field provides a RabbitMQ service connection.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
* @since 3.1.0
* @see RabbitConnectionDetails
* @see ServiceConnection
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.TYPE })
@ServiceConnection(RabbitConnectionDetails.class)
public @interface RabbitServiceConnection {
}

@ -23,22 +23,23 @@ import org.testcontainers.containers.CassandraContainer;
import org.springframework.boot.autoconfigure.cassandra.CassandraConnectionDetails;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/**
* {@link ContainerConnectionDetailsFactory} for
* {@link CassandraServiceConnection @CassandraServiceConnection}-annotated
* {@link CassandraContainer} fields.
* {@link ContainerConnectionDetailsFactory} to create {@link CassandraConnectionDetails}
* from a {@link ServiceConnection @ServiceConnection}-annotated
* {@link CassandraContainer}.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
*/
class CassandraContainerConnectionDetailsFactory extends
ContainerConnectionDetailsFactory<CassandraServiceConnection, CassandraConnectionDetails, CassandraContainer<?>> {
class CassandraContainerConnectionDetailsFactory
extends ContainerConnectionDetailsFactory<CassandraConnectionDetails, CassandraContainer<?>> {
@Override
protected CassandraConnectionDetails getContainerConnectionDetails(
ContainerConnectionSource<CassandraServiceConnection, CassandraConnectionDetails, CassandraContainer<?>> source) {
ContainerConnectionSource<CassandraConnectionDetails, CassandraContainer<?>> source) {
return new CassandraContainerConnectionDetails(source);
}
@ -51,7 +52,7 @@ class CassandraContainerConnectionDetailsFactory extends
private final CassandraContainer<?> container;
private CassandraContainerConnectionDetails(
ContainerConnectionSource<CassandraServiceConnection, CassandraConnectionDetails, CassandraContainer<?>> source) {
ContainerConnectionSource<CassandraConnectionDetails, CassandraContainer<?>> source) {
super(source);
this.container = source.getContainer();
}

@ -1,44 +0,0 @@
/*
* Copyright 2012-2023 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 org.springframework.boot.testcontainers.service.connection.cassandra;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.cassandra.CassandraConnectionDetails;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/**
* Annotation that indicates that a field provides a Cassandra service connection.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
* @since 3.1.0
* @see CassandraConnectionDetails
* @see ServiceConnection
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.TYPE })
@ServiceConnection(CassandraConnectionDetails.class)
public @interface CassandraServiceConnection {
}

@ -21,22 +21,23 @@ import org.testcontainers.couchbase.CouchbaseContainer;
import org.springframework.boot.autoconfigure.couchbase.CouchbaseConnectionDetails;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/**
* {@link ContainerConnectionDetailsFactory} for
* {@link CouchbaseServiceConnection @CouchbaseServiceConnection}-annotated
* {@link CouchbaseContainer} fields.
* {@link ContainerConnectionDetailsFactory} to create {@link CouchbaseConnectionDetails}
* from a {@link ServiceConnection @ServiceConnection}-annotated
* {@link CouchbaseContainer}.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
*/
class CouchbaseContainerConnectionDetailsFactory extends
ContainerConnectionDetailsFactory<CouchbaseServiceConnection, CouchbaseConnectionDetails, CouchbaseContainer> {
class CouchbaseContainerConnectionDetailsFactory
extends ContainerConnectionDetailsFactory<CouchbaseConnectionDetails, CouchbaseContainer> {
@Override
protected CouchbaseConnectionDetails getContainerConnectionDetails(
ContainerConnectionSource<CouchbaseServiceConnection, CouchbaseConnectionDetails, CouchbaseContainer> source) {
ContainerConnectionSource<CouchbaseConnectionDetails, CouchbaseContainer> source) {
return new CouchbaseContainerConnectionDetails(source);
}
@ -49,7 +50,7 @@ class CouchbaseContainerConnectionDetailsFactory extends
private final CouchbaseContainer container;
private CouchbaseContainerConnectionDetails(
ContainerConnectionSource<CouchbaseServiceConnection, CouchbaseConnectionDetails, CouchbaseContainer> source) {
ContainerConnectionSource<CouchbaseConnectionDetails, CouchbaseContainer> source) {
super(source);
this.container = source.getContainer();
}

@ -1,44 +0,0 @@
/*
* Copyright 2012-2023 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 org.springframework.boot.testcontainers.service.connection.couchbase;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.couchbase.CouchbaseConnectionDetails;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/**
* Annotation that indicates that a field provides a Couchbase service connection.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
* @since 3.1.0
* @see CouchbaseConnectionDetails
* @see ServiceConnection
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.TYPE })
@ServiceConnection(CouchbaseConnectionDetails.class)
public @interface CouchbaseServiceConnection {
}

@ -18,30 +18,31 @@ package org.springframework.boot.testcontainers.service.connection.elasticsearch
import java.util.List;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.elasticsearch.ElasticsearchContainer;
import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails;
import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails.Node.Protocol;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/**
* {@link ContainerConnectionDetailsFactory} for
* {@link ElasticsearchServiceConnection @ElasticsearchServiceConnection}-annotated
* {@link GenericContainer} fields.
* {@link ContainerConnectionDetailsFactory} to create
* {@link ElasticsearchConnectionDetails} from a
* {@link ServiceConnection @ServiceConnection}-annotated {@link ElasticsearchContainer}.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
*/
class ElasticsearchContainerConnectionDetailsFactory extends
ContainerConnectionDetailsFactory<ElasticsearchServiceConnection, ElasticsearchConnectionDetails, GenericContainer<?>> {
class ElasticsearchContainerConnectionDetailsFactory
extends ContainerConnectionDetailsFactory<ElasticsearchConnectionDetails, ElasticsearchContainer> {
private static final int DEFAULT_PORT = 9200;
@Override
protected ElasticsearchConnectionDetails getContainerConnectionDetails(
ContainerConnectionSource<ElasticsearchServiceConnection, ElasticsearchConnectionDetails, GenericContainer<?>> source) {
ContainerConnectionSource<ElasticsearchConnectionDetails, ElasticsearchContainer> source) {
return new ElasticsearchContainerConnectionDetails(source);
}
@ -55,7 +56,7 @@ class ElasticsearchContainerConnectionDetailsFactory extends
private final List<Node> nodes;
private ElasticsearchContainerConnectionDetails(
ContainerConnectionSource<ElasticsearchServiceConnection, ElasticsearchConnectionDetails, GenericContainer<?>> source) {
ContainerConnectionSource<ElasticsearchConnectionDetails, ElasticsearchContainer> source) {
super(source);
this.nodes = List.of(new Node(source.getContainer().getHost(),
source.getContainer().getMappedPort(DEFAULT_PORT), Protocol.HTTP, null, null));

@ -1,44 +0,0 @@
/*
* Copyright 2012-2023 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 org.springframework.boot.testcontainers.service.connection.elasticsearch;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/**
* Annotation that indicates that a field provides a Elasticsearch service connection.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
* @since 3.1.0
* @see ElasticsearchConnectionDetails
* @see ServiceConnection
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.TYPE })
@ServiceConnection(ElasticsearchConnectionDetails.class)
public @interface ElasticsearchServiceConnection {
}

@ -24,18 +24,18 @@ import org.springframework.boot.testcontainers.service.connection.ContainerConne
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/**
* {@link ContainerConnectionDetailsFactory} for
* {@link ServiceConnection @ServiceConnection}-annotated {@link JdbcDatabaseContainer}
* fields that should produce {@link FlywayConnectionDetails}.
* {@link ContainerConnectionDetailsFactory} to create {@link FlywayConnectionDetails}
* from a {@link ServiceConnection @ServiceConnection}-annotated
* {@link JdbcDatabaseContainer}.
*
* @author Andy Wilkinson
*/
class FlywayContainerConnectionDetailsFactory extends
ContainerConnectionDetailsFactory<ServiceConnection, FlywayConnectionDetails, JdbcDatabaseContainer<?>> {
class FlywayContainerConnectionDetailsFactory
extends ContainerConnectionDetailsFactory<FlywayConnectionDetails, JdbcDatabaseContainer<?>> {
@Override
protected FlywayConnectionDetails getContainerConnectionDetails(
ContainerConnectionSource<ServiceConnection, FlywayConnectionDetails, JdbcDatabaseContainer<?>> source) {
ContainerConnectionSource<FlywayConnectionDetails, JdbcDatabaseContainer<?>> source) {
return new FlywayContainerConnectionDetails(source);
}
@ -48,7 +48,7 @@ class FlywayContainerConnectionDetailsFactory extends
private final JdbcDatabaseContainer<?> container;
private FlywayContainerConnectionDetails(
ContainerConnectionSource<ServiceConnection, FlywayConnectionDetails, JdbcDatabaseContainer<?>> source) {
ContainerConnectionSource<FlywayConnectionDetails, JdbcDatabaseContainer<?>> source) {
super(source);
this.container = source.getContainer();
}

@ -23,22 +23,23 @@ import org.testcontainers.containers.InfluxDBContainer;
import org.springframework.boot.autoconfigure.influx.InfluxDbConnectionDetails;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/**
* {@link ContainerConnectionDetailsFactory} for
* {@link InfluxDbServiceConnection @InfluxDbServiceConnection}-annotated
* {@link InfluxDBContainer} fields.
* {@link ContainerConnectionDetailsFactory} to create {@link InfluxDbConnectionDetails}
* from a {@link ServiceConnection @ServiceConnection}-annotated
* {@link InfluxDBContainer}.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
*/
class InfluxDbContainerConnectionDetailsFactory extends
ContainerConnectionDetailsFactory<InfluxDbServiceConnection, InfluxDbConnectionDetails, InfluxDBContainer<?>> {
class InfluxDbContainerConnectionDetailsFactory
extends ContainerConnectionDetailsFactory<InfluxDbConnectionDetails, InfluxDBContainer<?>> {
@Override
protected InfluxDbConnectionDetails getContainerConnectionDetails(
ContainerConnectionSource<InfluxDbServiceConnection, InfluxDbConnectionDetails, InfluxDBContainer<?>> source) {
ContainerConnectionSource<InfluxDbConnectionDetails, InfluxDBContainer<?>> source) {
return new InfluxDbContainerConnectionDetails(source);
}
@ -51,7 +52,7 @@ class InfluxDbContainerConnectionDetailsFactory extends
private final InfluxDBContainer<?> container;
private InfluxDbContainerConnectionDetails(
ContainerConnectionSource<InfluxDbServiceConnection, InfluxDbConnectionDetails, InfluxDBContainer<?>> source) {
ContainerConnectionSource<InfluxDbConnectionDetails, InfluxDBContainer<?>> source) {
super(source);
this.container = source.getContainer();
}

@ -1,44 +0,0 @@
/*
* Copyright 2012-2023 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 org.springframework.boot.testcontainers.service.connection.influx;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.influx.InfluxDbConnectionDetails;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/**
* Annotation that indicates that a field provides an InfluxDB service connection.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
* @since 3.1.0
* @see InfluxDbConnectionDetails
* @see ServiceConnection
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.TYPE })
@ServiceConnection(InfluxDbConnectionDetails.class)
public @interface InfluxDbServiceConnection {
}

@ -21,22 +21,22 @@ import org.testcontainers.containers.JdbcDatabaseContainer;
import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/**
* {@link ContainerConnectionDetailsFactory} for
* {@link JdbcServiceConnection @JdbcServiceConnection}-annotated
* {@link JdbcDatabaseContainer} fields.
* {@link ContainerConnectionDetailsFactory} to create {@link JdbcConnectionDetails} from
* a {@link ServiceConnection @ServiceConnection}-annotated {@link JdbcDatabaseContainer}.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
*/
class JdbcContainerConnectionDetailsFactory extends
ContainerConnectionDetailsFactory<JdbcServiceConnection, JdbcConnectionDetails, JdbcDatabaseContainer<?>> {
class JdbcContainerConnectionDetailsFactory
extends ContainerConnectionDetailsFactory<JdbcConnectionDetails, JdbcDatabaseContainer<?>> {
@Override
protected JdbcConnectionDetails getContainerConnectionDetails(
ContainerConnectionSource<JdbcServiceConnection, JdbcConnectionDetails, JdbcDatabaseContainer<?>> source) {
ContainerConnectionSource<JdbcConnectionDetails, JdbcDatabaseContainer<?>> source) {
return new JdbcContainerConnectionDetails(source);
}
@ -49,7 +49,7 @@ class JdbcContainerConnectionDetailsFactory extends
private final JdbcDatabaseContainer<?> container;
private JdbcContainerConnectionDetails(
ContainerConnectionSource<JdbcServiceConnection, JdbcConnectionDetails, JdbcDatabaseContainer<?>> source) {
ContainerConnectionSource<JdbcConnectionDetails, JdbcDatabaseContainer<?>> source) {
super(source);
this.container = source.getContainer();
}

@ -1,43 +0,0 @@
/*
* Copyright 2012-2023 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 org.springframework.boot.testcontainers.service.connection.jdbc;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/**
* Annotation that indicates that a field provides a JDBC database service connection.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @since 3.1.0
* @see JdbcConnectionDetails
* @see ServiceConnection
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.TYPE })
@ServiceConnection(JdbcConnectionDetails.class)
public @interface JdbcServiceConnection {
}

@ -24,22 +24,22 @@ import org.testcontainers.containers.KafkaContainer;
import org.springframework.boot.autoconfigure.kafka.KafkaConnectionDetails;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/**
* {@link ContainerConnectionDetailsFactory} for
* {@link KafkaServiceConnection @KafkaServiceConnection}-annotated {@link KafkaContainer}
* fields.
* {@link ContainerConnectionDetailsFactory} to create {@link KafkaConnectionDetails} from
* a {@link ServiceConnection @ServiceConnection}-annotated {@link KafkaContainer}.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
*/
class KafkaContainerConnectionDetailsFactory
extends ContainerConnectionDetailsFactory<KafkaServiceConnection, KafkaConnectionDetails, KafkaContainer> {
extends ContainerConnectionDetailsFactory<KafkaConnectionDetails, KafkaContainer> {
@Override
protected KafkaConnectionDetails getContainerConnectionDetails(
ContainerConnectionSource<KafkaServiceConnection, KafkaConnectionDetails, KafkaContainer> source) {
ContainerConnectionSource<KafkaConnectionDetails, KafkaContainer> source) {
return new KafkaContainerConnectionDetails(source);
}
@ -52,7 +52,7 @@ class KafkaContainerConnectionDetailsFactory
private final KafkaContainer container;
private KafkaContainerConnectionDetails(
ContainerConnectionSource<KafkaServiceConnection, KafkaConnectionDetails, KafkaContainer> source) {
ContainerConnectionSource<KafkaConnectionDetails, KafkaContainer> source) {
super(source);
this.container = source.getContainer();
}

@ -1,43 +0,0 @@
/*
* Copyright 2012-2023 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 org.springframework.boot.testcontainers.service.connection.kafka;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.kafka.KafkaConnectionDetails;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/**
* Annotation that indicates that a field provides a Kafka service connection.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @since 3.1.0
* @see KafkaConnectionDetails
* @see ServiceConnection
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.TYPE })
@ServiceConnection(KafkaConnectionDetails.class)
public @interface KafkaServiceConnection {
}

@ -24,18 +24,18 @@ import org.springframework.boot.testcontainers.service.connection.ContainerConne
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/**
* {@link ContainerConnectionDetailsFactory} for
* {@link ServiceConnection @ServiceConnection}-annotated {@link JdbcDatabaseContainer}
* fields that should produce {@link LiquibaseConnectionDetails}.
* {@link ContainerConnectionDetailsFactory} to create {@link LiquibaseConnectionDetails}
* from a {@link ServiceConnection @ServiceConnection}-annotated
* {@link JdbcDatabaseContainer}.
*
* @author Andy Wilkinson
*/
class LiquibaseContainerConnectionDetailsFactory extends
ContainerConnectionDetailsFactory<ServiceConnection, LiquibaseConnectionDetails, JdbcDatabaseContainer<?>> {
class LiquibaseContainerConnectionDetailsFactory
extends ContainerConnectionDetailsFactory<LiquibaseConnectionDetails, JdbcDatabaseContainer<?>> {
@Override
protected LiquibaseConnectionDetails getContainerConnectionDetails(
ContainerConnectionSource<ServiceConnection, LiquibaseConnectionDetails, JdbcDatabaseContainer<?>> source) {
ContainerConnectionSource<LiquibaseConnectionDetails, JdbcDatabaseContainer<?>> source) {
return new LiquibaseContainerConnectionDetails(source);
}
@ -48,7 +48,7 @@ class LiquibaseContainerConnectionDetailsFactory extends
private final JdbcDatabaseContainer<?> container;
private LiquibaseContainerConnectionDetails(
ContainerConnectionSource<ServiceConnection, LiquibaseConnectionDetails, JdbcDatabaseContainer<?>> source) {
ContainerConnectionSource<LiquibaseConnectionDetails, JdbcDatabaseContainer<?>> source) {
super(source);
this.container = source.getContainer();
}

@ -22,22 +22,26 @@ import org.testcontainers.containers.MongoDBContainer;
import org.springframework.boot.autoconfigure.mongo.MongoConnectionDetails;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/**
* {@link ContainerConnectionDetailsFactory} for
* {@link MongoServiceConnection @MongoServiceConnection}-annotated
* {@link MongoDBContainer} fields.
* {@link ContainerConnectionDetailsFactory} to create {@link MongoConnectionDetails} from
* a {@link ServiceConnection @ServiceConnection}-annotated {@link MongoDBContainer}.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
*/
class MongoContainerConnectionDetailsFactory
extends ContainerConnectionDetailsFactory<MongoServiceConnection, MongoConnectionDetails, MongoDBContainer> {
extends ContainerConnectionDetailsFactory<MongoConnectionDetails, MongoDBContainer> {
MongoContainerConnectionDetailsFactory() {
super(ANY_CONNECTION_NAME, "com.mongodb.ConnectionString");
}
@Override
protected MongoConnectionDetails getContainerConnectionDetails(
ContainerConnectionSource<MongoServiceConnection, MongoConnectionDetails, MongoDBContainer> source) {
ContainerConnectionSource<MongoConnectionDetails, MongoDBContainer> source) {
return new MongoContainerConnectionDetails(source);
}
@ -50,7 +54,7 @@ class MongoContainerConnectionDetailsFactory
private final ConnectionString connectionString;
private MongoContainerConnectionDetails(
ContainerConnectionSource<MongoServiceConnection, MongoConnectionDetails, MongoDBContainer> source) {
ContainerConnectionSource<MongoConnectionDetails, MongoDBContainer> source) {
super(source);
this.connectionString = new ConnectionString(source.getContainer().getReplicaSetUrl());
}

@ -1,43 +0,0 @@
/*
* Copyright 2012-2023 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 org.springframework.boot.testcontainers.service.connection.mongo;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.mongo.MongoConnectionDetails;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/**
* Annotation that indicates that a field provides a Mongo service connection.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @since 3.1.0
* @see MongoConnectionDetails
* @see ServiceConnection
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.TYPE })
@ServiceConnection(MongoConnectionDetails.class)
public @interface MongoServiceConnection {
}

@ -25,22 +25,26 @@ import org.testcontainers.containers.Neo4jContainer;
import org.springframework.boot.autoconfigure.neo4j.Neo4jConnectionDetails;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/**
* {@link ContainerConnectionDetailsFactory} for
* {@link Neo4jServiceConnection @Neo4jServiceConnection}-annotated {@link Neo4jContainer}
* fields.
* {@link ContainerConnectionDetailsFactory} to create {@link Neo4jConnectionDetails} from
* a {@link ServiceConnection @ServiceConnection}-annotated {@link Neo4jContainer}.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
*/
class Neo4jContainerConnectionDetailsFactory
extends ContainerConnectionDetailsFactory<Neo4jServiceConnection, Neo4jConnectionDetails, Neo4jContainer<?>> {
extends ContainerConnectionDetailsFactory<Neo4jConnectionDetails, Neo4jContainer<?>> {
Neo4jContainerConnectionDetailsFactory() {
super(ANY_CONNECTION_NAME, "org.neo4j.driver.AuthToken");
}
@Override
protected Neo4jConnectionDetails getContainerConnectionDetails(
ContainerConnectionSource<Neo4jServiceConnection, Neo4jConnectionDetails, Neo4jContainer<?>> source) {
ContainerConnectionSource<Neo4jConnectionDetails, Neo4jContainer<?>> source) {
return new Neo4jContainerConnectionDetails(source);
}
@ -53,7 +57,7 @@ class Neo4jContainerConnectionDetailsFactory
private final Neo4jContainer<?> container;
private Neo4jContainerConnectionDetails(
ContainerConnectionSource<Neo4jServiceConnection, Neo4jConnectionDetails, Neo4jContainer<?>> source) {
ContainerConnectionSource<Neo4jConnectionDetails, Neo4jContainer<?>> source) {
super(source);
this.container = source.getContainer();
}

@ -1,43 +0,0 @@
/*
* Copyright 2012-2023 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 org.springframework.boot.testcontainers.service.connection.neo4j;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.neo4j.Neo4jConnectionDetails;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/**
* Annotation that indicates a field provides a Neo4j service connection.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @since 3.1.0
* @see Neo4jConnectionDetails
* @see ServiceConnection
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.TYPE })
@ServiceConnection(Neo4jConnectionDetails.class)
public @interface Neo4jServiceConnection {
}

@ -15,6 +15,6 @@
*/
/**
* General support for auto-configuration of service connections in tests.
* General support for service connections in tests.
*/
package org.springframework.boot.testcontainers.service.connection;

@ -21,25 +21,28 @@ import org.testcontainers.containers.MariaDBContainer;
import org.testcontainers.containers.MariaDBR2DBCDatabaseContainer;
import org.springframework.boot.autoconfigure.r2dbc.R2dbcConnectionDetails;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/**
* {@link ConnectionDetailsFactory} for
* {@link R2dbcServiceConnection @R2dbcServiceConnection}- annotated
* {@link MariaDBContainer} fields.
* {@link ContainerConnectionDetailsFactory} to create {@link R2dbcConnectionDetails} from
* a {@link ServiceConnection @ServiceConnection}-annotated {@link MariaDBContainer}.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
*/
class MariaDbR2dbcContainerConnectionDetailsFactory
extends ContainerConnectionDetailsFactory<R2dbcServiceConnection, R2dbcConnectionDetails, MariaDBContainer<?>> {
extends ContainerConnectionDetailsFactory<R2dbcConnectionDetails, MariaDBContainer<?>> {
MariaDbR2dbcContainerConnectionDetailsFactory() {
super(ANY_CONNECTION_NAME, "io.r2dbc.spi.ConnectionFactoryOptions");
}
@Override
public R2dbcConnectionDetails getContainerConnectionDetails(
ContainerConnectionSource<R2dbcServiceConnection, R2dbcConnectionDetails, MariaDBContainer<?>> source) {
ContainerConnectionSource<R2dbcConnectionDetails, MariaDBContainer<?>> source) {
return new R2dbcDatabaseContainerConnectionDetails(source.getContainer());
}

@ -21,25 +21,28 @@ import org.testcontainers.containers.MSSQLR2DBCDatabaseContainer;
import org.testcontainers.containers.MSSQLServerContainer;
import org.springframework.boot.autoconfigure.r2dbc.R2dbcConnectionDetails;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/**
* {@link ConnectionDetailsFactory} for
* {@link R2dbcServiceConnection @R2dbcServiceConnection}- annotated
* {@link MSSQLServerContainer} fields.
* {@link ContainerConnectionDetailsFactory} to create {@link R2dbcConnectionDetails} from
* a {@link ServiceConnection @ServiceConnection}-annotated {@link MSSQLServerContainer}.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
*/
class SqlServerR2dbcContainerConnectionDetailsFactory extends
ContainerConnectionDetailsFactory<R2dbcServiceConnection, R2dbcConnectionDetails, MSSQLServerContainer<?>> {
class MsSqlServerR2dbcContainerConnectionDetailsFactory
extends ContainerConnectionDetailsFactory<R2dbcConnectionDetails, MSSQLServerContainer<?>> {
MsSqlServerR2dbcContainerConnectionDetailsFactory() {
super(ANY_CONNECTION_NAME, "io.r2dbc.spi.ConnectionFactoryOptions");
}
@Override
public R2dbcConnectionDetails getContainerConnectionDetails(
ContainerConnectionSource<R2dbcServiceConnection, R2dbcConnectionDetails, MSSQLServerContainer<?>> source) {
ContainerConnectionSource<R2dbcConnectionDetails, MSSQLServerContainer<?>> source) {
return new R2dbcDatabaseContainerConnectionDetails(source.getContainer());
}

@ -21,25 +21,28 @@ import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.containers.MySQLR2DBCDatabaseContainer;
import org.springframework.boot.autoconfigure.r2dbc.R2dbcConnectionDetails;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/**
* {@link ConnectionDetailsFactory} for
* {@link R2dbcServiceConnection @R2dbcServiceConnection}- annotated
* {@link MySQLContainer} fields.
* {@link ContainerConnectionDetailsFactory} to create {@link R2dbcConnectionDetails} from
* a {@link ServiceConnection @ServiceConnection}-annotated {@link MySQLContainer}.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
*/
class MySqlR2dbcContainerConnectionDetailsFactory
extends ContainerConnectionDetailsFactory<R2dbcServiceConnection, R2dbcConnectionDetails, MySQLContainer<?>> {
extends ContainerConnectionDetailsFactory<R2dbcConnectionDetails, MySQLContainer<?>> {
MySqlR2dbcContainerConnectionDetailsFactory() {
super(ANY_CONNECTION_NAME, "io.r2dbc.spi.ConnectionFactoryOptions");
}
@Override
public R2dbcConnectionDetails getContainerConnectionDetails(
ContainerConnectionSource<R2dbcServiceConnection, R2dbcConnectionDetails, MySQLContainer<?>> source) {
ContainerConnectionSource<R2dbcConnectionDetails, MySQLContainer<?>> source) {
return new R2dbcDatabaseContainerConnectionDetails(source.getContainer());
}

@ -21,24 +21,28 @@ import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.containers.PostgreSQLR2DBCDatabaseContainer;
import org.springframework.boot.autoconfigure.r2dbc.R2dbcConnectionDetails;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/**
* {@link ConnectionDetailsFactory} for {@link R2dbcServiceConnection @R2dbcConnection}
* annotated {@link PostgreSQLContainer} fields.
* {@link ContainerConnectionDetailsFactory} to create {@link R2dbcConnectionDetails} from
* a {@link ServiceConnection @ServiceConnection}-annotated {@link PostgreSQLContainer}.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
*/
class PostgresR2dbcContainerConnectionDetailsFactory extends
ContainerConnectionDetailsFactory<R2dbcServiceConnection, R2dbcConnectionDetails, PostgreSQLContainer<?>> {
class PostgresR2dbcContainerConnectionDetailsFactory
extends ContainerConnectionDetailsFactory<R2dbcConnectionDetails, PostgreSQLContainer<?>> {
PostgresR2dbcContainerConnectionDetailsFactory() {
super(ANY_CONNECTION_NAME, "io.r2dbc.spi.ConnectionFactoryOptions");
}
@Override
public R2dbcConnectionDetails getContainerConnectionDetails(
ContainerConnectionSource<R2dbcServiceConnection, R2dbcConnectionDetails, PostgreSQLContainer<?>> source) {
ContainerConnectionSource<R2dbcConnectionDetails, PostgreSQLContainer<?>> source) {
return new R2dbcDatabaseContainerConnectionDetails(source.getContainer());
}

@ -1,43 +0,0 @@
/*
* Copyright 2012-2023 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 org.springframework.boot.testcontainers.service.connection.r2dbc;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.r2dbc.R2dbcConnectionDetails;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/**
* Annotation that indicates a field provides a R2DBC database service connection.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @since 3.1.0
* @see ServiceConnection
* @see R2dbcConnectionDetails
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.TYPE })
@ServiceConnection(R2dbcConnectionDetails.class)
public @interface R2dbcServiceConnection {
}

@ -16,27 +16,33 @@
package org.springframework.boot.testcontainers.service.connection.redis;
import org.testcontainers.containers.Container;
import org.testcontainers.containers.GenericContainer;
import org.springframework.boot.autoconfigure.data.redis.RedisConnectionDetails;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/**
* {@link ContainerConnectionDetailsFactory} for
* {@link RedisServiceConnection @RedisServiceConnection}-annotated
* {@link GenericContainer} fields.
* {@link ContainerConnectionDetailsFactory} to create {@link RedisConnectionDetails} from
* a {@link ServiceConnection @ServiceConnection}-annotated {@link GenericContainer} using
* the {@code "redis"} image.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
*/
class RedisContainerConnectionDetailsFactory
extends ContainerConnectionDetailsFactory<RedisServiceConnection, RedisConnectionDetails, GenericContainer<?>> {
extends ContainerConnectionDetailsFactory<RedisConnectionDetails, Container<?>> {
RedisContainerConnectionDetailsFactory() {
super("redis");
}
@Override
public RedisConnectionDetails getContainerConnectionDetails(
ContainerConnectionSource<RedisServiceConnection, RedisConnectionDetails, GenericContainer<?>> source) {
ContainerConnectionSource<RedisConnectionDetails, Container<?>> source) {
return new RedisContainerConnectionDetails(source);
}
@ -49,7 +55,7 @@ class RedisContainerConnectionDetailsFactory
private final Standalone standalone;
private RedisContainerConnectionDetails(
ContainerConnectionSource<RedisServiceConnection, RedisConnectionDetails, GenericContainer<?>> source) {
ContainerConnectionSource<RedisConnectionDetails, Container<?>> source) {
super(source);
this.standalone = Standalone.of(source.getContainer().getHost(),
source.getContainer().getFirstMappedPort());

@ -1,43 +0,0 @@
/*
* Copyright 2012-2023 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 org.springframework.boot.testcontainers.service.connection.redis;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.data.redis.RedisConnectionDetails;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/**
* Annotation that indicates that a field provides a Redis service connection.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @since 3.1.0
* @see RedisConnectionDetails
* @see ServiceConnection
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.TYPE })
@ServiceConnection(RedisConnectionDetails.class)
public @interface RedisServiceConnection {
}

@ -8,7 +8,6 @@ org.springframework.boot.testcontainers.service.connection.amqp.RabbitContainerC
org.springframework.boot.testcontainers.service.connection.cassandra.CassandraContainerConnectionDetailsFactory,\
org.springframework.boot.testcontainers.service.connection.couchbase.CouchbaseContainerConnectionDetailsFactory,\
org.springframework.boot.testcontainers.service.connection.flyway.FlywayContainerConnectionDetailsFactory,\
org.springframework.boot.testcontainers.service.connection.redis.RedisContainerConnectionDetailsFactory,\
org.springframework.boot.testcontainers.service.connection.elasticsearch.ElasticsearchContainerConnectionDetailsFactory,\
org.springframework.boot.testcontainers.service.connection.influx.InfluxDbContainerConnectionDetailsFactory,\
org.springframework.boot.testcontainers.service.connection.jdbc.JdbcContainerConnectionDetailsFactory,\
@ -17,6 +16,7 @@ org.springframework.boot.testcontainers.service.connection.liquibase.LiquibaseCo
org.springframework.boot.testcontainers.service.connection.mongo.MongoContainerConnectionDetailsFactory,\
org.springframework.boot.testcontainers.service.connection.neo4j.Neo4jContainerConnectionDetailsFactory,\
org.springframework.boot.testcontainers.service.connection.r2dbc.MariaDbR2dbcContainerConnectionDetailsFactory,\
org.springframework.boot.testcontainers.service.connection.r2dbc.MsSqlServerR2dbcContainerConnectionDetailsFactory,\
org.springframework.boot.testcontainers.service.connection.r2dbc.MySqlR2dbcContainerConnectionDetailsFactory,\
org.springframework.boot.testcontainers.service.connection.r2dbc.PostgresR2dbcContainerConnectionDetailsFactory,\
org.springframework.boot.testcontainers.service.connection.r2dbc.SqlServerR2dbcContainerConnectionDetailsFactory
org.springframework.boot.testcontainers.service.connection.redis.RedisContainerConnectionDetailsFactory

@ -0,0 +1,156 @@
/*
* Copyright 2012-2023 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 org.springframework.boot.testcontainers.service.connection;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.elasticsearch.ElasticsearchContainer;
import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory;
import org.springframework.boot.origin.Origin;
import org.springframework.core.annotation.MergedAnnotation;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link ContainerConnectionDetailsFactory}.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
*/
class ContainerConnectionDetailsFactoryTests {
private String beanNameSuffix;
private Origin origin;
private JdbcDatabaseContainer<?> container;
private MergedAnnotation<ServiceConnection> annotation;
private ContainerConnectionSource<?, ?> source;
@BeforeEach
void setup() {
this.beanNameSuffix = "MyBean";
this.origin = mock(Origin.class);
this.container = mock(PostgreSQLContainer.class);
this.annotation = MergedAnnotation.of(ServiceConnection.class,
Map.of("name", "myname", "type", new Class<?>[0]));
this.source = new ContainerConnectionSource<>(this.beanNameSuffix, this.origin, this.container,
this.annotation);
}
@Test
void getConnectionDetailsWhenTypesMatchAndNoNameRestrictionReturnsDetails() {
TestContainerConnectionDetailsFactory factory = new TestContainerConnectionDetailsFactory();
ConnectionDetails connectionDetails = getConnectionDetails(factory, this.source);
assertThat(connectionDetails).isNotNull();
}
@Test
void getConnectionDetailsWhenTypesMatchAndNameRestrictionMatchesReturnsDetails() {
TestContainerConnectionDetailsFactory factory = new TestContainerConnectionDetailsFactory("myname");
ConnectionDetails connectionDetails = getConnectionDetails(factory, this.source);
assertThat(connectionDetails).isNotNull();
}
@Test
void getConnectionDetailsWhenTypesMatchAndNameRestrictionDoesNotMatchReturnsNull() {
TestContainerConnectionDetailsFactory factory = new TestContainerConnectionDetailsFactory("notmyname");
ConnectionDetails connectionDetails = getConnectionDetails(factory, this.source);
assertThat(connectionDetails).isNull();
}
@Test
void getConnectionDetailsWhenContainerTypeDoesNotMatchReturnsNull() {
ElasticsearchContainer container = mock(ElasticsearchContainer.class);
ContainerConnectionSource<?, ?> source = new ContainerConnectionSource<>(this.beanNameSuffix, this.origin,
container, this.annotation);
TestContainerConnectionDetailsFactory factory = new TestContainerConnectionDetailsFactory();
ConnectionDetails connectionDetails = getConnectionDetails(factory, source);
assertThat(connectionDetails).isNull();
}
@Test
void getConnectionDetailsHasOrigin() {
TestContainerConnectionDetailsFactory factory = new TestContainerConnectionDetailsFactory();
ConnectionDetails connectionDetails = getConnectionDetails(factory, this.source);
assertThat(Origin.from(connectionDetails)).isSameAs(this.origin);
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private ConnectionDetails getConnectionDetails(ConnectionDetailsFactory<?, ?> factory,
ContainerConnectionSource<?, ?> source) {
return ((ConnectionDetailsFactory) factory).getConnectionDetails(source);
}
/**
* Test {@link ContainerConnectionDetailsFactory}.
*/
static class TestContainerConnectionDetailsFactory
extends ContainerConnectionDetailsFactory<JdbcConnectionDetails, JdbcDatabaseContainer<?>> {
TestContainerConnectionDetailsFactory() {
this(ANY_CONNECTION_NAME);
}
TestContainerConnectionDetailsFactory(String connectionName) {
super(connectionName);
}
@Override
protected JdbcConnectionDetails getContainerConnectionDetails(
ContainerConnectionSource<JdbcConnectionDetails, JdbcDatabaseContainer<?>> source) {
return new TestContainerConnectionDetails(source);
}
static class TestContainerConnectionDetails extends ContainerConnectionDetails
implements JdbcConnectionDetails {
TestContainerConnectionDetails(ContainerConnectionSource<?, ?> source) {
super(source);
}
@Override
public String getUsername() {
return "user";
}
@Override
public String getPassword() {
return "secret";
}
@Override
public String getJdbcUrl() {
return "jdbc:example";
}
}
}
}

@ -0,0 +1,183 @@
/*
* Copyright 2012-2023 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 org.springframework.boot.testcontainers.service.connection;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.elasticsearch.ElasticsearchContainer;
import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails;
import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails;
import org.springframework.boot.origin.Origin;
import org.springframework.core.annotation.MergedAnnotation;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link ContainerConnectionSource}.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
*/
class ContainerConnectionSourceTests {
private String beanNameSuffix;
private Origin origin;
private JdbcDatabaseContainer<?> container;
private MergedAnnotation<ServiceConnection> annotation;
private ContainerConnectionSource<?, ?> source;
@BeforeEach
void setup() {
this.beanNameSuffix = "MyBean";
this.origin = mock(Origin.class);
this.container = mock(PostgreSQLContainer.class);
given(this.container.getDockerImageName()).willReturn("postgres");
this.annotation = MergedAnnotation.of(ServiceConnection.class, Map.of("name", "", "type", new Class<?>[0]));
this.source = new ContainerConnectionSource<>(this.beanNameSuffix, this.origin, this.container,
this.annotation);
}
@Test
void acceptsWhenContainerIsNotInstanceOfContainerTypeReturnsFalse() {
String connectionName = null;
Class<?> connectionDetailsType = JdbcConnectionDetails.class;
Class<?> containerType = ElasticsearchContainer.class;
assertThat(this.source.accepts(connectionName, connectionDetailsType, containerType)).isFalse();
}
@Test
void acceptsWhenContainerIsInstanceOfContainerTypeReturnsTrue() {
String connectionName = null;
Class<?> connectionDetailsType = JdbcConnectionDetails.class;
Class<?> containerType = JdbcDatabaseContainer.class;
assertThat(this.source.accepts(connectionName, connectionDetailsType, containerType)).isTrue();
}
@Test
void acceptsWhenConnectionNameDoesNotMatchNameTakenFromAnnotationReturnsFalse() {
setupSourceAnnotatedWithName("myname");
String connectionName = "othername";
Class<?> connectionDetailsType = JdbcConnectionDetails.class;
Class<?> containerType = JdbcDatabaseContainer.class;
assertThat(this.source.accepts(connectionName, connectionDetailsType, containerType)).isFalse();
}
@Test
void acceptsWhenConnectionNameDoesNotMatchNameTakenFromContainerReturnsFalse() {
String connectionName = "othername";
Class<?> connectionDetailsType = JdbcConnectionDetails.class;
Class<?> containerType = JdbcDatabaseContainer.class;
assertThat(this.source.accepts(connectionName, connectionDetailsType, containerType)).isFalse();
}
@Test
void acceptsWhenConnectionNameIsUnrestrictedReturnsTrue() {
String connectionName = null;
Class<?> connectionDetailsType = JdbcConnectionDetails.class;
Class<?> containerType = JdbcDatabaseContainer.class;
assertThat(this.source.accepts(connectionName, connectionDetailsType, containerType)).isTrue();
}
@Test
void acceptsWhenConnectionNameMatchesNameTakenFromAnnotationReturnsTrue() {
setupSourceAnnotatedWithName("myname");
String connectionName = "myname";
Class<?> connectionDetailsType = JdbcConnectionDetails.class;
Class<?> containerType = JdbcDatabaseContainer.class;
assertThat(this.source.accepts(connectionName, connectionDetailsType, containerType)).isTrue();
}
@Test
void acceptsWhenConnectionNameMatchesNameTakenFromContainerReturnsTrue() {
String connectionName = "postgres";
Class<?> connectionDetailsType = JdbcConnectionDetails.class;
Class<?> containerType = JdbcDatabaseContainer.class;
assertThat(this.source.accepts(connectionName, connectionDetailsType, containerType)).isTrue();
}
@Test
void acceptsWhenConnectionDetailsTypeNotInAnnotationRestrictionReturnsFalse() {
setupSourceAnnotatedWithType(ElasticsearchConnectionDetails.class);
String connectionName = null;
Class<?> connectionDetailsType = JdbcConnectionDetails.class;
Class<?> containerType = JdbcDatabaseContainer.class;
assertThat(this.source.accepts(connectionName, connectionDetailsType, containerType)).isFalse();
}
@Test
void acceptsWhenConnectionDetailsTypeInAnnotationRestrictionReturnsTrue() {
setupSourceAnnotatedWithType(JdbcConnectionDetails.class);
String connectionName = null;
Class<?> connectionDetailsType = JdbcConnectionDetails.class;
Class<?> containerType = JdbcDatabaseContainer.class;
assertThat(this.source.accepts(connectionName, connectionDetailsType, containerType)).isTrue();
}
@Test
void acceptsWhenConnectionDetailsTypeIsNotRestrictedReturnsTrue() {
String connectionName = null;
Class<?> connectionDetailsType = JdbcConnectionDetails.class;
Class<?> containerType = JdbcDatabaseContainer.class;
assertThat(this.source.accepts(connectionName, connectionDetailsType, containerType)).isTrue();
}
@Test
void getBeanNameSuffixReturnsBeanNameSuffix() {
assertThat(this.source.getBeanNameSuffix()).isEqualTo(this.beanNameSuffix);
}
@Test
void getOriginReturnsOrigin() {
assertThat(this.source.getOrigin()).isEqualTo(this.origin);
}
@Test
void getContainerReturnsContainer() {
assertThat(this.source.getContainer()).isSameAs(this.container);
}
@Test
void toStringReturnsSensibleString() {
assertThat(this.source.toString()).startsWith("@ServiceConnection source for Mock for Origin");
}
private void setupSourceAnnotatedWithName(String name) {
this.annotation = MergedAnnotation.of(ServiceConnection.class, Map.of("name", name, "type", new Class<?>[0]));
this.source = new ContainerConnectionSource<>(this.beanNameSuffix, this.origin, this.container,
this.annotation);
}
private void setupSourceAnnotatedWithType(Class<?> type) {
this.annotation = MergedAnnotation.of(ServiceConnection.class,
Map.of("name", "", "type", new Class<?>[] { type }));
this.source = new ContainerConnectionSource<>(this.beanNameSuffix, this.origin, this.container,
this.annotation);
}
}

@ -0,0 +1,65 @@
/*
* Copyright 2012-2023 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 org.springframework.boot.testcontainers.service.connection;
import org.junit.jupiter.api.Test;
import org.springframework.boot.origin.Origin;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link FieldOrigin}.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
*/
class FieldOriginTests {
@Test
void createWhenFieldIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new FieldOrigin(null))
.withMessage("Field must not be null");
}
@Test
void equalsAndHashCode() {
Origin o1 = new FieldOrigin(ReflectionUtils.findField(Fields.class, "one"));
Origin o2 = new FieldOrigin(ReflectionUtils.findField(Fields.class, "one"));
Origin o3 = new FieldOrigin(ReflectionUtils.findField(Fields.class, "two"));
assertThat(o1).isEqualTo(o1).isEqualTo(o2).isNotEqualTo(o3);
assertThat(o1.hashCode()).isEqualTo(o2.hashCode());
}
@Test
void toStringReturnsSensibleString() {
Origin origin = new FieldOrigin(ReflectionUtils.findField(Fields.class, "one"));
assertThat(origin).hasToString("FieldOriginTests.Fields.one");
}
static class Fields {
String one;
String two;
}
}

@ -16,32 +16,35 @@
package org.springframework.boot.testcontainers.service.connection;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.Container;
import org.testcontainers.containers.GenericContainer;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
import org.springframework.boot.testcontainers.service.connection.ServiceConnectionContextCustomizerFactoryTests.ServiceConnections.NestedClass;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link ServiceConnectionContextCustomizerFactory}.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
*/
public class ServiceConnectionContextCustomizerFactoryTests {
class ServiceConnectionContextCustomizerFactoryTests {
private final ServiceConnectionContextCustomizerFactory factory = new ServiceConnectionContextCustomizerFactory();
@Test
void whenClassHasNoServiceConnectionsThenCreateReturnsNull() {
void createContextCustomizerWhenNoServiceConnectionsReturnsNull() {
assertThat(this.factory.createContextCustomizer(NoServiceConnections.class, null)).isNull();
}
@Test
void whenClassHasServiceConnectionsThenCreateReturnsCustomizer() {
void createContextCustomizerWhenClassHasServiceConnectionsReturnsCustomizer() {
ServiceConnectionContextCustomizer customizer = (ServiceConnectionContextCustomizer) this.factory
.createContextCustomizer(ServiceConnections.class, null);
assertThat(customizer).isNotNull();
@ -49,37 +52,79 @@ public class ServiceConnectionContextCustomizerFactoryTests {
}
@Test
void whenEnclosingClassHasServiceConnectionsThenCreateReturnsCustomizer() {
void createContextCustomizerWhenEnclosingClassHasServiceConnectionsReturnsCustomizer() {
ServiceConnectionContextCustomizer customizer = (ServiceConnectionContextCustomizer) this.factory
.createContextCustomizer(NestedClass.class, null);
.createContextCustomizer(ServiceConnections.NestedClass.class, null);
assertThat(customizer).isNotNull();
assertThat(customizer.getSources()).hasSize(3);
}
@Test
void whenClassHasNonStaticServiceConnectionThenCreateShouldFailWithHelpfulIllegalStateException() {
void createContextCustomizerWhenClassHasNonStaticServiceConnectionFailsWithHepfulException() {
assertThatIllegalStateException()
.isThrownBy(() -> this.factory.createContextCustomizer(NonStaticServiceConnection.class, null))
.withMessage("@ServiceConnection field 'service' must be static");
}
@Test
void createContextCustomizerWhenClassHasAnnotationOnNonConnectionFieldFailsWithHepfulException() {
assertThatIllegalStateException()
.isThrownBy(() -> this.factory.createContextCustomizer(ServiceConnectionOnWrongFieldType.class, null))
.withMessage("Field 'service2' in " + ServiceConnectionOnWrongFieldType.class.getName()
+ " must be a org.testcontainers.containers.Container");
}
@Test
void createContextCustomizerCreatesCustomizerSourceWithSensibleBeanNameSuffix() {
ServiceConnectionContextCustomizer customizer = (ServiceConnectionContextCustomizer) this.factory
.createContextCustomizer(SingleServiceConnection.class, null);
ContainerConnectionSource<?, ?> source = customizer.getSources().get(0);
assertThat(source.getBeanNameSuffix()).isEqualTo("SingleServiceConnectionService1");
}
@Test
void createContextCustomizerCreatesCustomizerSourceWithSensibleOrigin() {
ServiceConnectionContextCustomizer customizer = (ServiceConnectionContextCustomizer) this.factory
.createContextCustomizer(SingleServiceConnection.class, null);
ContainerConnectionSource<?, ?> source = customizer.getSources().get(0);
assertThat(source.getOrigin())
.hasToString("ServiceConnectionContextCustomizerFactoryTests.SingleServiceConnection.service1");
}
@Test
void createContextCustomizerCreatesCustomizerSourceWithSensibleToString() {
ServiceConnectionContextCustomizer customizer = (ServiceConnectionContextCustomizer) this.factory
.createContextCustomizer(SingleServiceConnection.class, null);
ContainerConnectionSource<?, ?> source = customizer.getSources().get(0);
assertThat(source).hasToString(
"@ServiceConnection source for ServiceConnectionContextCustomizerFactoryTests.SingleServiceConnection.service1");
}
static class NoServiceConnections {
}
static class SingleServiceConnection {
@ServiceConnection
private static GenericContainer<?> service1 = new MockContainer();
}
static class ServiceConnections {
@ServiceConnection(TestConnectionDetails.class)
private static GenericContainer<?> service1 = new GenericContainer<>("example");
@ServiceConnection
private static Container<?> service1 = new MockContainer();
@ServiceConnection(TestConnectionDetails.class)
private static GenericContainer<?> service2 = new GenericContainer<>("example");
@ServiceConnection
private static Container<?> service2 = new MockContainer();
@Nested
class NestedClass {
@ServiceConnection(TestConnectionDetails.class)
private static GenericContainer<?> service3 = new GenericContainer<>("example");
@ServiceConnection
private static Container<?> service3 = new MockContainer();
}
@ -87,12 +132,35 @@ public class ServiceConnectionContextCustomizerFactoryTests {
static class NonStaticServiceConnection {
@ServiceConnection(TestConnectionDetails.class)
private GenericContainer<?> service = new GenericContainer<>("example");
@ServiceConnection
private Container<?> service = new MockContainer("example");
}
static class TestConnectionDetails implements ConnectionDetails {
static class ServiceConnectionOnWrongFieldType {
@ServiceConnection
private static InputStream service2 = new ByteArrayInputStream(new byte[0]);
}
static class MockContainer extends GenericContainer<MockContainer> {
private final String dockerImageName;
MockContainer() {
this("example");
}
MockContainer(String dockerImageName) {
super(dockerImageName);
this.dockerImageName = dockerImageName;
}
@Override
public String getDockerImageName() {
return this.dockerImageName;
}
}

@ -0,0 +1,133 @@
/*
* Copyright 2012-2023 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 org.springframework.boot.testcontainers.service.connection;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.containers.PostgreSQLContainer;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactories;
import org.springframework.boot.origin.Origin;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.test.context.MergedContextConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
/**
* Tests for {@link ServiceConnectionContextCustomizer}.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
*/
class ServiceConnectionContextCustomizerTests {
private String beanNameSuffix;
private Origin origin;
private JdbcDatabaseContainer<?> container;
private MergedAnnotation<ServiceConnection> annotation;
private ContainerConnectionSource<?, ?> source;
private ConnectionDetailsFactories factories;
@BeforeEach
void setup() {
this.beanNameSuffix = "MyBean";
this.origin = mock(Origin.class);
this.container = mock(PostgreSQLContainer.class);
this.annotation = MergedAnnotation.of(ServiceConnection.class,
Map.of("name", "myname", "type", new Class<?>[0]));
this.source = new ContainerConnectionSource<>(this.beanNameSuffix, this.origin, this.container,
this.annotation);
this.factories = mock(ConnectionDetailsFactories.class);
}
@Test
void customizeContextRegistersServiceConnections() {
ServiceConnectionContextCustomizer customizer = new ServiceConnectionContextCustomizer(List.of(this.source),
this.factories);
ConfigurableApplicationContext context = mock(ConfigurableApplicationContext.class);
DefaultListableBeanFactory beanFactory = spy(new DefaultListableBeanFactory());
given(context.getBeanFactory()).willReturn(beanFactory);
MergedContextConfiguration mergedConfig = mock(MergedContextConfiguration.class);
JdbcConnectionDetails connectionDetails = new TestJdbcConnectionDetails();
given(this.factories.getConnectionDetails(this.source))
.willReturn(Map.of(JdbcConnectionDetails.class, connectionDetails));
customizer.customizeContext(context, mergedConfig);
ArgumentCaptor<BeanDefinition> beanDefinitionCaptor = ArgumentCaptor.forClass(BeanDefinition.class);
then(beanFactory).should()
.registerBeanDefinition(eq("testJdbcConnectionDetailsForMyBean"), beanDefinitionCaptor.capture());
RootBeanDefinition beanDefinition = (RootBeanDefinition) beanDefinitionCaptor.getValue();
assertThat(beanDefinition.getInstanceSupplier().get()).isSameAs(connectionDetails);
assertThat(beanDefinition.getBeanClass()).isEqualTo(TestJdbcConnectionDetails.class);
}
@Test
void customizeContextWhenFactoriesHasNoConnectionDetailsThrowsException() {
ServiceConnectionContextCustomizer customizer = new ServiceConnectionContextCustomizer(List.of(this.source),
this.factories);
ConfigurableApplicationContext context = mock(ConfigurableApplicationContext.class);
DefaultListableBeanFactory beanFactory = spy(new DefaultListableBeanFactory());
given(context.getBeanFactory()).willReturn(beanFactory);
MergedContextConfiguration mergedConfig = mock(MergedContextConfiguration.class);
assertThatIllegalStateException().isThrownBy(() -> customizer.customizeContext(context, mergedConfig))
.withMessageStartingWith("No connection details created for @ServiceConnection source");
}
/**
* Test {@link JdbcConnectionDetails}.
*/
static class TestJdbcConnectionDetails implements JdbcConnectionDetails {
@Override
public String getUsername() {
return null;
}
@Override
public String getPassword() {
return null;
}
@Override
public String getJdbcUrl() {
return null;
}
}
}

@ -25,7 +25,6 @@ import org.junit.jupiter.api.Test;
import org.testcontainers.containers.RabbitMQContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
@ -34,6 +33,8 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration;
import org.springframework.boot.autoconfigure.amqp.RabbitConnectionDetails;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.testcontainers.DockerImageNames;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
@ -41,7 +42,7 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link RabbitServiceConnection}.
* Tests for {@link RabbitContainerConnectionDetailsFactory}.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
@ -49,11 +50,12 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
@SpringJUnitConfig
@Testcontainers(disabledWithoutDocker = true)
class RabbitServiceConnectionTests {
class RabbitContainerConnectionDetailsFactoryIntegrationTests {
@Container
@RabbitServiceConnection
static final RabbitMQContainer rabbit = new RabbitMQContainer(DockerImageName.parse("rabbitmq:3.11-alpine"));
@ServiceConnection
static final RabbitMQContainer rabbit = new RabbitMQContainer(DockerImageNames.rabbit())
.withStartupTimeout(Duration.ofMinutes(4));
@Autowired(required = false)
private RabbitConnectionDetails connectionDetails;

@ -21,18 +21,19 @@ import org.junit.jupiter.api.Test;
import org.testcontainers.containers.InfluxDBContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.influx.InfluxDbAutoConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.testcontainers.DockerImageNames;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link InfluxDbServiceConnection}.
* Tests for {@link InfluxDbContainerConnectionDetailsFactory}.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
@ -40,21 +41,18 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
@SpringJUnitConfig
@Testcontainers(disabledWithoutDocker = true)
class InfluxDbServiceConnectionTests {
private static final String INFLUXDB_VERSION = "2.6.1";
class InfluxDbContainerConnectionDetailsFactoryIntegrationTests {
@Container
@InfluxDbServiceConnection
static final InfluxDBContainer<?> influxDbService = new InfluxDBContainer<>(
DockerImageName.parse("influxdb").withTag(INFLUXDB_VERSION));
@ServiceConnection
static final InfluxDBContainer<?> influxDbService = new InfluxDBContainer<>(DockerImageNames.influxDb());
@Autowired
private InfluxDB influxDb;
@Test
void connectionCanBeMadeToInfluxDbContainer() {
assertThat(this.influxDb.version()).isEqualTo("v" + INFLUXDB_VERSION);
assertThat(this.influxDb.version()).isEqualTo("v" + DockerImageNames.influxDb().getVersionPart());
}
@Configuration(proxyBeanMethods = false)

@ -25,11 +25,12 @@ import org.junit.jupiter.api.Test;
import org.testcontainers.containers.KafkaContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.testcontainers.DockerImageNames;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.annotation.KafkaListener;
@ -40,7 +41,7 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link KafkaServiceConnection}.
* Tests for {@link KafkaContainerConnectionDetailsFactory}.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
@ -50,11 +51,11 @@ import static org.assertj.core.api.Assertions.assertThat;
@Testcontainers(disabledWithoutDocker = true)
@TestPropertySource(properties = { "spring.kafka.consumer.group-id=test-group",
"spring.kafka.consumer.auto-offset-reset=earliest" })
class KafkaServiceConnectionTests {
class KafkaContainerConnectionDetailsFactoryIntegrationTests {
@Container
@KafkaServiceConnection
static final KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:5.4.3"));
@ServiceConnection
static final KafkaContainer kafka = new KafkaContainer(DockerImageNames.kafka());
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;

@ -22,7 +22,7 @@ import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.redis.RedisServiceConnection;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.testcontainers.RedisContainer;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
@ -34,7 +34,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class SampleCacheApplicationRedisTests {
@Container
@RedisServiceConnection
@ServiceConnection
private static final RedisContainer redis = new RedisContainer();
@Autowired

@ -23,10 +23,8 @@ import org.testcontainers.junit.jupiter.Testcontainers;
import reactor.test.StepVerifier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.flyway.FlywayConnectionDetails;
import org.springframework.boot.test.autoconfigure.data.r2dbc.DataR2dbcTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testcontainers.service.connection.r2dbc.R2dbcServiceConnection;
import org.springframework.boot.testsupport.testcontainers.DockerImageNames;
import static org.assertj.core.api.Assertions.assertThat;
@ -43,8 +41,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class CityRepositoryTests {
@Container
@R2dbcServiceConnection
@ServiceConnection(FlywayConnectionDetails.class)
@ServiceConnection
static PostgreSQLContainer<?> postgresql = new PostgreSQLContainer<>(DockerImageNames.postgresql())
.withDatabaseName("test_flyway");

@ -23,10 +23,8 @@ import org.testcontainers.junit.jupiter.Testcontainers;
import reactor.test.StepVerifier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.liquibase.LiquibaseConnectionDetails;
import org.springframework.boot.test.autoconfigure.data.r2dbc.DataR2dbcTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testcontainers.service.connection.r2dbc.R2dbcServiceConnection;
import org.springframework.boot.testsupport.testcontainers.DockerImageNames;
import static org.assertj.core.api.Assertions.assertThat;
@ -43,8 +41,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class CityRepositoryTests {
@Container
@R2dbcServiceConnection
@ServiceConnection(LiquibaseConnectionDetails.class)
@ServiceConnection
static PostgreSQLContainer<?> postgresql = new PostgreSQLContainer<>(DockerImageNames.postgresql())
.withDatabaseName("test_liquibase");

@ -31,7 +31,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.boot.testcontainers.service.connection.mongo.MongoServiceConnection;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.testcontainers.DockerImageNames;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
@ -65,7 +65,7 @@ class SampleSessionMongoApplicationTests {
private int port;
@Container
@MongoServiceConnection
@ServiceConnection
static MongoDBContainer mongo = new MongoDBContainer(DockerImageNames.mongo()).withStartupAttempts(3)
.withStartupTimeout(Duration.ofMinutes(2));

@ -28,7 +28,7 @@ import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.testcontainers.service.connection.redis.RedisServiceConnection;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.testcontainers.RedisContainer;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
@ -56,7 +56,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class SampleSessionRedisApplicationTests {
@Container
@RedisServiceConnection
@ServiceConnection
static RedisContainer redis = new RedisContainer();
@Autowired

@ -28,7 +28,7 @@ import reactor.util.function.Tuples;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.boot.testcontainers.service.connection.mongo.MongoServiceConnection;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.testcontainers.DockerImageNames;
import org.springframework.http.HttpStatus;
import org.springframework.web.reactive.function.client.WebClient;
@ -49,7 +49,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class SampleSessionWebFluxMongoApplicationTests {
@Container
@MongoServiceConnection
@ServiceConnection
private static final MongoDBContainer mongo = new MongoDBContainer(DockerImageNames.mongo()).withStartupAttempts(3)
.withStartupTimeout(Duration.ofMinutes(2));

@ -27,7 +27,7 @@ import reactor.util.function.Tuples;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.boot.testcontainers.service.connection.redis.RedisServiceConnection;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.testcontainers.RedisContainer;
import org.springframework.http.HttpStatus;
import org.springframework.web.reactive.function.client.WebClient;
@ -48,7 +48,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class SampleSessionWebFluxRedisApplicationTests {
@Container
@RedisServiceConnection
@ServiceConnection
private static final RedisContainer redis = new RedisContainer();
@LocalServerPort

Loading…
Cancel
Save