Add support for using WebFlux's WebTestClient to document an API

Closes gh-384
This commit is contained in:
Andy Wilkinson
2017-10-30 09:40:24 +00:00
parent 1cd74a5c1d
commit cfb1fbc85d
52 changed files with 2948 additions and 58 deletions

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2014-2017 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.restdocs.webtestclient;
import reactor.core.publisher.Mono;
import org.springframework.restdocs.config.OperationPreprocessorsConfigurer;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.ExchangeFunction;
/**
* A configurer that can be used to configure the operation preprocessors.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
public final class WebTestClientOperationPreprocessorsConfigurer extends
OperationPreprocessorsConfigurer<WebTestClientRestDocumentationConfigurer, WebTestClientOperationPreprocessorsConfigurer>
implements ExchangeFilterFunction {
WebTestClientOperationPreprocessorsConfigurer(WebTestClientRestDocumentationConfigurer parent) {
super(parent);
}
@Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
return and().filter(request, next);
}
}

View File

@@ -0,0 +1,154 @@
/*
* Copyright 2014-2017 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.restdocs.webtestclient;
import java.io.ByteArrayOutputStream;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import reactor.core.publisher.Flux;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ReactiveHttpInputMessage;
import org.springframework.http.codec.FormHttpMessageReader;
import org.springframework.http.codec.multipart.FilePart;
import org.springframework.http.codec.multipart.MultipartHttpMessageReader;
import org.springframework.http.codec.multipart.Part;
import org.springframework.http.codec.multipart.SynchronossPartHttpMessageReader;
import org.springframework.restdocs.operation.OperationRequest;
import org.springframework.restdocs.operation.OperationRequestFactory;
import org.springframework.restdocs.operation.OperationRequestPart;
import org.springframework.restdocs.operation.OperationRequestPartFactory;
import org.springframework.restdocs.operation.Parameters;
import org.springframework.restdocs.operation.QueryStringParser;
import org.springframework.restdocs.operation.RequestConverter;
import org.springframework.restdocs.operation.RequestCookie;
import org.springframework.test.web.reactive.server.ExchangeResult;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.util.ClassUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* A {@link RequestConverter} for creating an {@link OperationRequest} derived from an
* {@link ExchangeResult}.
*
* @author Andy Wilkinson
*/
class WebTestClientRequestConverter implements RequestConverter<ExchangeResult> {
private static final ResolvableType FORM_DATA_TYPE = ResolvableType
.forClassWithGenerics(MultiValueMap.class, String.class, String.class);
private final QueryStringParser queryStringParser = new QueryStringParser();
private final FormHttpMessageReader formDataReader = new FormHttpMessageReader();
@Override
public OperationRequest convert(ExchangeResult result) {
return new OperationRequestFactory().create(result.getUrl(), result.getMethod(),
result.getRequestBodyContent(), extractRequestHeaders(result),
extractParameters(result), extractRequestParts(result),
extractCookies(result));
}
private HttpHeaders extractRequestHeaders(ExchangeResult result) {
HttpHeaders extracted = new HttpHeaders();
extracted.putAll(result.getRequestHeaders());
extracted.remove(WebTestClient.WEBTESTCLIENT_REQUEST_ID);
return extracted;
}
private Parameters extractParameters(ExchangeResult result) {
if (result.getMethod() == HttpMethod.GET) {
return this.queryStringParser.parse(result.getUrl());
}
Parameters parameters = new Parameters();
if (result.getRequestHeaders().getContentType()
.equals(MediaType.APPLICATION_FORM_URLENCODED)) {
parameters.addAll(this.formDataReader
.readMono(FORM_DATA_TYPE,
new ExchangeResultReactiveHttpInputMessage(result), null)
.block());
}
return parameters;
}
private List<OperationRequestPart> extractRequestParts(ExchangeResult result) {
if (!ClassUtils.isPresent(
"org.synchronoss.cloud.nio.multipart.NioMultipartParserListener",
getClass().getClassLoader())) {
return Collections.emptyList();
}
return new MultipartHttpMessageReader(new SynchronossPartHttpMessageReader())
.readMono(null, new ExchangeResultReactiveHttpInputMessage(result), null)
.onErrorReturn(new LinkedMultiValueMap<>()).block().values().stream()
.flatMap((parts) -> parts.stream().map(this::createOperationRequestPart))
.collect(Collectors.toList());
}
private OperationRequestPart createOperationRequestPart(Part part) {
ByteArrayOutputStream content = readPartBodyContent(part);
return new OperationRequestPartFactory().create(part.name(),
part instanceof FilePart ? ((FilePart) part).filename() : null,
content.toByteArray(), part.headers());
}
private ByteArrayOutputStream readPartBodyContent(Part part) {
ByteArrayOutputStream contentStream = new ByteArrayOutputStream();
DataBufferUtils.write(part.content(), contentStream).blockFirst();
return contentStream;
}
private Collection<RequestCookie> extractCookies(ExchangeResult result) {
// Cookies are not available. See https://jira.spring.io/browse/SPR-16124.
return Collections.emptyList();
}
private final class ExchangeResultReactiveHttpInputMessage
implements ReactiveHttpInputMessage {
private final ExchangeResult result;
private ExchangeResultReactiveHttpInputMessage(ExchangeResult result) {
this.result = result;
}
@Override
public HttpHeaders getHeaders() {
return this.result.getRequestHeaders();
}
@Override
public Flux<DataBuffer> getBody() {
DefaultDataBuffer buffer = new DefaultDataBufferFactory().allocateBuffer();
buffer.write(this.result.getRequestBodyContent());
return Flux.fromArray(new DataBuffer[] { buffer });
}
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2014-2017 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.restdocs.webtestclient;
import org.springframework.restdocs.operation.OperationResponse;
import org.springframework.restdocs.operation.OperationResponseFactory;
import org.springframework.restdocs.operation.ResponseConverter;
import org.springframework.test.web.reactive.server.ExchangeResult;
/**
* A {@link ResponseConverter} for creating an {@link OperationResponse} derived from an
* {@link ExchangeResult}.
*
* @author Andy Wilkinson
*/
class WebTestClientResponseConverter implements ResponseConverter<ExchangeResult> {
@Override
public OperationResponse convert(ExchangeResult result) {
return new OperationResponseFactory().create(result.getStatus(),
result.getResponseHeaders(), result.getResponseBodyContent());
}
}

View File

@@ -0,0 +1,154 @@
/*
* Copyright 2014-2017 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.restdocs.webtestclient;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import org.springframework.restdocs.RestDocumentationContextProvider;
import org.springframework.restdocs.generate.RestDocumentationGenerator;
import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor;
import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor;
import org.springframework.restdocs.snippet.Snippet;
import org.springframework.test.web.reactive.server.ExchangeResult;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.reactive.server.WebTestClient.BodyContentSpec;
import org.springframework.test.web.reactive.server.WebTestClient.BodySpec;
import org.springframework.test.web.reactive.server.WebTestClient.Builder;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
/**
* Static factory methods for documenting RESTful APIs using WebFlux's
* {@link WebTestClient}.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
public abstract class WebTestClientRestDocumentation {
private static final WebTestClientRequestConverter REQUEST_CONVERTER = new WebTestClientRequestConverter();
private static final WebTestClientResponseConverter RESPONSE_CONVERTER = new WebTestClientResponseConverter();
private WebTestClientRestDocumentation() {
}
/**
* Provides access to a {@link ExchangeFilterFunction} that can be used to configure a
* {@link WebTestClient} instance using the given {@code contextProvider}.
*
* @param contextProvider the context provider
* @return the configurer
* @see Builder#filter(ExchangeFilterFunction)
*/
public static WebTestClientRestDocumentationConfigurer documentationConfiguration(
RestDocumentationContextProvider contextProvider) {
return new WebTestClientRestDocumentationConfigurer(contextProvider);
}
/**
* Returns a {@link Consumer} that, when called, documents the API call with the given
* {@code identifier} using the given {@code snippets} in addition to any default
* snippets.
*
* @param identifier an identifier for the API call that is being documented
* @param snippets the snippets
* @param <T> the type of {@link ExchangeResult} that will be consumed
* @return the {@link Consumer} that will document the API call represented by the
* {@link ExchangeResult}.
* @see BodySpec#consumeWith(Consumer)
* @see BodyContentSpec#consumeWith(Consumer)
*/
public static <T extends ExchangeResult> Consumer<T> document(String identifier,
Snippet... snippets) {
return (result) -> new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER,
RESPONSE_CONVERTER, snippets).handle(result, result,
retrieveConfiguration(result));
}
/**
* Documents the API call with the given {@code identifier} using the given
* {@code snippets} in addition to any default snippets. The given
* {@code requestPreprocessor} is applied to the request before it is documented.
*
* @param identifier an identifier for the API call that is being documented
* @param requestPreprocessor the request preprocessor
* @param snippets the snippets
* @param <T> the type of {@link ExchangeResult} that will be consumed
* @return the {@link Consumer} that will document the API call represented by the
* {@link ExchangeResult}.
*/
public static <T extends ExchangeResult> Consumer<T> document(String identifier,
OperationRequestPreprocessor requestPreprocessor, Snippet... snippets) {
return (result) -> new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER,
RESPONSE_CONVERTER, requestPreprocessor, snippets).handle(result, result,
retrieveConfiguration(result));
}
/**
* Documents the API call with the given {@code identifier} using the given
* {@code snippets} in addition to any default snippets. The given
* {@code responsePreprocessor} is applied to the request before it is documented.
*
* @param identifier an identifier for the API call that is being documented
* @param responsePreprocessor the response preprocessor
* @param snippets the snippets
* @param <T> the type of {@link ExchangeResult} that will be consumed
* @return the {@link Consumer} that will document the API call represented by the
* {@link ExchangeResult}.
*/
public static <T extends ExchangeResult> Consumer<T> document(String identifier,
OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) {
return (result) -> new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER,
RESPONSE_CONVERTER, responsePreprocessor, snippets).handle(result, result,
retrieveConfiguration(result));
}
/**
* Documents the API call with the given {@code identifier} using the given
* {@code snippets} in addition to any default snippets. The given
* {@code requestPreprocessor} and {@code responsePreprocessor} are applied to the
* request and response respectively before they are documented.
*
* @param identifier an identifier for the API call that is being documented
* @param requestPreprocessor the request preprocessor
* @param responsePreprocessor the response preprocessor
* @param snippets the snippets
* @param <T> the type of {@link ExchangeResult} that will be consumed
* @return the {@link Consumer} that will document the API call represented by the
* {@link ExchangeResult}.
*/
public static <T extends ExchangeResult> Consumer<T> document(String identifier,
OperationRequestPreprocessor requestPreprocessor,
OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) {
return (result) -> new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER,
RESPONSE_CONVERTER, requestPreprocessor, responsePreprocessor, snippets)
.handle(result, result, retrieveConfiguration(result));
}
private static Map<String, Object> retrieveConfiguration(ExchangeResult result) {
Map<String, Object> configuration = new HashMap<>(
WebTestClientRestDocumentationConfigurer
.retrieveConfiguration(result.getRequestHeaders()));
configuration.put(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE,
result.getUriTemplate());
return configuration;
}
}

