Use RestClient in tests and infrastructure code

Closes gh-51120
This commit is contained in:
Stéphane Nicoll
2026-07-24 15:10:43 +02:00
parent 6e2bba6915
commit b9ebff0084
24 changed files with 292 additions and 230 deletions
@@ -19,7 +19,6 @@ package org.springframework.boot.context.embedded;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
@@ -49,9 +48,10 @@ import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.util.FileSystemUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.client.NoOpResponseErrorHandler;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriTemplateHandler;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestClient.ResponseSpec.ErrorHandler;
import org.springframework.web.util.DefaultUriBuilderFactory;
import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;
/**
* {@link TestTemplateInvocationContextProvider} for templated
@@ -152,7 +152,7 @@ class EmbeddedServerContainerInvocationContextProvider
@Override
public List<Extension> getAdditionalExtensions() {
return Arrays.asList(this.launcher, new RestTemplateParameterResolver(this.launcher));
return Arrays.asList(this.launcher, new RestClientParameterResolver(this.launcher));
}
@Override
@@ -165,7 +165,7 @@ class EmbeddedServerContainerInvocationContextProvider
if (parameterContext.getParameter().getType().equals(AbstractApplicationLauncher.class)) {
return true;
}
return parameterContext.getParameter().getType().equals(RestTemplate.class);
return parameterContext.getParameter().getType().equals(RestClient.class);
}
@Override
@@ -178,41 +178,35 @@ class EmbeddedServerContainerInvocationContextProvider
}
private static final class RestTemplateParameterResolver implements ParameterResolver {
private static final class RestClientParameterResolver implements ParameterResolver {
private static final ErrorHandler NOOP_ERROR_HANDLER = (request, response) -> {
};
private final AbstractApplicationLauncher launcher;
private RestTemplateParameterResolver(AbstractApplicationLauncher launcher) {
private RestClientParameterResolver(AbstractApplicationLauncher launcher) {
this.launcher = launcher;
}
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return parameterContext.getParameter().getType().equals(RestTemplate.class);
return parameterContext.getParameter().getType().equals(RestClient.class);
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
RestTemplate rest = new RestTemplate(new HttpComponentsClientHttpRequestFactory(HttpClients.custom()
.setRetryStrategy(new DefaultHttpRequestRetryStrategy(10, TimeValue.of(1, TimeUnit.SECONDS)))
.build()));
rest.setErrorHandler(new NoOpResponseErrorHandler());
rest.setUriTemplateHandler(new UriTemplateHandler() {
@Override
public URI expand(String uriTemplate, Object... uriVariables) {
return URI.create("http://localhost:" + RestTemplateParameterResolver.this.launcher.getHttpPort()
+ uriTemplate);
}
@Override
public URI expand(String uriTemplate, Map<String, ?> uriVariables) {
return URI.create("http://localhost:" + RestTemplateParameterResolver.this.launcher.getHttpPort()
+ uriTemplate);
}
});
return rest;
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(
"http://localhost:" + this.launcher.getHttpPort());
// Do not double encode test paths
uriBuilderFactory.setEncodingMode(EncodingMode.NONE);
return RestClient.builder()
.uriBuilderFactory(uriBuilderFactory)
.requestFactory(new HttpComponentsClientHttpRequestFactory(HttpClients.custom()
.setRetryStrategy(new DefaultHttpRequestRetryStrategy(10, TimeValue.of(1, TimeUnit.SECONDS)))
.build()))
.defaultStatusHandler((status) -> true, NOOP_ERROR_HANDLER)
.build();
}
}
@@ -22,7 +22,7 @@ import org.junit.jupiter.api.condition.OS;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.client.RestClient;
import static org.assertj.core.api.Assertions.assertThat;
@@ -37,25 +37,31 @@ import static org.assertj.core.api.Assertions.assertThat;
class EmbeddedServletContainerJarDevelopmentIntegrationTests {
@TestTemplate
void metaInfResourceFromDependencyIsAvailableViaHttp(RestTemplate rest) {
ResponseEntity<String> entity = rest.getForEntity("/nested-meta-inf-resource.txt", String.class);
void metaInfResourceFromDependencyIsAvailableViaHttp(RestClient rest) {
ResponseEntity<String> entity = rest.get()
.uri("/nested-meta-inf-resource.txt")
.retrieve()
.toEntity(String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@TestTemplate
@DisabledOnOs(OS.WINDOWS)
void metaInfResourceFromDependencyWithNameThatContainsReservedCharactersIsAvailableViaHttp(RestTemplate rest) {
ResponseEntity<String> entity = rest.getForEntity(
"/nested-reserved-%21%23%24%25%26%28%29%2A%2B%2C%3A%3D%3F%40%5B%5D-meta-inf-resource.txt",
String.class);
void metaInfResourceFromDependencyWithNameThatContainsReservedCharactersIsAvailableViaHttp(RestClient rest) {
ResponseEntity<String> entity = rest.get()
.uri("/nested-reserved-%21%23%24%25%26%28%29%2A%2B%2C%3A%3D%3F%40%5B%5D-meta-inf-resource.txt")
.retrieve()
.toEntity(String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(entity.getBody()).isEqualTo("encoded-name");
}
@TestTemplate
void metaInfResourceFromDependencyIsAvailableViaServletContext(RestTemplate rest) {
ResponseEntity<String> entity = rest.getForEntity("/servletContext?/nested-meta-inf-resource.txt",
String.class);
void metaInfResourceFromDependencyIsAvailableViaServletContext(RestClient rest) {
ResponseEntity<String> entity = rest.get()
.uri("/servletContext?/nested-meta-inf-resource.txt")
.retrieve()
.toEntity(String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@@ -22,7 +22,7 @@ import org.junit.jupiter.api.condition.OS;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.client.RestClient;
import static org.assertj.core.api.Assertions.assertThat;
@@ -37,51 +37,64 @@ import static org.assertj.core.api.Assertions.assertThat;
class EmbeddedServletContainerJarPackagingIntegrationTests {
@TestTemplate
void nestedMetaInfResourceIsAvailableViaHttp(RestTemplate rest) {
ResponseEntity<String> entity = rest.getForEntity("/nested-meta-inf-resource.txt", String.class);
void nestedMetaInfResourceIsAvailableViaHttp(RestClient rest) {
ResponseEntity<String> entity = rest.get()
.uri("/nested-meta-inf-resource.txt")
.retrieve()
.toEntity(String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@TestTemplate
@DisabledOnOs(OS.WINDOWS)
void nestedMetaInfResourceWithNameThatContainsReservedCharactersIsAvailableViaHttp(RestTemplate rest) {
ResponseEntity<String> entity = rest.getForEntity(
"/nested-reserved-%21%23%24%25%26%28%29%2A%2B%2C%3A%3D%3F%40%5B%5D-meta-inf-resource.txt",
String.class);
void nestedMetaInfResourceWithNameThatContainsReservedCharactersIsAvailableViaHttp(RestClient rest) {
ResponseEntity<String> entity = rest.get()
.uri("/nested-reserved-%21%23%24%25%26%28%29%2A%2B%2C%3A%3D%3F%40%5B%5D-meta-inf-resource.txt")
.retrieve()
.toEntity(String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(entity.getBody()).isEqualTo("encoded-name");
}
@TestTemplate
void nestedMetaInfResourceIsAvailableViaServletContext(RestTemplate rest) {
ResponseEntity<String> entity = rest.getForEntity("/servletContext?/nested-meta-inf-resource.txt",
String.class);
void nestedMetaInfResourceIsAvailableViaServletContext(RestClient rest) {
ResponseEntity<String> entity = rest.get()
.uri("/servletContext?/nested-meta-inf-resource.txt")
.retrieve()
.toEntity(String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@TestTemplate
void nestedJarIsNotAvailableViaHttp(RestTemplate rest) {
ResponseEntity<String> entity = rest.getForEntity("/BOOT-INF/lib/resources-1.0.jar", String.class);
void nestedJarIsNotAvailableViaHttp(RestClient rest) {
ResponseEntity<String> entity = rest.get()
.uri("/BOOT-INF/lib/resources-1.0.jar")
.retrieve()
.toEntity(String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@TestTemplate
void applicationClassesAreNotAvailableViaHttp(RestTemplate rest) {
ResponseEntity<String> entity = rest
.getForEntity("/BOOT-INF/classes/com/example/ResourceHandlingApplication.class", String.class);
void applicationClassesAreNotAvailableViaHttp(RestClient rest) {
ResponseEntity<String> entity = rest.get()
.uri("/BOOT-INF/classes/com/example/ResourceHandlingApplication.class")
.retrieve()
.toEntity(String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@TestTemplate
void launcherIsNotAvailableViaHttp(RestTemplate rest) {
ResponseEntity<String> entity = rest.getForEntity("/org/springframework/boot/loader/Launcher.class",
String.class);
void launcherIsNotAvailableViaHttp(RestClient rest) {
ResponseEntity<String> entity = rest.get()
.uri("/org/springframework/boot/loader/Launcher.class")
.retrieve()
.toEntity(String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@TestTemplate
void conditionalOnWarDeploymentBeanIsNotAvailableForEmbeddedServer(RestTemplate rest) {
ResponseEntity<String> entity = rest.getForEntity("/war", String.class);
void conditionalOnWarDeploymentBeanIsNotAvailableForEmbeddedServer(RestClient rest) {
ResponseEntity<String> entity = rest.get().uri("/war").retrieve().toEntity(String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@@ -28,7 +28,7 @@ import org.junit.jupiter.api.condition.OS;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.client.RestClient;
import static org.assertj.core.api.Assertions.assertThat;
@@ -43,37 +43,43 @@ import static org.assertj.core.api.Assertions.assertThat;
class EmbeddedServletContainerWarDevelopmentIntegrationTests {
@TestTemplate
void metaInfResourceFromDependencyIsAvailableViaHttp(RestTemplate rest) {
ResponseEntity<String> entity = rest.getForEntity("/nested-meta-inf-resource.txt", String.class);
void metaInfResourceFromDependencyIsAvailableViaHttp(RestClient rest) {
ResponseEntity<String> entity = rest.get()
.uri("/nested-meta-inf-resource.txt")
.retrieve()
.toEntity(String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@TestTemplate
@DisabledOnOs(OS.WINDOWS)
void metaInfResourceFromDependencyWithNameThatContainsReservedCharactersIsAvailableViaHttp(RestTemplate rest) {
ResponseEntity<String> entity = rest.getForEntity(
"/nested-reserved-%21%23%24%25%26%28%29%2A%2B%2C%3A%3D%3F%40%5B%5D-meta-inf-resource.txt",
String.class);
void metaInfResourceFromDependencyWithNameThatContainsReservedCharactersIsAvailableViaHttp(RestClient rest) {
ResponseEntity<String> entity = rest.get()
.uri("/nested-reserved-%21%23%24%25%26%28%29%2A%2B%2C%3A%3D%3F%40%5B%5D-meta-inf-resource.txt")
.retrieve()
.toEntity(String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(entity.getBody()).isEqualTo("encoded-name");
}
@TestTemplate
void metaInfResourceFromDependencyIsAvailableViaServletContext(RestTemplate rest) {
ResponseEntity<String> entity = rest.getForEntity("/servletContext?/nested-meta-inf-resource.txt",
String.class);
void metaInfResourceFromDependencyIsAvailableViaServletContext(RestClient rest) {
ResponseEntity<String> entity = rest.get()
.uri("/servletContext?/nested-meta-inf-resource.txt")
.retrieve()
.toEntity(String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@TestTemplate
void webappResourcesAreAvailableViaHttp(RestTemplate rest) {
ResponseEntity<String> entity = rest.getForEntity("/webapp-resource.txt", String.class);
void webappResourcesAreAvailableViaHttp(RestClient rest) {
ResponseEntity<String> entity = rest.get().uri("/webapp-resource.txt").retrieve().toEntity(String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@TestTemplate
void loaderClassesAreNotAvailableViaResourcePaths(RestTemplate rest) {
ResponseEntity<String> entity = rest.getForEntity("/resourcePaths", String.class);
void loaderClassesAreNotAvailableViaResourcePaths(RestClient rest) {
ResponseEntity<String> entity = rest.get().uri("/resourcePaths").retrieve().toEntity(String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(readLines(entity.getBody()))
.noneMatch((resourcePath) -> resourcePath.startsWith("/org/springframework/boot/loader"));
@@ -28,7 +28,7 @@ import org.junit.jupiter.api.condition.OS;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.client.RestClient;
import static org.assertj.core.api.Assertions.assertThat;
@@ -43,68 +43,85 @@ import static org.assertj.core.api.Assertions.assertThat;
class EmbeddedServletContainerWarPackagingIntegrationTests {
@TestTemplate
void nestedMetaInfResourceIsAvailableViaHttp(RestTemplate rest) {
ResponseEntity<String> entity = rest.getForEntity("/nested-meta-inf-resource.txt", String.class);
void nestedMetaInfResourceIsAvailableViaHttp(RestClient rest) {
ResponseEntity<String> entity = rest.get()
.uri("/nested-meta-inf-resource.txt")
.retrieve()
.toEntity(String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@TestTemplate
@DisabledOnOs(OS.WINDOWS)
void nestedMetaInfResourceWithNameThatContainsReservedCharactersIsAvailableViaHttp(RestTemplate rest) {
ResponseEntity<String> entity = rest.getForEntity(
"/nested-reserved-%21%23%24%25%26%28%29%2A%2B%2C%3A%3D%3F%40%5B%5D-meta-inf-resource.txt",
String.class);
void nestedMetaInfResourceWithNameThatContainsReservedCharactersIsAvailableViaHttp(RestClient rest) {
ResponseEntity<String> entity = rest.get()
.uri("/nested-reserved-%21%23%24%25%26%28%29%2A%2B%2C%3A%3D%3F%40%5B%5D-meta-inf-resource.txt")
.retrieve()
.toEntity(String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(entity.getBody()).isEqualTo("encoded-name");
}
@TestTemplate
void nestedMetaInfResourceIsAvailableViaServletContext(RestTemplate rest) {
ResponseEntity<String> entity = rest.getForEntity("/servletContext?/nested-meta-inf-resource.txt",
String.class);
void nestedMetaInfResourceIsAvailableViaServletContext(RestClient rest) {
ResponseEntity<String> entity = rest.get()
.uri("/servletContext?/nested-meta-inf-resource.txt")
.retrieve()
.toEntity(String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@TestTemplate
void nestedJarIsNotAvailableViaHttp(RestTemplate rest) {
ResponseEntity<String> entity = rest.getForEntity("/WEB-INF/lib/resources-1.0.jar", String.class);
void nestedJarIsNotAvailableViaHttp(RestClient rest) {
ResponseEntity<String> entity = rest.get()
.uri("/WEB-INF/lib/resources-1.0.jar")
.retrieve()
.toEntity(String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@TestTemplate
void applicationClassesAreNotAvailableViaHttp(RestTemplate rest) {
ResponseEntity<String> entity = rest
.getForEntity("/WEB-INF/classes/com/example/ResourceHandlingApplication.class", String.class);
void applicationClassesAreNotAvailableViaHttp(RestClient rest) {
ResponseEntity<String> entity = rest.get()
.uri("/WEB-INF/classes/com/example/ResourceHandlingApplication.class")
.retrieve()
.toEntity(String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@TestTemplate
void webappResourcesAreAvailableViaHttp(RestTemplate rest) {
ResponseEntity<String> entity = rest.getForEntity("/webapp-resource.txt", String.class);
void webappResourcesAreAvailableViaHttp(RestClient rest) {
ResponseEntity<String> entity = rest.get().uri("/webapp-resource.txt").retrieve().toEntity(String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@TestTemplate
void loaderClassesAreNotAvailableViaHttp(RestTemplate rest) {
ResponseEntity<String> entity = rest.getForEntity("/org/springframework/boot/loader/Launcher.class",
String.class);
void loaderClassesAreNotAvailableViaHttp(RestClient rest) {
ResponseEntity<String> entity = rest.get()
.uri("/org/springframework/boot/loader/Launcher.class")
.retrieve()
.toEntity(String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
entity = rest.getForEntity("/org/springframework/../springframework/boot/loader/Launcher.class", String.class);
entity = rest.get()
.uri("/org/springframework/../springframework/boot/loader/Launcher.class")
.retrieve()
.toEntity(String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@TestTemplate
void loaderClassesAreNotAvailableViaResourcePaths(RestTemplate rest) {
ResponseEntity<String> entity = rest.getForEntity("/resourcePaths", String.class);
void loaderClassesAreNotAvailableViaResourcePaths(RestClient rest) {
ResponseEntity<String> entity = rest.get().uri("/resourcePaths").retrieve().toEntity(String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(readLines(entity.getBody()))
.noneMatch((resourcePath) -> resourcePath.startsWith("/org/springframework/boot/loader"));
}
@TestTemplate
void conditionalOnWarDeploymentBeanIsNotAvailableForEmbeddedServer(RestTemplate rest) {
assertThat(rest.getForEntity("/always", String.class).getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(rest.getForEntity("/conditionalOnWar", String.class).getStatusCode())
void conditionalOnWarDeploymentBeanIsNotAvailableForEmbeddedServer(RestClient rest) {
assertThat(rest.get().uri("/always").retrieve().toEntity(String.class).getStatusCode())
.isEqualTo(HttpStatus.OK);
assertThat(rest.get().uri("/conditionalOnWar").retrieve().toEntity(String.class).getStatusCode())
.isEqualTo(HttpStatus.NOT_FOUND);
}