mirror of
https://github.com/spring-projects/spring-boot.git
synced 2024-08-29 03:06:45 +08:00
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:
parent
c21cf31853
commit
b4cd2572d5
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
*/
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -94,7 +94,7 @@ class ServiceConnectionAutoConfigurationTests {
|
||||
static class ContainerConfiguration {
|
||||
|
||||
@Bean
|
||||
@ServiceConnection
|
||||
@ServiceConnection("redis")
|
||||
RedisContainer redisContainer() {
|
||||
return new RedisContainer();
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user