Browse Source

WebTestClient support for API versioning

Closes gh-34568
pull/34896/head
rstoyanchev 12 months ago
parent
commit
fb94109c09
  1. 28
      spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java
  2. 20
      spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java
  3. 22
      spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java
  4. 93
      spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ApiVersionTests.java

28
spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java

@ -56,6 +56,7 @@ import org.springframework.util.CollectionUtils; @@ -56,6 +56,7 @@ import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MimeType;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.ApiVersionInserter;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ClientRequest;
@ -88,6 +89,8 @@ class DefaultWebTestClient implements WebTestClient { @@ -88,6 +89,8 @@ class DefaultWebTestClient implements WebTestClient {
private final @Nullable MultiValueMap<String, String> defaultCookies;
private final @Nullable ApiVersionInserter apiVersionInserter;
private final Consumer<EntityExchangeResult<?>> entityResultConsumer;
private final Duration responseTimeout;
@ -97,10 +100,11 @@ class DefaultWebTestClient implements WebTestClient { @@ -97,10 +100,11 @@ class DefaultWebTestClient implements WebTestClient {
private final AtomicLong requestIndex = new AtomicLong();
DefaultWebTestClient(ClientHttpConnector connector, ExchangeStrategies exchangeStrategies,
DefaultWebTestClient(
ClientHttpConnector connector, ExchangeStrategies exchangeStrategies,
Function<ClientHttpConnector, ExchangeFunction> exchangeFactory, UriBuilderFactory uriBuilderFactory,
@Nullable HttpHeaders headers, @Nullable MultiValueMap<String, String> cookies,
Consumer<EntityExchangeResult<?>> entityResultConsumer,
@Nullable ApiVersionInserter apiVersionInserter, Consumer<EntityExchangeResult<?>> entityResultConsumer,
@Nullable Duration responseTimeout, DefaultWebTestClientBuilder clientBuilder) {
this.wiretapConnector = new WiretapConnector(connector);
@ -110,6 +114,7 @@ class DefaultWebTestClient implements WebTestClient { @@ -110,6 +114,7 @@ class DefaultWebTestClient implements WebTestClient {
this.uriBuilderFactory = uriBuilderFactory;
this.defaultHeaders = headers;
this.defaultCookies = cookies;
this.apiVersionInserter = apiVersionInserter;
this.entityResultConsumer = entityResultConsumer;
this.responseTimeout = (responseTimeout != null ? responseTimeout : Duration.ofSeconds(5));
this.builder = clientBuilder;
@ -186,6 +191,8 @@ class DefaultWebTestClient implements WebTestClient { @@ -186,6 +191,8 @@ class DefaultWebTestClient implements WebTestClient {
private @Nullable MultiValueMap<String, String> cookies;
private @Nullable Object apiVersion;
private @Nullable BodyInserter<?, ? super ClientHttpRequest> inserter;
private final Map<String, Object> attributes = new LinkedHashMap<>(4);
@ -310,6 +317,12 @@ class DefaultWebTestClient implements WebTestClient { @@ -310,6 +317,12 @@ class DefaultWebTestClient implements WebTestClient {
return this;
}
@Override
public RequestBodySpec apiVersion(Object version) {
this.apiVersion = version;
return this;
}
@Override
public RequestHeadersSpec<?> bodyValue(Object body) {
this.inserter = BodyInserters.fromValue(body);
@ -373,6 +386,10 @@ class DefaultWebTestClient implements WebTestClient { @@ -373,6 +386,10 @@ class DefaultWebTestClient implements WebTestClient {
if (!this.headers.isEmpty()) {
headersToUse.putAll(this.headers);
}
if (this.apiVersion != null) {
Assert.state(apiVersionInserter != null, "No ApiVersionInserter configured");
apiVersionInserter.insertVersion(this.apiVersion, headersToUse);
}
})
.cookies(cookiesToUse -> {
if (!CollectionUtils.isEmpty(DefaultWebTestClient.this.defaultCookies)) {
@ -386,7 +403,12 @@ class DefaultWebTestClient implements WebTestClient { @@ -386,7 +403,12 @@ class DefaultWebTestClient implements WebTestClient {
}
private URI initUri() {
return (this.uri != null ? this.uri : DefaultWebTestClient.this.uriBuilderFactory.expand(""));
URI uriToUse = this.uri != null ? this.uri : DefaultWebTestClient.this.uriBuilderFactory.expand("");
if (this.apiVersion != null) {
Assert.state(apiVersionInserter != null, "No ApiVersionInserter configured");
uriToUse = apiVersionInserter.insertVersion(this.apiVersion, uriToUse);
}
return uriToUse;
}
}

20
spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java

@ -37,6 +37,7 @@ import org.springframework.util.ClassUtils; @@ -37,6 +37,7 @@ import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.ApiVersionInserter;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.ExchangeFunction;
import org.springframework.web.reactive.function.client.ExchangeFunctions;
@ -85,6 +86,8 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder { @@ -85,6 +86,8 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder {
private @Nullable MultiValueMap<String, String> defaultCookies;
private @Nullable ApiVersionInserter apiVersionInserter;
private @Nullable List<ExchangeFilterFunction> filters;
private Consumer<EntityExchangeResult<?>> entityResultConsumer = result -> {};
@ -142,6 +145,7 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder { @@ -142,6 +145,7 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder {
}
this.defaultCookies = (other.defaultCookies != null ?
new LinkedMultiValueMap<>(other.defaultCookies) : null);
this.apiVersionInserter = other.apiVersionInserter;
this.filters = (other.filters != null ? new ArrayList<>(other.filters) : null);
this.entityResultConsumer = other.entityResultConsumer;
this.strategies = other.strategies;
@ -200,6 +204,12 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder { @@ -200,6 +204,12 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder {
return this.defaultCookies;
}
@Override
public WebTestClient.Builder apiVersionInserter(ApiVersionInserter apiVersionInserter) {
this.apiVersionInserter = apiVersionInserter;
return this;
}
@Override
public WebTestClient.Builder filter(ExchangeFilterFunction filter) {
Assert.notNull(filter, "ExchangeFilterFunction is required");
@ -283,10 +293,12 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder { @@ -283,10 +293,12 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder {
.orElse(exchange);
};
return new DefaultWebTestClient(connectorToUse, exchangeStrategies, exchangeFactory, initUriBuilderFactory(),
this.defaultHeaders != null ? HttpHeaders.readOnlyHttpHeaders(this.defaultHeaders) : null,
this.defaultCookies != null ? CollectionUtils.unmodifiableMultiValueMap(this.defaultCookies) : null,
this.entityResultConsumer, this.responseTimeout, new DefaultWebTestClientBuilder(this));
return new DefaultWebTestClient(
connectorToUse, exchangeStrategies, exchangeFactory, initUriBuilderFactory(),
(this.defaultHeaders != null ? HttpHeaders.readOnlyHttpHeaders(this.defaultHeaders) : null),
(this.defaultCookies != null ? CollectionUtils.unmodifiableMultiValueMap(this.defaultCookies) : null),
this.apiVersionInserter, this.entityResultConsumer,
this.responseTimeout, new DefaultWebTestClientBuilder(this));
}
private static ClientHttpConnector initConnector() {

22
spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java

@ -45,6 +45,8 @@ import org.springframework.test.json.JsonCompareMode; @@ -45,6 +45,8 @@ import org.springframework.test.json.JsonCompareMode;
import org.springframework.test.json.JsonComparison;
import org.springframework.util.MultiValueMap;
import org.springframework.validation.Validator;
import org.springframework.web.client.ApiVersionFormatter;
import org.springframework.web.client.ApiVersionInserter;
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
import org.springframework.web.reactive.config.BlockingExecutionConfigurer;
import org.springframework.web.reactive.config.CorsRegistry;
@ -428,6 +430,15 @@ public interface WebTestClient { @@ -428,6 +430,15 @@ public interface WebTestClient {
*/
Builder defaultCookies(Consumer<MultiValueMap<String, String>> cookiesConsumer);
/**
* Configure an {@link ApiVersionInserter} to abstract how an API version
* specified via {@link RequestHeadersSpec#apiVersion(Object)}
* is inserted into the request.
* @param apiVersionInserter the inserter to use
* @since 7.0
*/
Builder apiVersionInserter(ApiVersionInserter apiVersionInserter);
/**
* Add the given filter to the filter chain.
* @param filter the filter to be added to the chain
@ -643,6 +654,17 @@ public interface WebTestClient { @@ -643,6 +654,17 @@ public interface WebTestClient {
*/
S headers(Consumer<HttpHeaders> headersConsumer);
/**
* Set an API version for the request. The version is inserted into the
* request by the {@link Builder#apiVersionInserter(ApiVersionInserter)
* configured} {@code ApiVersionInserter}.
* @param version the API version of the request; this can be a String or
* some Object that can be formatted the inserter, e.g. through an
* {@link ApiVersionFormatter}.
* @since 7.0
*/
S apiVersion(Object version);
/**
* Set the attribute with the given name to the given value.
* @param name the name of the attribute to add

93
spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ApiVersionTests.java

@ -0,0 +1,93 @@ @@ -0,0 +1,93 @@
/*
* Copyright 2002-2025 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.test.web.reactive.server.samples;
import java.net.URI;
import java.util.Map;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.DefaultApiVersionInserter;
import static org.assertj.core.api.Assertions.assertThat;
/**
* {@link WebTestClient} tests for sending API versions.
*
* @author Rossen Stoyanchev
*/
public class ApiVersionTests {
private static final String HEADER_NAME = "X-API-Version";
@Test
void header() {
Map<String, String> result = performRequest(builder -> builder.fromHeader("X-API-Version"));
assertThat(result.get(HEADER_NAME)).isEqualTo("1.2");
}
@Test
void queryParam() {
Map<String, String> result = performRequest(builder -> builder.fromQueryParam("api-version"));
assertThat(result.get("query")).isEqualTo("api-version=1.2");
}
@Test
void pathSegment() {
Map<String, String> result = performRequest(builder -> builder.fromPathSegment(0));
assertThat(result.get("path")).isEqualTo("/1.2/path");
}
@SuppressWarnings("unchecked")
private Map<String, String> performRequest(Consumer<DefaultApiVersionInserter.Builder> consumer) {
DefaultApiVersionInserter.Builder builder = DefaultApiVersionInserter.builder();
consumer.accept(builder);
return (Map<String, String>) WebTestClient.bindToController(new TestController())
.configureClient()
.baseUrl("/path")
.apiVersionInserter(builder.build())
.build()
.get()
.apiVersion(1.2)
.exchange()
.returnResult(Map.class)
.getResponseBody()
.blockFirst();
}
@RestController
static class TestController {
@GetMapping("/**")
Map<String, String> handle(ServerHttpRequest request) {
URI uri = request.getURI();
String query = uri.getQuery();
String header = request.getHeaders().getFirst(HEADER_NAME);
return Map.of("path", uri.getRawPath(),
"query", (query != null ? query : ""),
HEADER_NAME, (header != null ? header : ""));
}
}
}
Loading…
Cancel
Save