Prevent early initialization of Container beans

Update testcontainers auto-configuration so that `Container` bean
instances are no longer needed when registering `ConnectionDetails`
beans. Registration now occurs based on the bean type and the `name`
attribute of `@ServiceConnection`.

Fixes gh-35168
This commit is contained in:
Phillip Webb 2023-04-30 19:23:49 -07:00
parent c21cf31853
commit b4cd2572d5
12 changed files with 356 additions and 151 deletions

View File

@ -31,6 +31,7 @@ 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.boot.autoconfigure.service.connection.ConnectionDetailsFactoryNotFoundException;
import org.springframework.core.log.LogMessage;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
@ -61,10 +62,21 @@ class ConnectionDetailsRegistrar {
sources.forEach((source) -> registerBeanDefinitions(registry, source));
}
private void registerBeanDefinitions(BeanDefinitionRegistry registry, ContainerConnectionSource<?> source) {
this.connectionDetailsFactories.getConnectionDetails(source, true)
.forEach((connectionDetailsType, connectionDetails) -> registerBeanDefinition(registry, source,
connectionDetailsType, connectionDetails));
void registerBeanDefinitions(BeanDefinitionRegistry registry, ContainerConnectionSource<?> source) {
try {
this.connectionDetailsFactories.getConnectionDetails(source, true)
.forEach((connectionDetailsType, connectionDetails) -> registerBeanDefinition(registry, source,
connectionDetailsType, connectionDetails));
}
catch (ConnectionDetailsFactoryNotFoundException ex) {
if (!StringUtils.hasText(source.getConnectionName())) {
StringBuilder message = new StringBuilder(ex.getMessage());
message.append((!message.toString().endsWith(".")) ? "." : "");
message.append(" You may need to add a 'name' to your @ServiceConnection annotation");
throw new ConnectionDetailsFactoryNotFoundException(message.toString(), ex.getCause());
}
throw ex;
}
}
@SuppressWarnings("unchecked")

View File

@ -20,6 +20,7 @@ import java.util.Arrays;
import org.testcontainers.containers.Container;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory;
import org.springframework.boot.origin.Origin;
@ -108,16 +109,18 @@ public abstract class ContainerConnectionDetailsFactory<C extends Container<?>,
protected abstract D getContainerConnectionDetails(ContainerConnectionSource<C> source);
/**
* Convenient base class for {@link ConnectionDetails} results that are backed by a
* Base class for {@link ConnectionDetails} results that are backed by a
* {@link ContainerConnectionSource}.
*
* @param <C> the container type
*/
protected static class ContainerConnectionDetails<C extends Container<?>>
implements ConnectionDetails, OriginProvider {
implements ConnectionDetails, OriginProvider, InitializingBean {
private final ContainerConnectionSource<C> source;
private volatile C container;
/**
* Create a new {@link ContainerConnectionDetails} instance.
* @param source the source {@link ContainerConnectionSource}
@ -127,8 +130,20 @@ public abstract class ContainerConnectionDetailsFactory<C extends Container<?>,
this.source = source;
}
@Override
public void afterPropertiesSet() throws Exception {
this.container = this.source.getContainerSupplier().get();
}
/**
* Return the container that back this connection details instance. This method
* can only be called once the connection details bean has been initialized.
* @return the container instance
*/
protected final C getContainer() {
return this.source.getContainer();
Assert.state(this.container != null,
"Container cannot be obtained before the connection details bean has been initialized");
return this.container;
}
@Override

View File

@ -17,6 +17,7 @@
package org.springframework.boot.testcontainers.service.connection;
import java.util.Set;
import java.util.function.Supplier;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@ -49,63 +50,72 @@ public final class ContainerConnectionSource<C extends Container<?>> implements
private final Origin origin;
private final C container;
private final Class<C> containerType;
private final String acceptedConnectionName;
private final String connectionName;
private final Set<Class<?>> acceptedConnectionDetailsTypes;
private final Set<Class<?>> connectionDetailsTypes;
ContainerConnectionSource(String beanNameSuffix, Origin origin, C container,
MergedAnnotation<ServiceConnection> annotation) {
private Supplier<C> containerSupplier;
ContainerConnectionSource(String beanNameSuffix, Origin origin, Class<C> containerType, String containerImageName,
MergedAnnotation<ServiceConnection> annotation, Supplier<C> containerSupplier) {
this.beanNameSuffix = beanNameSuffix;
this.origin = origin;
this.container = container;
this.acceptedConnectionName = getConnectionName(container, annotation.getString("name"));
this.acceptedConnectionDetailsTypes = Set.of(annotation.getClassArray("type"));
this.containerType = containerType;
this.connectionName = getOrDeduceConnectionName(annotation.getString("name"), containerImageName);
this.connectionDetailsTypes = Set.of(annotation.getClassArray("type"));
this.containerSupplier = containerSupplier;
}
ContainerConnectionSource(String beanNameSuffix, Origin origin, C container, ServiceConnection annotation) {
ContainerConnectionSource(String beanNameSuffix, Origin origin, Class<C> containerType, String containerImageName,
ServiceConnection annotation, Supplier<C> containerSupplier) {
this.beanNameSuffix = beanNameSuffix;
this.origin = origin;
this.container = container;
this.acceptedConnectionName = getConnectionName(container, annotation.name());
this.acceptedConnectionDetailsTypes = Set.of(annotation.type());
this.containerType = containerType;
this.connectionName = getOrDeduceConnectionName(annotation.name(), containerImageName);
this.connectionDetailsTypes = Set.of(annotation.type());
this.containerSupplier = containerSupplier;
}
private static String getConnectionName(Container<?> container, String connectionName) {
if (StringUtils.hasLength(connectionName)) {
private static String getOrDeduceConnectionName(String connectionName, String containerImageName) {
if (StringUtils.hasText(connectionName)) {
return connectionName;
}
try {
DockerImageName imageName = DockerImageName.parse(container.getDockerImageName());
if (StringUtils.hasText(containerImageName)) {
DockerImageName imageName = DockerImageName.parse(containerImageName);
imageName.assertValid();
return imageName.getRepository();
}
catch (IllegalArgumentException ex) {
return container.getDockerImageName();
}
return null;
}
boolean accepts(String connectionName, Class<?> connectionDetailsType, Class<?> containerType) {
if (!containerType.isInstance(this.container)) {
logger.trace(LogMessage.of(() -> "%s not accepted as %s is not an instance of %s".formatted(this,
this.container.getClass().getName(), containerType.getName())));
boolean accepts(String requiredConnectionName, Class<?> requiredContainerType,
Class<?> requiredConnectionDetailsType) {
if (StringUtils.hasText(requiredConnectionName)
&& !requiredConnectionName.equalsIgnoreCase(this.connectionName)) {
logger.trace(LogMessage
.of(() -> "%s not accepted as source connection name '%s' does not match required connection name '%s'"
.formatted(this, this.connectionName, requiredConnectionName)));
return false;
}
if (StringUtils.hasLength(connectionName) && !connectionName.equalsIgnoreCase(this.acceptedConnectionName)) {
logger.trace(LogMessage.of(() -> "%s not accepted as connection names '%s' and '%s' do not match"
.formatted(this, connectionName, this.acceptedConnectionName)));
if (!requiredContainerType.isAssignableFrom(this.containerType)) {
logger.trace(LogMessage.of(() -> "%s not accepted as source container type %s is not assignable from %s"
.formatted(this, this.containerType.getName(), requiredContainerType.getName())));
return false;
}
if (!this.acceptedConnectionDetailsTypes.isEmpty() && this.acceptedConnectionDetailsTypes.stream()
.noneMatch((candidate) -> candidate.isAssignableFrom(connectionDetailsType))) {
logger.trace(LogMessage.of(() -> "%s not accepted as connection details type %s not in %s".formatted(this,
connectionDetailsType, this.acceptedConnectionDetailsTypes)));
if (!this.connectionDetailsTypes.isEmpty() && this.connectionDetailsTypes.stream()
.noneMatch((candidate) -> candidate.isAssignableFrom(requiredConnectionDetailsType))) {
logger.trace(LogMessage
.of(() -> "%s not accepted as source connection details types %s has no element assignable from %s"
.formatted(this, this.connectionDetailsTypes.stream().map(Class::getName).toList(),
requiredConnectionDetailsType.getName())));
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())));
logger.trace(
LogMessage.of(() -> "%s accepted for connection name '%s' container type %s, connection details type %s"
.formatted(this, requiredConnectionName, requiredContainerType.getName(),
requiredConnectionDetailsType.getName())));
return true;
}
@ -118,8 +128,12 @@ public final class ContainerConnectionSource<C extends Container<?>> implements
return this.origin;
}
C getContainer() {
return this.container;
String getConnectionName() {
return this.connectionName;
}
Supplier<C> getContainerSupplier() {
return this.containerSupplier;
}
@Override

View File

@ -22,8 +22,10 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.testcontainers.containers.Container;
import org.testcontainers.utility.DockerImageName;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
import org.springframework.context.annotation.Bean;
import org.springframework.core.annotation.AliasFor;
/**
@ -40,9 +42,18 @@ import org.springframework.core.annotation.AliasFor;
public @interface ServiceConnection {
/**
* 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.
* The name of the service being connected to. Container names are used to determine
* the connection details that should be created when a technology-specific
* {@link Container} subclass is not available.
* <p>
* If not specified, and if the {@link Container} instance is available, the
* {@link DockerImageName#getRepository() repository} part of the
* {@link Container#getDockerImageName() docker image name} will be used. Note that
* {@link Container} instances are <em>not</em> available early enough when the
* container is defined as a {@link Bean @Bean} method. All
* {@link ServiceConnection @ServiceConnection} {@link Bean @Bean} methods that need
* to match on the connection name <em>must</em> declare this attribute.
* <p>
* This attribute is an alias for {@link #name()}.
* @return the name of the service
* @see #name()
@ -52,8 +63,19 @@ public @interface ServiceConnection {
/**
* 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.
* The name of the service being connected to. Container names are used to determine
* the connection details that should be created when a technology-specific
* {@link Container} subclass is not available.
* <p>
* If not specified, and if the {@link Container} instance is available, the
* {@link DockerImageName#getRepository() repository} part of the
* {@link Container#getDockerImageName() docker image name} will be used. Note that
* {@link Container} instances are <em>not</em> available early enough when the
* container is defined as a {@link Bean @Bean} method. All
* {@link ServiceConnection @ServiceConnection} {@link Bean @Bean} methods that need
* to match on the connection name <em>must</em> declare this attribute.
* <p>
* This attribute is an alias for {@link #value()}.
* @return the name of the service
* @see #value()
*/

View File

@ -16,13 +16,12 @@
package org.springframework.boot.testcontainers.service.connection;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.testcontainers.containers.Container;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
@ -48,33 +47,43 @@ class ServiceConnectionAutoConfigurationRegistrar implements ImportBeanDefinitio
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
if (this.beanFactory instanceof ConfigurableListableBeanFactory listableBeanFactory) {
ConnectionDetailsFactories connectionDetailsFactories = new ConnectionDetailsFactories();
List<ContainerConnectionSource<?>> sources = getSources(listableBeanFactory);
new ConnectionDetailsRegistrar(listableBeanFactory, connectionDetailsFactories)
.registerBeanDefinitions(registry, sources);
registerBeanDefinitions(listableBeanFactory, registry);
}
}
private List<ContainerConnectionSource<?>> getSources(ConfigurableListableBeanFactory beanFactory) {
List<ContainerConnectionSource<?>> sources = new ArrayList<>();
for (String candidate : beanFactory.getBeanNamesForType(Container.class)) {
Set<ServiceConnection> annotations = beanFactory.findAllAnnotationsOnBean(candidate,
ServiceConnection.class, false);
if (!annotations.isEmpty()) {
addSources(sources, beanFactory, candidate, annotations);
private void registerBeanDefinitions(ConfigurableListableBeanFactory beanFactory, BeanDefinitionRegistry registry) {
ConnectionDetailsRegistrar registrar = new ConnectionDetailsRegistrar(beanFactory,
new ConnectionDetailsFactories());
for (String beanName : beanFactory.getBeanNamesForType(Container.class)) {
BeanDefinition beanDefinition = getBeanDefinition(beanFactory, beanName);
for (ServiceConnection annotation : getAnnotations(beanFactory, beanName)) {
ContainerConnectionSource<?> source = createSource(beanFactory, beanName, beanDefinition, annotation);
registrar.registerBeanDefinitions(registry, source);
}
}
return sources;
}
private void addSources(List<ContainerConnectionSource<?>> sources, ConfigurableListableBeanFactory beanFactory,
String beanName, Set<ServiceConnection> annotations) {
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
Origin origin = new BeanOrigin(beanName, beanDefinition);
Container<?> container = beanFactory.getBean(beanName, Container.class);
for (ServiceConnection annotation : annotations) {
sources.add(new ContainerConnectionSource<>(beanName, origin, container, annotation));
private Set<ServiceConnection> getAnnotations(ConfigurableListableBeanFactory beanFactory, String beanName) {
return beanFactory.findAllAnnotationsOnBean(beanName, ServiceConnection.class, false);
}
private BeanDefinition getBeanDefinition(ConfigurableListableBeanFactory beanFactory, String beanName) {
try {
return beanFactory.getBeanDefinition(beanName);
}
catch (NoSuchBeanDefinitionException ex) {
return null;
}
}
@SuppressWarnings("unchecked")
private <C extends Container<?>> ContainerConnectionSource<C> createSource(
ConfigurableListableBeanFactory beanFactory, String beanName, BeanDefinition beanDefinition,
ServiceConnection annotation) {
Origin origin = new BeanOrigin(beanName, beanDefinition);
Class<C> containerType = (Class<C>) beanFactory.getType(beanName, false);
return new ContainerConnectionSource<>(beanName, origin, containerType, null, annotation,
() -> beanFactory.getBean(beanName, containerType));
}
}

View File

@ -31,9 +31,7 @@ import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.ContextCustomizerFactory;
import org.springframework.test.context.TestContextAnnotationUtils;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
* Spring Test {@link ContextCustomizerFactory} to support
@ -65,17 +63,19 @@ class ServiceConnectionContextCustomizerFactory implements ContextCustomizerFact
}
}
private ContainerConnectionSource<?> createSource(Field field, MergedAnnotation<ServiceConnection> annotation) {
@SuppressWarnings("unchecked")
private <C extends Container<?>> ContainerConnectionSource<?> createSource(Field field,
MergedAnnotation<ServiceConnection> annotation) {
Assert.state(Modifier.isStatic(field.getModifiers()),
() -> "@ServiceConnection field '%s' must be static".formatted(field.getName()));
String beanNameSuffix = StringUtils.capitalize(ClassUtils.getShortNameAsProperty(field.getDeclaringClass()))
+ StringUtils.capitalize(field.getName());
Origin origin = new FieldOrigin(field);
Object fieldValue = getFieldValue(field);
Assert.state(fieldValue instanceof Container, () -> "Field '%s' in %s must be a %s".formatted(field.getName(),
field.getDeclaringClass().getName(), Container.class.getName()));
Container<?> container = (Container<?>) fieldValue;
return new ContainerConnectionSource<>(beanNameSuffix, origin, container, annotation);
Class<C> containerType = (Class<C>) fieldValue.getClass();
C container = (C) fieldValue;
return new ContainerConnectionSource<>("test", origin, containerType, container.getDockerImageName(),
annotation, () -> container);
}
private Object getFieldValue(Field field) {

View File

@ -0,0 +1,105 @@
/*
* 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.PostgreSQLContainer;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
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.boot.autoconfigure.service.connection.ConnectionDetailsFactoryNotFoundException;
import org.springframework.boot.origin.Origin;
import org.springframework.core.annotation.MergedAnnotation;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link ConnectionDetailsRegistrar}.
*
* @author Phillip Webb
*/
class ConnectionDetailsRegistrarTests {
private Origin origin;
private PostgreSQLContainer<?> container;
private MergedAnnotation<ServiceConnection> annotation;
private ContainerConnectionSource<?> source;
private ConnectionDetailsFactories factories;
@BeforeEach
void setup() {
this.origin = mock(Origin.class);
this.container = mock(PostgreSQLContainer.class);
this.annotation = MergedAnnotation.of(ServiceConnection.class, Map.of("name", "", "type", new Class<?>[0]));
this.source = new ContainerConnectionSource<>("test", this.origin, PostgreSQLContainer.class, null,
this.annotation, () -> this.container);
this.factories = mock(ConnectionDetailsFactories.class);
}
@Test
void registerBeanDefinitionsWhenConnectionDetailsFactoryNotFoundAndNoConnectionNameThrowsExceptionWithBetterMessage() {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
ConnectionDetailsRegistrar registrar = new ConnectionDetailsRegistrar(beanFactory, this.factories);
given(this.factories.getConnectionDetails(this.source, true))
.willThrow(new ConnectionDetailsFactoryNotFoundException("fail"));
assertThatExceptionOfType(ConnectionDetailsFactoryNotFoundException.class)
.isThrownBy(() -> registrar.registerBeanDefinitions(beanFactory, this.source))
.withMessage("fail. You may need to add a 'name' to your @ServiceConnection annotation");
}
@Test
void registerBeanDefinitionsWhenExistingBeanSkipsRegistration() {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
beanFactory.registerBeanDefinition("testbean", new RootBeanDefinition(CustomTestConnectionDetails.class));
ConnectionDetailsRegistrar registrar = new ConnectionDetailsRegistrar(beanFactory, this.factories);
given(this.factories.getConnectionDetails(this.source, true))
.willReturn(Map.of(TestConnectionDetails.class, new TestConnectionDetails()));
registrar.registerBeanDefinitions(beanFactory, this.source);
assertThat(beanFactory.getBean(TestConnectionDetails.class)).isInstanceOf(CustomTestConnectionDetails.class);
}
@Test
void registerBeanDefinitionsRegistersDefinition() {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
ConnectionDetailsRegistrar registrar = new ConnectionDetailsRegistrar(beanFactory, this.factories);
given(this.factories.getConnectionDetails(this.source, true))
.willReturn(Map.of(TestConnectionDetails.class, new TestConnectionDetails()));
registrar.registerBeanDefinitions(beanFactory, this.source);
assertThat(beanFactory.getBean(TestConnectionDetails.class)).isNotNull();
}
static class TestConnectionDetails implements ConnectionDetails {
}
static class CustomTestConnectionDetails extends TestConnectionDetails {
}
}

View File

@ -28,9 +28,11 @@ 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.boot.testcontainers.service.connection.ContainerConnectionDetailsFactoryTests.TestContainerConnectionDetailsFactory.TestContainerConnectionDetails;
import org.springframework.core.annotation.MergedAnnotation;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.Mockito.mock;
/**
@ -59,8 +61,8 @@ class ContainerConnectionDetailsFactoryTests {
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.source = new ContainerConnectionSource<>(this.beanNameSuffix, this.origin, PostgreSQLContainer.class,
this.container.getDockerImageName(), this.annotation, () -> this.container);
}
@Test
@ -88,7 +90,7 @@ class ContainerConnectionDetailsFactoryTests {
void getConnectionDetailsWhenContainerTypeDoesNotMatchReturnsNull() {
ElasticsearchContainer container = mock(ElasticsearchContainer.class);
ContainerConnectionSource<?> source = new ContainerConnectionSource<>(this.beanNameSuffix, this.origin,
container, this.annotation);
ElasticsearchContainer.class, container.getDockerImageName(), this.annotation, () -> container);
TestContainerConnectionDetailsFactory factory = new TestContainerConnectionDetailsFactory();
ConnectionDetails connectionDetails = getConnectionDetails(factory, source);
assertThat(connectionDetails).isNull();
@ -101,10 +103,26 @@ class ContainerConnectionDetailsFactoryTests {
assertThat(Origin.from(connectionDetails)).isSameAs(this.origin);
}
@Test
void getContainerWhenNotInitializedThrowsException() {
TestContainerConnectionDetailsFactory factory = new TestContainerConnectionDetailsFactory();
TestContainerConnectionDetails connectionDetails = getConnectionDetails(factory, this.source);
assertThatIllegalStateException().isThrownBy(() -> connectionDetails.callGetContainer())
.withMessage("Container cannot be obtained before the connection details bean has been initialized");
}
@Test
void getContainerWhenInitializedReturnsSuppliedContainer() throws Exception {
TestContainerConnectionDetailsFactory factory = new TestContainerConnectionDetailsFactory();
TestContainerConnectionDetails connectionDetails = getConnectionDetails(factory, this.source);
connectionDetails.afterPropertiesSet();
assertThat(connectionDetails.callGetContainer()).isSameAs(this.container);
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private ConnectionDetails getConnectionDetails(ConnectionDetailsFactory<?, ?> factory,
private TestContainerConnectionDetails getConnectionDetails(ConnectionDetailsFactory<?, ?> factory,
ContainerConnectionSource<?> source) {
return ((ConnectionDetailsFactory) factory).getConnectionDetails(source);
return (TestContainerConnectionDetails) ((ConnectionDetailsFactory) factory).getConnectionDetails(source);
}
/**
@ -127,8 +145,8 @@ class ContainerConnectionDetailsFactoryTests {
return new TestContainerConnectionDetails(source);
}
private static final class TestContainerConnectionDetails
extends ContainerConnectionDetails<JdbcDatabaseContainer<?>> implements JdbcConnectionDetails {
static final class TestContainerConnectionDetails extends ContainerConnectionDetails<JdbcDatabaseContainer<?>>
implements JdbcConnectionDetails {
private TestContainerConnectionDetails(ContainerConnectionSource<JdbcDatabaseContainer<?>> source) {
super(source);
@ -149,6 +167,10 @@ class ContainerConnectionDetailsFactoryTests {
return "jdbc:example";
}
JdbcDatabaseContainer<?> callGetContainer() {
return super.getContainer();
}
}
}

View File

@ -46,7 +46,7 @@ class ContainerConnectionSourceTests {
private Origin origin;
private JdbcDatabaseContainer<?> container;
private PostgreSQLContainer<?> container;
private MergedAnnotation<ServiceConnection> annotation;
@ -59,92 +59,102 @@ class ContainerConnectionSourceTests {
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);
this.source = new ContainerConnectionSource<>(this.beanNameSuffix, this.origin, PostgreSQLContainer.class,
this.container.getDockerImageName(), this.annotation, () -> this.container);
}
@Test
void acceptsWhenContainerIsNotInstanceOfContainerTypeReturnsFalse() {
String connectionName = null;
Class<?> connectionDetailsType = JdbcConnectionDetails.class;
Class<?> containerType = ElasticsearchContainer.class;
assertThat(this.source.accepts(connectionName, connectionDetailsType, containerType)).isFalse();
void acceptsWhenContainerIsNotInstanceOfRequiredContainerTypeReturnsFalse() {
String requiredConnectionName = null;
Class<?> requiredContainerType = ElasticsearchContainer.class;
Class<?> requiredConnectionDetailsType = JdbcConnectionDetails.class;
assertThat(this.source.accepts(requiredConnectionName, requiredContainerType, requiredConnectionDetailsType))
.isFalse();
}
@Test
void acceptsWhenContainerIsInstanceOfContainerTypeReturnsTrue() {
String connectionName = null;
Class<?> connectionDetailsType = JdbcConnectionDetails.class;
Class<?> containerType = JdbcDatabaseContainer.class;
assertThat(this.source.accepts(connectionName, connectionDetailsType, containerType)).isTrue();
void acceptsWhenContainerIsInstanceOfRequiredContainerTypeReturnsTrue() {
String requiredConnectionName = null;
Class<?> requiredContainerType = JdbcDatabaseContainer.class;
Class<?> requiredConnectionDetailsType = JdbcConnectionDetails.class;
assertThat(this.source.accepts(requiredConnectionName, requiredContainerType, requiredConnectionDetailsType))
.isTrue();
}
@Test
void acceptsWhenConnectionNameDoesNotMatchNameTakenFromAnnotationReturnsFalse() {
void acceptsWhenRequiredConnectionNameDoesNotMatchNameTakenFromAnnotationReturnsFalse() {
setupSourceAnnotatedWithName("myname");
String connectionName = "othername";
Class<?> connectionDetailsType = JdbcConnectionDetails.class;
Class<?> containerType = JdbcDatabaseContainer.class;
assertThat(this.source.accepts(connectionName, connectionDetailsType, containerType)).isFalse();
String requiredConnectionName = "othername";
Class<?> requiredContainerType = JdbcDatabaseContainer.class;
Class<?> requiredConnectionDetailsType = JdbcConnectionDetails.class;
assertThat(this.source.accepts(requiredConnectionName, requiredContainerType, requiredConnectionDetailsType))
.isFalse();
}
@Test
void acceptsWhenConnectionNameDoesNotMatchNameTakenFromContainerReturnsFalse() {
String connectionName = "othername";
Class<?> connectionDetailsType = JdbcConnectionDetails.class;
Class<?> containerType = JdbcDatabaseContainer.class;
assertThat(this.source.accepts(connectionName, connectionDetailsType, containerType)).isFalse();
void acceptsWhenRequiredConnectionNameDoesNotMatchNameTakenFromContainerReturnsFalse() {
String requiredConnectionName = "othername";
Class<?> requiredContainerType = JdbcDatabaseContainer.class;
Class<?> requiredConnectionDetailsType = JdbcConnectionDetails.class;
assertThat(this.source.accepts(requiredConnectionName, requiredContainerType, requiredConnectionDetailsType))
.isFalse();
}
@Test
void acceptsWhenConnectionNameIsUnrestrictedReturnsTrue() {
String connectionName = null;
Class<?> connectionDetailsType = JdbcConnectionDetails.class;
Class<?> containerType = JdbcDatabaseContainer.class;
assertThat(this.source.accepts(connectionName, connectionDetailsType, containerType)).isTrue();
void acceptsWhenRequiredConnectionNameIsUnrestrictedReturnsTrue() {
String requiredConnectionName = null;
Class<?> requiredContainerType = JdbcDatabaseContainer.class;
Class<?> requiredConnectionDetailsType = JdbcConnectionDetails.class;
assertThat(this.source.accepts(requiredConnectionName, requiredContainerType, requiredConnectionDetailsType))
.isTrue();
}
@Test
void acceptsWhenConnectionNameMatchesNameTakenFromAnnotationReturnsTrue() {
void acceptsWhenRequiredConnectionNameMatchesNameTakenFromAnnotationReturnsTrue() {
setupSourceAnnotatedWithName("myname");
String connectionName = "myname";
Class<?> connectionDetailsType = JdbcConnectionDetails.class;
Class<?> containerType = JdbcDatabaseContainer.class;
assertThat(this.source.accepts(connectionName, connectionDetailsType, containerType)).isTrue();
String requiredConnectionName = "myname";
Class<?> requiredContainerType = JdbcDatabaseContainer.class;
Class<?> requiredConnectionDetailsType = JdbcConnectionDetails.class;
assertThat(this.source.accepts(requiredConnectionName, requiredContainerType, requiredConnectionDetailsType))
.isTrue();
}
@Test
void acceptsWhenConnectionNameMatchesNameTakenFromContainerReturnsTrue() {
String connectionName = "postgres";
Class<?> connectionDetailsType = JdbcConnectionDetails.class;
Class<?> containerType = JdbcDatabaseContainer.class;
assertThat(this.source.accepts(connectionName, connectionDetailsType, containerType)).isTrue();
void acceptsWhenRequiredConnectionNameMatchesNameTakenFromContainerReturnsTrue() {
String requiredConnectionName = "postgres";
Class<?> requiredContainerType = JdbcDatabaseContainer.class;
Class<?> requiredConnectionDetailsType = JdbcConnectionDetails.class;
assertThat(this.source.accepts(requiredConnectionName, requiredContainerType, requiredConnectionDetailsType))
.isTrue();
}
@Test
void acceptsWhenConnectionDetailsTypeNotInAnnotationRestrictionReturnsFalse() {
void acceptsWhenRequiredConnectionDetailsTypeNotInAnnotationRestrictionReturnsFalse() {
setupSourceAnnotatedWithType(ElasticsearchConnectionDetails.class);
String connectionName = null;
Class<?> connectionDetailsType = JdbcConnectionDetails.class;
Class<?> containerType = JdbcDatabaseContainer.class;
assertThat(this.source.accepts(connectionName, connectionDetailsType, containerType)).isFalse();
String requiredConnectionName = null;
Class<?> requiredContainerType = JdbcDatabaseContainer.class;
Class<?> requiredConnectionDetailsType = JdbcConnectionDetails.class;
assertThat(this.source.accepts(requiredConnectionName, requiredContainerType, requiredConnectionDetailsType))
.isFalse();
}
@Test
void acceptsWhenConnectionDetailsTypeInAnnotationRestrictionReturnsTrue() {
void acceptsWhenRequiredConnectionDetailsTypeInAnnotationRestrictionReturnsTrue() {
setupSourceAnnotatedWithType(JdbcConnectionDetails.class);
String connectionName = null;
Class<?> connectionDetailsType = JdbcConnectionDetails.class;
Class<?> containerType = JdbcDatabaseContainer.class;
assertThat(this.source.accepts(connectionName, connectionDetailsType, containerType)).isTrue();
String requiredConnectionName = null;
Class<?> requiredContainerType = JdbcDatabaseContainer.class;
Class<?> requiredConnectionDetailsType = JdbcConnectionDetails.class;
assertThat(this.source.accepts(requiredConnectionName, requiredContainerType, requiredConnectionDetailsType))
.isTrue();
}
@Test
void acceptsWhenConnectionDetailsTypeIsNotRestrictedReturnsTrue() {
String connectionName = null;
Class<?> connectionDetailsType = JdbcConnectionDetails.class;
Class<?> containerType = JdbcDatabaseContainer.class;
assertThat(this.source.accepts(connectionName, connectionDetailsType, containerType)).isTrue();
void acceptsWhenRequiredConnectionDetailsTypeIsNotRestrictedReturnsTrue() {
String requiredConnectionName = null;
Class<?> requiredContainerType = JdbcDatabaseContainer.class;
Class<?> requiredConnectionDetailsType = JdbcConnectionDetails.class;
assertThat(this.source.accepts(requiredConnectionName, requiredContainerType, requiredConnectionDetailsType))
.isTrue();
}
@Test
@ -158,8 +168,8 @@ class ContainerConnectionSourceTests {
}
@Test
void getContainerReturnsContainer() {
assertThat(this.source.getContainer()).isSameAs(this.container);
void getContainerSupplierReturnsSupplierSupplyingContainer() {
assertThat(this.source.getContainerSupplier().get()).isSameAs(this.container);
}
@Test
@ -169,15 +179,15 @@ class ContainerConnectionSourceTests {
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);
this.source = new ContainerConnectionSource<>(this.beanNameSuffix, this.origin, PostgreSQLContainer.class,
this.container.getDockerImageName(), this.annotation, () -> this.container);
}
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);
this.source = new ContainerConnectionSource<>(this.beanNameSuffix, this.origin, PostgreSQLContainer.class,
this.container.getDockerImageName(), this.annotation, () -> this.container);
}
}

View File

@ -94,7 +94,7 @@ class ServiceConnectionAutoConfigurationTests {
static class ContainerConfiguration {
@Bean
@ServiceConnection
@ServiceConnection("redis")
RedisContainer redisContainer() {
return new RedisContainer();
}

View File

@ -80,7 +80,7 @@ class ServiceConnectionContextCustomizerFactoryTests {
ServiceConnectionContextCustomizer customizer = (ServiceConnectionContextCustomizer) this.factory
.createContextCustomizer(SingleServiceConnection.class, null);
ContainerConnectionSource<?> source = customizer.getSources().get(0);
assertThat(source.getBeanNameSuffix()).isEqualTo("SingleServiceConnectionService1");
assertThat(source.getBeanNameSuffix()).isEqualTo("test");
}
@Test

View File

@ -22,7 +22,6 @@ 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;
@ -51,11 +50,9 @@ import static org.mockito.Mockito.spy;
*/
class ServiceConnectionContextCustomizerTests {
private String beanNameSuffix;
private Origin origin;
private JdbcDatabaseContainer<?> container;
private PostgreSQLContainer<?> container;
private MergedAnnotation<ServiceConnection> annotation;
@ -65,13 +62,12 @@ class ServiceConnectionContextCustomizerTests {
@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.source = new ContainerConnectionSource<>("test", this.origin, PostgreSQLContainer.class,
this.container.getDockerImageName(), this.annotation, () -> this.container);
this.factories = mock(ConnectionDetailsFactories.class);
}
@ -89,7 +85,7 @@ class ServiceConnectionContextCustomizerTests {
customizer.customizeContext(context, mergedConfig);
ArgumentCaptor<BeanDefinition> beanDefinitionCaptor = ArgumentCaptor.forClass(BeanDefinition.class);
then(beanFactory).should()
.registerBeanDefinition(eq("testJdbcConnectionDetailsForMyBean"), beanDefinitionCaptor.capture());
.registerBeanDefinition(eq("testJdbcConnectionDetailsForTest"), beanDefinitionCaptor.capture());
RootBeanDefinition beanDefinition = (RootBeanDefinition) beanDefinitionCaptor.getValue();
assertThat(beanDefinition.getInstanceSupplier().get()).isSameAs(connectionDetails);
assertThat(beanDefinition.getBeanClass()).isEqualTo(TestJdbcConnectionDetails.class);