Refactor reactive mock request and response support

MockServerHttpRequest and MockServerHttpResponse now extend the same
abstract base classes that server-specific implementations do and
therefore approximate their behavior more closely.

As an immediate consequence MockServerHttpRequest is read-only after
it is created. Instead it now exposes static builder methods similar
to those found in RequestEntity. This enforces more strictness as well
as recycling of requests in tests and provides nicer builder methods.

To simplify tests DefaultServerWebExchange now offers a constructor
with just a request and response, and automatically creating a
DefaultWebSessionManager.

The spring-test module now also contains client-side reactive mock
request and response implementations. The mock client request extends
the same AbstractClientHttpRequest as client-specific implementations
do. There is no abstract base class for client responses.

Issue: SPR-14590
This commit is contained in:
Rossen Stoyanchev
2017-01-13 15:08:03 -05:00
parent 4d6c1d0d3f
commit ba3cc535f1
91 changed files with 2314 additions and 1776 deletions

View File

@@ -0,0 +1,106 @@
/*
* Copyright 2002-2016 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
*
* http://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.mock.http.client.reactive;
import java.net.URI;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.reactive.AbstractClientHttpRequest;
import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.web.util.UriComponentsBuilder;
/**
* Mock implementation of {@link ClientHttpRequest}.
* @author Brian Clozel
* @author Rossen Stoyanchev
* @since 5.0
*/
public class MockClientHttpRequest extends AbstractClientHttpRequest {
private HttpMethod httpMethod;
private URI url;
private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory();
private Flux<DataBuffer> body;
public MockClientHttpRequest(HttpMethod httpMethod, String urlTemplate, Object... vars) {
this(httpMethod, UriComponentsBuilder.fromUriString(urlTemplate).buildAndExpand(vars).encode().toUri());
}
public MockClientHttpRequest(HttpMethod httpMethod, URI url) {
this.httpMethod = httpMethod;
this.url = url;
}
@Override
public HttpMethod getMethod() {
return this.httpMethod;
}
@Override
public URI getURI() {
return this.url;
}
@Override
public DataBufferFactory bufferFactory() {
return this.bufferFactory;
}
public Flux<DataBuffer> getBody() {
return this.body;
}
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
this.body = Flux.from(body);
return doCommit(() -> {
this.body = Flux.from(body);
return Mono.empty();
});
}
@Override
public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
return writeWith(Flux.from(body).flatMap(p -> p));
}
@Override
protected void applyHeaders() {
}
@Override
protected void applyCookies() {
}
@Override
public Mono<Void> setComplete() {
return doCommit(Mono::empty);
}
}

View File

@@ -0,0 +1,132 @@
/*
* Copyright 2002-2016 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
*
* http://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.mock.http.client.reactive;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseCookie;
import org.springframework.http.client.reactive.ClientHttpResponse;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* Mock implementation of {@link ClientHttpResponse}.
* @author Brian Clozel
* @author Rossen Stoyanchev
* @since 5.0
*/
public class MockClientHttpResponse implements ClientHttpResponse {
private final HttpStatus status;
private final HttpHeaders headers = new HttpHeaders();
private final MultiValueMap<String, ResponseCookie> cookies = new LinkedMultiValueMap<>();
private Flux<DataBuffer> body = Flux.empty();
private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory();
public MockClientHttpResponse(HttpStatus status) {
Assert.notNull(status, "HttpStatus is required");
this.status = status;
}
public HttpStatus getStatusCode() {
return this.status;
}
@Override
public HttpHeaders getHeaders() {
return this.headers;
}
public MultiValueMap<String, ResponseCookie> getCookies() {
return this.cookies;
}
public void setBody(Publisher<DataBuffer> body) {
this.body = Flux.from(body);
}
public void setBody(String body) {
setBody(body, StandardCharsets.UTF_8);
}
public void setBody(String body, Charset charset) {
DataBuffer buffer = toDataBuffer(body, charset);
this.body = Flux.just(buffer);
}
private DataBuffer toDataBuffer(String body, Charset charset) {
byte[] bytes = body.getBytes(charset);
ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
return this.bufferFactory.wrap(byteBuffer);
}
public Flux<DataBuffer> getBody() {
return this.body;
}
/**
* Return the response body aggregated and converted to a String using the
* charset of the Content-Type response or otherwise as "UTF-8".
*/
public Mono<String> getBodyAsString() {
Charset charset = getCharset();
return Flux.from(getBody())
.reduce(bufferFactory.allocateBuffer(), (previous, current) -> {
previous.write(current);
DataBufferUtils.release(current);
return previous;
})
.map(buffer -> dumpString(buffer, charset));
}
private static String dumpString(DataBuffer buffer, Charset charset) {
Assert.notNull(charset, "'charset' must not be null");
byte[] bytes = new byte[buffer.readableByteCount()];
buffer.read(bytes);
return new String(bytes, charset);
}
private Charset getCharset() {
Charset charset = null;
MediaType contentType = getHeaders().getContentType();
if (contentType != null) {
charset = contentType.getCharset();
}
return (charset != null ? charset : StandardCharsets.UTF_8);
}
}

