diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/client/DefaultRestTestClient.java b/spring-test/src/main/java/org/springframework/test/web/servlet/client/DefaultRestTestClient.java index 4e69ffdc134..1289c8af449 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/client/DefaultRestTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/client/DefaultRestTestClient.java @@ -118,6 +118,8 @@ class DefaultRestTestClient implements RestTestClient { private final RestClient.RequestBodyUriSpec requestHeadersUriSpec; + private @Nullable String uriTemplate; + DefaultRequestBodyUriSpec(RestClient.RequestBodyUriSpec spec) { this.requestHeadersUriSpec = spec; String requestId = String.valueOf(requestIndex.incrementAndGet()); @@ -126,24 +128,28 @@ class DefaultRestTestClient implements RestTestClient { @Override public RequestBodySpec uri(String uriTemplate, @Nullable Object... uriVariables) { + this.uriTemplate = uriTemplate; this.requestHeadersUriSpec.uri(uriTemplate, uriVariables); return this; } @Override public RequestBodySpec uri(String uri, Map uriVariables) { + this.uriTemplate = uri; this.requestHeadersUriSpec.uri(uri, uriVariables); return this; } @Override public RequestBodySpec uri(Function uriFunction) { + this.uriTemplate = null; this.requestHeadersUriSpec.uri(uriFunction); return this; } @Override public RequestBodySpec uri(URI uri) { + this.uriTemplate = null; this.requestHeadersUriSpec.uri(uri); return this; } @@ -229,8 +235,8 @@ class DefaultRestTestClient implements RestTestClient { @Override public ResponseSpec exchange() { return new DefaultResponseSpec( - this.requestHeadersUriSpec.exchangeForRequiredValue( - (request, response) -> new ExchangeResult(response), false)); + this.requestHeadersUriSpec.exchangeForRequiredValue((request, response) -> + new ExchangeResult(request, response, this.uriTemplate), false)); } } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/client/ExchangeResult.java b/spring-test/src/main/java/org/springframework/test/web/servlet/client/ExchangeResult.java index a440f016d45..def8eee72b6 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/client/ExchangeResult.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/client/ExchangeResult.java @@ -18,10 +18,12 @@ package org.springframework.test.web.servlet.client; import java.io.IOException; import java.net.HttpCookie; +import java.net.URI; import java.util.List; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -29,6 +31,9 @@ import org.jspecify.annotations.Nullable; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpRequest; +import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseCookie; import org.springframework.util.Assert; @@ -54,23 +59,60 @@ public class ExchangeResult { private static final Log logger = LogFactory.getLog(ExchangeResult.class); + private final HttpRequest request; + private final ConvertibleClientHttpResponse clientResponse; + private final @Nullable String uriTemplate; + /** Ensure single logging; for example, for expectAll. */ private boolean diagnosticsLogged; - ExchangeResult(@Nullable ConvertibleClientHttpResponse response) { - Assert.notNull(response, "Response must not be null"); + ExchangeResult( + HttpRequest request, ConvertibleClientHttpResponse response, @Nullable String uriTemplate) { + + Assert.notNull(request, "HttpRequest must not be null"); + Assert.notNull(response, "ClientHttpResponse must not be null"); + this.request = request; this.clientResponse = response; + this.uriTemplate = uriTemplate; } ExchangeResult(ExchangeResult result) { - this(result.clientResponse); + this(result.request, result.clientResponse, result.uriTemplate); this.diagnosticsLogged = result.diagnosticsLogged; } + /** + * Return the method of the request. + */ + public HttpMethod getMethod() { + return this.request.getMethod(); + } + + /** + * Return the URI of the request. + */ + public URI getUrl() { + return this.request.getURI(); + } + + /** + * Return the original URI template used to prepare the request, if any. + */ + public @Nullable String getUriTemplate() { + return this.uriTemplate; + } + + /** + * Return the request headers sent to the server. + */ + public HttpHeaders getRequestHeaders() { + return this.request.getHeaders(); + } + /** * Return the HTTP status code as an {@link HttpStatusCode} value. */ @@ -146,4 +188,28 @@ public class ExchangeResult { } } + @Override + public String toString() { + return "\n" + + "> " + getMethod() + " " + getUrl() + "\n" + + "> " + formatHeaders(getRequestHeaders(), "\n> ") + "\n" + + "\n" + + "< " + formatStatus(getStatus()) + "\n" + + "< " + formatHeaders(getResponseHeaders(), "\n< ") + "\n"; + } + + private String formatStatus(HttpStatusCode statusCode) { + String result = statusCode.toString(); + if (statusCode instanceof HttpStatus status) { + result += " " + status.getReasonPhrase(); + } + return result; + } + + private String formatHeaders(HttpHeaders headers, String delimiter) { + return headers.headerSet().stream() + .map(entry -> entry.getKey() + ": " + entry.getValue()) + .collect(Collectors.joining(delimiter)); + } + } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/client/CookieAssertionsTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/client/CookieAssertionsTests.java index 51783fd3bc0..4412adba029 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/client/CookieAssertionsTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/client/CookieAssertionsTests.java @@ -16,12 +16,16 @@ package org.springframework.test.web.servlet.client; +import java.io.IOException; import java.time.Duration; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseCookie; +import org.springframework.mock.http.client.MockClientHttpRequest; import org.springframework.web.client.RestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -47,9 +51,14 @@ public class CookieAssertionsTests { .sameSite("Lax") .build(); - private final CookieAssertions assertions = cookieAssertions(cookie); + private CookieAssertions assertions; + @BeforeEach + void setUp() throws IOException { + this.assertions = cookieAssertions(cookie); + } + @Test void valueEquals() { assertions.valueEquals("foo", "bar"); @@ -135,12 +144,13 @@ public class CookieAssertionsTests { } - private CookieAssertions cookieAssertions(ResponseCookie cookie) { + private CookieAssertions cookieAssertions(ResponseCookie cookie) throws IOException { RestClient.RequestHeadersSpec.ConvertibleClientHttpResponse response = mock(); var headers = new HttpHeaders(); headers.set(HttpHeaders.SET_COOKIE, cookie.toString()); when(response.getHeaders()).thenReturn(headers); - ExchangeResult result = new ExchangeResult(response); + when(response.getStatusCode()).thenReturn(HttpStatusCode.valueOf(200)); + ExchangeResult result = new ExchangeResult(new MockClientHttpRequest(), response, null); return new CookieAssertions(result, mock()); } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/client/HeaderAssertionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/client/HeaderAssertionTests.java index 200210e6eed..3dd307acab4 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/client/HeaderAssertionTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/client/HeaderAssertionTests.java @@ -16,6 +16,7 @@ package org.springframework.test.web.servlet.client; +import java.io.IOException; import java.net.URI; import java.time.ZoneId; import java.time.ZonedDateTime; @@ -26,7 +27,9 @@ import org.junit.jupiter.api.Test; import org.springframework.http.CacheControl; import org.springframework.http.ContentDisposition; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; +import org.springframework.mock.http.client.MockClientHttpRequest; import org.springframework.web.client.RestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -311,10 +314,16 @@ class HeaderAssertionTests { } private HeaderAssertions headerAssertions(HttpHeaders responseHeaders) { + try { RestClient.RequestHeadersSpec.ConvertibleClientHttpResponse response = mock(); + when(response.getStatusCode()).thenReturn(HttpStatusCode.valueOf(200)); when(response.getHeaders()).thenReturn(responseHeaders); - ExchangeResult result = new ExchangeResult(response); + ExchangeResult result = new ExchangeResult(new MockClientHttpRequest(), response, null); return new HeaderAssertions(result, mock()); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } } } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/client/StatusAssertionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/client/StatusAssertionTests.java index ed2a836d295..099ca06f5bf 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/client/StatusAssertionTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/client/StatusAssertionTests.java @@ -20,8 +20,10 @@ import java.io.IOException; import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; +import org.springframework.mock.http.client.MockClientHttpRequest; import org.springframework.web.client.RestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -255,7 +257,8 @@ class StatusAssertionTests { try { RestClient.RequestHeadersSpec.ConvertibleClientHttpResponse response = mock(); when(response.getStatusCode()).thenReturn(HttpStatusCode.valueOf(status)); - ExchangeResult result = new ExchangeResult(response); + when(response.getHeaders()).thenReturn(new HttpHeaders()); + ExchangeResult result = new ExchangeResult(new MockClientHttpRequest(), response, null); return new StatusAssertions(result, mock()); } catch (IOException ex) {