Move RestTestClientBuilderCustomizer to spring-boot-test

Relocate `RestTestClientBuilderCustomizer` to `spring-boot-test`
and break the direct link to web-server by making use of
`spring.factories` and the new `BaseUrlProviders` class.

See gh-46356
This commit is contained in:
Phillip Webb
2025-10-01 21:51:20 -07:00
parent 6f909114e7
commit bba56ffc8b
125 changed files with 291 additions and 192 deletions
@@ -0,0 +1,180 @@
/*
* Copyright 2012-present 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.restclient.test;
import org.jspecify.annotations.Nullable;
import org.springframework.aot.AotDetector;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.ListableBeanFactory;
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;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.restclient.RestTemplateBuilder;
import org.springframework.boot.restclient.test.TestRestTemplate.HttpClientOption;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.http.server.BaseUrl;
import org.springframework.boot.test.http.server.BaseUrlProviders;
import org.springframework.boot.test.web.servlet.client.BaseUrlUriBuilderFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ConfigurationClassPostProcessor;
import org.springframework.core.Ordered;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.TestContextAnnotationUtils;
import org.springframework.util.Assert;
/**
* {@link ContextCustomizer} for {@link TestRestTemplate}.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
class TestRestTemplateContextCustomizer implements ContextCustomizer {
@Override
public void customizeContext(ConfigurableApplicationContext context,
MergedContextConfiguration mergedContextConfiguration) {
if (AotDetector.useGeneratedArtifacts()) {
return;
}
SpringBootTest springBootTest = TestContextAnnotationUtils
.findMergedAnnotation(mergedContextConfiguration.getTestClass(), SpringBootTest.class);
Assert.state(springBootTest != null, "'springBootTest' must not be null");
if (springBootTest.webEnvironment().isEmbedded()) {
registerTestRestTemplate(context);
}
}
private void registerTestRestTemplate(ConfigurableApplicationContext context) {
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
if (beanFactory instanceof BeanDefinitionRegistry registry) {
registerTestRestTemplate(registry);
}
}
private void registerTestRestTemplate(BeanDefinitionRegistry registry) {
RootBeanDefinition definition = new RootBeanDefinition(TestRestTemplateRegistrar.class);
definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
registry.registerBeanDefinition(TestRestTemplateRegistrar.class.getName(), definition);
}
@Override
public boolean equals(@Nullable Object obj) {
return (obj != null) && (obj.getClass() == getClass());
}
@Override
public int hashCode() {
return getClass().hashCode();
}
/**
* {@link BeanDefinitionRegistryPostProcessor} that runs after the
* {@link ConfigurationClassPostProcessor} and add a {@link TestRestTemplateFactory}
* bean definition when a {@link TestRestTemplate} hasn't already been registered.
*/
static class TestRestTemplateRegistrar implements BeanDefinitionRegistryPostProcessor, Ordered, BeanFactoryAware {
@SuppressWarnings("NullAway.Init")
private BeanFactory beanFactory;
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
if (AotDetector.useGeneratedArtifacts()) {
return;
}
if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors((ListableBeanFactory) this.beanFactory,
TestRestTemplate.class, false, false).length == 0) {
registry.registerBeanDefinition(TestRestTemplate.class.getName(),
new RootBeanDefinition(TestRestTemplateFactory.class));
}
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
}
/**
* {@link FactoryBean} used to create and configure a {@link TestRestTemplate}.
*/
public static class TestRestTemplateFactory implements FactoryBean<TestRestTemplate>, ApplicationContextAware {
private static final HttpClientOption[] DEFAULT_OPTIONS = {};
private static final HttpClientOption[] SSL_OPTIONS = { HttpClientOption.SSL };
@SuppressWarnings("NullAway.Init")
private TestRestTemplate template;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
RestTemplateBuilder builder = getRestTemplateBuilder(applicationContext);
BaseUrl baseUrl = new BaseUrlProviders(applicationContext).getBaseUrlOrDefault();
boolean sslEnabled = baseUrl != null && baseUrl.isHttps();
this.template = new TestRestTemplate(builder, null, null, sslEnabled ? SSL_OPTIONS : DEFAULT_OPTIONS);
this.template.setUriTemplateHandler(BaseUrlUriBuilderFactory.get(baseUrl));
}
private RestTemplateBuilder getRestTemplateBuilder(ApplicationContext applicationContext) {
try {
return applicationContext.getBean(RestTemplateBuilder.class);
}
catch (NoSuchBeanDefinitionException ex) {
return new RestTemplateBuilder();
}
}
@Override
public boolean isSingleton() {
return true;
}
@Override
public Class<?> getObjectType() {
return TestRestTemplate.class;
}
@Override
public TestRestTemplate getObject() throws Exception {
return this.template;
}
}
}
@@ -0,0 +1,53 @@
/*
* Copyright 2012-present 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.restclient.test;
import java.util.List;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfigurationAttributes;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.ContextCustomizerFactory;
import org.springframework.test.context.TestContextAnnotationUtils;
import org.springframework.util.ClassUtils;
/**
* {@link ContextCustomizerFactory} for {@link TestRestTemplate}.
*
* @author Andy Wilkinson
* @see TestRestTemplateContextCustomizer
*/
class TestRestTemplateContextCustomizerFactory implements ContextCustomizerFactory {
private static final boolean REST_TEMPLATE_BUILDER_PRESENT = ClassUtils.isPresent(
"org.springframework.boot.restclient.RestTemplateBuilder",
TestRestTemplateContextCustomizerFactory.class.getClassLoader());
@Override
public @Nullable ContextCustomizer createContextCustomizer(Class<?> testClass,
List<ContextConfigurationAttributes> configAttributes) {
if (!REST_TEMPLATE_BUILDER_PRESENT) {
return null;
}
SpringBootTest springBootTest = TestContextAnnotationUtils.findMergedAnnotation(testClass,
SpringBootTest.class);
return (springBootTest != null) ? new TestRestTemplateContextCustomizer() : null;
}
}
@@ -0,0 +1,279 @@
/*
* Copyright 2012-present 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.restclient.test
import org.springframework.core.ParameterizedTypeReference
import org.springframework.http.HttpEntity
import org.springframework.http.HttpMethod
import org.springframework.http.RequestEntity
import org.springframework.http.ResponseEntity
import org.springframework.web.client.RestClientException
import java.net.URI
/**
* Extension for [TestRestTemplate.getForObject] providing a `getForObject<Foo>(...)`
* variant leveraging Kotlin reified type parameters. Like the original Java method, this
* extension is subject to type erasure. Use [exchange] if you need to retain actual
* generic type arguments.
*
* @author Sebastien Deleuze
* @since 4.0.0
*/
@Throws(RestClientException::class)
inline fun <reified T : Any> TestRestTemplate.getForObject(url: String, vararg uriVariables: Any): T? =
getForObject(url, T::class.java, *uriVariables)
/**
* Extension for [TestRestTemplate.getForObject] providing a `getForObject<Foo>(...)`
* variant leveraging Kotlin reified type parameters. Like the original Java method, this
* extension is subject to type erasure. Use [exchange] if you need to retain actual
* generic type arguments.
*
* @author Sebastien Deleuze
* @since 2.0.0
*/
@Throws(RestClientException::class)
inline fun <reified T : Any> TestRestTemplate.getForObject(url: String, uriVariables: Map<String, Any?>): T? =
getForObject(url, T::class.java, uriVariables)
/**
* Extension for [TestRestTemplate.getForObject] providing a `getForObject<Foo>(...)`
* variant leveraging Kotlin reified type parameters. Like the original Java method, this
* extension is subject to type erasure. Use [exchange] if you need to retain actual
* generic type arguments.
*
* @author Sebastien Deleuze
* @since 2.0.0
*/
@Throws(RestClientException::class)
inline fun <reified T : Any> TestRestTemplate.getForObject(url: URI): T? =
getForObject(url, T::class.java)
/**
* Extension for [TestRestTemplate.getForEntity] providing a `getForEntity<Foo>(...)`
* variant leveraging Kotlin reified type parameters. Like the original Java method, this
* extension is subject to type erasure. Use [exchange] if you need to retain actual
* generic type arguments.
*
* @author Sebastien Deleuze
* @since 2.0.0
*/
@Throws(RestClientException::class)
inline fun <reified T : Any> TestRestTemplate.getForEntity(url: URI): ResponseEntity<T> =
getForEntity(url, T::class.java)
/**
* Extension for [TestRestTemplate.getForEntity] providing a `getForEntity<Foo>(...)`
* variant leveraging Kotlin reified type parameters. Like the original Java method, this
* extension is subject to type erasure. Use [exchange] if you need to retain actual
* generic type arguments.
*
* @author Sebastien Deleuze
* @since 2.0.0
*/
@Throws(RestClientException::class)
inline fun <reified T : Any> TestRestTemplate.getForEntity(url: String, vararg uriVariables: Any): ResponseEntity<T> =
getForEntity(url, T::class.java, *uriVariables)
/**
* Extension for [TestRestTemplate.getForEntity] providing a `getForEntity<Foo>(...)`
* variant leveraging Kotlin reified type parameters. Like the original Java method, this
* extension is subject to type erasure. Use [exchange] if you need to retain actual
* generic type arguments.
*
* @author Sebastien Deleuze
* @since 2.0.0
*/
@Throws(RestClientException::class)
inline fun <reified T : Any> TestRestTemplate.getForEntity(url: String,
uriVariables: Map<String, *>): ResponseEntity<T> =
getForEntity(url, T::class.java, uriVariables)
/**
* Extension for [TestRestTemplate.patchForObject] providing a `patchForObject<Foo>(...)`
* variant leveraging Kotlin reified type parameters. Like the original Java method, this
* extension is subject to type erasure. Use [exchange] if you need to retain actual
* generic type arguments.
*
* @author Sebastien Deleuze
* @since 2.0.0
*/
@Throws(RestClientException::class)
inline fun <reified T : Any> TestRestTemplate.patchForObject(url: String, request: Any? = null,
vararg uriVariables: Any): T? =
patchForObject(url, request, T::class.java, *uriVariables)
/**
* Extension for [TestRestTemplate.patchForObject] providing a `patchForObject<Foo>(...)`
* variant leveraging Kotlin reified type parameters. Like the original Java method, this
* extension is subject to type erasure. Use [exchange] if you need to retain actual
* generic type arguments.
*
* @author Sebastien Deleuze
* @since 2.0.0
*/
@Throws(RestClientException::class)
inline fun <reified T : Any> TestRestTemplate.patchForObject(url: String, request: Any? = null,
uriVariables: Map<String, *>): T? =
patchForObject(url, request, T::class.java, uriVariables)
/**
* Extension for [TestRestTemplate.patchForObject] providing a `patchForObject<Foo>(...)`
* variant leveraging Kotlin reified type parameters. Like the original Java method, this
* extension is subject to type erasure. Use [exchange] if you need to retain actual
* generic type arguments.
*
* @author Sebastien Deleuze
* @since 2.0.0
*/
@Throws(RestClientException::class)
inline fun <reified T : Any> TestRestTemplate.patchForObject(url: URI, request: Any? = null): T? =
patchForObject(url, request, T::class.java)
/**
* Extension for [TestRestTemplate.postForObject] providing a `postForObject<Foo>(...)`
* variant leveraging Kotlin reified type parameters. Like the original Java method, this
* extension is subject to type erasure. Use [exchange] if you need to retain actual
* generic type arguments.
*
* @author Sebastien Deleuze
* @since 2.0.0
*/
@Throws(RestClientException::class)
inline fun <reified T : Any> TestRestTemplate.postForObject(url: String, request: Any? = null,
vararg uriVariables: Any): T? =
postForObject(url, request, T::class.java, *uriVariables)
/**
* Extension for [TestRestTemplate.postForObject] providing a `postForObject<Foo>(...)`
* variant leveraging Kotlin reified type parameters. Like the original Java method, this
* extension is subject to type erasure. Use [exchange] if you need to retain actual
* generic type arguments.
*
* @author Sebastien Deleuze
* @since 2.0.0
*/
@Throws(RestClientException::class)
inline fun <reified T : Any> TestRestTemplate.postForObject(url: String, request: Any? = null,
uriVariables: Map<String, *>): T? =
postForObject(url, request, T::class.java, uriVariables)
/**
* Extension for [TestRestTemplate.postForObject] providing a `postForObject<Foo>(...)`
* variant leveraging Kotlin reified type parameters. Like the original Java method, this
* extension is subject to type erasure. Use [exchange] if you need to retain actual
* generic type arguments.
*
* @author Sebastien Deleuze
* @since 2.0.0
*/
@Throws(RestClientException::class)
inline fun <reified T : Any> TestRestTemplate.postForObject(url: URI, request: Any? = null): T? =
postForObject(url, request, T::class.java)
/**
* Extension for [TestRestTemplate.postForEntity] providing a `postForEntity<Foo>(...)`
* variant leveraging Kotlin reified type parameters. Like the original Java method, this
* extension is subject to type erasure. Use [exchange] if you need to retain actual
* generic type arguments.
*
* @author Sebastien Deleuze
* @since 2.0.0
*/
@Throws(RestClientException::class)
inline fun <reified T : Any> TestRestTemplate.postForEntity(url: String, request: Any? = null,
vararg uriVariables: Any): ResponseEntity<T> =
postForEntity(url, request, T::class.java, *uriVariables)
/**
* Extension for [TestRestTemplate.postForEntity] providing a `postForEntity<Foo>(...)`
* variant leveraging Kotlin reified type parameters. Like the original Java method, this
* extension is subject to type erasure. Use [exchange] if you need to retain actual
* generic type arguments.
*
* @author Sebastien Deleuze
* @since 2.0.0
*/
@Throws(RestClientException::class)
inline fun <reified T : Any> TestRestTemplate.postForEntity(url: String, request: Any? = null,
uriVariables: Map<String, *>): ResponseEntity<T> =
postForEntity(url, request, T::class.java, uriVariables)
/**
* Extension for [TestRestTemplate.postForEntity] providing a `postForEntity<Foo>(...)`
* variant leveraging Kotlin reified type parameters. Like the original Java method, this
* extension is subject to type erasure. Use [exchange] if you need to retain actual
* generic type arguments.
*
* @author Sebastien Deleuze
* @since 2.0.0
*/
@Throws(RestClientException::class)
inline fun <reified T : Any> TestRestTemplate.postForEntity(url: URI, request: Any? = null): ResponseEntity<T> =
postForEntity(url, request, T::class.java)
/**
* Extension for [TestRestTemplate.exchange] providing an `exchange<Foo>(...)`
* variant leveraging Kotlin reified type parameters. This extension is not subject to
* type erasure and retains actual generic type arguments.
*
* @author Sebastien Deleuze
* @since 2.0.0
*/
@Throws(RestClientException::class)
inline fun <reified T : Any> TestRestTemplate.exchange(url: String, method: HttpMethod,
requestEntity: HttpEntity<*>? = null, vararg uriVariables: Any): ResponseEntity<T> =
exchange(url, method, requestEntity, object : ParameterizedTypeReference<T>() {}, *uriVariables)
/**
* Extension for [TestRestTemplate.exchange] providing an `exchange<Foo>(...)`
* variant leveraging Kotlin reified type parameters. This extension is not subject to
* type erasure and retains actual generic type arguments.
*
* @author Sebastien Deleuze
* @since 2.0.0
*/
@Throws(RestClientException::class)
inline fun <reified T : Any> TestRestTemplate.exchange(url: String, method: HttpMethod,
requestEntity: HttpEntity<*>? = null, uriVariables: Map<String, *>): ResponseEntity<T> =
exchange(url, method, requestEntity, object : ParameterizedTypeReference<T>() {}, uriVariables)
/**
* Extension for [TestRestTemplate.exchange] providing an `exchange<Foo>(...)`
* variant leveraging Kotlin reified type parameters. This extension is not subject to
* type erasure and retains actual generic type arguments.
*
* @author Sebastien Deleuze
* @since 2.0.0
*/
@Throws(RestClientException::class)
inline fun <reified T : Any> TestRestTemplate.exchange(url: URI, method: HttpMethod,
requestEntity: HttpEntity<*>? = null): ResponseEntity<T> =
exchange(url, method, requestEntity, object : ParameterizedTypeReference<T>() {})
/**
* Extension for [TestRestTemplate.exchange] providing an `exchange<Foo>(...)`
* variant leveraging Kotlin reified type parameters. This extension is not subject to
* type erasure and retains actual generic type arguments.
*
* @author Sebastien Deleuze
* @since 2.0.0
*/
@Throws(RestClientException::class)
inline fun <reified T : Any> TestRestTemplate.exchange(requestEntity: RequestEntity<*>): ResponseEntity<T> =
exchange(requestEntity, object : ParameterizedTypeReference<T>() {})
@@ -1,3 +1,7 @@
# Spring Test Execution Listeners
org.springframework.test.context.TestExecutionListener=\
org.springframework.boot.restclient.test.autoconfigure.MockRestServiceServerResetTestExecutionListener
# Spring Test Context Customizer Factories
org.springframework.test.context.ContextCustomizerFactory=\
org.springframework.boot.restclient.test.TestRestTemplateContextCustomizerFactory
@@ -0,0 +1,487 @@
/*
* Copyright 2012-present 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.restclient.test;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpClient.Redirect;
import java.util.Base64;
import java.util.stream.Stream;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
import org.apache.hc.client5.http.impl.classic.RedirectExec;
import org.apache.hc.client5.http.protocol.RedirectStrategy;
import org.assertj.core.extractor.Extractors;
import org.junit.jupiter.api.Test;
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
import org.springframework.boot.http.client.HttpRedirects;
import org.springframework.boot.restclient.RestTemplateBuilder;
import org.springframework.boot.restclient.test.TestRestTemplate.HttpClientOption;
import org.springframework.boot.test.http.server.BaseUrl;
import org.springframework.boot.test.web.servlet.client.BaseUrlUriBuilderFactory;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.RequestEntity;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.JdkClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.mock.http.client.MockClientHttpRequest;
import org.springframework.mock.http.client.MockClientHttpResponse;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.MethodCallback;
import org.springframework.web.client.NoOpResponseErrorHandler;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.DefaultUriBuilderFactory;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link TestRestTemplate}.
*
* @author Dave Syer
* @author Phillip Webb
* @author Stephane Nicoll
* @author Andy Wilkinson
* @author Kristine Jetzke
* @author Yanming Zhou
*/
class TestRestTemplateTests {
@Test
void fromRestTemplateBuilder() {
RestTemplateBuilder builder = mock(RestTemplateBuilder.class);
RestTemplate delegate = new RestTemplate();
given(builder.build()).willReturn(delegate);
assertThat(new TestRestTemplate(builder).getRestTemplate()).isEqualTo(delegate);
}
@Test
void simple() {
// The Apache client is on the classpath so we get the fully-fledged factory
assertThat(new TestRestTemplate().getRestTemplate().getRequestFactory())
.isInstanceOf(HttpComponentsClientHttpRequestFactory.class);
}
@Test
void doNotReplaceCustomRequestFactory() {
RestTemplateBuilder builder = new RestTemplateBuilder().requestFactory(TestClientHttpRequestFactory.class);
TestRestTemplate testRestTemplate = new TestRestTemplate(builder);
assertThat(testRestTemplate.getRestTemplate().getRequestFactory())
.isInstanceOf(TestClientHttpRequestFactory.class);
}
@Test
void useTheSameRequestFactoryClassWithBasicAuth() {
TestClientHttpRequestFactory customFactory = new TestClientHttpRequestFactory();
RestTemplateBuilder builder = new RestTemplateBuilder().requestFactory(() -> customFactory);
TestRestTemplate testRestTemplate = new TestRestTemplate(builder).withBasicAuth("test", "test");
RestTemplate restTemplate = testRestTemplate.getRestTemplate();
assertThat(restTemplate.getRequestFactory()).isEqualTo(customFactory).hasSameClassAs(customFactory);
}
@Test
void getRootUriRootUriSetViaRestTemplateBuilder() {
String rootUri = "https://example.com";
RestTemplateBuilder delegate = new RestTemplateBuilder().rootUri(rootUri);
assertThat(new TestRestTemplate(delegate).getRootUri()).isEqualTo(rootUri);
}
@Test
void getRootUriRootUriNotSet() {
assertThat(new TestRestTemplate().getRootUri()).isEmpty();
}
@Test
void authenticated() {
TestRestTemplate restTemplate = new TestRestTemplate("user", "password");
assertBasicAuthorizationCredentials(restTemplate, "user", "password");
}
@Test
void options() {
RequestConfig config = getRequestConfig(new TestRestTemplate(HttpClientOption.ENABLE_COOKIES));
assertThat(config.getCookieSpec()).isEqualTo("strict");
}
@Test
void jdkBuilderCanBeSpecifiedWithSpecificRedirects() {
RestTemplateBuilder builder = new RestTemplateBuilder()
.requestFactoryBuilder(ClientHttpRequestFactoryBuilder.jdk());
TestRestTemplate templateWithRedirects = new TestRestTemplate(builder.redirects(HttpRedirects.FOLLOW));
assertThat(getJdkHttpClient(templateWithRedirects).followRedirects()).isEqualTo(Redirect.NORMAL);
TestRestTemplate templateWithoutRedirects = new TestRestTemplate(builder.redirects(HttpRedirects.DONT_FOLLOW));
assertThat(getJdkHttpClient(templateWithoutRedirects).followRedirects()).isEqualTo(Redirect.NEVER);
}
@Test
@SuppressWarnings("removal")
void httpComponentsAreBuiltConsideringSettingsInRestTemplateBuilder() {
RestTemplateBuilder builder = new RestTemplateBuilder()
.requestFactoryBuilder(ClientHttpRequestFactoryBuilder.httpComponents());
assertThat(getRedirectStrategy((RestTemplateBuilder) null)).matches(this::isFollowStrategy);
assertThat(getRedirectStrategy(builder)).matches(this::isFollowStrategy);
assertThat(getRedirectStrategy(builder.redirects(HttpRedirects.DONT_FOLLOW)))
.matches(this::isDontFollowStrategy);
}
@Test
void withRequestFactorySettingsRedirectsForHttpComponents() {
TestRestTemplate template = new TestRestTemplate();
assertThat(getRedirectStrategy(template)).matches(this::isFollowStrategy);
assertThat(getRedirectStrategy(template.withRequestFactorySettings(
ClientHttpRequestFactorySettings.defaults().withRedirects(HttpRedirects.FOLLOW))))
.matches(this::isFollowStrategy);
assertThat(getRedirectStrategy(template.withRequestFactorySettings(
ClientHttpRequestFactorySettings.defaults().withRedirects(HttpRedirects.DONT_FOLLOW))))
.matches(this::isDontFollowStrategy);
}
@Test
void withRedirects() {
TestRestTemplate template = new TestRestTemplate();
assertThat(getRedirectStrategy(template)).matches(this::isFollowStrategy);
assertThat(getRedirectStrategy(template.withRedirects(HttpRedirects.FOLLOW))).matches(this::isFollowStrategy);
assertThat(getRedirectStrategy(template.withRedirects(HttpRedirects.DONT_FOLLOW)))
.matches(this::isDontFollowStrategy);
}
@Test
void withRequestFactorySettingsRedirectsForJdk() {
TestRestTemplate template = new TestRestTemplate(
new RestTemplateBuilder().requestFactoryBuilder(ClientHttpRequestFactoryBuilder.jdk()));
assertThat(getJdkHttpClient(template).followRedirects()).isEqualTo(Redirect.NORMAL);
assertThat(getJdkHttpClient(template.withRequestFactorySettings(
ClientHttpRequestFactorySettings.defaults().withRedirects(HttpRedirects.DONT_FOLLOW)))
.followRedirects()).isEqualTo(Redirect.NEVER);
}
@Test
void withRequestFactorySettingsUpdateRedirectsForJdk() {
TestRestTemplate template = new TestRestTemplate(
new RestTemplateBuilder().requestFactoryBuilder(ClientHttpRequestFactoryBuilder.jdk()));
assertThat(getJdkHttpClient(template).followRedirects()).isEqualTo(Redirect.NORMAL);
assertThat(getJdkHttpClient(
template.withRequestFactorySettings((settings) -> settings.withRedirects(HttpRedirects.DONT_FOLLOW)))
.followRedirects()).isEqualTo(Redirect.NEVER);
}
private RequestConfig getRequestConfig(TestRestTemplate template) {
ClientHttpRequestFactory requestFactory = template.getRestTemplate().getRequestFactory();
return (RequestConfig) Extractors.byName("httpClient.defaultConfig").apply(requestFactory);
}
private RedirectStrategy getRedirectStrategy(RestTemplateBuilder builder, HttpClientOption... httpClientOptions) {
builder = (builder != null) ? builder : new RestTemplateBuilder();
TestRestTemplate template = new TestRestTemplate(builder, null, null, httpClientOptions);
return getRedirectStrategy(template);
}
private RedirectStrategy getRedirectStrategy(TestRestTemplate template) {
ClientHttpRequestFactory requestFactory = template.getRestTemplate().getRequestFactory();
Object chain = Extractors.byName("httpClient.execChain").apply(requestFactory);
while (chain != null) {
Object handler = Extractors.byName("handler").apply(chain);
if (handler instanceof RedirectExec) {
return (RedirectStrategy) Extractors.byName("redirectStrategy").apply(handler);
}
chain = Extractors.byName("next").apply(chain);
}
return null;
}
private boolean isFollowStrategy(RedirectStrategy redirectStrategy) {
return redirectStrategy instanceof DefaultRedirectStrategy;
}
private boolean isDontFollowStrategy(RedirectStrategy redirectStrategy) {
return redirectStrategy.getClass().getName().contains("NoFollow");
}
private HttpClient getJdkHttpClient(TestRestTemplate template) {
JdkClientHttpRequestFactory requestFactory = (JdkClientHttpRequestFactory) template.getRestTemplate()
.getRequestFactory();
return (HttpClient) ReflectionTestUtils.getField(requestFactory, "httpClient");
}
@Test
void restOperationsAreAvailable() {
RestTemplate delegate = mock(RestTemplate.class);
given(delegate.getRequestFactory()).willReturn(new SimpleClientHttpRequestFactory());
given(delegate.getUriTemplateHandler()).willReturn(new DefaultUriBuilderFactory());
RestTemplateBuilder builder = mock(RestTemplateBuilder.class);
given(builder.build()).willReturn(delegate);
TestRestTemplate restTemplate = new TestRestTemplate(builder);
ReflectionUtils.doWithMethods(RestOperations.class, new MethodCallback() {
@Override
public void doWith(Method method) {
Method equivalent = ReflectionUtils.findMethod(TestRestTemplate.class, method.getName(),
method.getParameterTypes());
assertThat(equivalent).as("Method %s not found", method).isNotNull();
assertThat(Modifier.isPublic(equivalent.getModifiers()))
.as("Method %s should have been public", equivalent)
.isTrue();
try {
equivalent.invoke(restTemplate, mockArguments(method.getParameterTypes()));
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
private Object[] mockArguments(Class<?>[] parameterTypes) throws Exception {
Object[] arguments = new Object[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
arguments[i] = mockArgument(parameterTypes[i]);
}
return arguments;
}
@SuppressWarnings("rawtypes")
private Object mockArgument(Class<?> type) throws Exception {
if (String.class.equals(type)) {
return "String";
}
if (Object[].class.equals(type)) {
return new Object[0];
}
if (URI.class.equals(type)) {
return new URI("http://localhost");
}
if (HttpMethod.class.equals(type)) {
return HttpMethod.GET;
}
if (Class.class.equals(type)) {
return Object.class;
}
if (RequestEntity.class.equals(type)) {
return new RequestEntity(HttpMethod.GET, new URI("http://localhost"));
}
return mock(type);
}
}, (method) -> Modifier.isPublic(method.getModifiers()));
}
@Test
void withBasicAuthAddsBasicAuthWhenNotAlreadyPresent() {
TestRestTemplate original = new TestRestTemplate();
TestRestTemplate basicAuth = original.withBasicAuth("user", "password");
assertThat(getConverterClasses(original)).containsExactlyElementsOf(getConverterClasses(basicAuth).toList());
assertThat(basicAuth.getRestTemplate().getInterceptors()).isEmpty();
assertBasicAuthorizationCredentials(original, null, null);
assertBasicAuthorizationCredentials(basicAuth, "user", "password");
}
@Test
void withBasicAuthReplacesBasicAuthWhenAlreadyPresent() {
TestRestTemplate original = new TestRestTemplate("foo", "bar").withBasicAuth("replace", "replace");
TestRestTemplate basicAuth = original.withBasicAuth("user", "password");
assertThat(getConverterClasses(basicAuth)).containsExactlyElementsOf(getConverterClasses(original).toList());
assertBasicAuthorizationCredentials(original, "replace", "replace");
assertBasicAuthorizationCredentials(basicAuth, "user", "password");
}
private Stream<Class<?>> getConverterClasses(TestRestTemplate testRestTemplate) {
return testRestTemplate.getRestTemplate().getMessageConverters().stream().map(Object::getClass);
}
@Test
void withBasicAuthShouldUseNoOpErrorHandler() {
TestRestTemplate originalTemplate = new TestRestTemplate("foo", "bar");
ResponseErrorHandler errorHandler = mock(ResponseErrorHandler.class);
originalTemplate.getRestTemplate().setErrorHandler(errorHandler);
TestRestTemplate basicAuthTemplate = originalTemplate.withBasicAuth("user", "password");
assertThat(basicAuthTemplate.getRestTemplate().getErrorHandler()).isInstanceOf(NoOpResponseErrorHandler.class);
}
@Test
void exchangeWithRelativeTemplatedUrlRequestEntity() throws Exception {
RequestEntity<Void> entity = RequestEntity.get("/a/b/c.{ext}", "txt").build();
TestRestTemplate template = new TestRestTemplate();
ClientHttpRequestFactory requestFactory = mock(ClientHttpRequestFactory.class);
MockClientHttpRequest request = new MockClientHttpRequest();
request.setResponse(new MockClientHttpResponse(new byte[0], HttpStatus.OK));
URI absoluteUri = URI.create("http://localhost:8080/a/b/c.txt");
given(requestFactory.createRequest(eq(absoluteUri), eq(HttpMethod.GET))).willReturn(request);
template.getRestTemplate().setRequestFactory(requestFactory);
template.setUriTemplateHandler(BaseUrlUriBuilderFactory.get(BaseUrl.of("http://localhost:8080")));
template.exchange(entity, String.class);
then(requestFactory).should().createRequest(eq(absoluteUri), eq(HttpMethod.GET));
}
@Test
void exchangeWithAbsoluteTemplatedUrlRequestEntity() throws Exception {
RequestEntity<Void> entity = RequestEntity.get("https://api.example.com/a/b/c.{ext}", "txt").build();
TestRestTemplate template = new TestRestTemplate();
ClientHttpRequestFactory requestFactory = mock(ClientHttpRequestFactory.class);
MockClientHttpRequest request = new MockClientHttpRequest();
request.setResponse(new MockClientHttpResponse(new byte[0], HttpStatus.OK));
URI absoluteUri = URI.create("https://api.example.com/a/b/c.txt");
given(requestFactory.createRequest(eq(absoluteUri), eq(HttpMethod.GET))).willReturn(request);
template.getRestTemplate().setRequestFactory(requestFactory);
template.exchange(entity, String.class);
then(requestFactory).should().createRequest(eq(absoluteUri), eq(HttpMethod.GET));
}
@Test
void deleteHandlesRelativeUris() throws IOException {
verifyRelativeUriHandling(TestRestTemplate::delete);
}
@Test
void exchangeWithRequestEntityAndClassHandlesRelativeUris() throws IOException {
verifyRelativeUriHandling((testRestTemplate, relativeUri) -> testRestTemplate
.exchange(new RequestEntity<>(HttpMethod.GET, relativeUri), String.class));
}
@Test
void exchangeWithRequestEntityAndParameterizedTypeReferenceHandlesRelativeUris() throws IOException {
verifyRelativeUriHandling((testRestTemplate, relativeUri) -> testRestTemplate
.exchange(new RequestEntity<>(HttpMethod.GET, relativeUri), new ParameterizedTypeReference<String>() {
}));
}
@Test
void exchangeHandlesRelativeUris() throws IOException {
verifyRelativeUriHandling((testRestTemplate, relativeUri) -> testRestTemplate.exchange(relativeUri,
HttpMethod.GET, new HttpEntity<>(new byte[0]), String.class));
}
@Test
void exchangeWithParameterizedTypeReferenceHandlesRelativeUris() throws IOException {
verifyRelativeUriHandling((testRestTemplate, relativeUri) -> testRestTemplate.exchange(relativeUri,
HttpMethod.GET, new HttpEntity<>(new byte[0]), new ParameterizedTypeReference<String>() {
}));
}
@Test
void executeHandlesRelativeUris() throws IOException {
verifyRelativeUriHandling(
(testRestTemplate, relativeUri) -> testRestTemplate.execute(relativeUri, HttpMethod.GET, null, null));
}
@Test
void getForEntityHandlesRelativeUris() throws IOException {
verifyRelativeUriHandling(
(testRestTemplate, relativeUri) -> testRestTemplate.getForEntity(relativeUri, String.class));
}
@Test
void getForObjectHandlesRelativeUris() throws IOException {
verifyRelativeUriHandling(
(testRestTemplate, relativeUri) -> testRestTemplate.getForObject(relativeUri, String.class));
}
@Test
void headForHeadersHandlesRelativeUris() throws IOException {
verifyRelativeUriHandling(TestRestTemplate::headForHeaders);
}
@Test
void optionsForAllowHandlesRelativeUris() throws IOException {
verifyRelativeUriHandling(TestRestTemplate::optionsForAllow);
}
@Test
void patchForObjectHandlesRelativeUris() throws IOException {
verifyRelativeUriHandling(
(testRestTemplate, relativeUri) -> testRestTemplate.patchForObject(relativeUri, "hello", String.class));
}
@Test
void postForEntityHandlesRelativeUris() throws IOException {
verifyRelativeUriHandling(
(testRestTemplate, relativeUri) -> testRestTemplate.postForEntity(relativeUri, "hello", String.class));
}
@Test
void postForLocationHandlesRelativeUris() throws IOException {
verifyRelativeUriHandling(
(testRestTemplate, relativeUri) -> testRestTemplate.postForLocation(relativeUri, "hello"));
}
@Test
void postForObjectHandlesRelativeUris() throws IOException {
verifyRelativeUriHandling(
(testRestTemplate, relativeUri) -> testRestTemplate.postForObject(relativeUri, "hello", String.class));
}
@Test
void putHandlesRelativeUris() throws IOException {
verifyRelativeUriHandling((testRestTemplate, relativeUri) -> testRestTemplate.put(relativeUri, "hello"));
}
private void verifyRelativeUriHandling(TestRestTemplateCallback callback) throws IOException {
ClientHttpRequestFactory requestFactory = mock(ClientHttpRequestFactory.class);
MockClientHttpRequest request = new MockClientHttpRequest();
request.setResponse(new MockClientHttpResponse(new byte[0], HttpStatus.OK));
URI absoluteUri = URI.create("http://localhost:8080/a/b/c.txt?param=%7Bsomething%7D");
given(requestFactory.createRequest(eq(absoluteUri), any(HttpMethod.class))).willReturn(request);
TestRestTemplate template = new TestRestTemplate();
template.getRestTemplate().setRequestFactory(requestFactory);
template.setUriTemplateHandler(BaseUrlUriBuilderFactory.get(BaseUrl.of("http://localhost:8080")));
callback.doWithTestRestTemplate(template, URI.create("/a/b/c.txt?param=%7Bsomething%7D"));
then(requestFactory).should().createRequest(eq(absoluteUri), any(HttpMethod.class));
}
private void assertBasicAuthorizationCredentials(TestRestTemplate testRestTemplate, String username,
String password) {
ClientHttpRequest request = ReflectionTestUtils.invokeMethod(testRestTemplate.getRestTemplate(),
"createRequest", URI.create("http://localhost"), HttpMethod.POST);
if (username == null) {
assertThat(request.getHeaders().headerNames()).doesNotContain(HttpHeaders.AUTHORIZATION);
}
else {
assertThat(request.getHeaders().headerNames()).contains(HttpHeaders.AUTHORIZATION);
assertThat(request.getHeaders().get(HttpHeaders.AUTHORIZATION)).containsExactly("Basic "
+ Base64.getEncoder().encodeToString(String.format("%s:%s", username, password).getBytes()));
}
}
interface TestRestTemplateCallback {
void doWithTestRestTemplate(TestRestTemplate testRestTemplate, URI relativeUri);
}
static class TestClientHttpRequestFactory extends SimpleClientHttpRequestFactory {
}
}
@@ -1,58 +0,0 @@
/*
* Copyright 2012-present 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.restclient.test.scan;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
/**
* A simple factory bean with no generics. Used to test early initialization doesn't
* occur.
*
* @author Madhura Bhave
*/
@Component
@SuppressWarnings("rawtypes")
public class SimpleFactoryBean implements FactoryBean {
private static boolean isInitializedEarly = false;
public SimpleFactoryBean() {
isInitializedEarly = true;
throw new RuntimeException();
}
@Autowired
public SimpleFactoryBean(ApplicationContext context) {
if (isInitializedEarly) {
throw new RuntimeException();
}
}
@Override
public Object getObject() {
return new Object();
}
@Override
public Class<?> getObjectType() {
return Object.class;
}
}