View File

@@ -0,0 +1,4 @@
/**
* Mock implementations of reactive HTTP client contracts.
*/
package org.springframework.mock.http.client.reactive;

View File

@@ -19,6 +19,8 @@ import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
@@ -28,140 +30,394 @@ import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRange;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.AbstractServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.util.UriComponentsBuilder;
/**
* Mock implementation of {@link ServerHttpRequest}.
*
* <p><strong>Note:</strong> this class extends the same
* {@link AbstractServerHttpRequest} base class as actual server-specific
* implementation and is therefore read-only once created. Use static builder
* methods in this class to build up request instances.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public class MockServerHttpRequest implements ServerHttpRequest {
public class MockServerHttpRequest extends AbstractServerHttpRequest {
private HttpMethod httpMethod;
private final HttpMethod httpMethod;
private URI url;
private final String contextPath;
private String contextPath = "";
private final MultiValueMap<String, HttpCookie> cookies;
private final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
private final HttpHeaders headers = new HttpHeaders();
private final MultiValueMap<String, HttpCookie> cookies = new LinkedMultiValueMap<>();
private Flux<DataBuffer> body = Flux.empty();
private final Flux<DataBuffer> body;
/**
* Create a new instance where the HTTP method and/or URL can be set later
* via {@link #setHttpMethod(HttpMethod)} and {@link #setUri(URI)}.
*/
public MockServerHttpRequest() {
}
private MockServerHttpRequest(HttpMethod httpMethod, URI uri, String contextPath,
HttpHeaders headers, MultiValueMap<String, HttpCookie> cookies,
Publisher<? extends DataBuffer> body) {
/**
* Convenience alternative to {@link #MockServerHttpRequest(HttpMethod, URI)}
* that accepts a String URL.
*/
public MockServerHttpRequest(HttpMethod httpMethod, String url) {
this(httpMethod, (url != null ? URI.create(url) : null));
}
/**
* Create a new instance with the given HTTP method and URL.
*/
public MockServerHttpRequest(HttpMethod httpMethod, URI url) {
super(uri, headers);
this.httpMethod = httpMethod;
this.url = url;
this.contextPath = (contextPath != null ? contextPath : "");
this.cookies = cookies;
this.body = Flux.from(body);
}
public void setHttpMethod(HttpMethod httpMethod) {
this.httpMethod = httpMethod;
}
@Override
public HttpMethod getMethod() {
return this.httpMethod;
}
public MockServerHttpRequest setUri(String url) {
this.url = URI.create(url);
return this;
}
public MockServerHttpRequest setUri(URI uri) {
this.url = uri;
return this;
}
@Override
public URI getURI() {
return this.url;
}
public void setContextPath(String contextPath) {
this.contextPath = contextPath;
}
@Override
public String getContextPath() {
return this.contextPath;
}
public MockServerHttpRequest addHeader(String name, String value) {
getHeaders().add(name, value);
return this;
}
public MockServerHttpRequest setHeader(String name, String value) {
getHeaders().set(name, value);
return this;
}
@Override
public HttpHeaders getHeaders() {
return this.headers;
}
@Override
public MultiValueMap<String, String> getQueryParams() {
return this.queryParams;
}
@Override
public MultiValueMap<String, HttpCookie> getCookies() {
return this.cookies;
}
public MockServerHttpRequest setBody(Publisher<DataBuffer> body) {
this.body = Flux.from(body);
return this;
}
public MockServerHttpRequest setBody(String body) {
DataBuffer buffer = toDataBuffer(body, StandardCharsets.UTF_8);
this.body = Flux.just(buffer);
return this;
}
public MockServerHttpRequest setBody(String body, Charset charset) {
DataBuffer buffer = toDataBuffer(body, charset);
this.body = Flux.just(buffer);
return this;
}
private DataBuffer toDataBuffer(String body, Charset charset) {
byte[] bytes = body.getBytes(charset);
ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
return new DefaultDataBufferFactory().wrap(byteBuffer);
}
@Override
public Flux<DataBuffer> getBody() {
return this.body;
}
}
@Override
protected MultiValueMap<String, HttpCookie> initCookies() {
return this.cookies;
}
// Static builder methods
/**
* Create a builder with the given HTTP method and a {@link URI}.
* @param method the HTTP method (GET, POST, etc)
* @param url the URL
* @return the created builder
*/
public static BodyBuilder method(HttpMethod method, URI url) {
return new DefaultBodyBuilder(method, url);
}
/**
* Alternative to {@link #method(HttpMethod, URI)} that accepts a URI template.
* @param method the HTTP method (GET, POST, etc)
* @param urlTemplate the URL template
* @param vars variables to expand into the template
* @return the created builder
*/
public static BodyBuilder method(HttpMethod method, String urlTemplate, Object... vars) {
URI url = UriComponentsBuilder.fromUriString(urlTemplate).buildAndExpand(vars).encode().toUri();
return new DefaultBodyBuilder(method, url);
}
/**
* Create an HTTP GET builder with the given url.
* @param urlTemplate a URL template; the resulting URL will be encoded
* @param uriVars zero or more URI variables
* @return the created builder
*/
public static BaseBuilder<?> get(String urlTemplate, Object... uriVars) {
return method(HttpMethod.GET, urlTemplate, uriVars);
}
/**
* Create an HTTP HEAD builder with the given url.
* @param urlTemplate a URL template; the resulting URL will be encoded
* @param uriVars zero or more URI variables
* @return the created builder
*/
public static BaseBuilder<?> head(String urlTemplate, Object... uriVars) {
return method(HttpMethod.HEAD, urlTemplate, uriVars);
}
/**
* Create an HTTP POST builder with the given url.
* @param urlTemplate a URL template; the resulting URL will be encoded
* @param uriVars zero or more URI variables
* @return the created builder
*/
public static BodyBuilder post(String urlTemplate, Object... uriVars) {
return method(HttpMethod.POST, urlTemplate, uriVars);
}
/**
* Create an HTTP PUT builder with the given url.
* @param urlTemplate a URL template; the resulting URL will be encoded
* @param uriVars zero or more URI variables
* @return the created builder
*/
public static BodyBuilder put(String urlTemplate, Object... uriVars) {
return method(HttpMethod.PUT, urlTemplate, uriVars);
}
/**
* Create an HTTP PATCH builder with the given url.
* @param urlTemplate a URL template; the resulting URL will be encoded
* @param uriVars zero or more URI variables
* @return the created builder
*/
public static BodyBuilder patch(String urlTemplate, Object... uriVars) {
return method(HttpMethod.PATCH, urlTemplate, uriVars);
}
/**
* Create an HTTP DELETE builder with the given url.
* @param urlTemplate a URL template; the resulting URL will be encoded
* @param uriVars zero or more URI variables
* @return the created builder
*/
public static BaseBuilder<?> delete(String urlTemplate, Object... uriVars) {
return method(HttpMethod.DELETE, urlTemplate, uriVars);
}
/**
* Creates an HTTP OPTIONS builder with the given url.
* @param urlTemplate a URL template; the resulting URL will be encoded
* @param uriVars zero or more URI variables
* @return the created builder
*/
public static BaseBuilder<?> options(String urlTemplate, Object... uriVars) {
return method(HttpMethod.OPTIONS, urlTemplate, uriVars);
}
/**
* Defines a builder that adds headers to the request.
* @param <B> the builder subclass
*/
public interface BaseBuilder<B extends BaseBuilder<B>> {
/**
* Set the contextPath to return.
*/
B contextPath(String contextPath);
/**
* Add one or more cookies.
*/
B cookie(String path, HttpCookie... cookie);
/**
* Add the given, single header value under the given name.
* @param headerName the header name
* @param headerValues the header value(s)
* @see HttpHeaders#add(String, String)
*/
B header(String headerName, String... headerValues);
/**
* Set the list of acceptable {@linkplain MediaType media types}, as
* specified by the {@code Accept} header.
* @param acceptableMediaTypes the acceptable media types
*/
B accept(MediaType... acceptableMediaTypes);
/**
* Set the list of acceptable {@linkplain Charset charsets}, as specified
* by the {@code Accept-Charset} header.
* @param acceptableCharsets the acceptable charsets
*/
B acceptCharset(Charset... acceptableCharsets);
/**
* 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
*/
B ifModifiedSince(long ifModifiedSince);
/**
* Set the (new) value of the {@code If-Unmodified-Since} header.
* <p>The date should be specified as the number of milliseconds since
* January 1, 1970 GMT.
* @param ifUnmodifiedSince the new value of the header
* @see HttpHeaders#setIfUnmodifiedSince(long)
*/
B ifUnmodifiedSince(long ifUnmodifiedSince);
/**
* Set the values of the {@code If-None-Match} header.
* @param ifNoneMatches the new value of the header
*/
B ifNoneMatch(String... ifNoneMatches);
/**
* Set the (new) value of the Range header.
* @param ranges the HTTP ranges
* @see HttpHeaders#setRange(List)
*/
B range(HttpRange... ranges);
/**
* Builds the request with no body.
* @return the request
* @see BodyBuilder#body(Publisher)
* @see BodyBuilder#body(String)
*/
MockServerHttpRequest build();
}
/**
* A builder that adds a body to the request.
*/
public interface BodyBuilder extends BaseBuilder<BodyBuilder> {
/**
* Set the length of the body in bytes, as specified by the
* {@code Content-Length} header.
* @param contentLength the content length
* @return this builder
* @see HttpHeaders#setContentLength(long)
*/
BodyBuilder contentLength(long contentLength);
/**
* Set the {@linkplain MediaType media type} of the body, as specified
* by the {@code Content-Type} header.
* @param contentType the content type
* @return this builder
* @see HttpHeaders#setContentType(MediaType)
*/
BodyBuilder contentType(MediaType contentType);
/**
* Set the body of the request and build it.
* @param body the body
* @return the built request entity
*/
MockServerHttpRequest body(Publisher<? extends DataBuffer> body);
/**
* Set the body of the request and build it.
* <p>The String is assumed to be UTF-8 encoded unless the request has a
* "content-type" header with a charset attribute.
* @param body the body as text
* @return the built request entity
*/
MockServerHttpRequest body(String body);
}
private static class DefaultBodyBuilder implements BodyBuilder {
private final HttpMethod method;
private final URI url;
private String contextPath;
private final HttpHeaders headers = new HttpHeaders();
private final MultiValueMap<String, HttpCookie> cookies = new LinkedMultiValueMap<>();
public DefaultBodyBuilder(HttpMethod method, URI url) {
this.method = method;
this.url = url;
}
@Override
public BodyBuilder contextPath(String contextPath) {
this.contextPath = contextPath;
return this;
}
@Override
public BodyBuilder cookie(String path, HttpCookie... cookies) {
this.cookies.put(path, Arrays.asList(cookies));
return this;
}
@Override
public BodyBuilder header(String headerName, String... headerValues) {
for (String headerValue : headerValues) {
this.headers.add(headerName, headerValue);
}
return this;
}
@Override
public BodyBuilder accept(MediaType... acceptableMediaTypes) {
this.headers.setAccept(Arrays.asList(acceptableMediaTypes));
return this;
}
@Override
public BodyBuilder acceptCharset(Charset... acceptableCharsets) {
this.headers.setAcceptCharset(Arrays.asList(acceptableCharsets));
return this;
}
@Override
public BodyBuilder contentLength(long contentLength) {
this.headers.setContentLength(contentLength);
return this;
}
@Override
public BodyBuilder contentType(MediaType contentType) {
this.headers.setContentType(contentType);
return this;
}
@Override
public BodyBuilder ifModifiedSince(long ifModifiedSince) {
this.headers.setIfModifiedSince(ifModifiedSince);
return this;
}
@Override
public BodyBuilder ifUnmodifiedSince(long ifUnmodifiedSince) {
this.headers.setIfUnmodifiedSince(ifUnmodifiedSince);
return this;
}
@Override
public BodyBuilder ifNoneMatch(String... ifNoneMatches) {
this.headers.setIfNoneMatch(Arrays.asList(ifNoneMatches));
return this;
}
@Override
public BodyBuilder range(HttpRange... ranges) {
this.headers.setRange(Arrays.asList(ranges));
return this;
}
@Override
public MockServerHttpRequest body(Publisher<? extends DataBuffer> body) {
return new MockServerHttpRequest(this.method, this.url, this.contextPath,
this.headers, this.cookies, body);
}
@Override
public MockServerHttpRequest body(String body) {
Charset charset = getCharset();
byte[] bytes = body.getBytes(charset);
ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
DataBuffer buffer = new DefaultDataBufferFactory().wrap(byteBuffer);
return body(Flux.just(buffer));
}
private Charset getCharset() {
MediaType contentType = this.headers.getContentType();
Charset charset = (contentType != null ? contentType.getCharset() : null);
charset = charset != null ? charset : StandardCharsets.UTF_8;
return charset;
}
@Override
public MockServerHttpRequest build() {
return body(Flux.empty());
}
}
}

