mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Add RestTestClient
See gh-34428 Signed-off-by: Rob Worsnop <rworsnop@gmail.com>
This commit is contained in:
+30
-1
@@ -20,6 +20,9 @@ import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -31,6 +34,8 @@ import org.springframework.mock.http.client.MockClientHttpRequest;
|
||||
import org.springframework.mock.http.client.MockClientHttpResponse;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.client.RestTestClient;
|
||||
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@@ -41,7 +46,9 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 3.2
|
||||
* @deprecated in favor of {@link RestTestClient#bindTo(MockMvc)}
|
||||
*/
|
||||
@Deprecated(since = "7.0")
|
||||
public class MockMvcClientHttpRequestFactory implements ClientHttpRequestFactory {
|
||||
|
||||
private final MockMvc mockMvc;
|
||||
@@ -67,8 +74,14 @@ public class MockMvcClientHttpRequestFactory implements ClientHttpRequestFactory
|
||||
HttpMethod httpMethod, URI uri, HttpHeaders requestHeaders, byte[] requestBody) {
|
||||
|
||||
try {
|
||||
Cookie[] cookies = parseCookies(requestHeaders.get(HttpHeaders.COOKIE));
|
||||
MockHttpServletRequestBuilder requestBuilder = request(httpMethod, uri)
|
||||
.content(requestBody).headers(requestHeaders);
|
||||
if (cookies.length > 0) {
|
||||
requestBuilder.cookie(cookies);
|
||||
}
|
||||
MockHttpServletResponse servletResponse = this.mockMvc
|
||||
.perform(request(httpMethod, uri).content(requestBody).headers(requestHeaders))
|
||||
.perform(requestBuilder)
|
||||
.andReturn()
|
||||
.getResponse();
|
||||
|
||||
@@ -92,6 +105,22 @@ public class MockMvcClientHttpRequestFactory implements ClientHttpRequestFactory
|
||||
}
|
||||
}
|
||||
|
||||
private static Cookie[] parseCookies(@Nullable List<String> headerValues) {
|
||||
if (headerValues == null) {
|
||||
return new Cookie[0];
|
||||
}
|
||||
return headerValues.stream()
|
||||
.flatMap(header -> StringUtils.commaDelimitedListToSet(header).stream())
|
||||
.map(MockMvcClientHttpRequestFactory::parseCookie)
|
||||
.toArray(Cookie[]::new);
|
||||
}
|
||||
|
||||
private static Cookie parseCookie(String cookie) {
|
||||
String[] parts = StringUtils.split(cookie, "=");
|
||||
Assert.isTrue(parts != null && parts.length == 2, "Invalid cookie: '" + cookie + "'");
|
||||
return new Cookie(parts[0], parts[1]);
|
||||
}
|
||||
|
||||
private HttpHeaders getResponseHeaders(MockHttpServletResponse response) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
for (String name : response.getHeaderNames()) {
|
||||
|
||||
+236
@@ -0,0 +1,236 @@
|
||||
/*
|
||||
* Copyright 2002-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.test.web.servlet.client;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
import org.hamcrest.MatcherAssert;
|
||||
|
||||
import org.springframework.http.ResponseCookie;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.springframework.test.util.AssertionErrors.assertEquals;
|
||||
import static org.springframework.test.util.AssertionErrors.fail;
|
||||
|
||||
/**
|
||||
* Assertions on cookies of the response.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
*/
|
||||
public class CookieAssertions {
|
||||
|
||||
private final ExchangeResult exchangeResult;
|
||||
|
||||
private final RestTestClient.ResponseSpec responseSpec;
|
||||
|
||||
public CookieAssertions(ExchangeResult exchangeResult, RestTestClient.ResponseSpec responseSpec) {
|
||||
this.exchangeResult = exchangeResult;
|
||||
this.responseSpec = responseSpec;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Expect a response cookie with the given name to match the specified value.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec valueEquals(String name, String value) {
|
||||
String cookieValue = getCookie(name).getValue();
|
||||
this.exchangeResult.assertWithDiagnostics(() -> {
|
||||
String message = getMessage(name);
|
||||
assertEquals(message, value, cookieValue);
|
||||
});
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the value of the response cookie with the given name with a Hamcrest
|
||||
* {@link Matcher}.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec value(String name, Matcher<? super String> matcher) {
|
||||
String value = getCookie(name).getValue();
|
||||
this.exchangeResult.assertWithDiagnostics(() -> {
|
||||
String message = getMessage(name);
|
||||
MatcherAssert.assertThat(message, value, matcher);
|
||||
});
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume the value of the response cookie with the given name.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec value(String name, Consumer<String> consumer) {
|
||||
String value = getCookie(name).getValue();
|
||||
this.exchangeResult.assertWithDiagnostics(() -> consumer.accept(value));
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect that the cookie with the given name is present.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec exists(String name) {
|
||||
getCookie(name);
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect that the cookie with the given name is not present.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec doesNotExist(String name) {
|
||||
ResponseCookie cookie = this.exchangeResult.getResponseCookies().getFirst(name);
|
||||
if (cookie != null) {
|
||||
String message = getMessage(name) + " exists with value=[" + cookie.getValue() + "]";
|
||||
this.exchangeResult.assertWithDiagnostics(() -> fail(message));
|
||||
}
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert a cookie's "Max-Age" attribute.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec maxAge(String name, Duration expected) {
|
||||
Duration maxAge = getCookie(name).getMaxAge();
|
||||
this.exchangeResult.assertWithDiagnostics(() -> {
|
||||
String message = getMessage(name) + " maxAge";
|
||||
assertEquals(message, expected, maxAge);
|
||||
});
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert a cookie's "Max-Age" attribute with a Hamcrest {@link Matcher}.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec maxAge(String name, Matcher<? super Long> matcher) {
|
||||
long maxAge = getCookie(name).getMaxAge().getSeconds();
|
||||
this.exchangeResult.assertWithDiagnostics(() -> {
|
||||
String message = getMessage(name) + " maxAge";
|
||||
assertThat(message, maxAge, matcher);
|
||||
});
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert a cookie's "Path" attribute.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec path(String name, String expected) {
|
||||
String path = getCookie(name).getPath();
|
||||
this.exchangeResult.assertWithDiagnostics(() -> {
|
||||
String message = getMessage(name) + " path";
|
||||
assertEquals(message, expected, path);
|
||||
});
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert a cookie's "Path" attribute with a Hamcrest {@link Matcher}.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec path(String name, Matcher<? super String> matcher) {
|
||||
String path = getCookie(name).getPath();
|
||||
this.exchangeResult.assertWithDiagnostics(() -> {
|
||||
String message = getMessage(name) + " path";
|
||||
assertThat(message, path, matcher);
|
||||
});
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert a cookie's "Domain" attribute.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec domain(String name, String expected) {
|
||||
String path = getCookie(name).getDomain();
|
||||
this.exchangeResult.assertWithDiagnostics(() -> {
|
||||
String message = getMessage(name) + " domain";
|
||||
assertEquals(message, expected, path);
|
||||
});
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert a cookie's "Domain" attribute with a Hamcrest {@link Matcher}.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec domain(String name, Matcher<? super String> matcher) {
|
||||
String domain = getCookie(name).getDomain();
|
||||
this.exchangeResult.assertWithDiagnostics(() -> {
|
||||
String message = getMessage(name) + " domain";
|
||||
assertThat(message, domain, matcher);
|
||||
});
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert a cookie's "Secure" attribute.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec secure(String name, boolean expected) {
|
||||
boolean isSecure = getCookie(name).isSecure();
|
||||
this.exchangeResult.assertWithDiagnostics(() -> {
|
||||
String message = getMessage(name) + " secure";
|
||||
assertEquals(message, expected, isSecure);
|
||||
});
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert a cookie's "HttpOnly" attribute.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec httpOnly(String name, boolean expected) {
|
||||
boolean isHttpOnly = getCookie(name).isHttpOnly();
|
||||
this.exchangeResult.assertWithDiagnostics(() -> {
|
||||
String message = getMessage(name) + " httpOnly";
|
||||
assertEquals(message, expected, isHttpOnly);
|
||||
});
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert a cookie's "Partitioned" attribute.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec partitioned(String name, boolean expected) {
|
||||
boolean isPartitioned = getCookie(name).isPartitioned();
|
||||
this.exchangeResult.assertWithDiagnostics(() -> {
|
||||
String message = getMessage(name) + " isPartitioned";
|
||||
assertEquals(message, expected, isPartitioned);
|
||||
});
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert a cookie's "SameSite" attribute.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec sameSite(String name, String expected) {
|
||||
String sameSite = getCookie(name).getSameSite();
|
||||
this.exchangeResult.assertWithDiagnostics(() -> {
|
||||
String message = getMessage(name) + " sameSite";
|
||||
assertEquals(message, expected, sameSite);
|
||||
});
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
private ResponseCookie getCookie(String name) {
|
||||
ResponseCookie cookie = this.exchangeResult.getResponseCookies().getFirst(name);
|
||||
if (cookie != null) {
|
||||
return cookie;
|
||||
}
|
||||
else {
|
||||
this.exchangeResult.assertWithDiagnostics(() -> fail("No cookie with name '" + name + "'"));
|
||||
}
|
||||
throw new IllegalStateException("This code path should not be reachable");
|
||||
}
|
||||
|
||||
private static String getMessage(String cookie) {
|
||||
return "Response cookie '" + cookie + "'";
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2002-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.test.web.servlet.client;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.test.web.servlet.MockMvcBuilder;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link RestTestClient.MockServerBuilder}.
|
||||
* @author Rob Worsnop
|
||||
* @param <M> the type of the {@link MockMvcBuilder} to use for building the mock server
|
||||
*/
|
||||
class DefaultMockServerBuilder<M extends MockMvcBuilder>
|
||||
extends DefaultRestTestClientBuilder<RestTestClient.MockServerBuilder<M>>
|
||||
implements RestTestClient.MockServerBuilder<M> {
|
||||
|
||||
private final M builder;
|
||||
|
||||
public DefaultMockServerBuilder(M builder) {
|
||||
this.builder = builder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestTestClient.MockServerBuilder<M> configureServer(Consumer<M> consumer) {
|
||||
consumer.accept(this.builder);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestTestClient build() {
|
||||
this.restClientBuilder.requestFactory(new MockMvcClientHttpRequestFactory(this.builder.build()));
|
||||
return super.build();
|
||||
}
|
||||
}
|
||||
+429
@@ -0,0 +1,429 @@
|
||||
/*
|
||||
* Copyright 2002-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.test.web.servlet.client;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
import org.hamcrest.MatcherAssert;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.json.JsonAssert;
|
||||
import org.springframework.test.json.JsonComparator;
|
||||
import org.springframework.test.json.JsonCompareMode;
|
||||
import org.springframework.test.util.AssertionErrors;
|
||||
import org.springframework.test.util.ExceptionCollector;
|
||||
import org.springframework.test.util.XmlExpectationsHelper;
|
||||
import org.springframework.util.MimeType;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.util.UriBuilder;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link RestTestClient}.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
*/
|
||||
class DefaultRestTestClient implements RestTestClient {
|
||||
|
||||
private final RestClient restClient;
|
||||
|
||||
private final AtomicLong requestIndex = new AtomicLong();
|
||||
|
||||
private final RestClient.Builder restClientBuilder;
|
||||
|
||||
DefaultRestTestClient(RestClient.Builder restClientBuilder) {
|
||||
this.restClient = restClientBuilder.build();
|
||||
this.restClientBuilder = restClientBuilder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestHeadersUriSpec<?> get() {
|
||||
return methodInternal(HttpMethod.GET);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestHeadersUriSpec<?> head() {
|
||||
return methodInternal(HttpMethod.HEAD);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodyUriSpec post() {
|
||||
return methodInternal(HttpMethod.POST);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodyUriSpec put() {
|
||||
return methodInternal(HttpMethod.PUT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodyUriSpec patch() {
|
||||
return methodInternal(HttpMethod.PATCH);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestHeadersUriSpec<?> delete() {
|
||||
return methodInternal(HttpMethod.DELETE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestHeadersUriSpec<?> options() {
|
||||
return methodInternal(HttpMethod.OPTIONS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodyUriSpec method(HttpMethod method) {
|
||||
return methodInternal(method);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <B extends Builder<B>> Builder<B> mutate() {
|
||||
return new DefaultRestTestClientBuilder<>(this.restClientBuilder);
|
||||
}
|
||||
|
||||
private RequestBodyUriSpec methodInternal(HttpMethod httpMethod) {
|
||||
return new DefaultRequestBodyUriSpec(this.restClient.method(httpMethod));
|
||||
}
|
||||
|
||||
|
||||
private class DefaultRequestBodyUriSpec implements RequestBodyUriSpec {
|
||||
|
||||
private final RestClient.RequestBodyUriSpec requestHeadersUriSpec;
|
||||
private RestClient.RequestBodySpec requestBodySpec;
|
||||
private final String requestId;
|
||||
|
||||
|
||||
public DefaultRequestBodyUriSpec(RestClient.RequestBodyUriSpec spec) {
|
||||
this.requestHeadersUriSpec = spec;
|
||||
this.requestBodySpec = spec;
|
||||
this.requestId = String.valueOf(requestIndex.incrementAndGet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec accept(MediaType... acceptableMediaTypes) {
|
||||
this.requestBodySpec = this.requestHeadersUriSpec.accept(acceptableMediaTypes);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec uri(URI uri) {
|
||||
this.requestBodySpec = this.requestHeadersUriSpec.uri(uri);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec uri(String uriTemplate, Object... uriVariables) {
|
||||
this.requestBodySpec = this.requestHeadersUriSpec.uri(uriTemplate, uriVariables);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec uri(String uri, Map<String, ?> uriVariables) {
|
||||
this.requestBodySpec = this.requestHeadersUriSpec.uri(uri, uriVariables);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec uri(Function<UriBuilder, URI> uriFunction) {
|
||||
this.requestBodySpec = this.requestHeadersUriSpec.uri(uriFunction);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec cookie(String name, String value) {
|
||||
this.requestBodySpec = this.requestHeadersUriSpec.cookie(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec cookies(Consumer<MultiValueMap<String, String>> cookiesConsumer) {
|
||||
this.requestBodySpec = this.requestHeadersUriSpec.cookies(cookiesConsumer);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec header(String headerName, String... headerValues) {
|
||||
this.requestBodySpec = this.requestHeadersUriSpec.header(headerName, headerValues);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec contentType(MediaType contentType) {
|
||||
this.requestBodySpec = this.requestHeadersUriSpec.contentType(contentType);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestHeadersSpec<?> body(Object body) {
|
||||
this.requestHeadersUriSpec.body(body);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec acceptCharset(Charset... acceptableCharsets) {
|
||||
this.requestBodySpec = this.requestHeadersUriSpec.acceptCharset(acceptableCharsets);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec ifModifiedSince(ZonedDateTime ifModifiedSince) {
|
||||
this.requestBodySpec = this.requestHeadersUriSpec.ifModifiedSince(ifModifiedSince);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec ifNoneMatch(String... ifNoneMatches) {
|
||||
this.requestBodySpec = this.requestHeadersUriSpec.ifNoneMatch(ifNoneMatches);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec headers(Consumer<HttpHeaders> headersConsumer) {
|
||||
this.requestBodySpec = this.requestHeadersUriSpec.headers(headersConsumer);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec attribute(String name, Object value) {
|
||||
this.requestBodySpec = this.requestHeadersUriSpec.attribute(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec attributes(Consumer<Map<String, Object>> attributesConsumer) {
|
||||
this.requestBodySpec = this.requestHeadersUriSpec.attributes(attributesConsumer);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseSpec exchange() {
|
||||
this.requestBodySpec = this.requestBodySpec.header(RESTTESTCLIENT_REQUEST_ID, this.requestId);
|
||||
ExchangeResult exchangeResult = this.requestBodySpec.exchange(
|
||||
(clientRequest, clientResponse) -> new ExchangeResult(clientResponse),
|
||||
false);
|
||||
return new DefaultResponseSpec(Objects.requireNonNull(exchangeResult));
|
||||
}
|
||||
}
|
||||
|
||||
private static class DefaultResponseSpec implements ResponseSpec {
|
||||
|
||||
private final ExchangeResult exchangeResult;
|
||||
|
||||
public DefaultResponseSpec(ExchangeResult exchangeResult) {
|
||||
this.exchangeResult = exchangeResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StatusAssertions expectStatus() {
|
||||
return new StatusAssertions(this.exchangeResult, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BodyContentSpec expectBody() {
|
||||
byte[] body = this.exchangeResult.getBody(byte[].class);
|
||||
return new DefaultBodyContentSpec( new EntityExchangeResult<>(this.exchangeResult, body));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <B> BodySpec<B, ?> expectBody(Class<B> bodyType) {
|
||||
B body = this.exchangeResult.getBody(bodyType);
|
||||
return new DefaultBodySpec<>(new EntityExchangeResult<>(this.exchangeResult, body));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <B> BodySpec<B, ?> expectBody(ParameterizedTypeReference<B> bodyType) {
|
||||
B body = this.exchangeResult.getBody(bodyType);
|
||||
return new DefaultBodySpec<>(new EntityExchangeResult<>(this.exchangeResult, body));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CookieAssertions expectCookie() {
|
||||
return new CookieAssertions(this.exchangeResult, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HeaderAssertions expectHeader() {
|
||||
return new HeaderAssertions(this.exchangeResult, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseSpec expectAll(ResponseSpecConsumer... consumers) {
|
||||
ExceptionCollector exceptionCollector = new ExceptionCollector();
|
||||
for (ResponseSpecConsumer consumer : consumers) {
|
||||
exceptionCollector.execute(() -> consumer.accept(this));
|
||||
}
|
||||
try {
|
||||
exceptionCollector.assertEmpty();
|
||||
}
|
||||
catch (RuntimeException ex) {
|
||||
throw ex;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
// In theory, a ResponseSpecConsumer should never throw an Exception
|
||||
// that is not a RuntimeException, but since ExceptionCollector may
|
||||
// throw a checked Exception, we handle this to appease the compiler
|
||||
// and in case someone uses a "sneaky throws" technique.
|
||||
throw new AssertionError(ex.getMessage(), ex);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> EntityExchangeResult<T> returnResult(Class<T> elementClass) {
|
||||
return new EntityExchangeResult<>(this.exchangeResult, this.exchangeResult.getBody(elementClass));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> EntityExchangeResult<T> returnResult(ParameterizedTypeReference<T> elementTypeRef) {
|
||||
return new EntityExchangeResult<>(this.exchangeResult, this.exchangeResult.getBody(elementTypeRef));
|
||||
}
|
||||
}
|
||||
|
||||
private static class DefaultBodyContentSpec implements BodyContentSpec {
|
||||
private final EntityExchangeResult<byte[]> result;
|
||||
|
||||
public DefaultBodyContentSpec(EntityExchangeResult<byte[]> result) {
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityExchangeResult<Void> isEmpty() {
|
||||
this.result.assertWithDiagnostics(() ->
|
||||
AssertionErrors.assertTrue("Expected empty body",
|
||||
this.result.getBody(byte[].class) == null));
|
||||
return new EntityExchangeResult<>(this.result, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BodyContentSpec json(String expectedJson, JsonCompareMode compareMode) {
|
||||
return json(expectedJson, JsonAssert.comparator(compareMode));
|
||||
}
|
||||
|
||||
@Override
|
||||
public BodyContentSpec json(String expectedJson, JsonComparator comparator) {
|
||||
this.result.assertWithDiagnostics(() -> {
|
||||
try {
|
||||
comparator.assertIsMatch(expectedJson, getBodyAsString());
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new AssertionError("JSON parsing error", ex);
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BodyContentSpec xml(String expectedXml) {
|
||||
this.result.assertWithDiagnostics(() -> {
|
||||
try {
|
||||
new XmlExpectationsHelper().assertXmlEqual(expectedXml, getBodyAsString());
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new AssertionError("XML parsing error", ex);
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonPathAssertions jsonPath(String expression) {
|
||||
return new JsonPathAssertions(this, getBodyAsString(), expression, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public XpathAssertions xpath(String expression, @Nullable Map<String, String> namespaces, Object... args) {
|
||||
return new XpathAssertions(this, expression, namespaces, args);
|
||||
}
|
||||
|
||||
private String getBodyAsString() {
|
||||
byte[] body = this.result.getResponseBody();
|
||||
if (body == null || body.length == 0) {
|
||||
return "";
|
||||
}
|
||||
Charset charset = Optional.ofNullable(this.result.getResponseHeaders().getContentType())
|
||||
.map(MimeType::getCharset).orElse(StandardCharsets.UTF_8);
|
||||
return new String(body, charset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityExchangeResult<byte[]> returnResult() {
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
|
||||
private static class DefaultBodySpec<B, S extends BodySpec<B, S>> implements BodySpec<B, S> {
|
||||
|
||||
private final EntityExchangeResult<B> result;
|
||||
|
||||
public DefaultBodySpec(@Nullable EntityExchangeResult<B> result) {
|
||||
this.result = Objects.requireNonNull(result, "exchangeResult must be non-null");
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityExchangeResult<B> returnResult() {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends S> T isEqualTo(B expected) {
|
||||
this.result.assertWithDiagnostics(() ->
|
||||
AssertionErrors.assertEquals("Response body", expected, this.result.getResponseBody()));
|
||||
return self();
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("NullAway") // https://github.com/uber/NullAway/issues/1129
|
||||
public <T extends S, R> T value(Function<B, R> bodyMapper, Matcher<? super R> matcher) {
|
||||
this.result.assertWithDiagnostics(() -> {
|
||||
B body = this.result.getResponseBody();
|
||||
MatcherAssert.assertThat(bodyMapper.apply(body), matcher);
|
||||
});
|
||||
return self();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends S> T value(Consumer<B> consumer) {
|
||||
this.result.assertWithDiagnostics(() -> consumer.accept(this.result.getResponseBody()));
|
||||
return self();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends S> T consumeWith(Consumer<EntityExchangeResult<B>> consumer) {
|
||||
this.result.assertWithDiagnostics(() -> consumer.accept(this.result));
|
||||
return self();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T extends S> T self() {
|
||||
return (T) this;
|
||||
}
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2002-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.test.web.servlet.client;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.util.UriBuilderFactory;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link RestTestClient.Builder}.
|
||||
* @author Rob Worsnop
|
||||
* @param <B> the type of the builder
|
||||
*/
|
||||
class DefaultRestTestClientBuilder<B extends RestTestClient.Builder<B>> implements RestTestClient.Builder<B> {
|
||||
|
||||
protected final RestClient.Builder restClientBuilder;
|
||||
|
||||
DefaultRestTestClientBuilder() {
|
||||
this.restClientBuilder = RestClient.builder();
|
||||
}
|
||||
|
||||
DefaultRestTestClientBuilder(RestClient.Builder restClientBuilder) {
|
||||
this.restClientBuilder = restClientBuilder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestTestClient.Builder<B> apply(Consumer<RestTestClient.Builder<B>> builderConsumer) {
|
||||
builderConsumer.accept(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestTestClient.Builder<B> baseUrl(String baseUrl) {
|
||||
this.restClientBuilder.baseUrl(baseUrl);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestTestClient.Builder<B> defaultCookie(String cookieName, String... cookieValues) {
|
||||
this.restClientBuilder.defaultCookie(cookieName, cookieValues);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestTestClient.Builder<B> defaultCookies(Consumer<MultiValueMap<String, String>> cookiesConsumer) {
|
||||
this.restClientBuilder.defaultCookies(cookiesConsumer);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestTestClient.Builder<B> defaultHeader(String headerName, String... headerValues) {
|
||||
this.restClientBuilder.defaultHeader(headerName, headerValues);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestTestClient.Builder<B> defaultHeaders(Consumer<HttpHeaders> headersConsumer) {
|
||||
this.restClientBuilder.defaultHeaders(headersConsumer);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestTestClient.Builder<B> uriBuilderFactory(UriBuilderFactory uriFactory) {
|
||||
this.restClientBuilder.uriBuilderFactory(uriFactory);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestTestClient build() {
|
||||
return new DefaultRestTestClient(this.restClientBuilder);
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2002-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.test.web.servlet.client;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* {@code ExchangeResult} sub-class that exposes the response body fully
|
||||
* extracted to a representation of type {@code <T>}.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
* @param <T> the response body type
|
||||
*/
|
||||
public class EntityExchangeResult<T> extends ExchangeResult {
|
||||
|
||||
private final @Nullable T body;
|
||||
|
||||
|
||||
EntityExchangeResult(ExchangeResult result, @Nullable T body) {
|
||||
super(result);
|
||||
this.body = body;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the entity extracted from the response body.
|
||||
*/
|
||||
public @Nullable T getResponseBody() {
|
||||
return this.body;
|
||||
}
|
||||
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright 2002-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.test.web.servlet.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpCookie;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.ResponseCookie;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestClient.RequestHeadersSpec.ConvertibleClientHttpResponse;
|
||||
|
||||
/**
|
||||
* Container for request and response details for exchanges performed through
|
||||
* {@link RestTestClient}.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
*/
|
||||
public class ExchangeResult {
|
||||
private static final Pattern SAME_SITE_PATTERN = Pattern.compile("(?i).*SameSite=(Strict|Lax|None).*");
|
||||
private static final Pattern PARTITIONED_PATTERN = Pattern.compile("(?i).*;\\s*Partitioned(\\s*;.*|\\s*)$");
|
||||
|
||||
|
||||
private static final Log logger = LogFactory.getLog(ExchangeResult.class);
|
||||
|
||||
/** Ensure single logging; for example, for expectAll. */
|
||||
private boolean diagnosticsLogged;
|
||||
|
||||
private final ConvertibleClientHttpResponse clientResponse;
|
||||
|
||||
ExchangeResult(@Nullable ConvertibleClientHttpResponse clientResponse) {
|
||||
this.clientResponse = Objects.requireNonNull(clientResponse, "clientResponse must be non-null");
|
||||
}
|
||||
|
||||
ExchangeResult(ExchangeResult result) {
|
||||
this(result.clientResponse);
|
||||
this.diagnosticsLogged = result.diagnosticsLogged;
|
||||
}
|
||||
|
||||
public HttpStatusCode getStatus() {
|
||||
try {
|
||||
return this.clientResponse.getStatusCode();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new AssertionError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public HttpHeaders getResponseHeaders() {
|
||||
return this.clientResponse.getHeaders();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public <T> T getBody(Class<T> bodyType) {
|
||||
return this.clientResponse.bodyTo(bodyType);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public <T> T getBody(ParameterizedTypeReference<T> bodyType) {
|
||||
return this.clientResponse.bodyTo(bodyType);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Execute the given Runnable, catch any {@link AssertionError}, log details
|
||||
* about the request and response at ERROR level under the class log
|
||||
* category, and after that re-throw the error.
|
||||
*/
|
||||
public void assertWithDiagnostics(Runnable assertion) {
|
||||
try {
|
||||
assertion.run();
|
||||
}
|
||||
catch (AssertionError ex) {
|
||||
if (!this.diagnosticsLogged && logger.isErrorEnabled()) {
|
||||
this.diagnosticsLogged = true;
|
||||
logger.error("Request details for assertion failure:\n" + this);
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return response cookies received from the server.
|
||||
*/
|
||||
public MultiValueMap<String, ResponseCookie> getResponseCookies() {
|
||||
return Optional.ofNullable(this.clientResponse.getHeaders().get(HttpHeaders.SET_COOKIE)).orElse(List.of()).stream()
|
||||
.flatMap(header -> {
|
||||
Matcher matcher = SAME_SITE_PATTERN.matcher(header);
|
||||
String sameSite = (matcher.matches() ? matcher.group(1) : null);
|
||||
boolean partitioned = PARTITIONED_PATTERN.matcher(header).matches();
|
||||
return HttpCookie.parse(header).stream().map(cookie -> toResponseCookie(cookie, sameSite, partitioned));
|
||||
})
|
||||
.collect(LinkedMultiValueMap::new,
|
||||
(cookies, cookie) -> cookies.add(cookie.getName(), cookie),
|
||||
LinkedMultiValueMap::addAll);
|
||||
}
|
||||
|
||||
private static ResponseCookie toResponseCookie(HttpCookie cookie, @Nullable String sameSite, boolean partitioned) {
|
||||
return ResponseCookie.from(cookie.getName(), cookie.getValue())
|
||||
.domain(cookie.getDomain())
|
||||
.httpOnly(cookie.isHttpOnly())
|
||||
.maxAge(cookie.getMaxAge())
|
||||
.path(cookie.getPath())
|
||||
.secure(cookie.getSecure())
|
||||
.sameSite(sameSite)
|
||||
.partitioned(partitioned)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
+311
@@ -0,0 +1,311 @@
|
||||
/*
|
||||
* Copyright 2002-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.test.web.servlet.client;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.springframework.test.util.AssertionErrors.assertEquals;
|
||||
import static org.springframework.test.util.AssertionErrors.assertNotNull;
|
||||
import static org.springframework.test.util.AssertionErrors.assertTrue;
|
||||
import static org.springframework.test.util.AssertionErrors.fail;
|
||||
|
||||
/**
|
||||
* Assertions on headers of the response.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
* @see RestTestClient.ResponseSpec#expectHeader()
|
||||
*/
|
||||
public class HeaderAssertions {
|
||||
|
||||
private final ExchangeResult exchangeResult;
|
||||
|
||||
private final RestTestClient.ResponseSpec responseSpec;
|
||||
|
||||
public HeaderAssertions(ExchangeResult exchangeResult, RestTestClient.ResponseSpec responseSpec) {
|
||||
this.exchangeResult = exchangeResult;
|
||||
this.responseSpec = responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect a header with the given name to match the specified values.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec valueEquals(String headerName, String... values) {
|
||||
return assertHeader(headerName, Arrays.asList(values), getHeaders().getOrEmpty(headerName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect a header with the given name to match the given long value.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec valueEquals(String headerName, long value) {
|
||||
String actual = getHeaders().getFirst(headerName);
|
||||
this.exchangeResult.assertWithDiagnostics(() ->
|
||||
assertNotNull("Response does not contain header '" + headerName + "'", actual));
|
||||
return assertHeader(headerName, value, Long.parseLong(actual));
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect a header with the given name to match the specified long value
|
||||
* parsed into a date using the preferred date format described in RFC 7231.
|
||||
* <p>An {@link AssertionError} is thrown if the response does not contain
|
||||
* the specified header, or if the supplied {@code value} does not match the
|
||||
* primary header value.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec valueEqualsDate(String headerName, long value) {
|
||||
this.exchangeResult.assertWithDiagnostics(() -> {
|
||||
String headerValue = getHeaders().getFirst(headerName);
|
||||
assertNotNull("Response does not contain header '" + headerName + "'", headerValue);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setDate("expected", value);
|
||||
headers.set("actual", headerValue);
|
||||
|
||||
assertEquals(getMessage(headerName) + "='" + headerValue + "' " +
|
||||
"does not match expected value '" + headers.getFirst("expected") + "'",
|
||||
headers.getFirstDate("expected"), headers.getFirstDate("actual"));
|
||||
});
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Match the first value of the response header with a regex.
|
||||
* @param name the header name
|
||||
* @param pattern the regex pattern
|
||||
*/
|
||||
public RestTestClient.ResponseSpec valueMatches(String name, String pattern) {
|
||||
String value = getRequiredValue(name);
|
||||
String message = getMessage(name) + "=[" + value + "] does not match [" + pattern + "]";
|
||||
this.exchangeResult.assertWithDiagnostics(() -> assertTrue(message, value.matches(pattern)));
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Match all values of the response header with the given regex
|
||||
* patterns which are applied to the values of the header in the
|
||||
* same order. Note that the number of patterns must match the
|
||||
* number of actual values.
|
||||
* @param name the header name
|
||||
* @param patterns one or more regex patterns, one per expected value
|
||||
*/
|
||||
public RestTestClient.ResponseSpec valuesMatch(String name, String... patterns) {
|
||||
List<String> values = getRequiredValues(name);
|
||||
this.exchangeResult.assertWithDiagnostics(() -> {
|
||||
assertTrue(
|
||||
getMessage(name) + " has fewer or more values " + values +
|
||||
" than number of patterns to match with " + Arrays.toString(patterns),
|
||||
values.size() == patterns.length);
|
||||
for (int i = 0; i < values.size(); i++) {
|
||||
String value = values.get(i);
|
||||
String pattern = patterns[i];
|
||||
assertTrue(
|
||||
getMessage(name) + "[" + i + "]='" + value + "' does not match '" + pattern + "'",
|
||||
value.matches(pattern));
|
||||
}
|
||||
});
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the first value of the response header with a Hamcrest {@link Matcher}.
|
||||
* @param name the header name
|
||||
* @param matcher the matcher to use
|
||||
*/
|
||||
public RestTestClient.ResponseSpec value(String name, Matcher<? super String> matcher) {
|
||||
String value = getHeaders().getFirst(name);
|
||||
this.exchangeResult.assertWithDiagnostics(() -> {
|
||||
String message = getMessage(name);
|
||||
assertThat(message, value, matcher);
|
||||
});
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert all values of the response header with a Hamcrest {@link Matcher}.
|
||||
* @param name the header name
|
||||
* @param matcher the matcher to use
|
||||
*/
|
||||
public RestTestClient.ResponseSpec values(String name, Matcher<? super Iterable<String>> matcher) {
|
||||
List<String> values = getHeaders().get(name);
|
||||
this.exchangeResult.assertWithDiagnostics(() -> {
|
||||
String message = getMessage(name);
|
||||
assertThat(message, values, matcher);
|
||||
});
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume the first value of the named response header.
|
||||
* @param name the header name
|
||||
* @param consumer the consumer to use
|
||||
*/
|
||||
public RestTestClient.ResponseSpec value(String name, Consumer<String> consumer) {
|
||||
String value = getRequiredValue(name);
|
||||
this.exchangeResult.assertWithDiagnostics(() -> consumer.accept(value));
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume all values of the named response header.
|
||||
* @param name the header name
|
||||
* @param consumer the consumer to use
|
||||
*/
|
||||
public RestTestClient.ResponseSpec values(String name, Consumer<List<String>> consumer) {
|
||||
List<String> values = getRequiredValues(name);
|
||||
this.exchangeResult.assertWithDiagnostics(() -> consumer.accept(values));
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect that the header with the given name is present.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec exists(String name) {
|
||||
if (!this.exchangeResult.getResponseHeaders().containsHeader(name)) {
|
||||
String message = getMessage(name) + " does not exist";
|
||||
this.exchangeResult.assertWithDiagnostics(() -> fail(message));
|
||||
}
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect that the header with the given name is not present.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec doesNotExist(String name) {
|
||||
if (getHeaders().containsHeader(name)) {
|
||||
String message = getMessage(name) + " exists with value=[" + getHeaders().getFirst(name) + "]";
|
||||
this.exchangeResult.assertWithDiagnostics(() -> fail(message));
|
||||
}
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect a "Cache-Control" header with the given value.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec cacheControl(CacheControl cacheControl) {
|
||||
return assertHeader("Cache-Control", cacheControl.getHeaderValue(), getHeaders().getCacheControl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect a "Content-Disposition" header with the given value.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec contentDisposition(ContentDisposition contentDisposition) {
|
||||
return assertHeader("Content-Disposition", contentDisposition, getHeaders().getContentDisposition());
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect a "Content-Length" header with the given value.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec contentLength(long contentLength) {
|
||||
return assertHeader("Content-Length", contentLength, getHeaders().getContentLength());
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect a "Content-Type" header with the given value.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec contentType(MediaType mediaType) {
|
||||
return assertHeader("Content-Type", mediaType, getHeaders().getContentType());
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect a "Content-Type" header with the given value.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec contentType(String mediaType) {
|
||||
return contentType(MediaType.parseMediaType(mediaType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect a "Content-Type" header compatible with the given value.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec contentTypeCompatibleWith(MediaType mediaType) {
|
||||
MediaType actual = getHeaders().getContentType();
|
||||
String message = getMessage("Content-Type") + "=[" + actual + "] is not compatible with [" + mediaType + "]";
|
||||
this.exchangeResult.assertWithDiagnostics(() ->
|
||||
assertTrue(message, (actual != null && actual.isCompatibleWith(mediaType))));
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect a "Content-Type" header compatible with the given value.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec contentTypeCompatibleWith(String mediaType) {
|
||||
return contentTypeCompatibleWith(MediaType.parseMediaType(mediaType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect an "Expires" header with the given value.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec expires(long expires) {
|
||||
return assertHeader("Expires", expires, getHeaders().getExpires());
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect a "Last-Modified" header with the given value.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec lastModified(long lastModified) {
|
||||
return assertHeader("Last-Modified", lastModified, getHeaders().getLastModified());
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect a "Location" header with the given value.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec location(String location) {
|
||||
return assertHeader("Location", URI.create(location), getHeaders().getLocation());
|
||||
}
|
||||
|
||||
|
||||
private HttpHeaders getHeaders() {
|
||||
return this.exchangeResult.getResponseHeaders();
|
||||
}
|
||||
|
||||
private String getRequiredValue(String name) {
|
||||
return getRequiredValues(name).get(0);
|
||||
}
|
||||
|
||||
private List<String> getRequiredValues(String name) {
|
||||
List<String> values = getHeaders().get(name);
|
||||
if (!CollectionUtils.isEmpty(values)) {
|
||||
return values;
|
||||
}
|
||||
else {
|
||||
this.exchangeResult.assertWithDiagnostics(() -> fail(getMessage(name) + " not found"));
|
||||
}
|
||||
throw new IllegalStateException("This code path should not be reachable");
|
||||
}
|
||||
|
||||
private RestTestClient.ResponseSpec assertHeader(String name, @Nullable Object expected, @Nullable Object actual) {
|
||||
this.exchangeResult.assertWithDiagnostics(() -> {
|
||||
String message = getMessage(name);
|
||||
assertEquals(message, expected, actual);
|
||||
});
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
private static String getMessage(String headerName) {
|
||||
return "Response header '" + headerName + "'";
|
||||
}
|
||||
}
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
* Copyright 2002-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.test.web.servlet.client;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import com.jayway.jsonpath.Configuration;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.test.util.JsonPathExpectationsHelper;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* <a href="https://github.com/jayway/JsonPath">JsonPath</a> assertions.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
*
|
||||
* @see <a href="https://github.com/jayway/JsonPath">https://github.com/jayway/JsonPath</a>
|
||||
* @see JsonPathExpectationsHelper
|
||||
*/
|
||||
public class JsonPathAssertions {
|
||||
|
||||
private final RestTestClient.BodyContentSpec bodySpec;
|
||||
|
||||
private final String content;
|
||||
|
||||
private final JsonPathExpectationsHelper pathHelper;
|
||||
|
||||
|
||||
JsonPathAssertions(RestTestClient.BodyContentSpec spec, String content, String expression, @Nullable Configuration configuration) {
|
||||
Assert.hasText(expression, "expression must not be null or empty");
|
||||
this.bodySpec = spec;
|
||||
this.content = content;
|
||||
this.pathHelper = new JsonPathExpectationsHelper(expression, configuration);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Applies {@link JsonPathExpectationsHelper#assertValue(String, Object)}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec isEqualTo(Object expectedValue) {
|
||||
this.pathHelper.assertValue(this.content, expectedValue);
|
||||
return this.bodySpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies {@link JsonPathExpectationsHelper#exists(String)}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec exists() {
|
||||
this.pathHelper.exists(this.content);
|
||||
return this.bodySpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies {@link JsonPathExpectationsHelper#doesNotExist(String)}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec doesNotExist() {
|
||||
this.pathHelper.doesNotExist(this.content);
|
||||
return this.bodySpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies {@link JsonPathExpectationsHelper#assertValueIsEmpty(String)}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec isEmpty() {
|
||||
this.pathHelper.assertValueIsEmpty(this.content);
|
||||
return this.bodySpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies {@link JsonPathExpectationsHelper#assertValueIsNotEmpty(String)}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec isNotEmpty() {
|
||||
this.pathHelper.assertValueIsNotEmpty(this.content);
|
||||
return this.bodySpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies {@link JsonPathExpectationsHelper#hasJsonPath}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec hasJsonPath() {
|
||||
this.pathHelper.hasJsonPath(this.content);
|
||||
return this.bodySpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies {@link JsonPathExpectationsHelper#doesNotHaveJsonPath}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec doesNotHaveJsonPath() {
|
||||
this.pathHelper.doesNotHaveJsonPath(this.content);
|
||||
return this.bodySpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies {@link JsonPathExpectationsHelper#assertValueIsBoolean(String)}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec isBoolean() {
|
||||
this.pathHelper.assertValueIsBoolean(this.content);
|
||||
return this.bodySpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies {@link JsonPathExpectationsHelper#assertValueIsNumber(String)}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec isNumber() {
|
||||
this.pathHelper.assertValueIsNumber(this.content);
|
||||
return this.bodySpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies {@link JsonPathExpectationsHelper#assertValueIsArray(String)}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec isArray() {
|
||||
this.pathHelper.assertValueIsArray(this.content);
|
||||
return this.bodySpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies {@link JsonPathExpectationsHelper#assertValueIsMap(String)}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec isMap() {
|
||||
this.pathHelper.assertValueIsMap(this.content);
|
||||
return this.bodySpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates to {@link JsonPathExpectationsHelper#assertValue(String, Matcher)}.
|
||||
*/
|
||||
public <T> RestTestClient.BodyContentSpec value(Matcher<? super T> matcher) {
|
||||
this.pathHelper.assertValue(this.content, matcher);
|
||||
return this.bodySpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates to {@link JsonPathExpectationsHelper#assertValue(String, Matcher, Class)}.
|
||||
*/
|
||||
public <T> RestTestClient.BodyContentSpec value(Class<T> targetType, Matcher<? super T> matcher) {
|
||||
this.pathHelper.assertValue(this.content, matcher, targetType);
|
||||
return this.bodySpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates to {@link JsonPathExpectationsHelper#assertValue(String, Matcher, ParameterizedTypeReference)}.
|
||||
*/
|
||||
public <T> RestTestClient.BodyContentSpec value(ParameterizedTypeReference<T> targetType, Matcher<? super T> matcher) {
|
||||
this.pathHelper.assertValue(this.content, matcher, targetType);
|
||||
return this.bodySpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume the result of the JSONPath evaluation.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> RestTestClient.BodyContentSpec value(Consumer<T> consumer) {
|
||||
Object value = this.pathHelper.evaluateJsonPath(this.content);
|
||||
consumer.accept((T) value);
|
||||
return this.bodySpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume the result of the JSONPath evaluation and provide a target class.
|
||||
*/
|
||||
public <T> RestTestClient.BodyContentSpec value(Class<T> targetType, Consumer<T> consumer) {
|
||||
T value = this.pathHelper.evaluateJsonPath(this.content, targetType);
|
||||
consumer.accept(value);
|
||||
return this.bodySpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume the result of the JSONPath evaluation and provide a parameterized type.
|
||||
*/
|
||||
public <T> RestTestClient.BodyContentSpec value(ParameterizedTypeReference<T> targetType, Consumer<T> consumer) {
|
||||
T value = this.pathHelper.evaluateJsonPath(this.content, targetType);
|
||||
consumer.accept(value);
|
||||
return this.bodySpec;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
throw new AssertionError("Object#equals is disabled " +
|
||||
"to avoid being used in error instead of JsonPathAssertions#isEqualTo(String).");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return super.hashCode();
|
||||
}
|
||||
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Copyright 2002-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.test.web.servlet.client;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.client.ClientHttpRequest;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.mock.http.client.MockClientHttpRequest;
|
||||
import org.springframework.mock.http.client.MockClientHttpResponse;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.request;
|
||||
|
||||
/**
|
||||
* A {@link ClientHttpRequestFactory} for requests executed via {@link MockMvc}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Rob Worsnop
|
||||
* @since 7.0
|
||||
*/
|
||||
class MockMvcClientHttpRequestFactory implements ClientHttpRequestFactory {
|
||||
|
||||
private final MockMvc mockMvc;
|
||||
|
||||
|
||||
MockMvcClientHttpRequestFactory(MockMvc mockMvc) {
|
||||
Assert.notNull(mockMvc, "MockMvc must not be null");
|
||||
this.mockMvc = mockMvc;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) {
|
||||
return new MockClientHttpRequest(httpMethod, uri) {
|
||||
@Override
|
||||
public ClientHttpResponse executeInternal() {
|
||||
return getClientHttpResponse(httpMethod, uri, getHeaders(), getBodyAsBytes());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private ClientHttpResponse getClientHttpResponse(
|
||||
HttpMethod httpMethod, URI uri, HttpHeaders requestHeaders, byte[] requestBody) {
|
||||
|
||||
try {
|
||||
Cookie[] cookies = parseCookies(requestHeaders.get(HttpHeaders.COOKIE));
|
||||
MockHttpServletRequestBuilder requestBuilder = request(httpMethod, uri)
|
||||
.content(requestBody).headers(requestHeaders);
|
||||
if (cookies.length > 0) {
|
||||
requestBuilder.cookie(cookies);
|
||||
}
|
||||
MockHttpServletResponse servletResponse = this.mockMvc
|
||||
.perform(requestBuilder)
|
||||
.andReturn()
|
||||
.getResponse();
|
||||
|
||||
HttpStatusCode status = HttpStatusCode.valueOf(servletResponse.getStatus());
|
||||
byte[] body = servletResponse.getContentAsByteArray();
|
||||
if (body.length == 0) {
|
||||
String error = servletResponse.getErrorMessage();
|
||||
if (StringUtils.hasLength(error)) {
|
||||
// sendError message as default body
|
||||
body = error.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
MockClientHttpResponse clientResponse = new MockClientHttpResponse(body, status);
|
||||
clientResponse.getHeaders().putAll(getResponseHeaders(servletResponse));
|
||||
return clientResponse;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
byte[] body = ex.toString().getBytes(StandardCharsets.UTF_8);
|
||||
return new MockClientHttpResponse(body, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private static Cookie[] parseCookies(@Nullable List<String> headerValues) {
|
||||
if (headerValues == null) {
|
||||
return new Cookie[0];
|
||||
}
|
||||
return headerValues.stream()
|
||||
.flatMap(header -> StringUtils.commaDelimitedListToSet(header).stream())
|
||||
.map(MockMvcClientHttpRequestFactory::parseCookie)
|
||||
.toArray(Cookie[]::new);
|
||||
}
|
||||
|
||||
private static Cookie parseCookie(String cookie) {
|
||||
String[] parts = StringUtils.split(cookie, "=");
|
||||
Assert.isTrue(parts != null && parts.length == 2, "Invalid cookie: '" + cookie + "'");
|
||||
return new Cookie(parts[0], parts[1]);
|
||||
}
|
||||
|
||||
private HttpHeaders getResponseHeaders(MockHttpServletResponse response) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
for (String name : response.getHeaderNames()) {
|
||||
List<String> values = response.getHeaders(name);
|
||||
for (String value : values) {
|
||||
headers.add(name, value);
|
||||
}
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
}
|
||||
+656
@@ -0,0 +1,656 @@
|
||||
/*
|
||||
* Copyright 2002-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.test.web.servlet.client;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.charset.Charset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.test.json.JsonComparator;
|
||||
import org.springframework.test.json.JsonCompareMode;
|
||||
import org.springframework.test.json.JsonComparison;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MockMvcBuilder;
|
||||
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
import org.springframework.test.web.servlet.setup.RouterFunctionMockMvcBuilder;
|
||||
import org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.servlet.function.RouterFunction;
|
||||
import org.springframework.web.util.UriBuilder;
|
||||
import org.springframework.web.util.UriBuilderFactory;
|
||||
|
||||
/**
|
||||
* Client for testing web servers.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
*/
|
||||
public interface RestTestClient {
|
||||
|
||||
/**
|
||||
* The name of a request header used to assign a unique id to every request
|
||||
* performed through the {@code RestTestClient}. This can be useful for
|
||||
* storing contextual information at all phases of request processing (for example,
|
||||
* from a server-side component) under that id and later to look up
|
||||
* that information once an {@link ExchangeResult} is available.
|
||||
*/
|
||||
String RESTTESTCLIENT_REQUEST_ID = "RestTestClient-Request-Id";
|
||||
|
||||
/**
|
||||
* Prepare an HTTP GET request.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
RequestHeadersUriSpec<?> get();
|
||||
|
||||
/**
|
||||
* Prepare an HTTP HEAD request.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
RequestHeadersUriSpec<?> head();
|
||||
|
||||
/**
|
||||
* Prepare an HTTP POST request.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
RequestBodyUriSpec post();
|
||||
|
||||
/**
|
||||
* Prepare an HTTP PUT request.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
RequestBodyUriSpec put();
|
||||
|
||||
/**
|
||||
* Prepare an HTTP PATCH request.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
RequestBodyUriSpec patch();
|
||||
|
||||
/**
|
||||
* Prepare an HTTP DELETE request.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
RequestHeadersUriSpec<?> delete();
|
||||
|
||||
/**
|
||||
* Prepare an HTTP OPTIONS request.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
RequestHeadersUriSpec<?> options();
|
||||
|
||||
/**
|
||||
* Prepare a request for the specified {@code HttpMethod}.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
RequestBodyUriSpec method(HttpMethod method);
|
||||
|
||||
/**
|
||||
* Return a builder to mutate properties of this test client.
|
||||
*/
|
||||
<B extends Builder<B>> Builder<B> mutate();
|
||||
|
||||
/**
|
||||
* Begin creating a {@link RestTestClient} by providing the {@code @Controller}
|
||||
* instance(s) to handle requests with.
|
||||
* <p>Internally this is delegated to and equivalent to using
|
||||
* {@link org.springframework.test.web.servlet.setup.MockMvcBuilders#standaloneSetup(Object...)}
|
||||
* to initialize {@link MockMvc}.
|
||||
*/
|
||||
static MockServerBuilder<StandaloneMockMvcBuilder> standaloneSetup(Object... controllers) {
|
||||
StandaloneMockMvcBuilder builder = MockMvcBuilders.standaloneSetup(controllers);
|
||||
return new DefaultMockServerBuilder<>(builder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin creating a {@link RestTestClient} by providing the {@link RouterFunction}
|
||||
* instance(s) to handle requests with.
|
||||
* <p>Internally this is delegated to and equivalent to using
|
||||
* {@link org.springframework.test.web.servlet.setup.MockMvcBuilders#routerFunctions(RouterFunction[])}
|
||||
* to initialize {@link MockMvc}.
|
||||
*/
|
||||
static MockServerBuilder<RouterFunctionMockMvcBuilder> bindToRouterFunction(RouterFunction<?>... routerFunctions) {
|
||||
RouterFunctionMockMvcBuilder builder = MockMvcBuilders.routerFunctions(routerFunctions);
|
||||
return new DefaultMockServerBuilder<>(builder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin creating a {@link RestTestClient} by providing a
|
||||
* {@link WebApplicationContext} with Spring MVC infrastructure and
|
||||
* controllers.
|
||||
* <p>Internally this is delegated to and equivalent to using
|
||||
* {@link org.springframework.test.web.servlet.setup.MockMvcBuilders#webAppContextSetup(WebApplicationContext)}
|
||||
* to initialize {@code MockMvc}.
|
||||
*/
|
||||
static MockServerBuilder<DefaultMockMvcBuilder> bindToApplicationContext(WebApplicationContext context) {
|
||||
DefaultMockMvcBuilder builder = MockMvcBuilders.webAppContextSetup(context);
|
||||
return new DefaultMockServerBuilder<>(builder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin creating a {@link RestTestClient} by providing an already
|
||||
* initialized {@link MockMvc} instance to use as the server.
|
||||
*/
|
||||
static <B extends Builder<B>> Builder<B> bindTo(MockMvc mockMvc) {
|
||||
ClientHttpRequestFactory requestFactory = new MockMvcClientHttpRequestFactory(mockMvc);
|
||||
return RestTestClient.bindToServer(requestFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* This server setup option allows you to connect to a live server through
|
||||
* a client connector.
|
||||
* <p><pre class="code">
|
||||
* RestTestClient client = RestTestClient.bindToServer()
|
||||
* .baseUrl("http://localhost:8080")
|
||||
* .build();
|
||||
* </pre>
|
||||
* @return chained API to customize client config
|
||||
*/
|
||||
static <B extends Builder<B>> Builder<B> bindToServer() {
|
||||
return new DefaultRestTestClientBuilder<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* A variant of {@link #bindToServer()} with a pre-configured request factory.
|
||||
* @return chained API to customize client config
|
||||
*/
|
||||
static <B extends Builder<B>> Builder<B> bindToServer(ClientHttpRequestFactory requestFactory) {
|
||||
return new DefaultRestTestClientBuilder<>(RestClient.builder().requestFactory(requestFactory));
|
||||
}
|
||||
|
||||
/**
|
||||
* Specification for providing request headers and the URI of a request.
|
||||
*
|
||||
* @param <S> a self reference to the spec type
|
||||
*/
|
||||
interface RequestHeadersUriSpec<S extends RequestHeadersSpec<S>> extends UriSpec<S>, RequestHeadersSpec<S> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Specification for providing the body and the URI of a request.
|
||||
*/
|
||||
interface RequestBodyUriSpec extends RequestBodySpec, RequestHeadersUriSpec<RequestBodySpec> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Chained API for applying assertions to a response.
|
||||
*/
|
||||
interface ResponseSpec {
|
||||
/**
|
||||
* Assertions on the response status.
|
||||
*/
|
||||
StatusAssertions expectStatus();
|
||||
|
||||
/**
|
||||
* Consume and decode the response body to {@code byte[]} and then apply
|
||||
* assertions on the raw content (for example, isEmpty, JSONPath, etc.).
|
||||
*/
|
||||
BodyContentSpec expectBody();
|
||||
|
||||
/**
|
||||
* Consume and decode the response body to a single object of type
|
||||
* {@code <B>} and then apply assertions.
|
||||
* @param bodyType the expected body type
|
||||
*/
|
||||
<B> BodySpec<B, ?> expectBody(Class<B> bodyType);
|
||||
|
||||
/**
|
||||
* Alternative to {@link #expectBody(Class)} that accepts information
|
||||
* about a target type with generics.
|
||||
*/
|
||||
<B> BodySpec<B, ?> expectBody(ParameterizedTypeReference<B> bodyType);
|
||||
|
||||
/**
|
||||
* Assertions on the cookies of the response.
|
||||
*/
|
||||
CookieAssertions expectCookie();
|
||||
|
||||
/**
|
||||
* Assertions on the headers of the response.
|
||||
*/
|
||||
HeaderAssertions expectHeader();
|
||||
|
||||
/**
|
||||
* Apply multiple assertions to a response with the given
|
||||
* {@linkplain RestTestClient.ResponseSpec.ResponseSpecConsumer consumers}, with the guarantee that
|
||||
* all assertions will be applied even if one or more assertions fails
|
||||
* with an exception.
|
||||
* <p>If a single {@link Error} or {@link RuntimeException} is thrown,
|
||||
* it will be rethrown.
|
||||
* <p>If multiple exceptions are thrown, this method will throw an
|
||||
* {@link AssertionError} whose error message is a summary of all the
|
||||
* exceptions. In addition, each exception will be added as a
|
||||
* {@linkplain Throwable#addSuppressed(Throwable) suppressed exception} to
|
||||
* the {@code AssertionError}.
|
||||
* <p>This feature is similar to the {@code SoftAssertions} support in
|
||||
* AssertJ and the {@code assertAll()} support in JUnit Jupiter.
|
||||
*
|
||||
* <h4>Example</h4>
|
||||
* <pre class="code">
|
||||
* restTestClient.get().uri("/hello").exchange()
|
||||
* .expectAll(
|
||||
* responseSpec -> responseSpec.expectStatus().isOk(),
|
||||
* responseSpec -> responseSpec.expectBody(String.class).isEqualTo("Hello, World!")
|
||||
* );
|
||||
* </pre>
|
||||
* @param consumers the list of {@code ResponseSpec} consumers
|
||||
*/
|
||||
ResponseSpec expectAll(ResponseSpecConsumer... consumers);
|
||||
|
||||
/**
|
||||
* Exit the chained flow in order to consume the response body
|
||||
* externally.
|
||||
*/
|
||||
<T> EntityExchangeResult<T> returnResult(Class<T> elementClass);
|
||||
|
||||
/**
|
||||
* Alternative to {@link #returnResult(Class)} that accepts information
|
||||
* about a target type with generics.
|
||||
*/
|
||||
<T> EntityExchangeResult<T> returnResult(ParameterizedTypeReference<T> elementTypeRef);
|
||||
|
||||
/**
|
||||
* {@link Consumer} of a {@link RestTestClient.ResponseSpec}.
|
||||
* @see RestTestClient.ResponseSpec#expectAll(RestTestClient.ResponseSpec.ResponseSpecConsumer...)
|
||||
*/
|
||||
@FunctionalInterface
|
||||
interface ResponseSpecConsumer extends Consumer<ResponseSpec> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Spec for expectations on the response body content.
|
||||
*/
|
||||
interface BodyContentSpec {
|
||||
/**
|
||||
* Assert the response body is empty and return the exchange result.
|
||||
*/
|
||||
EntityExchangeResult<Void> isEmpty();
|
||||
|
||||
/**
|
||||
* Parse the expected and actual response content as JSON and perform a
|
||||
* comparison verifying that they contain the same attribute-value pairs
|
||||
* regardless of formatting with <em>lenient</em> checking (extensible
|
||||
* and non-strict array ordering).
|
||||
* <p>Use of this method requires the
|
||||
* <a href="https://jsonassert.skyscreamer.org/">JSONassert</a> library
|
||||
* to be on the classpath.
|
||||
* @param expectedJson the expected JSON content
|
||||
* @see #json(String, JsonCompareMode)
|
||||
*/
|
||||
default BodyContentSpec json(String expectedJson) {
|
||||
return json(expectedJson, JsonCompareMode.LENIENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the expected and actual response content as JSON and perform a
|
||||
* comparison using the given {@linkplain JsonCompareMode mode}. If the
|
||||
* comparison failed, throws an {@link AssertionError} with the message
|
||||
* of the {@link JsonComparison}.
|
||||
* <p>Use of this method requires the
|
||||
* <a href="https://jsonassert.skyscreamer.org/">JSONassert</a> library
|
||||
* to be on the classpath.
|
||||
* @param expectedJson the expected JSON content
|
||||
* @param compareMode the compare mode
|
||||
* @see #json(String)
|
||||
*/
|
||||
BodyContentSpec json(String expectedJson, JsonCompareMode compareMode);
|
||||
|
||||
/**
|
||||
* Parse the expected and actual response content as JSON and perform a
|
||||
* comparison using the given {@link JsonComparator}. If the comparison
|
||||
* failed, throws an {@link AssertionError} with the message of the
|
||||
* {@link JsonComparison}.
|
||||
* @param expectedJson the expected JSON content
|
||||
* @param comparator the comparator to use
|
||||
*/
|
||||
BodyContentSpec json(String expectedJson, JsonComparator comparator);
|
||||
|
||||
/**
|
||||
* Parse expected and actual response content as XML and assert that
|
||||
* the two are "similar", i.e. they contain the same elements and
|
||||
* attributes regardless of order.
|
||||
* <p>Use of this method requires the
|
||||
* <a href="https://github.com/xmlunit/xmlunit">XMLUnit</a> library on
|
||||
* the classpath.
|
||||
* @param expectedXml the expected XML content.
|
||||
* @see org.springframework.test.util.XmlExpectationsHelper#assertXmlEqual(String, String)
|
||||
*/
|
||||
BodyContentSpec xml(String expectedXml);
|
||||
|
||||
/**
|
||||
* Access to response body assertions using an XPath expression to
|
||||
* inspect a specific subset of the body.
|
||||
* <p>The XPath expression can be a parameterized string using
|
||||
* formatting specifiers as defined in {@link String#format}.
|
||||
* @param expression the XPath expression
|
||||
* @param args arguments to parameterize the expression
|
||||
* @see #xpath(String, Map, Object...)
|
||||
*/
|
||||
default XpathAssertions xpath(String expression, Object... args) {
|
||||
return xpath(expression, null, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Access to response body assertions with specific namespaces using an
|
||||
* XPath expression to inspect a specific subset of the body.
|
||||
* <p>The XPath expression can be a parameterized string using
|
||||
* formatting specifiers as defined in {@link String#format}.
|
||||
* @param expression the XPath expression
|
||||
* @param namespaces the namespaces to use
|
||||
* @param args arguments to parameterize the expression
|
||||
*/
|
||||
XpathAssertions xpath(String expression, @Nullable Map<String, String> namespaces, Object... args);
|
||||
|
||||
/**
|
||||
* Access to response body assertions using a
|
||||
* <a href="https://github.com/jayway/JsonPath">JsonPath</a> expression
|
||||
* to inspect a specific subset of the body.
|
||||
* @param expression the JsonPath expression
|
||||
*/
|
||||
JsonPathAssertions jsonPath(String expression);
|
||||
|
||||
/**
|
||||
* Exit the chained API and return an {@code ExchangeResult} with the
|
||||
* raw response content.
|
||||
*/
|
||||
EntityExchangeResult<byte[]> returnResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Spec for expectations on the response body decoded to a single Object.
|
||||
*
|
||||
* @param <S> a self reference to the spec type
|
||||
* @param <B> the body type
|
||||
*/
|
||||
interface BodySpec<B, S extends BodySpec<B, S>> {
|
||||
/**
|
||||
* Transform the extracted the body with a function, for example, extracting a
|
||||
* property, and assert the mapped value with a {@link Matcher}.
|
||||
*/
|
||||
<T extends S, R> T value(Function<B, R> bodyMapper, Matcher<? super R> matcher);
|
||||
|
||||
/**
|
||||
* Assert the extracted body with a {@link Consumer}.
|
||||
*/
|
||||
<T extends S> T value(Consumer<B> consumer);
|
||||
|
||||
/**
|
||||
* Assert the exchange result with the given {@link Consumer}.
|
||||
*/
|
||||
<T extends S> T consumeWith(Consumer<EntityExchangeResult<B>> consumer);
|
||||
|
||||
/**
|
||||
* Exit the chained API and return an {@code EntityExchangeResult} with the
|
||||
* decoded response content.
|
||||
*/
|
||||
EntityExchangeResult<B> returnResult();
|
||||
|
||||
/**
|
||||
* Assert the extracted body is equal to the given value.
|
||||
*/
|
||||
<T extends S> T isEqualTo(B expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specification for providing the URI of a request.
|
||||
*
|
||||
* @param <S> a self reference to the spec type
|
||||
*/
|
||||
interface UriSpec<S extends RequestHeadersSpec<?>> {
|
||||
/**
|
||||
* Specify the URI using an absolute, fully constructed {@link java.net.URI}.
|
||||
* <p>If a {@link UriBuilderFactory} was configured for the client with
|
||||
* a base URI, that base URI will <strong>not</strong> be applied to the
|
||||
* supplied {@code java.net.URI}. If you wish to have a base URI applied to a
|
||||
* {@code java.net.URI} you must invoke either {@link #uri(String, Object...)}
|
||||
* or {@link #uri(String, Map)} — for example, {@code uri(myUri.toString())}.
|
||||
* @return spec to add headers or perform the exchange
|
||||
*/
|
||||
S uri(URI uri);
|
||||
|
||||
/**
|
||||
* Specify the URI for the request using a URI template and URI variables.
|
||||
* <p>If a {@link UriBuilderFactory} was configured for the client (for example,
|
||||
* with a base URI) it will be used to expand the URI template.
|
||||
* @return spec to add headers or perform the exchange
|
||||
*/
|
||||
S uri(String uri, Object... uriVariables);
|
||||
|
||||
/**
|
||||
* Specify the URI for the request using a URI template and URI variables.
|
||||
* <p>If a {@link UriBuilderFactory} was configured for the client (for example,
|
||||
* with a base URI) it will be used to expand the URI template.
|
||||
* @return spec to add headers or perform the exchange
|
||||
*/
|
||||
S uri(String uri, Map<String, ?> uriVariables);
|
||||
|
||||
/**
|
||||
* Build the URI for the request with a {@link UriBuilder} obtained
|
||||
* through the {@link UriBuilderFactory} configured for this client.
|
||||
* @return spec to add headers or perform the exchange
|
||||
*/
|
||||
S uri(Function<UriBuilder, URI> uriFunction);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Specification for adding request headers and performing an exchange.
|
||||
*
|
||||
* @param <S> a self reference to the spec type
|
||||
*/
|
||||
interface RequestHeadersSpec<S extends RequestHeadersSpec<S>> {
|
||||
|
||||
/**
|
||||
* Set the list of acceptable {@linkplain MediaType media types}, as
|
||||
* specified by the {@code Accept} header.
|
||||
* @param acceptableMediaTypes the acceptable media types
|
||||
* @return the same instance
|
||||
*/
|
||||
S accept(MediaType... acceptableMediaTypes);
|
||||
|
||||
/**
|
||||
* Set the list of acceptable {@linkplain Charset charsets}, as specified
|
||||
* by the {@code Accept-Charset} header.
|
||||
* @param acceptableCharsets the acceptable charsets
|
||||
* @return the same instance
|
||||
*/
|
||||
S acceptCharset(Charset... acceptableCharsets);
|
||||
|
||||
/**
|
||||
* Add a cookie with the given name and value.
|
||||
* @param name the cookie name
|
||||
* @param value the cookie value
|
||||
* @return the same instance
|
||||
*/
|
||||
S cookie(String name, String value);
|
||||
|
||||
/**
|
||||
* Manipulate this request's cookies with the given consumer. The
|
||||
* map provided to the consumer is "live", so that the consumer can be used to
|
||||
* {@linkplain MultiValueMap#set(Object, Object) overwrite} existing header values,
|
||||
* {@linkplain MultiValueMap#remove(Object) remove} values, or use any of the other
|
||||
* {@link MultiValueMap} methods.
|
||||
* @param cookiesConsumer a function that consumes the cookies map
|
||||
* @return this builder
|
||||
*/
|
||||
S cookies(Consumer<MultiValueMap<String, String>> cookiesConsumer);
|
||||
|
||||
/**
|
||||
* Set the value of the {@code If-Modified-Since} header.
|
||||
* <p>The date should be specified as the number of milliseconds since
|
||||
* January 1, 1970 GMT.
|
||||
* @param ifModifiedSince the new value of the header
|
||||
* @return the same instance
|
||||
*/
|
||||
S ifModifiedSince(ZonedDateTime ifModifiedSince);
|
||||
|
||||
/**
|
||||
* Set the values of the {@code If-None-Match} header.
|
||||
* @param ifNoneMatches the new value of the header
|
||||
* @return the same instance
|
||||
*/
|
||||
S ifNoneMatch(String... ifNoneMatches);
|
||||
|
||||
/**
|
||||
* Add the given, single header value under the given name.
|
||||
* @param headerName the header name
|
||||
* @param headerValues the header value(s)
|
||||
* @return the same instance
|
||||
*/
|
||||
S header(String headerName, String... headerValues);
|
||||
|
||||
/**
|
||||
* Manipulate the request's headers with the given consumer. The
|
||||
* headers provided to the consumer are "live", so that the consumer can be used to
|
||||
* {@linkplain HttpHeaders#set(String, String) overwrite} existing header values,
|
||||
* {@linkplain HttpHeaders#remove(String) remove} values, or use any of the other
|
||||
* {@link HttpHeaders} methods.
|
||||
* @param headersConsumer a function that consumes the {@code HttpHeaders}
|
||||
* @return this builder
|
||||
*/
|
||||
S headers(Consumer<HttpHeaders> headersConsumer);
|
||||
|
||||
/**
|
||||
* Set the attribute with the given name to the given value.
|
||||
* @param name the name of the attribute to add
|
||||
* @param value the value of the attribute to add
|
||||
* @return this builder
|
||||
*/
|
||||
S attribute(String name, Object value);
|
||||
|
||||
/**
|
||||
* Manipulate the request attributes with the given consumer. The attributes provided to
|
||||
* the consumer are "live", so that the consumer can be used to inspect attributes,
|
||||
* remove attributes, or use any of the other map-provided methods.
|
||||
* @param attributesConsumer a function that consumes the attributes
|
||||
* @return this builder
|
||||
*/
|
||||
S attributes(Consumer<Map<String, Object>> attributesConsumer);
|
||||
|
||||
/**
|
||||
* Perform the exchange without a request body.
|
||||
* @return spec for decoding the response
|
||||
*/
|
||||
ResponseSpec exchange();
|
||||
}
|
||||
|
||||
/**
|
||||
* Specification for providing body of a request.
|
||||
*/
|
||||
interface RequestBodySpec extends RequestHeadersSpec<RequestBodySpec> {
|
||||
/**
|
||||
* Set the {@linkplain MediaType media type} of the body, as specified
|
||||
* by the {@code Content-Type} header.
|
||||
* @param contentType the content type
|
||||
* @return the same instance
|
||||
* @see HttpHeaders#setContentType(MediaType)
|
||||
*/
|
||||
RequestBodySpec contentType(MediaType contentType);
|
||||
|
||||
/**
|
||||
* Set the body to the given {@code Object} value. This method invokes the
|
||||
* {@link org.springframework.web.client.RestClient.RequestBodySpec#body(Object)} (Object)
|
||||
* bodyValue} method on the underlying {@code RestClient}.
|
||||
* @param body the value to write to the request body
|
||||
* @return spec for further declaration of the request
|
||||
*/
|
||||
RequestHeadersSpec<?> body(Object body);
|
||||
}
|
||||
|
||||
interface Builder<B extends Builder<B>> {
|
||||
/**
|
||||
* Apply the given {@code Consumer} to this builder instance.
|
||||
* <p>This can be useful for applying pre-packaged customizations.
|
||||
* @param builderConsumer the consumer to apply
|
||||
*/
|
||||
Builder<B> apply(Consumer<Builder<B>> builderConsumer);
|
||||
|
||||
/**
|
||||
* Add the given cookie to all requests.
|
||||
* @param cookieName the cookie name
|
||||
* @param cookieValues the cookie values
|
||||
*/
|
||||
Builder<B> defaultCookie(String cookieName, String... cookieValues);
|
||||
|
||||
/**
|
||||
* Manipulate the default cookies with the given consumer. The
|
||||
* map provided to the consumer is "live", so that the consumer can be used to
|
||||
* {@linkplain MultiValueMap#set(Object, Object) overwrite} existing header values,
|
||||
* {@linkplain MultiValueMap#remove(Object) remove} values, or use any of the other
|
||||
* {@link MultiValueMap} methods.
|
||||
* @param cookiesConsumer a function that consumes the cookies map
|
||||
* @return this builder
|
||||
*/
|
||||
Builder<B> defaultCookies(Consumer<MultiValueMap<String, String>> cookiesConsumer);
|
||||
|
||||
/**
|
||||
* Add the given header to all requests that haven't added it.
|
||||
* @param headerName the header name
|
||||
* @param headerValues the header values
|
||||
*/
|
||||
Builder<B> defaultHeader(String headerName, String... headerValues);
|
||||
|
||||
/**
|
||||
* Manipulate the default headers with the given consumer. The
|
||||
* headers provided to the consumer are "live", so that the consumer can be used to
|
||||
* {@linkplain HttpHeaders#set(String, String) overwrite} existing header values,
|
||||
* {@linkplain HttpHeaders#remove(String) remove} values, or use any of the other
|
||||
* {@link HttpHeaders} methods.
|
||||
* @param headersConsumer a function that consumes the {@code HttpHeaders}
|
||||
* @return this builder
|
||||
*/
|
||||
Builder<B> defaultHeaders(Consumer<HttpHeaders> headersConsumer);
|
||||
|
||||
/**
|
||||
* Provide a pre-configured {@link UriBuilderFactory} instance as an
|
||||
* alternative to and effectively overriding {@link #baseUrl(String)}.
|
||||
*/
|
||||
Builder<B> uriBuilderFactory(UriBuilderFactory uriFactory);
|
||||
|
||||
/**
|
||||
* Build the {@link RestTestClient} instance.
|
||||
*/
|
||||
RestTestClient build();
|
||||
|
||||
/**
|
||||
* Configure a base URI as described in
|
||||
* {@link RestClient#create(String)
|
||||
* WebClient.create(String)}.
|
||||
*/
|
||||
Builder<B> baseUrl(String baseUrl);
|
||||
}
|
||||
|
||||
interface MockServerBuilder<M extends MockMvcBuilder> extends Builder<MockServerBuilder<M>> {
|
||||
MockServerBuilder<M> configureServer(Consumer<M> consumer);
|
||||
}
|
||||
}
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
/*
|
||||
* Copyright 2002-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.test.web.servlet.client;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
import org.hamcrest.MatcherAssert;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.test.util.AssertionErrors;
|
||||
import org.springframework.test.web.servlet.client.RestTestClient.ResponseSpec;
|
||||
|
||||
import static org.springframework.test.util.AssertionErrors.assertNotNull;
|
||||
|
||||
/**
|
||||
* Assertions on the response status.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
*
|
||||
* @see ResponseSpec#expectStatus()
|
||||
*/
|
||||
public class StatusAssertions {
|
||||
|
||||
private final ExchangeResult exchangeResult;
|
||||
|
||||
private final ResponseSpec responseSpec;
|
||||
|
||||
public StatusAssertions(ExchangeResult exchangeResult, ResponseSpec responseSpec) {
|
||||
this.exchangeResult = exchangeResult;
|
||||
this.responseSpec = responseSpec;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Assert the response status as an {@link HttpStatusCode}.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec isEqualTo(HttpStatusCode status) {
|
||||
HttpStatusCode actual = this.exchangeResult.getStatus();
|
||||
this.exchangeResult.assertWithDiagnostics(() -> AssertionErrors.assertEquals("Status", status, actual));
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status as an integer.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec isEqualTo(int status) {
|
||||
return isEqualTo(HttpStatusCode.valueOf(status));
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.OK} (200).
|
||||
*/
|
||||
public RestTestClient.ResponseSpec isOk() {
|
||||
return assertStatusAndReturn(HttpStatus.OK);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.CREATED} (201).
|
||||
*/
|
||||
public RestTestClient.ResponseSpec isCreated() {
|
||||
return assertStatusAndReturn(HttpStatus.CREATED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.ACCEPTED} (202).
|
||||
*/
|
||||
public RestTestClient.ResponseSpec isAccepted() {
|
||||
return assertStatusAndReturn(HttpStatus.ACCEPTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.NO_CONTENT} (204).
|
||||
*/
|
||||
public RestTestClient.ResponseSpec isNoContent() {
|
||||
return assertStatusAndReturn(HttpStatus.NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.FOUND} (302).
|
||||
*/
|
||||
public RestTestClient.ResponseSpec isFound() {
|
||||
return assertStatusAndReturn(HttpStatus.FOUND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.SEE_OTHER} (303).
|
||||
*/
|
||||
public RestTestClient.ResponseSpec isSeeOther() {
|
||||
return assertStatusAndReturn(HttpStatus.SEE_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.NOT_MODIFIED} (304).
|
||||
*/
|
||||
public RestTestClient.ResponseSpec isNotModified() {
|
||||
return assertStatusAndReturn(HttpStatus.NOT_MODIFIED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.TEMPORARY_REDIRECT} (307).
|
||||
*/
|
||||
public RestTestClient.ResponseSpec isTemporaryRedirect() {
|
||||
return assertStatusAndReturn(HttpStatus.TEMPORARY_REDIRECT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.PERMANENT_REDIRECT} (308).
|
||||
*/
|
||||
public RestTestClient.ResponseSpec isPermanentRedirect() {
|
||||
return assertStatusAndReturn(HttpStatus.PERMANENT_REDIRECT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.BAD_REQUEST} (400).
|
||||
*/
|
||||
public RestTestClient.ResponseSpec isBadRequest() {
|
||||
return assertStatusAndReturn(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.UNAUTHORIZED} (401).
|
||||
*/
|
||||
public RestTestClient.ResponseSpec isUnauthorized() {
|
||||
return assertStatusAndReturn(HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.FORBIDDEN} (403).
|
||||
* @since 5.0.2
|
||||
*/
|
||||
public RestTestClient.ResponseSpec isForbidden() {
|
||||
return assertStatusAndReturn(HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.NOT_FOUND} (404).
|
||||
*/
|
||||
public RestTestClient.ResponseSpec isNotFound() {
|
||||
return assertStatusAndReturn(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response error message.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec reasonEquals(String reason) {
|
||||
String actual = getReasonPhrase(this.exchangeResult.getStatus());
|
||||
this.exchangeResult.assertWithDiagnostics(() ->
|
||||
AssertionErrors.assertEquals("Response status reason", reason, actual));
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
private static String getReasonPhrase(HttpStatusCode statusCode) {
|
||||
if (statusCode instanceof HttpStatus status) {
|
||||
return status.getReasonPhrase();
|
||||
}
|
||||
else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Assert the response status code is in the 1xx range.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec is1xxInformational() {
|
||||
return assertSeriesAndReturn(HttpStatus.Series.INFORMATIONAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is in the 2xx range.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec is2xxSuccessful() {
|
||||
return assertSeriesAndReturn(HttpStatus.Series.SUCCESSFUL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is in the 3xx range.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec is3xxRedirection() {
|
||||
return assertSeriesAndReturn(HttpStatus.Series.REDIRECTION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is in the 4xx range.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec is4xxClientError() {
|
||||
return assertSeriesAndReturn(HttpStatus.Series.CLIENT_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is in the 5xx range.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec is5xxServerError() {
|
||||
return assertSeriesAndReturn(HttpStatus.Series.SERVER_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Match the response status value with a Hamcrest matcher.
|
||||
* @param matcher the matcher to use
|
||||
* @since 5.1
|
||||
*/
|
||||
public RestTestClient.ResponseSpec value(Matcher<? super Integer> matcher) {
|
||||
int actual = this.exchangeResult.getStatus().value();
|
||||
this.exchangeResult.assertWithDiagnostics(() -> MatcherAssert.assertThat("Response status", actual, matcher));
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume the response status value as an integer.
|
||||
* @param consumer the consumer to use
|
||||
* @since 5.1
|
||||
*/
|
||||
public RestTestClient.ResponseSpec value(Consumer<Integer> consumer) {
|
||||
int actual = this.exchangeResult.getStatus().value();
|
||||
this.exchangeResult.assertWithDiagnostics(() -> consumer.accept(actual));
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
|
||||
private ResponseSpec assertStatusAndReturn(HttpStatus expected) {
|
||||
assertNotNull("exchangeResult unexpectedly null", this.exchangeResult);
|
||||
HttpStatusCode actual = this.exchangeResult.getStatus();
|
||||
this.exchangeResult.assertWithDiagnostics(() -> AssertionErrors.assertEquals("Status", expected, actual));
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
private RestTestClient.ResponseSpec assertSeriesAndReturn(HttpStatus.Series expected) {
|
||||
HttpStatusCode status = this.exchangeResult.getStatus();
|
||||
HttpStatus.Series series = HttpStatus.Series.resolve(status.value());
|
||||
this.exchangeResult.assertWithDiagnostics(() ->
|
||||
AssertionErrors.assertEquals("Range for response status value " + status, expected, series));
|
||||
return this.responseSpec;
|
||||
}
|
||||
}
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
* Copyright 2002-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.test.web.servlet.client;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.xml.xpath.XPathExpressionException;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.test.util.XpathExpectationsHelper;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MimeType;
|
||||
|
||||
/**
|
||||
* XPath assertions for the {@link RestTestClient}.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
*/
|
||||
public class XpathAssertions {
|
||||
|
||||
private final RestTestClient.BodyContentSpec bodySpec;
|
||||
|
||||
private final XpathExpectationsHelper xpathHelper;
|
||||
|
||||
|
||||
XpathAssertions(RestTestClient.BodyContentSpec spec,
|
||||
String expression, @Nullable Map<String, String> namespaces, Object... args) {
|
||||
|
||||
this.bodySpec = spec;
|
||||
this.xpathHelper = initXpathHelper(expression, namespaces, args);
|
||||
}
|
||||
|
||||
private static XpathExpectationsHelper initXpathHelper(
|
||||
String expression, @Nullable Map<String, String> namespaces, Object[] args) {
|
||||
|
||||
try {
|
||||
return new XpathExpectationsHelper(expression, namespaces, args);
|
||||
}
|
||||
catch (XPathExpressionException ex) {
|
||||
throw new AssertionError("XML parsing error", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delegates to {@link XpathExpectationsHelper#assertString(byte[], String, String)}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec isEqualTo(String expectedValue) {
|
||||
return assertWith(() -> this.xpathHelper.assertString(getContent(), getCharset(), expectedValue));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates to {@link XpathExpectationsHelper#assertNumber(byte[], String, Double)}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec isEqualTo(Double expectedValue) {
|
||||
return assertWith(() -> this.xpathHelper.assertNumber(getContent(), getCharset(), expectedValue));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates to {@link XpathExpectationsHelper#assertBoolean(byte[], String, boolean)}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec isEqualTo(boolean expectedValue) {
|
||||
return assertWith(() -> this.xpathHelper.assertBoolean(getContent(), getCharset(), expectedValue));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates to {@link XpathExpectationsHelper#exists(byte[], String)}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec exists() {
|
||||
return assertWith(() -> this.xpathHelper.exists(getContent(), getCharset()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates to {@link XpathExpectationsHelper#doesNotExist(byte[], String)}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec doesNotExist() {
|
||||
return assertWith(() -> this.xpathHelper.doesNotExist(getContent(), getCharset()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates to {@link XpathExpectationsHelper#assertNodeCount(byte[], String, int)}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec nodeCount(int expectedCount) {
|
||||
return assertWith(() -> this.xpathHelper.assertNodeCount(getContent(), getCharset(), expectedCount));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates to {@link XpathExpectationsHelper#assertString(byte[], String, Matcher)}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec string(Matcher<? super String> matcher){
|
||||
return assertWith(() -> this.xpathHelper.assertString(getContent(), getCharset(), matcher));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates to {@link XpathExpectationsHelper#assertNumber(byte[], String, Matcher)}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec number(Matcher<? super Double> matcher){
|
||||
return assertWith(() -> this.xpathHelper.assertNumber(getContent(), getCharset(), matcher));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates to {@link XpathExpectationsHelper#assertNodeCount(byte[], String, Matcher)}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec nodeCount(Matcher<? super Integer> matcher){
|
||||
return assertWith(() -> this.xpathHelper.assertNodeCount(getContent(), getCharset(), matcher));
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume the result of the XPath evaluation as a String.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec string(Consumer<String> consumer){
|
||||
return assertWith(() -> {
|
||||
String value = this.xpathHelper.evaluateXpath(getContent(), getCharset(), String.class);
|
||||
consumer.accept(value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume the result of the XPath evaluation as a Double.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec number(Consumer<Double> consumer){
|
||||
return assertWith(() -> {
|
||||
Double value = this.xpathHelper.evaluateXpath(getContent(), getCharset(), Double.class);
|
||||
consumer.accept(value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume the count of nodes as result of the XPath evaluation.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec nodeCount(Consumer<Integer> consumer){
|
||||
return assertWith(() -> {
|
||||
Integer value = this.xpathHelper.evaluateXpath(getContent(), getCharset(), Integer.class);
|
||||
consumer.accept(value);
|
||||
});
|
||||
}
|
||||
|
||||
private RestTestClient.BodyContentSpec assertWith(CheckedExceptionTask task) {
|
||||
try {
|
||||
task.run();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new AssertionError("XML parsing error", ex);
|
||||
}
|
||||
return this.bodySpec;
|
||||
}
|
||||
|
||||
private byte[] getContent() {
|
||||
byte[] body = this.bodySpec.returnResult().getResponseBody();
|
||||
Assert.notNull(body, "Expected body content");
|
||||
return body;
|
||||
}
|
||||
|
||||
private String getCharset() {
|
||||
return Optional.of(this.bodySpec.returnResult())
|
||||
.map(EntityExchangeResult::getResponseHeaders)
|
||||
.map(HttpHeaders::getContentType)
|
||||
.map(MimeType::getCharset)
|
||||
.orElse(StandardCharsets.UTF_8)
|
||||
.name();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
throw new AssertionError("Object#equals is disabled " +
|
||||
"to avoid being used in error instead of XPathAssertions#isEqualTo(String).");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return super.hashCode();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Lets us be able to use lambda expressions that could throw checked exceptions, since
|
||||
* {@link XpathExpectationsHelper} throws {@link Exception} on its methods.
|
||||
*/
|
||||
private interface CheckedExceptionTask {
|
||||
|
||||
void run() throws Exception;
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user