View File

@@ -0,0 +1,89 @@
/*
* Copyright 2014-2017 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.restdocs.webtestclient;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpHeaders;
import org.springframework.restdocs.RestDocumentationContext;
import org.springframework.restdocs.RestDocumentationContextProvider;
import org.springframework.restdocs.config.RestDocumentationConfigurer;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.ExchangeFunction;
/**
* A WebFlux-specific {@link RestDocumentationConfigurer}.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
public class WebTestClientRestDocumentationConfigurer extends
RestDocumentationConfigurer<WebTestClientSnippetConfigurer, WebTestClientOperationPreprocessorsConfigurer, WebTestClientRestDocumentationConfigurer>
implements ExchangeFilterFunction {
private final WebTestClientSnippetConfigurer snippetConfigurer = new WebTestClientSnippetConfigurer(
this);
private static final Map<String, Map<String, Object>> configurations = new ConcurrentHashMap<>();
private final WebTestClientOperationPreprocessorsConfigurer operationPreprocessorsConfigurer = new WebTestClientOperationPreprocessorsConfigurer(
this);
private final RestDocumentationContextProvider contextProvider;
WebTestClientRestDocumentationConfigurer(RestDocumentationContextProvider contextProvider) {
this.contextProvider = contextProvider;
}
@Override
public WebTestClientSnippetConfigurer snippets() {
return this.snippetConfigurer;
}
@Override
public WebTestClientOperationPreprocessorsConfigurer operationPreprocessors() {
return this.operationPreprocessorsConfigurer;
}
private Map<String, Object> createConfiguration() {
RestDocumentationContext context = this.contextProvider.beforeOperation();
Map<String, Object> configuration = new HashMap<>();
configuration.put(RestDocumentationContext.class.getName(), context);
apply(configuration, context);
return configuration;
}
static Map<String, Object> retrieveConfiguration(HttpHeaders headers) {
String requestId = headers.getFirst(WebTestClient.WEBTESTCLIENT_REQUEST_ID);
return configurations.remove(requestId);
}
@Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
String index = request.headers().getFirst(WebTestClient.WEBTESTCLIENT_REQUEST_ID);
configurations.put(index, createConfiguration());
return next.exchange(request);
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2014-2017 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.restdocs.webtestclient;
import reactor.core.publisher.Mono;
import org.springframework.restdocs.config.SnippetConfigurer;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.ExchangeFunction;
/**
* A {@link SnippetConfigurer} for WebFlux that can be used to configure the generated
* documentation snippets.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
public class WebTestClientSnippetConfigurer extends
SnippetConfigurer<WebTestClientRestDocumentationConfigurer, WebTestClientSnippetConfigurer>
implements ExchangeFilterFunction {
WebTestClientSnippetConfigurer(WebTestClientRestDocumentationConfigurer parent) {
super(parent);
}
@Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
return and().filter(request, next);
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2014-2017 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.
*/
/**
* Core classes for using Spring REST Docs with Spring Framework's WebTestClient.
*/
package org.springframework.restdocs.webtestclient;

View File

@@ -0,0 +1,208 @@
/*
* Copyright 2014-2017 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.restdocs.webtestclient;
import java.net.URI;
import java.util.Arrays;
import org.junit.Test;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.restdocs.operation.OperationRequest;
import org.springframework.restdocs.operation.OperationRequestPart;
import org.springframework.test.web.reactive.server.ExchangeResult;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyExtractors;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.RouterFunctions;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.Matchers.hasEntry;
import static org.junit.Assert.assertThat;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RequestPredicates.POST;
/**
* Tests for {@link WebTestClientRequestConverter}.
*
* @author Andy Wilkinson
*/
public class WebTestClientRequestConverterTests {
private final WebTestClientRequestConverter converter = new WebTestClientRequestConverter();
@Test
public void httpRequest() {
ExchangeResult result = WebTestClient
.bindToRouterFunction(RouterFunctions.route(GET("/foo"), (req) -> null))
.configureClient().baseUrl("http://localhost").build().get().uri("/foo")
.exchange().expectBody().returnResult();
OperationRequest request = this.converter.convert(result);
assertThat(request.getUri(), is(URI.create("http://localhost/foo")));
assertThat(request.getMethod(), is(HttpMethod.GET));
}
@Test
public void httpRequestWithCustomPort() {
ExchangeResult result = WebTestClient
.bindToRouterFunction(RouterFunctions.route(GET("/foo"), (req) -> null))
.configureClient().baseUrl("http://localhost:8080").build().get()
.uri("/foo").exchange().expectBody().returnResult();
OperationRequest request = this.converter.convert(result);
assertThat(request.getUri(), is(URI.create("http://localhost:8080/foo")));
assertThat(request.getMethod(), is(HttpMethod.GET));
}
@Test
public void requestWithHeaders() {
ExchangeResult result = WebTestClient
.bindToRouterFunction(RouterFunctions.route(GET("/"), (req) -> null))
.configureClient().baseUrl("http://localhost").build().get().uri("/foo")
.header("a", "alpha", "apple").header("b", "bravo").exchange()
.expectBody().returnResult();
OperationRequest request = this.converter.convert(result);
assertThat(request.getUri(), is(URI.create("http://localhost/foo")));
assertThat(request.getMethod(), is(HttpMethod.GET));
assertThat(request.getHeaders(), hasEntry("a", Arrays.asList("alpha", "apple")));
assertThat(request.getHeaders(), hasEntry("b", Arrays.asList("bravo")));
}
@Test
public void httpsRequest() {
ExchangeResult result = WebTestClient
.bindToRouterFunction(RouterFunctions.route(GET("/foo"), (req) -> null))
.configureClient().baseUrl("https://localhost").build().get().uri("/foo")
.exchange().expectBody().returnResult();
OperationRequest request = this.converter.convert(result);
assertThat(request.getUri(), is(URI.create("https://localhost/foo")));
assertThat(request.getMethod(), is(HttpMethod.GET));
}
@Test
public void httpsRequestWithCustomPort() {
ExchangeResult result = WebTestClient
.bindToRouterFunction(RouterFunctions.route(GET("/foo"), (req) -> null))
.configureClient().baseUrl("https://localhost:8443").build().get()
.uri("/foo").exchange().expectBody().returnResult();
OperationRequest request = this.converter.convert(result);
assertThat(request.getUri(), is(URI.create("https://localhost:8443/foo")));
assertThat(request.getMethod(), is(HttpMethod.GET));
}
@Test
public void getRequestWithQueryStringPopulatesParameters() throws Exception {
ExchangeResult result = WebTestClient
.bindToRouterFunction(RouterFunctions.route(GET("/foo"), (req) -> null))
.configureClient().baseUrl("http://localhost").build().get()
.uri("/foo?a=alpha&b=bravo").exchange().expectBody().returnResult();
OperationRequest request = this.converter.convert(result);
assertThat(request.getUri(),
is(URI.create("http://localhost/foo?a=alpha&b=bravo")));
assertThat(request.getParameters().size(), is(2));
assertThat(request.getParameters(), hasEntry("a", Arrays.asList("alpha")));
assertThat(request.getParameters(), hasEntry("b", Arrays.asList("bravo")));
assertThat(request.getMethod(), is(HttpMethod.GET));
}
@Test
public void postRequestWithParameters() throws Exception {
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.addAll("a", Arrays.asList("alpha", "apple"));
parameters.addAll("b", Arrays.asList("br&vo"));
ExchangeResult result = WebTestClient
.bindToRouterFunction(RouterFunctions.route(POST("/foo"), (req) -> {
req.body(BodyExtractors.toFormData()).block();
return null;
})).configureClient().baseUrl("http://localhost").build().post()
.uri("/foo").body(BodyInserters.fromFormData(parameters)).exchange()
.expectBody().returnResult();
OperationRequest request = this.converter.convert(result);
assertThat(request.getUri(), is(URI.create("http://localhost/foo")));
assertThat(request.getMethod(), is(HttpMethod.POST));
assertThat(request.getParameters().size(), is(2));
assertThat(request.getParameters(),
hasEntry("a", Arrays.asList("alpha", "apple")));
assertThat(request.getParameters(), hasEntry("b", Arrays.asList("br&vo")));
}
@Test
public void multipartUpload() throws Exception {
MultiValueMap<String, Object> multipartData = new LinkedMultiValueMap<>();
multipartData.add("file", new byte[] { 1, 2, 3, 4 });
ExchangeResult result = WebTestClient
.bindToRouterFunction(RouterFunctions.route(POST("/foo"), (req) -> {
req.body(BodyExtractors.toMultipartData()).block();
return null;
})).configureClient().baseUrl("http://localhost").build().post()
.uri("/foo").syncBody(multipartData).exchange().expectBody()
.returnResult();
OperationRequest request = this.converter.convert(result);
assertThat(request.getUri(), is(URI.create("http://localhost/foo")));
assertThat(request.getMethod(), is(HttpMethod.POST));
assertThat(request.getParts().size(), is(1));
OperationRequestPart part = request.getParts().iterator().next();
assertThat(part.getName(), is(equalTo("file")));
assertThat(part.getSubmittedFileName(), is(nullValue()));
assertThat(part.getHeaders().size(), is(2));
assertThat(part.getHeaders().getContentLength(), is(4L));
assertThat(part.getHeaders().getContentDisposition().getName(),
is(equalTo("file")));
assertThat(part.getContent(), is(equalTo(new byte[] { 1, 2, 3, 4 })));
}
@Test
public void multipartUploadFromResource() throws Exception {
MultiValueMap<String, Object> multipartData = new LinkedMultiValueMap<>();
multipartData.add("file", new ByteArrayResource(new byte[] { 1, 2, 3, 4 }) {
@Override
public String getFilename() {
return "image.png";
}
});
ExchangeResult result = WebTestClient
.bindToRouterFunction(RouterFunctions.route(POST("/foo"), (req) -> {
req.body(BodyExtractors.toMultipartData()).block();
return null;
})).configureClient().baseUrl("http://localhost").build().post()
.uri("/foo").syncBody(multipartData).exchange().expectBody()
.returnResult();
OperationRequest request = this.converter.convert(result);
assertThat(request.getUri(), is(URI.create("http://localhost/foo")));
assertThat(request.getMethod(), is(HttpMethod.POST));
assertThat(request.getParts().size(), is(1));
OperationRequestPart part = request.getParts().iterator().next();
assertThat(part.getName(), is(equalTo("file")));
assertThat(part.getSubmittedFileName(), is(equalTo("image.png")));
assertThat(part.getHeaders().size(), is(3));
assertThat(part.getHeaders().getContentLength(), is(4L));
ContentDisposition contentDisposition = part.getHeaders().getContentDisposition();
assertThat(contentDisposition.getName(), is(equalTo("file")));
assertThat(contentDisposition.getFilename(), is(equalTo("image.png")));
assertThat(part.getHeaders().getContentType(), is(equalTo(MediaType.IMAGE_PNG)));
assertThat(part.getContent(), is(equalTo(new byte[] { 1, 2, 3, 4 })));
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2014-2017 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.restdocs.webtestclient;
import org.junit.Test;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.restdocs.operation.OperationResponse;
import org.springframework.test.web.reactive.server.ExchangeResult;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
/**
* Tests for {@link WebTestClientResponseConverter}.
*
* @author Andy Wilkinson
*/
public class WebTestClientResponseConverterTests {
private final WebTestClientResponseConverter converter = new WebTestClientResponseConverter();
@Test
public void basicResponse() {
ExchangeResult result = WebTestClient
.bindToRouterFunction(RouterFunctions.route(GET("/foo"),
(req) -> ServerResponse.ok().syncBody("Hello, World!")))
.configureClient().baseUrl("http://localhost").build().get().uri("/foo")
.exchange().expectBody().returnResult();
OperationResponse response = this.converter.convert(result);
assertThat(response.getStatus(), is(HttpStatus.OK));
assertThat(response.getContentAsString(), is(equalTo("Hello, World!")));
assertThat(response.getHeaders().getContentType(),
is(MediaType.parseMediaType("text/plain;charset=UTF-8")));
assertThat(response.getHeaders().getContentLength(), is(13L));
}
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2014-2017 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.restdocs.webtestclient;
import java.util.Map;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.restdocs.JUnitRestDocumentation;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ExchangeFunction;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link WebTestClientRestDocumentationConfigurer}.
*
* @author Andy Wilkinson
*/
public class WebTestClientRestDocumentationConfigurerTests {
@Rule
public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation();
private final WebTestClientRestDocumentationConfigurer configurer = new WebTestClientRestDocumentationConfigurer(
this.restDocumentation);
@Test
public void configurationCanBeRetrievedButOnlyOnce() {
ClientRequest request = mock(ClientRequest.class);
HttpHeaders headers = new HttpHeaders();
headers.add(WebTestClient.WEBTESTCLIENT_REQUEST_ID, "1");
given(request.headers()).willReturn(headers);
this.configurer.filter(request, mock(ExchangeFunction.class));
Map<String, Object> configuration = WebTestClientRestDocumentationConfigurer
.retrieveConfiguration(headers);
assertThat(configuration, notNullValue());
assertThat(
WebTestClientRestDocumentationConfigurer.retrieveConfiguration(headers),
nullValue());
}
}

View File

@@ -0,0 +1,184 @@
/*
* Copyright 2014-2017 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.restdocs.webtestclient;
import java.io.File;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.http.HttpStatus;
import org.springframework.restdocs.JUnitRestDocumentation;
import org.springframework.restdocs.templates.TemplateFormats;
import org.springframework.test.web.reactive.server.EntityExchangeResult;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.util.FileSystemUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyExtractors;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.partWithName;
import static org.springframework.restdocs.request.RequestDocumentation.pathParameters;
import static org.springframework.restdocs.request.RequestDocumentation.requestParameters;
import static org.springframework.restdocs.request.RequestDocumentation.requestParts;
import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor;
import static org.springframework.restdocs.test.SnippetMatchers.snippet;
import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader;
import static org.springframework.restdocs.test.SnippetMatchers.tableWithTitleAndHeader;
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document;
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration;
import static org.springframework.web.reactive.function.BodyInserters.fromObject;
/**
* Integration tests for using Spring REST Docs with Spring Framework's WebTestClient.
*
* @author Andy Wilkinson
*/
public class WebTestClientRestDocumentationIntegrationTests {
@Rule
public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation();
private WebTestClient webTestClient;
@Before
public void setUp() {
RouterFunction<ServerResponse> route = RouterFunctions
.route(RequestPredicates.GET("/"),
(request) -> ServerResponse.status(HttpStatus.OK)
.body(fromObject(new Person("Jane", "Doe"))))
.andRoute(RequestPredicates.GET("/{foo}/{bar}"),
(request) -> ServerResponse.status(HttpStatus.OK)
.body(fromObject(new Person("Jane", "Doe"))))
.andRoute(RequestPredicates.POST("/upload"), (request) -> {
return request.body(BodyExtractors.toMultipartData()).map((parts) -> {
return ServerResponse.status(HttpStatus.OK).build().block();
});
});
this.webTestClient = WebTestClient.bindToRouterFunction(route).configureClient()
.baseUrl("https://api.example.com")
.filter(documentationConfiguration(this.restDocumentation)).build();
}
@Test
public void defaultSnippetGeneration() {
File outputDir = new File("build/generated-snippets/default-snippets");
FileSystemUtils.deleteRecursively(outputDir);
this.webTestClient.get().uri("/").exchange().expectStatus().isOk().expectBody()
.consumeWith(document("default-snippets"));
assertExpectedSnippetFilesExist(outputDir, "http-request.adoc",
"http-response.adoc", "curl-request.adoc", "httpie-request.adoc",
"request-body.adoc", "response-body.adoc");
}
@Test
public void pathParametersSnippet() {
this.webTestClient.get().uri("/{foo}/{bar}", "1", "2").exchange().expectStatus()
.isOk().expectBody()
.consumeWith(document("path-parameters", pathParameters(
parameterWithName("foo").description("Foo description"),
parameterWithName("bar").description("Bar description"))));
assertThat(
new File("build/generated-snippets/path-parameters/path-parameters.adoc"),
is(snippet(asciidoctor()).withContents(
tableWithTitleAndHeader(TemplateFormats.asciidoctor(),
"/{foo}/{bar}", "Parameter", "Description")
.row("`foo`", "Foo description")
.row("`bar`", "Bar description"))));
}
@Test
public void requestParametersSnippet() {
this.webTestClient.get().uri("/?a=alpha&b=bravo").exchange().expectStatus().isOk()
.expectBody()
.consumeWith(document("request-parameters", requestParameters(
parameterWithName("a").description("Alpha description"),
parameterWithName("b").description("Bravo description"))));
assertThat(
new File(
"build/generated-snippets/request-parameters/request-parameters.adoc"),
is(snippet(asciidoctor()).withContents(
tableWithHeader(TemplateFormats.asciidoctor(), "Parameter",
"Description").row("`a`", "Alpha description").row("`b`",
"Bravo description"))));
}
@Test
public void multipart() throws Exception {
MultiValueMap<String, Object> multipartData = new LinkedMultiValueMap<>();
multipartData.add("a", "alpha");
multipartData.add("b", "bravo");
Consumer<EntityExchangeResult<byte[]>> documentation = document("multipart",
requestParts(partWithName("a").description("Part a"),
partWithName("b").description("Part b")));
this.webTestClient.post().uri("/upload").syncBody(multipartData).exchange()
.expectStatus().isOk().expectBody().consumeWith(documentation);
assertThat(new File("build/generated-snippets/multipart/request-parts.adoc"),
is(snippet(asciidoctor())
.withContents(tableWithHeader(TemplateFormats.asciidoctor(),
"Part", "Description").row("`a`", "Part a").row("`b`",
"Part b"))));
}
private void assertExpectedSnippetFilesExist(File directory, String... snippets) {
Set<File> actual = new HashSet<>(Arrays.asList(directory.listFiles()));
Set<File> expected = Stream.of(snippets)
.map((snippet) -> new File(directory, snippet))
.collect(Collectors.toSet());
assertThat(actual, equalTo(expected));
}
/**
* A person.
*/
static class Person {
private final String firstName;
private final String lastName;
Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return this.firstName;
}
public String getLastName() {
return this.lastName;
}
}
}