View File

@@ -18,130 +18,57 @@ package org.springframework.mock.http.server.reactive;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.function.Function;
import java.util.function.Supplier;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseCookie;
import org.springframework.http.server.reactive.AbstractServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* Mock implementation of {@link ServerHttpResponse}.
* @author Rossen Stoyanchev
* @since 5.0
*/
public class MockServerHttpResponse implements ServerHttpResponse {
private HttpStatus status;
private final HttpHeaders headers = new HttpHeaders();
private final MultiValueMap<String, ResponseCookie> cookies = new LinkedMultiValueMap<>();
private Function<String, String> urlEncoder = url -> url;
public class MockServerHttpResponse extends AbstractServerHttpResponse {
private Flux<DataBuffer> body;
private Flux<Publisher<DataBuffer>> bodyWithFlushes;
private DataBufferFactory bufferFactory = new DefaultDataBufferFactory();
@Override
public boolean setStatusCode(HttpStatus status) {
this.status = status;
return true;
public MockServerHttpResponse() {
super(new DefaultDataBufferFactory());
}
@Override
public HttpStatus getStatusCode() {
return this.status;
}
@Override
public HttpHeaders getHeaders() {
return this.headers;
}
@Override
public MultiValueMap<String, ResponseCookie> getCookies() {
return this.cookies;
}
@Override
public String encodeUrl(String url) {
return (this.urlEncoder != null ? this.urlEncoder.apply(url) : url);
}
@Override
public void registerUrlEncoder(Function<String, String> encoder) {
this.urlEncoder = (this.urlEncoder != null ? this.urlEncoder.andThen(encoder) : encoder);
}
public Publisher<DataBuffer> getBody() {
/**
* Return the output Publisher used to write to the response.
*/
public Flux<DataBuffer> getBody() {
return this.body;
}
public Publisher<Publisher<DataBuffer>> getBodyWithFlush() {
return this.bodyWithFlushes;
}
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
this.body = Flux.from(body);
return this.body.then();
}
@Override
public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
this.bodyWithFlushes = Flux.from(body).map(Flux::from);
return this.bodyWithFlushes.then();
}
@Override
public void beforeCommit(Supplier<? extends Mono<Void>> action) {
}
@Override
public Mono<Void> setComplete() {
return Mono.empty();
}
@Override
public DataBufferFactory bufferFactory() {
return this.bufferFactory;
}
/**
* Return the body of the response aggregated and converted to a String
* using the charset of the Content-Type response or otherwise defaulting
* to "UTF-8".
* Return the response body aggregated and converted to a String using the
* charset of the Content-Type response or otherwise as "UTF-8".
*/
public Mono<String> getBodyAsString() {
Charset charset = getCharset();
return Flux.from(getBody())
return getBody()
.reduce(bufferFactory().allocateBuffer(), (previous, current) -> {
previous.write(current);
DataBufferUtils.release(current);
return previous;
})
.map(buffer -> dumpString(buffer, charset));
.map(buffer -> bufferToString(buffer, charset));
}
private static String dumpString(DataBuffer buffer, Charset charset) {
private static String bufferToString(DataBuffer buffer, Charset charset) {
Assert.notNull(charset, "'charset' must not be null");
byte[] bytes = new byte[buffer.readableByteCount()];
buffer.read(bytes);
@@ -157,4 +84,27 @@ public class MockServerHttpResponse implements ServerHttpResponse {
return (charset != null ? charset : StandardCharsets.UTF_8);
}
@Override
protected Mono<Void> writeWithInternal(Publisher<? extends DataBuffer> body) {
this.body = Flux.from(body);
return Mono.empty();
}
@Override
protected Mono<Void> writeAndFlushWithInternal(Publisher<? extends Publisher<? extends DataBuffer>> body) {
return writeWithInternal(Flux.from(body).flatMap(Flux::from));
}
@Override
protected void applyStatusCode() {
}
@Override
protected void applyHeaders() {
}
@Override
protected void applyCookies() {
}
}