Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 11dd0d6

Browse files
Provide access to raw content in RestTestClient
Closes gh-35399
1 parent 23d1b0e commit 11dd0d6

File tree

4 files changed

+119
-10
lines changed

4 files changed

+119
-10
lines changed

‎spring-test/src/main/java/org/springframework/test/web/servlet/client/DefaultRestTestClient.java‎

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@
1616

1717
package org.springframework.test.web.servlet.client;
1818

19+
import java.io.IOException;
1920
import java.net.URI;
2021
import java.nio.charset.Charset;
2122
import java.nio.charset.StandardCharsets;
2223
import java.time.ZonedDateTime;
2324
import java.util.Map;
2425
import java.util.Optional;
26+
import java.util.concurrent.ConcurrentHashMap;
2527
import java.util.concurrent.atomic.AtomicLong;
2628
import java.util.function.Consumer;
2729
import java.util.function.Function;
@@ -33,13 +35,18 @@
3335
import org.springframework.core.ParameterizedTypeReference;
3436
import org.springframework.http.HttpHeaders;
3537
import org.springframework.http.HttpMethod;
38+
import org.springframework.http.HttpRequest;
3639
import org.springframework.http.MediaType;
40+
import org.springframework.http.client.ClientHttpRequestExecution;
41+
import org.springframework.http.client.ClientHttpRequestInterceptor;
42+
import org.springframework.http.client.ClientHttpResponse;
3743
import org.springframework.test.json.JsonAssert;
3844
import org.springframework.test.json.JsonComparator;
3945
import org.springframework.test.json.JsonCompareMode;
4046
import org.springframework.test.util.AssertionErrors;
4147
import org.springframework.test.util.ExceptionCollector;
4248
import org.springframework.test.util.XmlExpectationsHelper;
49+
import org.springframework.util.Assert;
4350
import org.springframework.util.MimeType;
4451
import org.springframework.util.MultiValueMap;
4552
import org.springframework.web.client.RestClient;
@@ -56,6 +63,8 @@ class DefaultRestTestClient implements RestTestClient {
5663

5764
private final RestClient restClient;
5865

66+
private final WiretapInterceptor wiretapInterceptor = new WiretapInterceptor();
67+
5968
private final Consumer<EntityExchangeResult<?>> entityResultConsumer;
6069

6170
private final DefaultRestTestClientBuilder<?> restTestClientBuilder;
@@ -67,7 +76,7 @@ class DefaultRestTestClient implements RestTestClient {
6776
RestClient.Builder builder, Consumer<EntityExchangeResult<?>> entityResultConsumer,
6877
DefaultRestTestClientBuilder<?> restTestClientBuilder) {
6978

70-
this.restClient = builder.build();
79+
this.restClient = builder.requestInterceptor(this.wiretapInterceptor).build();
7180
this.entityResultConsumer = entityResultConsumer;
7281
this.restTestClientBuilder = restTestClientBuilder;
7382
}
@@ -128,12 +137,14 @@ private class DefaultRequestBodyUriSpec implements RequestBodyUriSpec {
128137

129138
private final RestClient.RequestBodyUriSpec requestHeadersUriSpec;
130139

140+
private final String requestId;
141+
131142
private @Nullable String uriTemplate;
132143

133144
DefaultRequestBodyUriSpec(RestClient.RequestBodyUriSpec spec) {
134145
this.requestHeadersUriSpec = spec;
135-
StringrequestId = String.valueOf(requestIndex.incrementAndGet());
136-
this.requestHeadersUriSpec.header(RESTTESTCLIENT_REQUEST_ID, requestId);
146+
this.requestId = String.valueOf(requestIndex.incrementAndGet());
147+
this.requestHeadersUriSpec.header(RESTTESTCLIENT_REQUEST_ID, this.requestId);
137148
}
138149

139150
@Override
@@ -252,7 +263,10 @@ public RequestHeadersSpec<?> body(Object body) {
252263
public ResponseSpec exchange() {
253264
return new DefaultResponseSpec(
254265
this.requestHeadersUriSpec.exchangeForRequiredValue(
255-
(request, response) -> new ExchangeResult(request, response, this.uriTemplate), false),
266+
(request, response) -> {
267+
byte[] requestBody = wiretapInterceptor.getRequestContent(this.requestId);
268+
return new ExchangeResult(request, response, this.uriTemplate, requestBody);
269+
}, false),
256270
DefaultRestTestClient.this.entityResultConsumer);
257271
}
258272
}
@@ -476,4 +490,29 @@ public EntityExchangeResult<byte[]> returnResult() {
476490
return this.result;
477491
}
478492
}
493+
494+
495+
private static class WiretapInterceptor implements ClientHttpRequestInterceptor {
496+
497+
private final Map<String, byte[]> requestContentMap = new ConcurrentHashMap<>();
498+
499+
@Override
500+
public ClientHttpResponse intercept(
501+
HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
502+
503+
String header = RestTestClient.RESTTESTCLIENT_REQUEST_ID;
504+
String requestId = request.getHeaders().getFirst(header);
505+
Assert.state(requestId != null, () -> "No \"" + header + "\" header");
506+
this.requestContentMap.put(requestId, body);
507+
return execution.execute(request, body);
508+
}
509+
510+
public byte[] getRequestContent(String requestId) {
511+
byte[] bytes = this.requestContentMap.remove(requestId);
512+
Assert.state(bytes != null, () ->
513+
"No match for %s=%s".formatted(RestTestClient.RESTTESTCLIENT_REQUEST_ID, requestId));
514+
return bytes;
515+
}
516+
}
517+
479518
}

