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; package org.springframework.boot.autoconfigure.service.connection;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.core.style.ToStringCreator; import org.springframework.util.Assert;
/** /**
* A registry of {@link ConnectionDetailsFactory} instances. * A registry of {@link ConnectionDetailsFactory} instances.
@ -49,30 +52,55 @@ public class ConnectionDetailsFactories {
registrations.filter(Objects::nonNull).forEach(this.registrations::add); 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") @SuppressWarnings("unchecked")
public <S> ConnectionDetailsFactory<S, ConnectionDetails> getConnectionDetailsFactory(S source) { <S> List<Registration<S, ?>> getRegistrations(S source) {
Class<S> sourceType = (Class<S>) source.getClass(); 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) { for (Registration<?, ?> candidate : this.registrations) {
if (candidate.sourceType().isAssignableFrom(sourceType)) { if (candidate.sourceType().isAssignableFrom(sourceType)) {
result.add((ConnectionDetailsFactory<S, ConnectionDetails>) candidate.factory()); result.add((Registration<S, ?>) candidate);
} }
} }
if (result.isEmpty()) { if (result.isEmpty()) {
throw new ConnectionDetailsFactoryNotFoundException(source); throw new ConnectionDetailsFactoryNotFoundException(source);
} }
AnnotationAwareOrderComparator.sort(result); result.sort(Comparator.comparing(Registration::factory, AnnotationAwareOrderComparator.INSTANCE));
return (result.size() != 1) ? new CompositeConnectionDetailsFactory<>(result) : result.get(0); return List.copyOf(result);
} }
/** /**
* A {@link ConnectionDetailsFactory} registration. * 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) { ConnectionDetailsFactory<S, D> factory) {
@SuppressWarnings("unchecked") @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; 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.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.Ordered;
import org.springframework.core.test.io.support.MockSpringFactoriesLoader; import org.springframework.core.test.io.support.MockSpringFactoriesLoader;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/** /**
* Tests for {@link ConnectionDetailsFactories}. * Tests for {@link ConnectionDetailsFactories}.
@ -38,43 +41,50 @@ class ConnectionDetailsFactoriesTests {
private final MockSpringFactoriesLoader loader = new MockSpringFactoriesLoader(); private final MockSpringFactoriesLoader loader = new MockSpringFactoriesLoader();
@Test @Test
void getConnectionDetailsFactoryShouldThrowWhenNoFactoryForSource() { void getConnectionDetailsWhenNoFactoryForSourceThrowsException() {
ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader); ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader);
assertThatExceptionOfType(ConnectionDetailsFactoryNotFoundException.class) assertThatExceptionOfType(ConnectionDetailsFactoryNotFoundException.class)
.isThrownBy(() -> factories.getConnectionDetailsFactory("source")); .isThrownBy(() -> factories.getConnectionDetails("source"));
} }
@Test @Test
void getConnectionDetailsFactoryShouldReturnSingleFactoryWhenSourceHasOneMatch() { void getConnectionDetailsWhenSourceHasOneMatchReturnsSingleResult() {
this.loader.addInstance(ConnectionDetailsFactory.class, new TestConnectionDetailsFactory()); this.loader.addInstance(ConnectionDetailsFactory.class, new TestConnectionDetailsFactory());
ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader); ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader);
ConnectionDetailsFactory<String, ConnectionDetails> factory = factories.getConnectionDetailsFactory("source"); Map<Class<?>, ConnectionDetails> connectionDetails = factories.getConnectionDetails("source");
assertThat(factory).isInstanceOf(TestConnectionDetailsFactory.class); 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 @Test
@SuppressWarnings("unchecked") void getConnectionDetailsWhenDuplicatesThrowsException() {
void getConnectionDetailsFactoryShouldReturnCompositeFactoryWhenSourceHasMultipleMatches() {
this.loader.addInstance(ConnectionDetailsFactory.class, new TestConnectionDetailsFactory(), this.loader.addInstance(ConnectionDetailsFactory.class, new TestConnectionDetailsFactory(),
new TestConnectionDetailsFactory()); new TestConnectionDetailsFactory());
ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader); ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader);
ConnectionDetailsFactory<String, ConnectionDetails> factory = factories.getConnectionDetailsFactory("source"); assertThatIllegalStateException().isThrownBy(() -> factories.getConnectionDetails("source"))
assertThat(factory).asInstanceOf(InstanceOfAssertFactories.type(CompositeConnectionDetailsFactory.class)) .withMessage("Duplicate connection details supplied for " + TestConnectionDetails.class.getName());
.satisfies((composite) -> assertThat(composite.getDelegates()).hasSize(2));
} }
@Test @Test
@SuppressWarnings("unchecked") void getRegistrationsReturnsOrderedDelegates() {
void compositeFactoryShouldHaveOrderedDelegates() {
TestConnectionDetailsFactory orderOne = new TestConnectionDetailsFactory(1); TestConnectionDetailsFactory orderOne = new TestConnectionDetailsFactory(1);
TestConnectionDetailsFactory orderTwo = new TestConnectionDetailsFactory(2); TestConnectionDetailsFactory orderTwo = new TestConnectionDetailsFactory(2);
TestConnectionDetailsFactory orderThree = new TestConnectionDetailsFactory(3); TestConnectionDetailsFactory orderThree = new TestConnectionDetailsFactory(3);
this.loader.addInstance(ConnectionDetailsFactory.class, orderOne, orderThree, orderTwo); this.loader.addInstance(ConnectionDetailsFactory.class, orderOne, orderThree, orderTwo);
ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader); ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader);
ConnectionDetailsFactory<String, ConnectionDetails> factory = factories.getConnectionDetailsFactory("source"); List<Registration<String, ?>> registrations = factories.getRegistrations("source");
assertThat(factory).asInstanceOf(InstanceOfAssertFactories.type(CompositeConnectionDetailsFactory.class)) assertThat(registrations.get(0).factory()).isEqualTo(orderOne);
.satisfies((composite) -> assertThat(composite.getDelegates()).containsExactly(orderOne, orderTwo, assertThat(registrations.get(1).factory()).isEqualTo(orderTwo);
orderThree)); assertThat(registrations.get(2).factory()).isEqualTo(orderThree);
} }
private static final class TestConnectionDetailsFactory private static final class TestConnectionDetailsFactory
@ -92,7 +102,7 @@ class ConnectionDetailsFactoriesTests {
@Override @Override
public TestConnectionDetails getConnectionDetails(String source) { public TestConnectionDetails getConnectionDetails(String source) {
return new TestConnectionDetails(); return new TestConnectionDetailsImpl();
} }
@Override @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]] [[howto.testing.testcontainers]]
=== Use Testcontainers for Integration Testing === Use Testcontainers for Integration Testing
The https://www.testcontainers.org/[Testcontainers] library provides a way to manage services running inside Docker containers. 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. 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 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: Testcontainers can be used in a Spring Boot test as follows:
include::code:vanilla/MyIntegrationTests[] 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. In most cases, you will need to configure the application to connect to the service running in the container.
[[howto.testing.testcontainers.service-connections]] [[howto.testing.testcontainers.service-connections]]
==== Service Connections ==== Service Connections
A service connection is a connection to any remote service. 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[] 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. 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`: 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.
- `@CassandraServiceConnection`
- `@CouchbaseServiceConnection` The following service connection factories are provided in the `spring-boot-testcontainers` jar:
- `@ElasticsearchServiceConnection`
- `@InfluxDbServiceConnection` |===
- `@JdbcServiceConnection` | Connection Details | Matched on
- `@KafkaServiceConnection`
- `@MongoServiceConnection` | `CassandraConnectionDetails`
- `@Neo4jServiceConnection` | Containers of type `CassandraContainer`
- `@R2dbcServiceConnection`
- `@RabbitServiceConnection` | `CouchbaseConnectionDetails`
- `@RedisServiceConnection` | Containers of type `CouchbaseContainer`
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. | `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.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.boot.test.context.SpringBootTest; 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 @SpringBootTest
@Testcontainers @Testcontainers
class MyIntegrationTests { class MyIntegrationTests {
@Container @Container
@Neo4jServiceConnection @ServiceConnection
static Neo4jContainer<?> neo4j = new Neo4jContainer<>("neo4j:5"); static Neo4jContainer<?> neo4j = new Neo4jContainer<>("neo4j:5");
@Test @Test

@ -17,14 +17,13 @@
package org.springframework.boot.docs.howto.testing.testcontainers.serviceconnection package org.springframework.boot.docs.howto.testing.testcontainers.serviceconnection
import org.junit.jupiter.api.Test 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.containers.Neo4jContainer
import org.testcontainers.junit.jupiter.Container import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers import org.testcontainers.junit.jupiter.Testcontainers
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
@SpringBootTest @SpringBootTest
@Testcontainers @Testcontainers
internal class MyIntegrationTests { internal class MyIntegrationTests {
@ -37,7 +36,7 @@ internal class MyIntegrationTests {
companion object { companion object {
@Container @Container
@Neo4jServiceConnection @ServiceConnection
var neo4j: Neo4jContainer<*> = Neo4jContainer<Nothing>("neo4j:5") 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.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.redis.ExampleService; import org.springframework.boot.test.autoconfigure.data.redis.ExampleService;
import org.springframework.boot.test.context.TestConfiguration; 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.boot.testsupport.testcontainers.CassandraContainer;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -52,7 +52,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
class DataCassandraTestIntegrationTests { class DataCassandraTestIntegrationTests {
@Container @Container
@CassandraServiceConnection @ServiceConnection
static final CassandraContainer cassandra = new CassandraContainer(); static final CassandraContainer cassandra = new CassandraContainer();
@Autowired @Autowired

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

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

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

@ -25,7 +25,7 @@ import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.beans.factory.annotation.Autowired; 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.boot.testsupport.testcontainers.DockerImageNames;
import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -48,7 +48,7 @@ class DataCouchbaseTestWithIncludeFilterIntegrationTests {
private static final String BUCKET_NAME = "cbbucket"; private static final String BUCKET_NAME = "cbbucket";
@Container @Container
@CouchbaseServiceConnection @ServiceConnection
static final CouchbaseContainer couchbase = new CouchbaseContainer(DockerImageNames.couchbase()) static final CouchbaseContainer couchbase = new CouchbaseContainer(DockerImageNames.couchbase())
.withStartupAttempts(5) .withStartupAttempts(5)
.withStartupTimeout(Duration.ofMinutes(10)) .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.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired; 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.boot.testsupport.testcontainers.DockerImageNames;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchTemplate; import org.springframework.data.elasticsearch.client.elc.ElasticsearchTemplate;
@ -47,7 +47,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
class DataElasticsearchTestIntegrationTests { class DataElasticsearchTestIntegrationTests {
@Container @Container
@ElasticsearchServiceConnection @ServiceConnection
static final ElasticsearchContainer elasticsearch = new ElasticsearchContainer(DockerImageNames.elasticsearch()) static final ElasticsearchContainer elasticsearch = new ElasticsearchContainer(DockerImageNames.elasticsearch())
.withStartupAttempts(5) .withStartupAttempts(5)
.withStartupTimeout(Duration.ofMinutes(10)); .withStartupTimeout(Duration.ofMinutes(10));

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

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

@ -25,7 +25,7 @@ import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.beans.factory.annotation.Autowired; 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.boot.testsupport.testcontainers.DockerImageNames;
import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -46,7 +46,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class DataElasticsearchTestWithIncludeFilterIntegrationTests { class DataElasticsearchTestWithIncludeFilterIntegrationTests {
@Container @Container
@ElasticsearchServiceConnection @ServiceConnection
static final ElasticsearchContainer elasticsearch = new ElasticsearchContainer(DockerImageNames.elasticsearch()) static final ElasticsearchContainer elasticsearch = new ElasticsearchContainer(DockerImageNames.elasticsearch())
.withStartupAttempts(5) .withStartupAttempts(5)
.withStartupTimeout(Duration.ofMinutes(10)); .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.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired; 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.boot.testsupport.testcontainers.DockerImageNames;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.MongoTemplate;
@ -46,7 +46,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
class DataMongoTestIntegrationTests { class DataMongoTestIntegrationTests {
@Container @Container
@MongoServiceConnection @ServiceConnection
static final MongoDBContainer mongoDB = new MongoDBContainer(DockerImageNames.mongo()).withStartupAttempts(5) static final MongoDBContainer mongoDB = new MongoDBContainer(DockerImageNames.mongo()).withStartupAttempts(5)
.withStartupTimeout(Duration.ofMinutes(5)); .withStartupTimeout(Duration.ofMinutes(5));

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

@ -24,7 +24,7 @@ import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.beans.factory.annotation.Autowired; 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.boot.testsupport.testcontainers.DockerImageNames;
import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -44,7 +44,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class DataMongoTestWithIncludeFilterIntegrationTests { class DataMongoTestWithIncludeFilterIntegrationTests {
@Container @Container
@MongoServiceConnection @ServiceConnection
static final MongoDBContainer mongoDB = new MongoDBContainer(DockerImageNames.mongo()).withStartupAttempts(5) static final MongoDBContainer mongoDB = new MongoDBContainer(DockerImageNames.mongo()).withStartupAttempts(5)
.withStartupTimeout(Duration.ofMinutes(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.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.TestConfiguration; 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.boot.testsupport.testcontainers.DockerImageNames;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.MongoDatabaseFactory;
@ -49,7 +49,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class TransactionalDataMongoTestIntegrationTests { class TransactionalDataMongoTestIntegrationTests {
@Container @Container
@MongoServiceConnection @ServiceConnection
static final MongoDBContainer mongoDB = new MongoDBContainer(DockerImageNames.mongo()).withStartupAttempts(5) static final MongoDBContainer mongoDB = new MongoDBContainer(DockerImageNames.mongo()).withStartupAttempts(5)
.withStartupTimeout(Duration.ofMinutes(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.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired; 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.boot.testsupport.testcontainers.DockerImageNames;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.data.neo4j.core.Neo4jTemplate; import org.springframework.data.neo4j.core.Neo4jTemplate;
@ -48,7 +48,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
class DataNeo4jTestIntegrationTests { class DataNeo4jTestIntegrationTests {
@Container @Container
@Neo4jServiceConnection @ServiceConnection
static final Neo4jContainer<?> neo4j = new Neo4jContainer<>(DockerImageNames.neo4j()).withStartupAttempts(5) static final Neo4jContainer<?> neo4j = new Neo4jContainer<>(DockerImageNames.neo4j()).withStartupAttempts(5)
.withStartupTimeout(Duration.ofMinutes(10)); .withStartupTimeout(Duration.ofMinutes(10));

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

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

@ -24,7 +24,7 @@ import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.beans.factory.annotation.Autowired; 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.boot.testsupport.testcontainers.DockerImageNames;
import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -45,7 +45,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class DataNeo4jTestWithIncludeFilterIntegrationTests { class DataNeo4jTestWithIncludeFilterIntegrationTests {
@Container @Container
@Neo4jServiceConnection @ServiceConnection
static final Neo4jContainer<?> neo4j = new Neo4jContainer<>(DockerImageNames.neo4j()).withoutAuthentication() static final Neo4jContainer<?> neo4j = new Neo4jContainer<>(DockerImageNames.neo4j()).withoutAuthentication()
.withStartupAttempts(5) .withStartupAttempts(5)
.withStartupTimeout(Duration.ofMinutes(10)); .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.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired; 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.boot.testsupport.testcontainers.RedisContainer;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.data.redis.core.RedisOperations; import org.springframework.data.redis.core.RedisOperations;
@ -48,7 +48,7 @@ class DataRedisTestIntegrationTests {
private static final Charset CHARSET = StandardCharsets.UTF_8; private static final Charset CHARSET = StandardCharsets.UTF_8;
@Container @Container
@RedisServiceConnection @ServiceConnection
static RedisContainer redis = new RedisContainer(); static RedisContainer redis = new RedisContainer();
@Autowired @Autowired

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

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

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

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

@ -16,9 +16,9 @@
package org.springframework.boot.testcontainers.service.connection; 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.ConnectionDetails;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory; 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.boot.origin.OriginProvider;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
/** /**
* Base class for {@link ConnectionDetailsFactory} implementations that provide * Base class for {@link ConnectionDetailsFactory} implementations that provide
* {@link ConnectionDetails} from a {@link ContainerConnectionSource}. * {@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 <D> the connection details type
* @param <C> the generic container type * @param <C> the generic container type
* @author Moritz Halbritter * @author Moritz Halbritter
@ -40,17 +40,58 @@ import org.springframework.util.Assert;
* @author Phillip Webb * @author Phillip Webb
* @since 3.1.0 * @since 3.1.0
*/ */
public abstract class ContainerConnectionDetailsFactory<A extends Annotation, D extends ConnectionDetails, C extends GenericContainer<?>> public abstract class ContainerConnectionDetailsFactory<D extends ConnectionDetails, C extends Container<?>>
implements ConnectionDetailsFactory<ContainerConnectionSource<A, D, C>, D> { 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 @Override
public final D getConnectionDetails(ContainerConnectionSource<A, D, C> source) { public final D getConnectionDetails(ContainerConnectionSource<D, C> source) {
Class<?>[] generics = resolveGenerics(); if (!hasRequiredClasses()) {
Class<?> annotationType = generics[0]; return null;
Class<?> connectionDetailsType = generics[1]; }
Class<?> containerType = generics[2]; try {
return (!source.accepts(annotationType, connectionDetailsType, containerType)) ? null Class<?>[] generics = resolveGenerics();
: 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() { private Class<?>[] resolveGenerics() {
@ -64,7 +105,7 @@ public abstract class ContainerConnectionDetailsFactory<A extends Annotation, D
* @param source the source * @param source the source
* @return the service connection or {@code null}. * @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 * 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. * Create a new {@link ContainerConnectionDetails} instance.
* @param source the source {@link ContainerConnectionSource} * @param source the source {@link ContainerConnectionSource}
*/ */
protected ContainerConnectionDetails(ContainerConnectionSource<?, ?, ?> source) { protected ContainerConnectionDetails(ContainerConnectionSource<?, ?> source) {
Assert.notNull(source, "Source must not be null"); Assert.notNull(source, "Source must not be null");
this.origin = source.getOrigin(); this.origin = source.getOrigin();
} }

@ -16,23 +16,25 @@
package org.springframework.boot.testcontainers.service.connection; package org.springframework.boot.testcontainers.service.connection;
import java.lang.annotation.Annotation; import java.util.Set;
import java.lang.reflect.Field;
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.autoconfigure.service.connection.ConnectionDetails;
import org.springframework.boot.origin.Origin; import org.springframework.boot.origin.Origin;
import org.springframework.boot.origin.OriginProvider; import org.springframework.boot.origin.OriginProvider;
import org.springframework.core.annotation.MergedAnnotation; 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 * Passed to {@link ContainerConnectionDetailsFactory} to provide details of the
* {@link ServiceConnection @ServiceConnection} annotation {@link GenericContainer} field * {@link ServiceConnection @ServiceConnection} annotated {@link Container} that provides
* that provides the service. * the service.
* *
* @param <A> the source annotation type. The annotation will mergable to a
* {@link ServiceConnection @ServiceConnection}
* @param <D> the connection details type * @param <D> the connection details type
* @param <C> the generic container type * @param <C> the generic container type
* @author Moritz Halbritter * @author Moritz Halbritter
@ -41,58 +43,77 @@ import org.springframework.core.annotation.MergedAnnotation;
* @since 3.1.0 * @since 3.1.0
* @see ContainerConnectionDetailsFactory * @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 { 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 C container;
private final AnnotatedFieldOrigin origin; private String acceptedConnectionName;
@SuppressWarnings("unchecked") private Set<Class<?>> acceptedConnectionDetailsTypes;
ContainerConnectionSource(Class<D> connectionDetailsType, Field field,
MergedAnnotation<ServiceConnection> annotation, C container) {
this(connectionDetailsType, field, (A) annotation.getRoot().synthesize(), container);
}
ContainerConnectionSource(Class<D> connectionDetailsType, Field field, A annotation, C container) { ContainerConnectionSource(String beanNameSuffix, Origin origin, C container,
this.connectionDetailsType = connectionDetailsType; MergedAnnotation<ServiceConnection> annotation) {
this.field = field; this.beanNameSuffix = beanNameSuffix;
this.annotation = annotation; this.origin = origin;
this.container = container; 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) { ContainerConnectionSource(String beanNameSuffix, Origin origin, C container, ServiceConnection annotation) {
return annotationType.isInstance(this.annotation) this.beanNameSuffix = beanNameSuffix;
&& connectionDetailsType.isAssignableFrom(this.connectionDetailsType) this.origin = origin;
&& containerType.isInstance(this.container); this.container = container;
this.acceptedConnectionName = getConnectionName(container, annotation.name());
this.acceptedConnectionDetailsTypes = Set.of(annotation.type());
} }
String getBeanName() { private static String getConnectionName(Container<?> container, String connectionName) {
return this.field.getName() + this.connectionDetailsType.getSimpleName() + "ConnectedContainer"; if (StringUtils.hasLength(connectionName)) {
return connectionName;
}
try {
DockerImageName imageName = DockerImageName.parse(container.getDockerImageName());
imageName.assertValid();
return imageName.getRepository();
}
catch (IllegalArgumentException ex) {
return container.getDockerImageName();
}
} }
/** boolean accepts(String connectionName, Class<?> connectionDetailsType, Class<?> containerType) {
* Return the source annotation that provided the connection to the container. This if (!containerType.isInstance(this.container)) {
* annotation will be mergable to {@link ServiceConnection @ServiceConnection}. logger.trace(LogMessage.of(() -> "%s not accepted as %s is not an instance of %s".formatted(this,
* @return the source annotation this.container.getClass().getName(), containerType.getName())));
*/ return false;
public A getAnnotation() { }
return this.annotation; 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;
} }
/** String getBeanNameSuffix() {
* Return the {@link GenericContainer} that implements the service being connected to. return this.beanNameSuffix;
* @return the {@link GenericContainer} providing the service
*/
public C getContainer() {
return this.container;
} }
@Override @Override
@ -100,9 +121,17 @@ public final class ContainerConnectionSource<A extends Annotation, D extends Con
return this.origin; 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 @Override
public String toString() { 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; package org.springframework.boot.testcontainers.service.connection;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.Objects;
import org.springframework.boot.origin.Origin; import org.springframework.boot.origin.Origin;
import org.springframework.util.Assert; 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 Moritz Halbritter
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Phillip Webb * @author Phillip Webb
*/ */
class AnnotatedFieldOrigin implements Origin { class FieldOrigin implements Origin {
private final Field field; private final Field field;
private final Annotation annotation; FieldOrigin(Field field) {
AnnotatedFieldOrigin(Field field, Annotation annotation) {
Assert.notNull(field, "Field must not be null"); Assert.notNull(field, "Field must not be null");
Assert.notNull(annotation, "Annotation must not be null");
this.field = field; this.field = field;
this.annotation = annotation;
} }
@Override @Override
@ -51,18 +46,18 @@ class AnnotatedFieldOrigin implements Origin {
if (obj == null || getClass() != obj.getClass()) { if (obj == null || getClass() != obj.getClass()) {
return false; return false;
} }
AnnotatedFieldOrigin other = (AnnotatedFieldOrigin) obj; FieldOrigin other = (FieldOrigin) obj;
return this.field.equals(other.field) && this.annotation.equals(other.annotation); return this.field.equals(other.field);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(this.field, this.annotation); return this.field.hashCode();
} }
@Override @Override
public String toString() { 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.RetentionPolicy;
import java.lang.annotation.Target; 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.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. * Annotation used to indicate that a field or method is a
* Typically used to meta-annotate a higher-level annotation. * {@link ContainerConnectionSource} which provides a service that can be connected to.
* <p>
* When used, a {@link ConnectionDetailsFactory} must be registered in
* {@code spring.factories} to provide {@link ConnectionDetails} for the field value.
* *
* @author Moritz Halbritter * @author Moritz Halbritter
* @author Andy Wilkinson * @author Andy Wilkinson
@ -37,14 +36,35 @@ import org.springframework.boot.autoconfigure.service.connection.ConnectionDetai
* @since 3.1.0 * @since 3.1.0
*/ */
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE }) @Target({ ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE })
public @interface ServiceConnection { public @interface ServiceConnection {
/** /**
* The type of {@link ConnectionDetails} that can describe how to connect to the * The name of the service being connected to. If not specified, the image name will
* service. * be used. Container names are used to determine the connection details that should
* @return the connection type * 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; package org.springframework.boot.testcontainers.service.connection;
import java.util.List; 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.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry; 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.ConnectionDetails;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactories; import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactories;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.MergedContextConfiguration; 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 Moritz Halbritter
* @author Andy Wilkinson * @author Andy Wilkinson
@ -39,46 +35,30 @@ import org.springframework.util.Assert;
*/ */
class ServiceConnectionContextCustomizer implements ContextCustomizer { 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.sources = sources;
this.connectionDetailsFactories = connectionDetailsFactories;
} }
@Override @Override
public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) { public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
if (beanFactory instanceof BeanDefinitionRegistry registry) { if (beanFactory instanceof BeanDefinitionRegistry registry) {
registerServiceConnections(registry); new ContainerConnectionSourcesRegistrar(beanFactory, this.connectionDetailsFactories, this.sources)
.registerBeanDefinitions(registry);
} }
} }
private void registerServiceConnections(BeanDefinitionRegistry registry) { List<ContainerConnectionSource<?, ?>> getSources() {
this.sources.forEach((source) -> registerServiceConnection(registry, source));
}
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() {
return this.sources; return this.sources;
} }

@ -21,9 +21,9 @@ import java.lang.reflect.Modifier;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; 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.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.test.context.ContextConfigurationAttributes; 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.ContextCustomizerFactory;
import org.springframework.test.context.TestContextAnnotationUtils; import org.springframework.test.context.TestContextAnnotationUtils;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils; 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 Moritz Halbritter
* @author Andy Wilkinson * @author Andy Wilkinson
@ -45,12 +49,12 @@ class ServiceConnectionContextCustomizerFactory implements ContextCustomizerFact
@Override @Override
public ContextCustomizer createContextCustomizer(Class<?> testClass, public ContextCustomizer createContextCustomizer(Class<?> testClass,
List<ContextConfigurationAttributes> configAttributes) { List<ContextConfigurationAttributes> configAttributes) {
List<ContainerConnectionSource<?, ?, ?>> sources = new ArrayList<>(); List<ContainerConnectionSource<?, ?>> sources = new ArrayList<>();
findSources(testClass, sources); findSources(testClass, sources);
return (sources.isEmpty()) ? null : new ServiceConnectionContextCustomizer(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) -> { ReflectionUtils.doWithFields(clazz, (field) -> {
MergedAnnotations annotations = MergedAnnotations.from(field); MergedAnnotations annotations = MergedAnnotations.from(field);
annotations.stream(ServiceConnection.class) annotations.stream(ServiceConnection.class)
@ -61,23 +65,17 @@ class ServiceConnectionContextCustomizerFactory implements ContextCustomizerFact
} }
} }
private ContainerConnectionSource<?, ?, ?> createSource(Field field, private ContainerConnectionSource<?, ?> createSource(Field field, MergedAnnotation<ServiceConnection> annotation) {
MergedAnnotation<ServiceConnection> annotation) { Assert.state(Modifier.isStatic(field.getModifiers()),
if (!Modifier.isStatic(field.getModifiers())) { () -> "@ServiceConnection field '%s' must be static".formatted(field.getName()));
throw new IllegalStateException("@ServiceConnection field '%s' must be static".formatted(field.getName())); String beanNameSuffix = StringUtils.capitalize(ClassUtils.getShortNameAsProperty(field.getDeclaringClass()))
} + StringUtils.capitalize(field.getName());
Class<? extends ConnectionDetails> connectionDetailsType = getConnectionDetailsType(annotation); Origin origin = new FieldOrigin(field);
Object fieldValue = getFieldValue(field); Object fieldValue = getFieldValue(field);
Assert.isInstanceOf(GenericContainer.class, fieldValue, Assert.state(Container.class.isInstance(fieldValue), () -> "Field '%s' in %s must be a %s"
"Field %s must be a %s".formatted(field.getName(), GenericContainer.class.getName())); .formatted(field.getName(), field.getDeclaringClass().getName(), Container.class.getName()));
GenericContainer<?> container = (GenericContainer<?>) fieldValue; Container<?> container = (Container<?>) fieldValue;
return new ContainerConnectionSource<>(connectionDetailsType, field, annotation, container); return new ContainerConnectionSource<>(beanNameSuffix, origin, container, annotation);
}
@SuppressWarnings("unchecked")
private Class<? extends ConnectionDetails> getConnectionDetailsType(
MergedAnnotation<ServiceConnection> annotation) {
return (Class<? extends ConnectionDetails>) annotation.getClass(MergedAnnotation.VALUE);
} }
private Object getFieldValue(Field field) { 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.autoconfigure.amqp.RabbitConnectionDetails;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/** /**
* {@link ContainerConnectionDetailsFactory} for * {@link ContainerConnectionDetailsFactory} to create {@link RabbitConnectionDetails}
* {@link RabbitServiceConnection @RabbitServiceConnection}-annotated * from a {@link ServiceConnection @ServiceConnection}-annotated
* {@link RabbitMQContainer} fields. * {@link RabbitMQContainer}.
* *
* @author Moritz Halbritter * @author Moritz Halbritter
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Phillip Webb * @author Phillip Webb
*/ */
class RabbitContainerConnectionDetailsFactory class RabbitContainerConnectionDetailsFactory
extends ContainerConnectionDetailsFactory<RabbitServiceConnection, RabbitConnectionDetails, RabbitMQContainer> { extends ContainerConnectionDetailsFactory<RabbitConnectionDetails, RabbitMQContainer> {
@Override @Override
protected RabbitConnectionDetails getContainerConnectionDetails( protected RabbitConnectionDetails getContainerConnectionDetails(
ContainerConnectionSource<RabbitServiceConnection, RabbitConnectionDetails, RabbitMQContainer> source) { ContainerConnectionSource<RabbitConnectionDetails, RabbitMQContainer> source) {
return new RabbitMqContainerConnectionDetails(source); return new RabbitMqContainerConnectionDetails(source);
} }
@ -52,7 +53,7 @@ class RabbitContainerConnectionDetailsFactory
private final RabbitMQContainer container; private final RabbitMQContainer container;
private RabbitMqContainerConnectionDetails( private RabbitMqContainerConnectionDetails(
ContainerConnectionSource<RabbitServiceConnection, RabbitConnectionDetails, RabbitMQContainer> source) { ContainerConnectionSource<RabbitConnectionDetails, RabbitMQContainer> source) {
super(source); super(source);
this.container = source.getContainer(); 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.autoconfigure.cassandra.CassandraConnectionDetails;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/** /**
* {@link ContainerConnectionDetailsFactory} for * {@link ContainerConnectionDetailsFactory} to create {@link CassandraConnectionDetails}
* {@link CassandraServiceConnection @CassandraServiceConnection}-annotated * from a {@link ServiceConnection @ServiceConnection}-annotated
* {@link CassandraContainer} fields. * {@link CassandraContainer}.
* *
* @author Moritz Halbritter * @author Moritz Halbritter
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Phillip Webb * @author Phillip Webb
*/ */
class CassandraContainerConnectionDetailsFactory extends class CassandraContainerConnectionDetailsFactory
ContainerConnectionDetailsFactory<CassandraServiceConnection, CassandraConnectionDetails, CassandraContainer<?>> { extends ContainerConnectionDetailsFactory<CassandraConnectionDetails, CassandraContainer<?>> {
@Override @Override
protected CassandraConnectionDetails getContainerConnectionDetails( protected CassandraConnectionDetails getContainerConnectionDetails(
ContainerConnectionSource<CassandraServiceConnection, CassandraConnectionDetails, CassandraContainer<?>> source) { ContainerConnectionSource<CassandraConnectionDetails, CassandraContainer<?>> source) {
return new CassandraContainerConnectionDetails(source); return new CassandraContainerConnectionDetails(source);
} }
@ -51,7 +52,7 @@ class CassandraContainerConnectionDetailsFactory extends
private final CassandraContainer<?> container; private final CassandraContainer<?> container;
private CassandraContainerConnectionDetails( private CassandraContainerConnectionDetails(
ContainerConnectionSource<CassandraServiceConnection, CassandraConnectionDetails, CassandraContainer<?>> source) { ContainerConnectionSource<CassandraConnectionDetails, CassandraContainer<?>> source) {
super(source); super(source);
this.container = source.getContainer(); 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.autoconfigure.couchbase.CouchbaseConnectionDetails;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/** /**
* {@link ContainerConnectionDetailsFactory} for * {@link ContainerConnectionDetailsFactory} to create {@link CouchbaseConnectionDetails}
* {@link CouchbaseServiceConnection @CouchbaseServiceConnection}-annotated * from a {@link ServiceConnection @ServiceConnection}-annotated
* {@link CouchbaseContainer} fields. * {@link CouchbaseContainer}.
* *
* @author Moritz Halbritter * @author Moritz Halbritter
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Phillip Webb * @author Phillip Webb
*/ */
class CouchbaseContainerConnectionDetailsFactory extends class CouchbaseContainerConnectionDetailsFactory
ContainerConnectionDetailsFactory<CouchbaseServiceConnection, CouchbaseConnectionDetails, CouchbaseContainer> { extends ContainerConnectionDetailsFactory<CouchbaseConnectionDetails, CouchbaseContainer> {
@Override @Override
protected CouchbaseConnectionDetails getContainerConnectionDetails( protected CouchbaseConnectionDetails getContainerConnectionDetails(
ContainerConnectionSource<CouchbaseServiceConnection, CouchbaseConnectionDetails, CouchbaseContainer> source) { ContainerConnectionSource<CouchbaseConnectionDetails, CouchbaseContainer> source) {
return new CouchbaseContainerConnectionDetails(source); return new CouchbaseContainerConnectionDetails(source);
} }
@ -49,7 +50,7 @@ class CouchbaseContainerConnectionDetailsFactory extends
private final CouchbaseContainer container; private final CouchbaseContainer container;
private CouchbaseContainerConnectionDetails( private CouchbaseContainerConnectionDetails(
ContainerConnectionSource<CouchbaseServiceConnection, CouchbaseConnectionDetails, CouchbaseContainer> source) { ContainerConnectionSource<CouchbaseConnectionDetails, CouchbaseContainer> source) {
super(source); super(source);
this.container = source.getContainer(); 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 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;
import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails.Node.Protocol; import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails.Node.Protocol;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/** /**
* {@link ContainerConnectionDetailsFactory} for * {@link ContainerConnectionDetailsFactory} to create
* {@link ElasticsearchServiceConnection @ElasticsearchServiceConnection}-annotated * {@link ElasticsearchConnectionDetails} from a
* {@link GenericContainer} fields. * {@link ServiceConnection @ServiceConnection}-annotated {@link ElasticsearchContainer}.
* *
* @author Moritz Halbritter * @author Moritz Halbritter
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Phillip Webb * @author Phillip Webb
*/ */
class ElasticsearchContainerConnectionDetailsFactory extends class ElasticsearchContainerConnectionDetailsFactory
ContainerConnectionDetailsFactory<ElasticsearchServiceConnection, ElasticsearchConnectionDetails, GenericContainer<?>> { extends ContainerConnectionDetailsFactory<ElasticsearchConnectionDetails, ElasticsearchContainer> {
private static final int DEFAULT_PORT = 9200; private static final int DEFAULT_PORT = 9200;
@Override @Override
protected ElasticsearchConnectionDetails getContainerConnectionDetails( protected ElasticsearchConnectionDetails getContainerConnectionDetails(
ContainerConnectionSource<ElasticsearchServiceConnection, ElasticsearchConnectionDetails, GenericContainer<?>> source) { ContainerConnectionSource<ElasticsearchConnectionDetails, ElasticsearchContainer> source) {
return new ElasticsearchContainerConnectionDetails(source); return new ElasticsearchContainerConnectionDetails(source);
} }
@ -55,7 +56,7 @@ class ElasticsearchContainerConnectionDetailsFactory extends
private final List<Node> nodes; private final List<Node> nodes;
private ElasticsearchContainerConnectionDetails( private ElasticsearchContainerConnectionDetails(
ContainerConnectionSource<ElasticsearchServiceConnection, ElasticsearchConnectionDetails, GenericContainer<?>> source) { ContainerConnectionSource<ElasticsearchConnectionDetails, ElasticsearchContainer> source) {
super(source); super(source);
this.nodes = List.of(new Node(source.getContainer().getHost(), this.nodes = List.of(new Node(source.getContainer().getHost(),
source.getContainer().getMappedPort(DEFAULT_PORT), Protocol.HTTP, null, null)); 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; import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/** /**
* {@link ContainerConnectionDetailsFactory} for * {@link ContainerConnectionDetailsFactory} to create {@link FlywayConnectionDetails}
* {@link ServiceConnection @ServiceConnection}-annotated {@link JdbcDatabaseContainer} * from a {@link ServiceConnection @ServiceConnection}-annotated
* fields that should produce {@link FlywayConnectionDetails}. * {@link JdbcDatabaseContainer}.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
*/ */
class FlywayContainerConnectionDetailsFactory extends class FlywayContainerConnectionDetailsFactory
ContainerConnectionDetailsFactory<ServiceConnection, FlywayConnectionDetails, JdbcDatabaseContainer<?>> { extends ContainerConnectionDetailsFactory<FlywayConnectionDetails, JdbcDatabaseContainer<?>> {
@Override @Override
protected FlywayConnectionDetails getContainerConnectionDetails( protected FlywayConnectionDetails getContainerConnectionDetails(
ContainerConnectionSource<ServiceConnection, FlywayConnectionDetails, JdbcDatabaseContainer<?>> source) { ContainerConnectionSource<FlywayConnectionDetails, JdbcDatabaseContainer<?>> source) {
return new FlywayContainerConnectionDetails(source); return new FlywayContainerConnectionDetails(source);
} }
@ -48,7 +48,7 @@ class FlywayContainerConnectionDetailsFactory extends
private final JdbcDatabaseContainer<?> container; private final JdbcDatabaseContainer<?> container;
private FlywayContainerConnectionDetails( private FlywayContainerConnectionDetails(
ContainerConnectionSource<ServiceConnection, FlywayConnectionDetails, JdbcDatabaseContainer<?>> source) { ContainerConnectionSource<FlywayConnectionDetails, JdbcDatabaseContainer<?>> source) {
super(source); super(source);
this.container = source.getContainer(); this.container = source.getContainer();
} }

@ -23,22 +23,23 @@ import org.testcontainers.containers.InfluxDBContainer;
import org.springframework.boot.autoconfigure.influx.InfluxDbConnectionDetails; import org.springframework.boot.autoconfigure.influx.InfluxDbConnectionDetails;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/** /**
* {@link ContainerConnectionDetailsFactory} for * {@link ContainerConnectionDetailsFactory} to create {@link InfluxDbConnectionDetails}
* {@link InfluxDbServiceConnection @InfluxDbServiceConnection}-annotated * from a {@link ServiceConnection @ServiceConnection}-annotated
* {@link InfluxDBContainer} fields. * {@link InfluxDBContainer}.
* *
* @author Moritz Halbritter * @author Moritz Halbritter
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Phillip Webb * @author Phillip Webb
*/ */
class InfluxDbContainerConnectionDetailsFactory extends class InfluxDbContainerConnectionDetailsFactory
ContainerConnectionDetailsFactory<InfluxDbServiceConnection, InfluxDbConnectionDetails, InfluxDBContainer<?>> { extends ContainerConnectionDetailsFactory<InfluxDbConnectionDetails, InfluxDBContainer<?>> {
@Override @Override
protected InfluxDbConnectionDetails getContainerConnectionDetails( protected InfluxDbConnectionDetails getContainerConnectionDetails(
ContainerConnectionSource<InfluxDbServiceConnection, InfluxDbConnectionDetails, InfluxDBContainer<?>> source) { ContainerConnectionSource<InfluxDbConnectionDetails, InfluxDBContainer<?>> source) {
return new InfluxDbContainerConnectionDetails(source); return new InfluxDbContainerConnectionDetails(source);
} }
@ -51,7 +52,7 @@ class InfluxDbContainerConnectionDetailsFactory extends
private final InfluxDBContainer<?> container; private final InfluxDBContainer<?> container;
private InfluxDbContainerConnectionDetails( private InfluxDbContainerConnectionDetails(
ContainerConnectionSource<InfluxDbServiceConnection, InfluxDbConnectionDetails, InfluxDBContainer<?>> source) { ContainerConnectionSource<InfluxDbConnectionDetails, InfluxDBContainer<?>> source) {
super(source); super(source);
this.container = source.getContainer(); 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.autoconfigure.jdbc.JdbcConnectionDetails;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/** /**
* {@link ContainerConnectionDetailsFactory} for * {@link ContainerConnectionDetailsFactory} to create {@link JdbcConnectionDetails} from
* {@link JdbcServiceConnection @JdbcServiceConnection}-annotated * a {@link ServiceConnection @ServiceConnection}-annotated {@link JdbcDatabaseContainer}.
* {@link JdbcDatabaseContainer} fields.
* *
* @author Moritz Halbritter * @author Moritz Halbritter
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Phillip Webb * @author Phillip Webb
*/ */
class JdbcContainerConnectionDetailsFactory extends class JdbcContainerConnectionDetailsFactory
ContainerConnectionDetailsFactory<JdbcServiceConnection, JdbcConnectionDetails, JdbcDatabaseContainer<?>> { extends ContainerConnectionDetailsFactory<JdbcConnectionDetails, JdbcDatabaseContainer<?>> {
@Override @Override
protected JdbcConnectionDetails getContainerConnectionDetails( protected JdbcConnectionDetails getContainerConnectionDetails(
ContainerConnectionSource<JdbcServiceConnection, JdbcConnectionDetails, JdbcDatabaseContainer<?>> source) { ContainerConnectionSource<JdbcConnectionDetails, JdbcDatabaseContainer<?>> source) {
return new JdbcContainerConnectionDetails(source); return new JdbcContainerConnectionDetails(source);
} }
@ -49,7 +49,7 @@ class JdbcContainerConnectionDetailsFactory extends
private final JdbcDatabaseContainer<?> container; private final JdbcDatabaseContainer<?> container;
private JdbcContainerConnectionDetails( private JdbcContainerConnectionDetails(
ContainerConnectionSource<JdbcServiceConnection, JdbcConnectionDetails, JdbcDatabaseContainer<?>> source) { ContainerConnectionSource<JdbcConnectionDetails, JdbcDatabaseContainer<?>> source) {
super(source); super(source);
this.container = source.getContainer(); 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.autoconfigure.kafka.KafkaConnectionDetails;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/** /**
* {@link ContainerConnectionDetailsFactory} for * {@link ContainerConnectionDetailsFactory} to create {@link KafkaConnectionDetails} from
* {@link KafkaServiceConnection @KafkaServiceConnection}-annotated {@link KafkaContainer} * a {@link ServiceConnection @ServiceConnection}-annotated {@link KafkaContainer}.
* fields.
* *
* @author Moritz Halbritter * @author Moritz Halbritter
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Phillip Webb * @author Phillip Webb
*/ */
class KafkaContainerConnectionDetailsFactory class KafkaContainerConnectionDetailsFactory
extends ContainerConnectionDetailsFactory<KafkaServiceConnection, KafkaConnectionDetails, KafkaContainer> { extends ContainerConnectionDetailsFactory<KafkaConnectionDetails, KafkaContainer> {
@Override @Override
protected KafkaConnectionDetails getContainerConnectionDetails( protected KafkaConnectionDetails getContainerConnectionDetails(
ContainerConnectionSource<KafkaServiceConnection, KafkaConnectionDetails, KafkaContainer> source) { ContainerConnectionSource<KafkaConnectionDetails, KafkaContainer> source) {
return new KafkaContainerConnectionDetails(source); return new KafkaContainerConnectionDetails(source);
} }
@ -52,7 +52,7 @@ class KafkaContainerConnectionDetailsFactory
private final KafkaContainer container; private final KafkaContainer container;
private KafkaContainerConnectionDetails( private KafkaContainerConnectionDetails(
ContainerConnectionSource<KafkaServiceConnection, KafkaConnectionDetails, KafkaContainer> source) { ContainerConnectionSource<KafkaConnectionDetails, KafkaContainer> source) {
super(source); super(source);
this.container = source.getContainer(); 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; import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/** /**
* {@link ContainerConnectionDetailsFactory} for * {@link ContainerConnectionDetailsFactory} to create {@link LiquibaseConnectionDetails}
* {@link ServiceConnection @ServiceConnection}-annotated {@link JdbcDatabaseContainer} * from a {@link ServiceConnection @ServiceConnection}-annotated
* fields that should produce {@link LiquibaseConnectionDetails}. * {@link JdbcDatabaseContainer}.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
*/ */
class LiquibaseContainerConnectionDetailsFactory extends class LiquibaseContainerConnectionDetailsFactory
ContainerConnectionDetailsFactory<ServiceConnection, LiquibaseConnectionDetails, JdbcDatabaseContainer<?>> { extends ContainerConnectionDetailsFactory<LiquibaseConnectionDetails, JdbcDatabaseContainer<?>> {
@Override @Override
protected LiquibaseConnectionDetails getContainerConnectionDetails( protected LiquibaseConnectionDetails getContainerConnectionDetails(
ContainerConnectionSource<ServiceConnection, LiquibaseConnectionDetails, JdbcDatabaseContainer<?>> source) { ContainerConnectionSource<LiquibaseConnectionDetails, JdbcDatabaseContainer<?>> source) {
return new LiquibaseContainerConnectionDetails(source); return new LiquibaseContainerConnectionDetails(source);
} }
@ -48,7 +48,7 @@ class LiquibaseContainerConnectionDetailsFactory extends
private final JdbcDatabaseContainer<?> container; private final JdbcDatabaseContainer<?> container;
private LiquibaseContainerConnectionDetails( private LiquibaseContainerConnectionDetails(
ContainerConnectionSource<ServiceConnection, LiquibaseConnectionDetails, JdbcDatabaseContainer<?>> source) { ContainerConnectionSource<LiquibaseConnectionDetails, JdbcDatabaseContainer<?>> source) {
super(source); super(source);
this.container = source.getContainer(); this.container = source.getContainer();
} }

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

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

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

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

@ -21,24 +21,28 @@ import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.containers.PostgreSQLR2DBCDatabaseContainer; import org.testcontainers.containers.PostgreSQLR2DBCDatabaseContainer;
import org.springframework.boot.autoconfigure.r2dbc.R2dbcConnectionDetails; 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.ContainerConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/** /**
* {@link ConnectionDetailsFactory} for {@link R2dbcServiceConnection @R2dbcConnection} * {@link ContainerConnectionDetailsFactory} to create {@link R2dbcConnectionDetails} from
* annotated {@link PostgreSQLContainer} fields. * a {@link ServiceConnection @ServiceConnection}-annotated {@link PostgreSQLContainer}.
* *
* @author Moritz Halbritter * @author Moritz Halbritter
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Phillip Webb * @author Phillip Webb
*/ */
class PostgresR2dbcContainerConnectionDetailsFactory extends class PostgresR2dbcContainerConnectionDetailsFactory
ContainerConnectionDetailsFactory<R2dbcServiceConnection, R2dbcConnectionDetails, PostgreSQLContainer<?>> { extends ContainerConnectionDetailsFactory<R2dbcConnectionDetails, PostgreSQLContainer<?>> {
PostgresR2dbcContainerConnectionDetailsFactory() {
super(ANY_CONNECTION_NAME, "io.r2dbc.spi.ConnectionFactoryOptions");
}
@Override @Override
public R2dbcConnectionDetails getContainerConnectionDetails( public R2dbcConnectionDetails getContainerConnectionDetails(
ContainerConnectionSource<R2dbcServiceConnection, R2dbcConnectionDetails, PostgreSQLContainer<?>> source) { ContainerConnectionSource<R2dbcConnectionDetails, PostgreSQLContainer<?>> source) {
return new R2dbcDatabaseContainerConnectionDetails(source.getContainer()); 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; package org.springframework.boot.testcontainers.service.connection.redis;
import org.testcontainers.containers.Container;
import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.GenericContainer;
import org.springframework.boot.autoconfigure.data.redis.RedisConnectionDetails; import org.springframework.boot.autoconfigure.data.redis.RedisConnectionDetails;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/** /**
* {@link ContainerConnectionDetailsFactory} for * {@link ContainerConnectionDetailsFactory} to create {@link RedisConnectionDetails} from
* {@link RedisServiceConnection @RedisServiceConnection}-annotated * a {@link ServiceConnection @ServiceConnection}-annotated {@link GenericContainer} using
* {@link GenericContainer} fields. * the {@code "redis"} image.
* *
* @author Moritz Halbritter * @author Moritz Halbritter
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Phillip Webb * @author Phillip Webb
*/ */
class RedisContainerConnectionDetailsFactory class RedisContainerConnectionDetailsFactory
extends ContainerConnectionDetailsFactory<RedisServiceConnection, RedisConnectionDetails, GenericContainer<?>> { extends ContainerConnectionDetailsFactory<RedisConnectionDetails, Container<?>> {
RedisContainerConnectionDetailsFactory() {
super("redis");
}
@Override @Override
public RedisConnectionDetails getContainerConnectionDetails( public RedisConnectionDetails getContainerConnectionDetails(
ContainerConnectionSource<RedisServiceConnection, RedisConnectionDetails, GenericContainer<?>> source) { ContainerConnectionSource<RedisConnectionDetails, Container<?>> source) {
return new RedisContainerConnectionDetails(source); return new RedisContainerConnectionDetails(source);
} }
@ -49,7 +55,7 @@ class RedisContainerConnectionDetailsFactory
private final Standalone standalone; private final Standalone standalone;
private RedisContainerConnectionDetails( private RedisContainerConnectionDetails(
ContainerConnectionSource<RedisServiceConnection, RedisConnectionDetails, GenericContainer<?>> source) { ContainerConnectionSource<RedisConnectionDetails, Container<?>> source) {
super(source); super(source);
this.standalone = Standalone.of(source.getContainer().getHost(), this.standalone = Standalone.of(source.getContainer().getHost(),
source.getContainer().getFirstMappedPort()); 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.cassandra.CassandraContainerConnectionDetailsFactory,\
org.springframework.boot.testcontainers.service.connection.couchbase.CouchbaseContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.couchbase.CouchbaseContainerConnectionDetailsFactory,\
org.springframework.boot.testcontainers.service.connection.flyway.FlywayContainerConnectionDetailsFactory,\ 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.elasticsearch.ElasticsearchContainerConnectionDetailsFactory,\
org.springframework.boot.testcontainers.service.connection.influx.InfluxDbContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.influx.InfluxDbContainerConnectionDetailsFactory,\
org.springframework.boot.testcontainers.service.connection.jdbc.JdbcContainerConnectionDetailsFactory,\ 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.mongo.MongoContainerConnectionDetailsFactory,\
org.springframework.boot.testcontainers.service.connection.neo4j.Neo4jContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.neo4j.Neo4jContainerConnectionDetailsFactory,\
org.springframework.boot.testcontainers.service.connection.r2dbc.MariaDbR2dbcContainerConnectionDetailsFactory,\ 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.MySqlR2dbcContainerConnectionDetailsFactory,\
org.springframework.boot.testcontainers.service.connection.r2dbc.PostgresR2dbcContainerConnectionDetailsFactory,\ 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; 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.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.testcontainers.containers.Container;
import org.testcontainers.containers.GenericContainer; 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.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/** /**
* Tests for {@link ServiceConnectionContextCustomizerFactory}. * Tests for {@link ServiceConnectionContextCustomizerFactory}.
* *
* @author Moritz Halbritter
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Phillip Webb
*/ */
public class ServiceConnectionContextCustomizerFactoryTests { class ServiceConnectionContextCustomizerFactoryTests {
private final ServiceConnectionContextCustomizerFactory factory = new ServiceConnectionContextCustomizerFactory(); private final ServiceConnectionContextCustomizerFactory factory = new ServiceConnectionContextCustomizerFactory();
@Test @Test
void whenClassHasNoServiceConnectionsThenCreateReturnsNull() { void createContextCustomizerWhenNoServiceConnectionsReturnsNull() {
assertThat(this.factory.createContextCustomizer(NoServiceConnections.class, null)).isNull(); assertThat(this.factory.createContextCustomizer(NoServiceConnections.class, null)).isNull();
} }
@Test @Test
void whenClassHasServiceConnectionsThenCreateReturnsCustomizer() { void createContextCustomizerWhenClassHasServiceConnectionsReturnsCustomizer() {
ServiceConnectionContextCustomizer customizer = (ServiceConnectionContextCustomizer) this.factory ServiceConnectionContextCustomizer customizer = (ServiceConnectionContextCustomizer) this.factory
.createContextCustomizer(ServiceConnections.class, null); .createContextCustomizer(ServiceConnections.class, null);
assertThat(customizer).isNotNull(); assertThat(customizer).isNotNull();
@ -49,37 +52,79 @@ public class ServiceConnectionContextCustomizerFactoryTests {
} }
@Test @Test
void whenEnclosingClassHasServiceConnectionsThenCreateReturnsCustomizer() { void createContextCustomizerWhenEnclosingClassHasServiceConnectionsReturnsCustomizer() {
ServiceConnectionContextCustomizer customizer = (ServiceConnectionContextCustomizer) this.factory ServiceConnectionContextCustomizer customizer = (ServiceConnectionContextCustomizer) this.factory
.createContextCustomizer(NestedClass.class, null); .createContextCustomizer(ServiceConnections.NestedClass.class, null);
assertThat(customizer).isNotNull(); assertThat(customizer).isNotNull();
assertThat(customizer.getSources()).hasSize(3); assertThat(customizer.getSources()).hasSize(3);
} }
@Test @Test
void whenClassHasNonStaticServiceConnectionThenCreateShouldFailWithHelpfulIllegalStateException() { void createContextCustomizerWhenClassHasNonStaticServiceConnectionFailsWithHepfulException() {
assertThatIllegalStateException() assertThatIllegalStateException()
.isThrownBy(() -> this.factory.createContextCustomizer(NonStaticServiceConnection.class, null)) .isThrownBy(() -> this.factory.createContextCustomizer(NonStaticServiceConnection.class, null))
.withMessage("@ServiceConnection field 'service' must be static"); .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 NoServiceConnections {
} }
static class SingleServiceConnection {
@ServiceConnection
private static GenericContainer<?> service1 = new MockContainer();
}
static class ServiceConnections { static class ServiceConnections {
@ServiceConnection(TestConnectionDetails.class) @ServiceConnection
private static GenericContainer<?> service1 = new GenericContainer<>("example"); private static Container<?> service1 = new MockContainer();
@ServiceConnection(TestConnectionDetails.class) @ServiceConnection
private static GenericContainer<?> service2 = new GenericContainer<>("example"); private static Container<?> service2 = new MockContainer();
@Nested @Nested
class NestedClass { class NestedClass {
@ServiceConnection(TestConnectionDetails.class) @ServiceConnection
private static GenericContainer<?> service3 = new GenericContainer<>("example"); private static Container<?> service3 = new MockContainer();
} }
@ -87,12 +132,35 @@ public class ServiceConnectionContextCustomizerFactoryTests {
static class NonStaticServiceConnection { static class NonStaticServiceConnection {
@ServiceConnection(TestConnectionDetails.class) @ServiceConnection
private GenericContainer<?> service = new GenericContainer<>("example"); 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.containers.RabbitMQContainer;
import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import org.springframework.amqp.rabbit.annotation.Queue; import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.RabbitListener; 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.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration; import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration;
import org.springframework.boot.autoconfigure.amqp.RabbitConnectionDetails; 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.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; 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; import static org.assertj.core.api.Assertions.assertThat;
/** /**
* Tests for {@link RabbitServiceConnection}. * Tests for {@link RabbitContainerConnectionDetailsFactory}.
* *
* @author Moritz Halbritter * @author Moritz Halbritter
* @author Andy Wilkinson * @author Andy Wilkinson
@ -49,11 +50,12 @@ import static org.assertj.core.api.Assertions.assertThat;
*/ */
@SpringJUnitConfig @SpringJUnitConfig
@Testcontainers(disabledWithoutDocker = true) @Testcontainers(disabledWithoutDocker = true)
class RabbitServiceConnectionTests { class RabbitContainerConnectionDetailsFactoryIntegrationTests {
@Container @Container
@RabbitServiceConnection @ServiceConnection
static final RabbitMQContainer rabbit = new RabbitMQContainer(DockerImageName.parse("rabbitmq:3.11-alpine")); static final RabbitMQContainer rabbit = new RabbitMQContainer(DockerImageNames.rabbit())
.withStartupTimeout(Duration.ofMinutes(4));
@Autowired(required = false) @Autowired(required = false)
private RabbitConnectionDetails connectionDetails; private RabbitConnectionDetails connectionDetails;

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

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

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

@ -23,10 +23,8 @@ import org.testcontainers.junit.jupiter.Testcontainers;
import reactor.test.StepVerifier; import reactor.test.StepVerifier;
import org.springframework.beans.factory.annotation.Autowired; 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.test.autoconfigure.data.r2dbc.DataR2dbcTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection; 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 org.springframework.boot.testsupport.testcontainers.DockerImageNames;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -43,8 +41,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class CityRepositoryTests { class CityRepositoryTests {
@Container @Container
@R2dbcServiceConnection @ServiceConnection
@ServiceConnection(LiquibaseConnectionDetails.class)
static PostgreSQLContainer<?> postgresql = new PostgreSQLContainer<>(DockerImageNames.postgresql()) static PostgreSQLContainer<?> postgresql = new PostgreSQLContainer<>(DockerImageNames.postgresql())
.withDatabaseName("test_liquibase"); .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.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort; 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.boot.testsupport.testcontainers.DockerImageNames;
import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
@ -65,7 +65,7 @@ class SampleSessionMongoApplicationTests {
private int port; private int port;
@Container @Container
@MongoServiceConnection @ServiceConnection
static MongoDBContainer mongo = new MongoDBContainer(DockerImageNames.mongo()).withStartupAttempts(3) static MongoDBContainer mongo = new MongoDBContainer(DockerImageNames.mongo()).withStartupAttempts(3)
.withStartupTimeout(Duration.ofMinutes(2)); .withStartupTimeout(Duration.ofMinutes(2));

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

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

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

Loading…
Cancel
Save