‎spring-test/src/main/java/org/springframework/test/web/servlet/client/DefaultRestTestClientBuilder.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ class DefaultRestTestClientBuilder<B extends RestTestClient.Builder<B>> implemen
6161
}
6262

6363
DefaultRestTestClientBuilder(RestClient.Builder restClientBuilder) {
64-
this.restClientBuilder = restClientBuilder;
64+
this.restClientBuilder = restClientBuilder.bufferContent((uri, httpMethod) -> true);
6565
}
6666

6767
DefaultRestTestClientBuilder(DefaultRestTestClientBuilder<B> other) {

‎spring-test/src/main/java/org/springframework/test/web/servlet/client/ExchangeResult.java‎

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import java.io.IOException;
2020
import java.net.HttpCookie;
2121
import java.net.URI;
22+
import java.nio.charset.Charset;
23+
import java.nio.charset.StandardCharsets;
2224
import java.util.List;
2325
import java.util.Optional;
2426
import java.util.regex.Matcher;
@@ -34,10 +36,12 @@
3436
import org.springframework.http.HttpRequest;
3537
import org.springframework.http.HttpStatus;
3638
import org.springframework.http.HttpStatusCode;
39+
import org.springframework.http.MediaType;
3740
import org.springframework.http.ResponseCookie;
3841
import org.springframework.util.Assert;
3942
import org.springframework.util.LinkedMultiValueMap;
4043
import org.springframework.util.MultiValueMap;
44+
import org.springframework.util.StreamUtils;
4145
import org.springframework.web.client.RestClient.RequestHeadersSpec.ConvertibleClientHttpResponse;
4246

4347
/**
@@ -54,6 +58,10 @@ public class ExchangeResult {
5458

5559
private static final Pattern PARTITIONED_PATTERN = Pattern.compile("(?i).*;\\s*Partitioned(\\s*;.*|\\s*)$");
5660

61+
private static final List<MediaType> PRINTABLE_MEDIA_TYPES = List.of(
62+
MediaType.parseMediaType("application/*+json"), MediaType.APPLICATION_XML,
63+
MediaType.parseMediaType("text/*"), MediaType.APPLICATION_FORM_URLENCODED);
64+
5765

5866
private static final Log logger = LogFactory.getLog(ExchangeResult.class);
5967

@@ -64,22 +72,26 @@ public class ExchangeResult {
6472

6573
private final @Nullable String uriTemplate;
6674

75+
private final byte[] requestBody;
76+
6777
/** Ensure single logging; for example, for expectAll. */
6878
private boolean diagnosticsLogged;
6979

7080

7181
ExchangeResult(
72-
HttpRequest request, ConvertibleClientHttpResponse response, @Nullable String uriTemplate) {
82+
HttpRequest request, ConvertibleClientHttpResponse response, @Nullable String uriTemplate,
83+
byte[] requestBody) {
7384

7485
Assert.notNull(request, "HttpRequest must not be null");
7586
Assert.notNull(response, "ClientHttpResponse must not be null");
7687
this.request = request;
7788
this.clientResponse = response;
7889
this.uriTemplate = uriTemplate;
90+
this.requestBody = requestBody;
7991
}
8092

8193
ExchangeResult(ExchangeResult result) {
82-
this(result.request, result.clientResponse, result.uriTemplate);
94+
this(result.request, result.clientResponse, result.uriTemplate, result.requestBody);
8395
this.diagnosticsLogged = result.diagnosticsLogged;
8496
}
8597

@@ -159,13 +171,32 @@ private static ResponseCookie toResponseCookie(HttpCookie cookie, @Nullable Stri
159171
.build();
160172
}
161173

174+
/**
175+
* Return the raw request body content written through the request.
176+
*/
177+
public byte[] getRequestBodyContent() {
178+
return this.requestBody;
179+
}
180+
162181
/**
163182
* Provide access to the response. For internal use to decode the body.
164183
*/
165184
ConvertibleClientHttpResponse getClientResponse() {
166185
return this.clientResponse;
167186
}
168187

188+
/**
189+
* Return the raw response body read through the response.
190+
*/
191+
public byte[] getResponseBodyContent() {
192+
try {
193+
return StreamUtils.copyToByteArray(this.clientResponse.getBody());
194+
}
195+
catch (IOException ex) {
196+
throw new IllegalStateException("Failed to get response content: " + ex);
197+
}
198+
}
199+
169200
/**
170201
* Execute the given Runnable, catch any {@link AssertionError}, log details
171202
* about the request and response at ERROR level under the class log
@@ -190,8 +221,12 @@ public String toString() {
190221
"> " + getMethod() + " " + getUrl() + "\n" +
191222
"> " + formatHeaders(getRequestHeaders(), "\n> ") + "\n" +
192223
"\n" +
224+
formatBody(getRequestHeaders().getContentType(), this.requestBody) + "\n" +
225+
"\n" +
193226
"< " + formatStatus(getStatus()) + "\n" +
194-
"< " + formatHeaders(getResponseHeaders(), "\n< ") + "\n";
227+
"< " + formatHeaders(getResponseHeaders(), "\n< ") + "\n" +
228+
"\n" +
229+
formatBody(getResponseHeaders().getContentType(), getResponseBodyContent()) +"\n";
195230
}
196231

197232
private String formatStatus(HttpStatusCode statusCode) {
@@ -208,4 +243,18 @@ private String formatHeaders(HttpHeaders headers, String delimiter) {
208243
.collect(Collectors.joining(delimiter));
209244
}
210245

246+
private String formatBody(@Nullable MediaType contentType, byte[] bytes) {
247+
if (contentType == null) {
248+
return bytes.length + " bytes of content (unknown content-type).";
249+
}
250+
Charset charset = contentType.getCharset();
251+
if (charset != null) {
252+
return new String(bytes, charset);
253+
}
254+
if (PRINTABLE_MEDIA_TYPES.stream().anyMatch(contentType::isCompatibleWith)) {
255+
return new String(bytes, StandardCharsets.UTF_8);
256+
}
257+
return bytes.length + " bytes of content.";
258+
}
259+
211260
}

‎spring-test/src/test/java/org/springframework/test/web/servlet/client/RestTestClientTests.java‎

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
import org.springframework.http.HttpHeaders;
3737
import org.springframework.http.HttpMethod;
3838
import org.springframework.http.MediaType;
39+
import org.springframework.web.bind.annotation.PostMapping;
40+
import org.springframework.web.bind.annotation.RequestBody;
3941
import org.springframework.web.bind.annotation.RequestHeader;
4042
import org.springframework.web.bind.annotation.RequestMapping;
4143
import org.springframework.web.bind.annotation.RestController;
@@ -317,6 +319,19 @@ void testReturnResultParameterizedTypeReference() {
317319
});
318320
assertThat(result.getResponseBody().get("uri")).isEqualTo("/test");
319321
}
322+
323+
@Test
324+
void testResultContent() {
325+
String body = "body-in";
326+
EntityExchangeResult<String> result = RestTestClientTests.this.client.post().uri("/body")
327+
.body(body)
328+
.exchange()
329+
.expectStatus().isOk()
330+
.expectBody(String.class)
331+
.returnResult();
332+
assertThat(result.getRequestBodyContent()).isEqualTo(body.getBytes(StandardCharsets.UTF_8));
333+
assertThat(result.getResponseBodyContent()).isEqualTo((body + "-out").getBytes(StandardCharsets.UTF_8));
334+
}
320335
}
321336

322337

@@ -325,14 +340,20 @@ static class TestController {
325340

326341
@RequestMapping(path = {"/test", "/test/*"}, produces = "application/json")
327342
public Map<String, Object> handle(
328-
@RequestHeader HttpHeaders headers,
329-
HttpServletRequestrequest, HttpServletResponseresponse) {
343+
@RequestHeader HttpHeaders headers,HttpServletRequestrequest, HttpServletResponseresponse) {
344+
330345
response.addCookie(new Cookie("session", "abc"));
346+
331347
return Map.of(
332348
"method", request.getMethod(),
333349
"uri", request.getRequestURI(),
334350
"headers", headers.toSingleValueMap()
335351
);
336352
}
353+
354+
@PostMapping("/body")
355+
public String echoBody(@RequestBody String body) {
356+
return body + "-out";
357+
}
337358
}
338359
}

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /