Make GraphQlTester builders and request strategies top level classes

This commit is contained in:
Rossen Stoyanchev
2021-10-01 12:16:34 +01:00
parent e644e5e192
commit 6777d7613b
15 changed files with 914 additions and 556 deletions

View File

@@ -16,38 +16,26 @@
package org.springframework.graphql.test.tester;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.PathNotFoundException;
import com.jayway.jsonpath.TypeRef;
import com.jayway.jsonpath.spi.json.JacksonJsonProvider;
import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
import graphql.ExecutionResult;
import graphql.GraphQLError;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.graphql.GraphQlService;
import org.springframework.graphql.RequestInput;
import org.springframework.lang.Nullable;
import org.springframework.test.util.AssertionErrors;
import org.springframework.test.util.JsonExpectationsHelper;
import org.springframework.test.util.JsonPathExpectationsHelper;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
@@ -74,280 +62,30 @@ class DefaultGraphQlTester implements GraphQlTester {
/**
* Base class support for
* {@link GraphQlTester.Builder} and {@link WebGraphQlTester.Builder}.
* Factory for {@link GraphQlTester.ResponseSpec}, for use from
* {@link RequestStrategy} implementations.
*
* @param documentContext the parsed response content
* @param errorFilter a globally defined filter for expected errors (to be ignored)
* @param assertDecorator decorator to apply around assertions, e.g. to add extra
*/
static class BuilderSupport {
static GraphQlTester.ResponseSpec createResponseSpec(
DocumentContext documentContext, @Nullable Predicate<GraphQLError> errorFilter,
Consumer<Runnable> assertDecorator) {
@Nullable
private Predicate<GraphQLError> errorFilter;
@Nullable
private Configuration jsonPathConfig;
@Nullable
private Duration responseTimeout = Duration.ofSeconds(5);
protected void addErrorFilter(Predicate<GraphQLError> predicate) {
this.errorFilter = (this.errorFilter != null ? errorFilter.and(predicate) : predicate);
}
@Nullable
protected Predicate<GraphQLError> getErrorFilter() {
return errorFilter;
}
protected void setJsonPathConfig(Configuration config) {
this.jsonPathConfig = config;
}
protected void setResponseTimeout(Duration timeout) {
Assert.notNull(timeout, "'timeout' is required");
this.responseTimeout = timeout;
}
@Nullable
protected Duration getResponseTimeout() {
return this.responseTimeout;
}
protected Configuration initJsonPathConfig() {
return JsonPathConfiguration.initialize(this.jsonPathConfig);
}
protected Duration initResponseTimeout() {
return (this.responseTimeout != null ? this.responseTimeout : Duration.ofSeconds(5));
}
}
/**
* Default implementation to build {@link GraphQlTester}.
*/
static class DefaultBuilder extends BuilderSupport implements Builder<DefaultBuilder> {
private final GraphQlService service;
DefaultBuilder(GraphQlService service) {
Assert.notNull(service, "GraphQlService is required.");
this.service = service;
}
@Override
public DefaultBuilder errorFilter(Predicate<GraphQLError> predicate) {
addErrorFilter(predicate);
return this;
}
@Override
public DefaultBuilder jsonPathConfig(Configuration config) {
setJsonPathConfig(config);
return this;
}
@Override
public DefaultBuilder responseTimeout(Duration timeout) {
setResponseTimeout(timeout);
return this;
}
@Override
public GraphQlTester build() {
RequestStrategy strategy = new GraphQlServiceRequestStrategy(
this.service, getErrorFilter(), initJsonPathConfig(), initResponseTimeout());
return new DefaultGraphQlTester(strategy);
}
}
/**
* Internal strategy abstracting how a GraphQL request is performed.
*/
interface RequestStrategy {
/**
* Perform a request with the given {@link RequestInput} container.
* @param input the request input
* @return the response spec
*/
ResponseSpec execute(RequestInput input);
/**
* Perform a subscription with the given {@link RequestInput} container.
* @param input the request input
* @return the subscription spec
*/
SubscriptionSpec executeSubscription(RequestInput input);
}
/**
* Base class support for {@link RequestStrategy} and
* {@link DefaultWebGraphQlTester.WebRequestStrategy} implementations.
*/
static class RequestStrategySupport {
@Nullable
private final Predicate<GraphQLError> errorFilter;
private final Configuration jsonPathConfig;
private final Duration responseTimeout;
protected RequestStrategySupport(
@Nullable Predicate<GraphQLError> errorFilter, Configuration jsonPathConfig, Duration timeout) {
this.errorFilter = errorFilter;
this.jsonPathConfig = jsonPathConfig;
this.responseTimeout = timeout;
}
protected Configuration getJsonPathConfig() {
return this.jsonPathConfig;
}
protected Duration getResponseTimeout() {
return this.responseTimeout;
}
protected ResponseSpec createResponseSpec(ExecutionResult result, Consumer<Runnable> assertDecorator) {
DocumentContext context = JsonPath.parse(result.toSpecification(), this.jsonPathConfig);
return createResponseSpec(context, assertDecorator);
}
protected ResponseSpec createResponseSpec(DocumentContext context, Consumer<Runnable> assertDecorator) {
return new DefaultResponseSpec(context, this.errorFilter, assertDecorator);
}
}
/**
* Base class for a {@link RequestStrategy} that perform GraphQL requests
* without an underlying transport and where {@link RequestInput} provides
* sufficient input.
*/
static class DirectRequestStrategySupport extends RequestStrategySupport {
protected DirectRequestStrategySupport(
@Nullable Predicate<GraphQLError> errorFilter, Configuration jsonPathConfig, Duration timeout) {
super(errorFilter, jsonPathConfig, timeout);
}
protected ResponseSpec createResponseSpec(RequestInput input, ExecutionResult result) {
return createResponseSpec(result, assertDecorator(input));
}
protected SubscriptionSpec createSubscriptionSpec(RequestInput input, ExecutionResult result) {
Consumer<Runnable> assertDecorator = assertDecorator(input);
assertDecorator.accept(() -> AssertionErrors.assertTrue(
"Subscription did not return Publisher",
result.getData() instanceof Publisher));
assertDecorator.accept(() -> AssertionErrors.assertTrue(
"Response has " + result.getErrors().size() + " unexpected error(s).",
CollectionUtils.isEmpty(result.getErrors())));
return () -> {
Publisher<? extends ExecutionResult> publisher = result.getData();
return Flux.from(publisher).map((current) -> createResponseSpec(current, assertDecorator));
};
}
private Consumer<Runnable> assertDecorator(RequestInput input) {
return (assertion) -> {
try {
assertion.run();
}
catch (AssertionError ex) {
throw new AssertionError(ex.getMessage() + "\nRequest: " + input, ex);
}
};
}
}
/**
* {@link RequestStrategy} that performs requests through a {@link GraphQlService}.
*/
protected static class GraphQlServiceRequestStrategy
extends DirectRequestStrategySupport implements RequestStrategy {
private final GraphQlService graphQlService;
protected GraphQlServiceRequestStrategy(GraphQlService service,
@Nullable Predicate<GraphQLError> errorFilter, Configuration jsonPathConfig, Duration timeout) {
super(errorFilter, jsonPathConfig, timeout);
Assert.notNull(service, "GraphQlService is required.");
this.graphQlService = service;
}
@Override
public ResponseSpec execute(RequestInput input) {
return createResponseSpec(input, executeInternal(input));
}
@Override
public SubscriptionSpec executeSubscription(RequestInput input) {
return createSubscriptionSpec(input, executeInternal(input));
}
private ExecutionResult executeInternal(RequestInput input) {
ExecutionResult result = this.graphQlService.execute(input).block(getResponseTimeout());
Assert.notNull(result, "Expected ExecutionResult");
return result;
}
}
/**
* Base class support for
* {@link GraphQlTester.RequestSpec} and {@link WebGraphQlTester.RequestSpec}.
*/
static class RequestSpecSupport {
private final String query;
@Nullable
private String operationName;
private final Map<String, Object> variables = new LinkedHashMap<>();
protected RequestSpecSupport(String query) {
Assert.notNull(query, "`query` is required");
this.query = query;
}
protected void setOperationName(@Nullable String name) {
this.operationName = name;
}
protected void addVariable(String name, Object value) {
this.variables.put(name, value);
}
protected void verify(ResponseSpec responseSpec) {
responseSpec.path("$.errors").valueIsEmpty();
}
protected RequestInput createRequestInput() {
return new RequestInput(this.query, this.operationName, this.variables);
}
return new DefaultResponseSpec(documentContext, errorFilter, assertDecorator);
}
/**
* {@link RequestSpec} that collects the query, operationName, and variables.
*/
static class DefaultRequestSpec extends RequestSpecSupport implements RequestSpec<DefaultRequestSpec> {
private static final class DefaultRequestSpec
extends GraphQlTesterRequestSpecSupport implements RequestSpec<DefaultRequestSpec> {
private final RequestStrategy requestStrategy;
protected DefaultRequestSpec(RequestStrategy requestStrategy, String query) {
private DefaultRequestSpec(RequestStrategy requestStrategy, String query) {
super(query);
Assert.notNull(requestStrategy, "RequestStrategy is required");
this.requestStrategy = requestStrategy;
@@ -383,9 +121,9 @@ class DefaultGraphQlTester implements GraphQlTester {
/**
* Container for a GraphQL response with access to data and errors.
* Container for GraphQL response data and errors along with convenience methods.
*/
private static class ResponseContainer {
private final static class ResponseContainer {
private static final TypeRef<List<TestGraphQlError>> ERROR_LIST_TYPE = new TypeRef<List<TestGraphQlError>>() {};
@@ -403,7 +141,7 @@ class DefaultGraphQlTester implements GraphQlTester {
private final Consumer<Runnable> assertDecorator;
ResponseContainer(
private ResponseContainer(
DocumentContext documentContext, @Nullable Predicate<GraphQLError> errorFilter,
Consumer<Runnable> assertDecorator) {
@@ -482,17 +220,11 @@ class DefaultGraphQlTester implements GraphQlTester {
/**
* {@link ResponseSpec} that operates on the response from a GraphQL HTTP request.
*/
protected static final class DefaultResponseSpec implements ResponseSpec, ErrorSpec {
private static final class DefaultResponseSpec implements ResponseSpec, ErrorSpec {
private final ResponseContainer responseContainer;
/**
* Class constructor.
* @param documentContext the parsed response content
* @param errorFilter a globally defined filter for expected errors (to be ignored)
* @param assertDecorator decorator to apply around assertions, e.g. to add extra
*/
protected DefaultResponseSpec(
private DefaultResponseSpec(
DocumentContext documentContext, @Nullable Predicate<GraphQLError> errorFilter,
Consumer<Runnable> assertDecorator) {
@@ -533,7 +265,7 @@ class DefaultGraphQlTester implements GraphQlTester {
/**
* {@link PathSpec} implementation.
*/
private static class DefaultPathSpec implements PathSpec {
private static final class DefaultPathSpec implements PathSpec {
private final String inputPath;
@@ -543,7 +275,7 @@ class DefaultGraphQlTester implements GraphQlTester {
private final JsonPathExpectationsHelper pathHelper;
DefaultPathSpec(String path, ResponseContainer responseContainer) {
private DefaultPathSpec(String path, ResponseContainer responseContainer) {
Assert.notNull(path, "`path` is required");
Assert.notNull(responseContainer, "ResponseContainer is required");
this.inputPath = path;
@@ -678,7 +410,7 @@ class DefaultGraphQlTester implements GraphQlTester {
private final String inputPath;
DefaultEntitySpec(D entity, ResponseContainer responseContainer, String path) {
protected DefaultEntitySpec(D entity, ResponseContainer responseContainer, String path) {
this.entity = entity;
this.responseContainer = responseContainer;
this.inputPath = path;
@@ -753,10 +485,10 @@ class DefaultGraphQlTester implements GraphQlTester {
/**
* {@link ListEntitySpec} implementation.
*/
private static class DefaultListEntitySpec<E> extends DefaultEntitySpec<List<E>, ListEntitySpec<E>>
private static final class DefaultListEntitySpec<E> extends DefaultEntitySpec<List<E>, ListEntitySpec<E>>
implements ListEntitySpec<E> {
DefaultListEntitySpec(List<E> entity, ResponseContainer responseContainer, String path) {
private DefaultListEntitySpec(List<E> entity, ResponseContainer responseContainer, String path) {
super(entity, responseContainer, path);
}
@@ -766,7 +498,7 @@ class DefaultGraphQlTester implements GraphQlTester {
doAssert(() -> {
List<E> expected = Arrays.asList(elements);
AssertionErrors.assertTrue("List at path '" + getInputPath() + "' does not contain " + expected,
(getEntity() != null && getEntity().containsAll(expected)));
getEntity().containsAll(expected));
});
return this;
}
@@ -778,7 +510,7 @@ class DefaultGraphQlTester implements GraphQlTester {
List<E> expected = Arrays.asList(elements);
AssertionErrors.assertTrue(
"List at path '" + getInputPath() + "' should not have contained " + expected,
(getEntity() == null || !getEntity().containsAll(expected)));
!getEntity().containsAll(expected));
});
return this;
}
@@ -790,7 +522,7 @@ class DefaultGraphQlTester implements GraphQlTester {
List<E> expected = Arrays.asList(elements);
AssertionErrors.assertTrue(
"List at path '" + getInputPath() + "' should have contained exactly " + expected,
(getEntity() != null && getEntity().containsAll(expected)));
getEntity().containsAll(expected));
});
return this;
}
@@ -798,7 +530,7 @@ class DefaultGraphQlTester implements GraphQlTester {
@Override
public ListEntitySpec<E> hasSize(int size) {
doAssert(() -> AssertionErrors.assertTrue("List at path '" + getInputPath() + "' should have size " + size,
(getEntity() != null && getEntity().size() == size)));
getEntity().size() == size));
return this;
}
@@ -806,7 +538,7 @@ class DefaultGraphQlTester implements GraphQlTester {
public ListEntitySpec<E> hasSizeLessThan(int boundary) {
doAssert(() -> AssertionErrors.assertTrue(
"List at path '" + getInputPath() + "' should have size less than " + boundary,
(getEntity() != null && getEntity().size() < boundary)));
getEntity().size() < boundary));
return this;
}
@@ -814,45 +546,9 @@ class DefaultGraphQlTester implements GraphQlTester {
public ListEntitySpec<E> hasSizeGreaterThan(int boundary) {
doAssert(() -> AssertionErrors.assertTrue(
"List at path '" + getInputPath() + "' should have size greater than " + boundary,
(getEntity() != null && getEntity().size() > boundary)));
getEntity().size() > boundary));
return this;
}
}
static class JsonPathConfiguration {
private static final boolean jackson2Present;
static {
ClassLoader classLoader = JsonPathConfiguration.class.getClassLoader();
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader)
&& ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
}
static Configuration initialize(@Nullable Configuration jsonPathConfig) {
if (jsonPathConfig != null) {
return jsonPathConfig;
}
else if (jackson2Present) {
return Jackson2Configuration.create();
}
else {
return Configuration.builder().build();
}
}
}
static class Jackson2Configuration {
static Configuration create() {
return Configuration.builder()
.jsonProvider(new JacksonJsonProvider())
.mappingProvider(new JacksonMappingProvider())
.build();
}
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.test.tester;
import java.time.Duration;
import java.util.function.Predicate;
import com.jayway.jsonpath.Configuration;
import graphql.GraphQLError;
import org.springframework.graphql.GraphQlService;
import org.springframework.util.Assert;
/**
* Default implementation of a {@link GraphQlTester.Builder}.
*
* @author Rossen Stoyanchev
* @since 1.0.0
*/
class DefaultGraphQlTesterBuilder
extends GraphQlTesterBuilderSupport implements GraphQlTester.Builder<DefaultGraphQlTesterBuilder> {
private final GraphQlService service;
DefaultGraphQlTesterBuilder(GraphQlService service) {
Assert.notNull(service, "GraphQlService is required.");
this.service = service;
}
@Override
public DefaultGraphQlTesterBuilder errorFilter(Predicate<GraphQLError> predicate) {
addErrorFilter(predicate);
return this;
}
@Override
public DefaultGraphQlTesterBuilder jsonPathConfig(Configuration config) {
setJsonPathConfig(config);
return this;
}
@Override
public DefaultGraphQlTesterBuilder responseTimeout(Duration timeout) {
setResponseTimeout(timeout);
return this;
}
@Override
public GraphQlTester build() {
RequestStrategy strategy = new GraphQlServiceRequestStrategy(
this.service, getErrorFilter(), initJsonPathConfig(), initResponseTimeout());
return new DefaultGraphQlTester(strategy);
}
}

View File

@@ -17,26 +17,13 @@
package org.springframework.graphql.test.tester;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.function.Consumer;
import java.util.function.Predicate;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import graphql.GraphQLError;
import reactor.core.publisher.Flux;
import org.springframework.graphql.web.WebGraphQlHandler;
import org.springframework.graphql.web.WebInput;
import org.springframework.graphql.web.WebOutput;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.test.web.reactive.server.EntityExchangeResult;
import org.springframework.test.web.reactive.server.FluxExchangeResult;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
@@ -67,215 +54,32 @@ class DefaultWebGraphQlTester implements WebGraphQlTester {
/**
* Default implementation to build {@link WebGraphQlTester}.
* Factory for {@link WebGraphQlTester.ResponseSpec}, for use from
* {@link WebRequestStrategy} implementations.
*/
final static class DefaultBuilder
extends DefaultGraphQlTester.BuilderSupport implements WebGraphQlTester.Builder {
static WebResponseSpec createResponseSpec(
ResponseSpec responseSpec, @Nullable HttpHeaders responseHeaders) {
@Nullable
private final WebTestClient client;
return new DefaultWebResponseSpec(responseSpec, responseHeaders);
}
@Nullable
private final WebGraphQlHandler handler;
/**
* Factory for {@link WebGraphQlTester.SubscriptionSpec}, for use from
* {@link WebRequestStrategy} implementations.
*/
static WebSubscriptionSpec createSubscriptionSpec(
SubscriptionSpec subscriptionSpec, @Nullable HttpHeaders responseHeaders) {
@Nullable
private HttpHeaders headers;
DefaultBuilder(WebTestClient client) {
Assert.notNull(client, "WebTestClient is required.");
this.client = client;
this.handler = null;
}
DefaultBuilder(WebGraphQlHandler handler) {
Assert.notNull(handler, "WebGraphQlHandler is required.");
this.handler = handler;
this.client = null;
}
@Override
public WebGraphQlTester.Builder errorFilter(Predicate<GraphQLError> predicate) {
addErrorFilter(predicate);
return this;
}
@Override
public DefaultBuilder jsonPathConfig(Configuration config) {
setJsonPathConfig(config);
return this;
}
@Override
public DefaultBuilder responseTimeout(Duration timeout) {
setResponseTimeout(timeout);
return this;
}
@Override
public DefaultBuilder defaultHeader(String headerName, String... headerValues) {
this.headers = (this.headers != null ? this.headers : new HttpHeaders());
for (String headerValue : headerValues) {
this.headers.add(headerName, headerValue);
}
return this;
}
@Override
public WebGraphQlTester.Builder defaultHeaders(Consumer<HttpHeaders> headersConsumer) {
this.headers = (this.headers != null ? this.headers : new HttpHeaders());
headersConsumer.accept(this.headers);
return this;
}
@Override
public WebGraphQlTester build() {
WebRequestStrategy requestStrategy;
if (this.client != null) {
WebTestClient clientToUse = this.client;
if (getResponseTimeout() != null) {
clientToUse = this.client.mutate().responseTimeout(getResponseTimeout()).build();
}
requestStrategy = new WebTestClientRequestStrategy(
clientToUse, getErrorFilter(), initJsonPathConfig(), getResponseTimeout());
}
else if (this.handler != null) {
requestStrategy = new WebGraphQlHandlerRequestStrategy(
this.handler, getErrorFilter(), initJsonPathConfig(), initResponseTimeout());
}
else {
throw new IllegalStateException("Neither client nor handler");
}
return new DefaultWebGraphQlTester(requestStrategy, this.headers);
}
return new DefaultWebSubscriptionSpec(subscriptionSpec, responseHeaders);
}
/**
* Extension of {@code RequestStrategy} for performing a GraphQL request
* in a web environment.
* {@link WebRequestSpec} that also collects HTTP request headers, in
* addition to the query, operationName, and variables.
*/
interface WebRequestStrategy {
/**
* Perform a request with the given {@link WebInput} container.
* @param input the request input
* @return the response spec
*/
WebResponseSpec execute(WebInput input);
/**
* Perform a subscription with the given {@link WebInput} container.
* @param input the request input
* @return the subscription spec
*/
WebSubscriptionSpec executeSubscription(WebInput input);
}
/**
* {@link WebRequestStrategy} that works as an HTTP client with requests executed through
* {@link WebTestClient} that in turn may work connect with or without a live server
* for Spring MVC and WebFlux.
*/
private static class WebTestClientRequestStrategy
extends DefaultGraphQlTester.RequestStrategySupport implements WebRequestStrategy {
private final WebTestClient client;
public WebTestClientRequestStrategy(WebTestClient client,
@Nullable Predicate<GraphQLError> errorFilter, Configuration jsonPathConfig, Duration responseTimeout) {
super(errorFilter, jsonPathConfig, responseTimeout);
this.client = client;
}
@Override
public WebResponseSpec execute(WebInput webInput) {
EntityExchangeResult<byte[]> result = this.client.post()
.contentType(MediaType.APPLICATION_JSON)
.headers(headers -> headers.putAll(webInput.getHeaders()))
.bodyValue(webInput.toMap())
.exchange()
.expectStatus()
.isOk()
.expectHeader()
.contentType(MediaType.APPLICATION_JSON)
.expectBody()
.returnResult();
byte[] bytes = result.getResponseBodyContent();
Assert.notNull(bytes, "Expected GraphQL response content");
String content = new String(bytes, StandardCharsets.UTF_8);
DocumentContext documentContext = JsonPath.parse(content, getJsonPathConfig());
ResponseSpec responseSpec = createResponseSpec(documentContext, result::assertWithDiagnostics);
return new DefaultWebResponseSpec(responseSpec, result.getResponseHeaders());
}
@Override
public WebSubscriptionSpec executeSubscription(WebInput webInput) {
FluxExchangeResult<TestExecutionResult> exchangeResult = this.client.post()
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.TEXT_EVENT_STREAM)
.headers(headers -> headers.putAll(webInput.getHeaders()))
.bodyValue(webInput.toMap())
.exchange()
.expectStatus()
.isOk()
.expectHeader()
.contentType(MediaType.TEXT_EVENT_STREAM)
.returnResult(TestExecutionResult.class);
Flux<ResponseSpec> flux = exchangeResult.getResponseBody()
.map((result) -> createResponseSpec(result, exchangeResult::assertWithDiagnostics));
return new DefaultWebSubscriptionSpec(() -> flux, exchangeResult.getResponseHeaders());
}
}
/**
* {@link WebRequestStrategy} that performs requests directly on
* {@link WebGraphQlHandler}, i.e. Web request testing without a transport.
*/
private static class WebGraphQlHandlerRequestStrategy
extends DefaultGraphQlTester.DirectRequestStrategySupport implements WebRequestStrategy {
private final WebGraphQlHandler graphQlHandler;
WebGraphQlHandlerRequestStrategy(WebGraphQlHandler handler,
@Nullable Predicate<GraphQLError> errorFilter, Configuration jsonPathConfig, Duration timeout) {
super(errorFilter, jsonPathConfig, timeout);
this.graphQlHandler = handler;
}
@Override
public WebResponseSpec execute(WebInput input) {
WebOutput webOutput = executeInternal(input);
ResponseSpec responseSpec = createResponseSpec(input, webOutput);
return new DefaultWebResponseSpec(responseSpec, webOutput.getResponseHeaders());
}
@Override
public WebSubscriptionSpec executeSubscription(WebInput input) {
WebOutput webOutput = executeInternal(input);
SubscriptionSpec subscriptionSpec = createSubscriptionSpec(input, webOutput);
return new DefaultWebSubscriptionSpec(subscriptionSpec, webOutput.getResponseHeaders());
}
private WebOutput executeInternal(WebInput webInput) {
WebOutput webOutput = this.graphQlHandler.handle(webInput).block(getResponseTimeout());
Assert.notNull(webOutput, "Expected WebOutput");
return webOutput;
}
}
private static final class DefaultWebRequestSpec
extends DefaultGraphQlTester.RequestSpecSupport implements WebRequestSpec {
extends GraphQlTesterRequestSpecSupport implements WebRequestSpec {
private static final URI DEFAULT_URL = URI.create("");
@@ -283,7 +87,7 @@ class DefaultWebGraphQlTester implements WebGraphQlTester {
private final HttpHeaders headers = new HttpHeaders();
DefaultWebRequestSpec(
private DefaultWebRequestSpec(
WebRequestStrategy requestStrategy, @Nullable HttpHeaders defaultHeaders, String query) {
super(query);
@@ -341,20 +145,24 @@ class DefaultWebGraphQlTester implements WebGraphQlTester {
}
/**
* {@link WebResponseSpec} that exposes response headers and delegates
* all other methods to the given {@link GraphQlTester.ResponseSpec}.
*/
private static final class DefaultWebResponseSpec implements WebResponseSpec {
private final ResponseSpec responseSpec;
private final HttpHeaders headers;
private final HttpHeaders responseHeaders;
public DefaultWebResponseSpec(ResponseSpec responseSpec, @Nullable HttpHeaders headers) {
public DefaultWebResponseSpec(ResponseSpec responseSpec, @Nullable HttpHeaders responseHeaders) {
this.responseSpec = responseSpec;
this.headers = (headers != null ? headers : new HttpHeaders());
this.responseHeaders = (responseHeaders != null ? responseHeaders : new HttpHeaders());
}
@Override
public ResponseSpec httpHeadersSatisfy(Consumer<HttpHeaders> consumer) {
consumer.accept(this.headers);
consumer.accept(this.responseHeaders);
return this;
}
@@ -370,13 +178,17 @@ class DefaultWebGraphQlTester implements WebGraphQlTester {
}
/**
* {@link WebSubscriptionSpec} that exposes response headers and delegates
* all other methods to the given {@link GraphQlTester.SubscriptionSpec}.
*/
private static final class DefaultWebSubscriptionSpec implements WebSubscriptionSpec {
private final SubscriptionSpec delegate;
private final HttpHeaders headers;
public DefaultWebSubscriptionSpec(SubscriptionSpec delegate, @Nullable HttpHeaders headers) {
private DefaultWebSubscriptionSpec(SubscriptionSpec delegate, @Nullable HttpHeaders headers) {
this.delegate = delegate;
this.headers = (headers != null ? headers : new HttpHeaders());
}

View File

@@ -0,0 +1,120 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.test.tester;
import java.time.Duration;
import java.util.function.Consumer;
import java.util.function.Predicate;
import com.jayway.jsonpath.Configuration;
import graphql.GraphQLError;
import org.springframework.graphql.web.WebGraphQlHandler;
import org.springframework.http.HttpHeaders;
import org.springframework.lang.Nullable;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.util.Assert;
/**
* Default implementation of a {@link WebGraphQlTester.Builder}.
*
* @author Rossen Stoyanchev
* @since 1.0.0
*/
final class DefaultWebGraphQlTesterBuilder
extends GraphQlTesterBuilderSupport implements WebGraphQlTester.Builder {
@Nullable
private final WebTestClient client;
@Nullable
private final WebGraphQlHandler handler;
@Nullable
private HttpHeaders headers;
DefaultWebGraphQlTesterBuilder(WebTestClient client) {
Assert.notNull(client, "WebTestClient is required.");
this.client = client;
this.handler = null;
}
DefaultWebGraphQlTesterBuilder(WebGraphQlHandler handler) {
Assert.notNull(handler, "WebGraphQlHandler is required.");
this.handler = handler;
this.client = null;
}
@Override
public WebGraphQlTester.Builder errorFilter(Predicate<GraphQLError> predicate) {
addErrorFilter(predicate);
return this;
}
@Override
public DefaultWebGraphQlTesterBuilder jsonPathConfig(Configuration config) {
setJsonPathConfig(config);
return this;
}
@Override
public DefaultWebGraphQlTesterBuilder responseTimeout(Duration timeout) {
setResponseTimeout(timeout);
return this;
}
@Override
public DefaultWebGraphQlTesterBuilder defaultHeader(String headerName, String... headerValues) {
this.headers = (this.headers != null ? this.headers : new HttpHeaders());
for (String headerValue : headerValues) {
this.headers.add(headerName, headerValue);
}
return this;
}
@Override
public WebGraphQlTester.Builder defaultHeaders(Consumer<HttpHeaders> headersConsumer) {
this.headers = (this.headers != null ? this.headers : new HttpHeaders());
headersConsumer.accept(this.headers);
return this;
}
@Override
public WebGraphQlTester build() {
return new DefaultWebGraphQlTester(initRequestStrategy(), this.headers);
}
private WebRequestStrategy initRequestStrategy() {
if (this.client != null) {
WebTestClient clientToUse = this.client;
if (getResponseTimeout() != null) {
clientToUse = this.client.mutate().responseTimeout(getResponseTimeout()).build();
}
return new WebTestClientRequestStrategy(
clientToUse, getErrorFilter(), initJsonPathConfig(), getResponseTimeout());
}
if (this.handler != null) {
return new WebGraphQlHandlerRequestStrategy(
this.handler, getErrorFilter(), initJsonPathConfig(), initResponseTimeout());
}
throw new IllegalStateException("Neither client nor handler");
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.test.tester;
import java.time.Duration;
import java.util.function.Consumer;
import java.util.function.Predicate;
import com.jayway.jsonpath.Configuration;
import graphql.ExecutionResult;
import graphql.GraphQLError;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import org.springframework.graphql.RequestInput;
import org.springframework.lang.Nullable;
import org.springframework.test.util.AssertionErrors;
import org.springframework.util.CollectionUtils;
/**
* Base class for a {@link RequestStrategy} that performs GraphQL requests
* directly against a GraphQL Java server, i.e. without a client.
*
* @author Rossen Stoyanchev
* @since 1.0.0
*/
class DirectRequestStrategySupport extends RequestStrategySupport {
protected DirectRequestStrategySupport(
@Nullable Predicate<GraphQLError> errorFilter, Configuration jsonPathConfig, Duration timeout) {
super(errorFilter, jsonPathConfig, timeout);
}
protected GraphQlTester.ResponseSpec createResponseSpec(RequestInput input, ExecutionResult result) {
return createResponseSpec(result, assertDecorator(input));
}
protected GraphQlTester.SubscriptionSpec createSubscriptionSpec(RequestInput input, ExecutionResult result) {
Consumer<Runnable> assertDecorator = assertDecorator(input);
assertDecorator.accept(() -> AssertionErrors.assertTrue(
"Subscription did not return Publisher",
result.getData() instanceof Publisher));
assertDecorator.accept(() -> AssertionErrors.assertTrue(
"Response has " + result.getErrors().size() + " unexpected error(s).",
CollectionUtils.isEmpty(result.getErrors())));
return () -> {
Publisher<? extends ExecutionResult> publisher = result.getData();
return Flux.from(publisher).map((current) -> createResponseSpec(current, assertDecorator));
};
}
private Consumer<Runnable> assertDecorator(RequestInput input) {
return (assertion) -> {
try {
assertion.run();
}
catch (AssertionError ex) {
throw new AssertionError(ex.getMessage() + "\nRequest: " + input, ex);
}
};
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.test.tester;
import java.time.Duration;
import java.util.function.Predicate;
import com.jayway.jsonpath.Configuration;
import graphql.ExecutionResult;
import graphql.GraphQLError;
import org.springframework.graphql.GraphQlService;
import org.springframework.graphql.RequestInput;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* {@link RequestStrategy} that performs requests via {@link GraphQlService}.
*
* @author Rossen Stoyanchev
* @since 1.0.0
*/
final class GraphQlServiceRequestStrategy extends DirectRequestStrategySupport implements RequestStrategy {
private final GraphQlService graphQlService;
public GraphQlServiceRequestStrategy(GraphQlService service,
@Nullable Predicate<GraphQLError> errorFilter, Configuration jsonPathConfig, Duration timeout) {
super(errorFilter, jsonPathConfig, timeout);
Assert.notNull(service, "GraphQlService is required.");
this.graphQlService = service;
}
@Override
public GraphQlTester.ResponseSpec execute(RequestInput input) {
return createResponseSpec(input, executeInternal(input));
}
@Override
public GraphQlTester.SubscriptionSpec executeSubscription(RequestInput input) {
return createSubscriptionSpec(input, executeInternal(input));
}
private ExecutionResult executeInternal(RequestInput input) {
ExecutionResult result = this.graphQlService.execute(input).block(getResponseTimeout());
Assert.notNull(result, "Expected ExecutionResult");
return result;
}
}

View File

@@ -69,7 +69,7 @@ public interface GraphQlTester {
* @return the builder to use
*/
static Builder<?> builder(GraphQlService service) {
return new DefaultGraphQlTester.DefaultBuilder(service);
return new DefaultGraphQlTesterBuilder(service);
}

View File

@@ -0,0 +1,110 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.test.tester;
import java.time.Duration;
import java.util.function.Predicate;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.spi.json.JacksonJsonProvider;
import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
import graphql.GraphQLError;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* Base class support for implementations of
* {@link GraphQlTester.Builder} and {@link WebGraphQlTester.Builder}.
*
* @author Rossen Stoyanchev
* @since 1.0.0
*/
class GraphQlTesterBuilderSupport {
private static final boolean jackson2Present;
static {
ClassLoader classLoader = GraphQlTesterBuilderSupport.class.getClassLoader();
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader)
&& ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
}
private static final Duration DEFAULT_RESPONSE_DURATION = Duration.ofSeconds(5);
@Nullable
private Predicate<GraphQLError> errorFilter;
@Nullable
private Configuration jsonPathConfig;
@Nullable
private Duration responseTimeout;
protected void addErrorFilter(Predicate<GraphQLError> predicate) {
this.errorFilter = (this.errorFilter != null ? errorFilter.and(predicate) : predicate);
}
@Nullable
protected Predicate<GraphQLError> getErrorFilter() {
return errorFilter;
}
protected void setJsonPathConfig(Configuration config) {
this.jsonPathConfig = config;
}
protected void setResponseTimeout(Duration timeout) {
Assert.notNull(timeout, "'timeout' is required");
this.responseTimeout = timeout;
}
@Nullable
protected Duration getResponseTimeout() {
return this.responseTimeout;
}
protected Configuration initJsonPathConfig() {
if (this.jsonPathConfig != null) {
return this.jsonPathConfig;
}
else if (jackson2Present) {
return Jackson2Configuration.create();
}
else {
return Configuration.builder().build();
}
}
protected Duration initResponseTimeout() {
return (this.responseTimeout != null ? this.responseTimeout : DEFAULT_RESPONSE_DURATION);
}
private static class Jackson2Configuration {
static Configuration create() {
return Configuration.builder()
.jsonProvider(new JacksonJsonProvider())
.mappingProvider(new JacksonMappingProvider())
.build();
}
}
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.test.tester;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.graphql.RequestInput;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Base class support for implementations of
* {@link GraphQlTester.RequestSpec} and {@link WebGraphQlTester.RequestSpec}.
*
* @author Rossen Stoyanchev
* @since 1.0.0
*/
class GraphQlTesterRequestSpecSupport {
private final String query;
@Nullable
private String operationName;
private final Map<String, Object> variables = new LinkedHashMap<>();
protected GraphQlTesterRequestSpecSupport(String query) {
Assert.notNull(query, "`query` is required");
this.query = query;
}
protected void setOperationName(@Nullable String name) {
this.operationName = name;
}
protected void addVariable(String name, Object value) {
this.variables.put(name, value);
}
protected void verify(GraphQlTester.ResponseSpec responseSpec) {
responseSpec.path("$.errors").valueIsEmpty();
}
protected RequestInput createRequestInput() {
return new RequestInput(this.query, this.operationName, this.variables);
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.test.tester;
import org.springframework.graphql.RequestInput;
/**
* Abstracts how a GraphQL request is performed, given {@link RequestInput}, and
* resulting in the creation of a response spec.
*
* <p>For internal use use from {@link DefaultGraphQlTester}.
*
* @author Rossen Stoyanchev
* @since 1.0.0
*/
interface RequestStrategy {
/**
* Perform a request with the given {@link RequestInput} container.
* @param input the request input
* @return the response spec
*/
GraphQlTester.ResponseSpec execute(RequestInput input);
/**
* Perform a subscription with the given {@link RequestInput} container.
* @param input the request input
* @return the subscription spec
*/
GraphQlTester.SubscriptionSpec executeSubscription(RequestInput input);
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.test.tester;
import java.time.Duration;
import java.util.function.Consumer;
import java.util.function.Predicate;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import graphql.ExecutionResult;
import graphql.GraphQLError;
import org.springframework.lang.Nullable;
/**
* Base class support for {@link RequestStrategy} and
* {@link WebRequestStrategy} implementations.
*
* @author Rossen Stoyanchev
* @since 1.0.0
*/
class RequestStrategySupport {
@Nullable
private final Predicate<GraphQLError> errorFilter;
private final Configuration jsonPathConfig;
private final Duration responseTimeout;
protected RequestStrategySupport(
@Nullable Predicate<GraphQLError> errorFilter, Configuration jsonPathConfig, Duration timeout) {
this.errorFilter = errorFilter;
this.jsonPathConfig = jsonPathConfig;
this.responseTimeout = timeout;
}
protected Configuration getJsonPathConfig() {
return this.jsonPathConfig;
}
protected Duration getResponseTimeout() {
return this.responseTimeout;
}
protected GraphQlTester.ResponseSpec createResponseSpec(
ExecutionResult result, Consumer<Runnable> assertDecorator) {
DocumentContext context = JsonPath.parse(result.toSpecification(), this.jsonPathConfig);
return createResponseSpec(context, assertDecorator);
}
protected GraphQlTester.ResponseSpec createResponseSpec(
DocumentContext context, Consumer<Runnable> assertDecorator) {
return DefaultGraphQlTester.createResponseSpec(context, this.errorFilter, assertDecorator);
}
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.test.tester;
import java.time.Duration;
import java.util.function.Predicate;
import com.jayway.jsonpath.Configuration;
import graphql.GraphQLError;
import org.springframework.graphql.web.WebGraphQlHandler;
import org.springframework.graphql.web.WebInput;
import org.springframework.graphql.web.WebOutput;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* {@link WebRequestStrategy} that performs requests directly against a
* {@link WebGraphQlHandler}, i.e. without a client.
*
* @author Rossen Stoyanchev
* @since 1.0.0
*/
class WebGraphQlHandlerRequestStrategy extends DirectRequestStrategySupport implements WebRequestStrategy {
private final WebGraphQlHandler graphQlHandler;
WebGraphQlHandlerRequestStrategy(WebGraphQlHandler handler,
@Nullable Predicate<GraphQLError> errorFilter, Configuration jsonPathConfig, Duration timeout) {
super(errorFilter, jsonPathConfig, timeout);
this.graphQlHandler = handler;
}
@Override
public WebGraphQlTester.WebResponseSpec execute(WebInput input) {
WebOutput webOutput = executeInternal(input);
GraphQlTester.ResponseSpec responseSpec = createResponseSpec(input, webOutput);
return DefaultWebGraphQlTester.createResponseSpec(responseSpec, webOutput.getResponseHeaders());
}
@Override
public WebGraphQlTester.WebSubscriptionSpec executeSubscription(WebInput input) {
WebOutput webOutput = executeInternal(input);
GraphQlTester.SubscriptionSpec subscriptionSpec = createSubscriptionSpec(input, webOutput);
return DefaultWebGraphQlTester.createSubscriptionSpec(subscriptionSpec, webOutput.getResponseHeaders());
}
private WebOutput executeInternal(WebInput webInput) {
WebOutput webOutput = this.graphQlHandler.handle(webInput).block(getResponseTimeout());
Assert.notNull(webOutput, "Expected WebOutput");
return webOutput;
}
}

View File

@@ -117,7 +117,7 @@ public interface WebGraphQlTester extends GraphQlTester {
* @return the builder to use
*/
static Builder builder(WebTestClient client) {
return new DefaultWebGraphQlTester.DefaultBuilder(client);
return new DefaultWebGraphQlTesterBuilder(client);
}
/**
@@ -126,7 +126,7 @@ public interface WebGraphQlTester extends GraphQlTester {
* @return the builder to use
*/
static Builder builder(WebGraphQlHandler handler) {
return new DefaultWebGraphQlTester.DefaultBuilder(handler);
return new DefaultWebGraphQlTesterBuilder(handler);
}
@@ -206,7 +206,7 @@ public interface WebGraphQlTester extends GraphQlTester {
/**
* Extension of {@code ResponseSpec} with access to HTTP response headers.
* Extension of {@code ResponseSpec} to expose access to HTTP response headers.
*/
interface WebResponseSpec extends ResponseSpec {
@@ -221,7 +221,7 @@ public interface WebGraphQlTester extends GraphQlTester {
/**
* Extension of {@code SubscriptionSpec} with access to HTTP response headers.
* Extension of {@code SubscriptionSpec} to expose access to HTTP response headers.
*/
interface WebSubscriptionSpec extends SubscriptionSpec {

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.test.tester;
import org.springframework.graphql.web.WebInput;
/**
* Abstracts how a GraphQL request is performed in the a Web context, given
* {@link WebInput}, and resulting in the creation of a response spec.
*
* <p>For internal use use from {@link DefaultWebGraphQlTester}.
*
* @author Rossen Stoyanchev
* @since 1.0.0
*/
interface WebRequestStrategy {
/**
* Perform a request with the given {@link WebInput}.
* @param input the request input
* @return the response spec
*/
WebGraphQlTester.WebResponseSpec execute(WebInput input);
/**
* Perform a subscription with the given {@link WebInput}.
* @param input the request input
* @return the subscription spec
*/
WebGraphQlTester.WebSubscriptionSpec executeSubscription(WebInput input);
}

View File

@@ -0,0 +1,100 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.test.tester;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.function.Predicate;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import graphql.GraphQLError;
import reactor.core.publisher.Flux;
import org.springframework.graphql.web.WebInput;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.test.web.reactive.server.EntityExchangeResult;
import org.springframework.test.web.reactive.server.FluxExchangeResult;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.util.Assert;
/**
* {@link WebRequestStrategy} that uses {@link WebTestClient} to perform
* requests. Depending on how the client is configured, this may be used with
* Spring MVC and WebFlux controllers, without a server, or against a live server.
*
* @author Rossen Stoyanchev
* @since 1.0.0
*/
final class WebTestClientRequestStrategy extends RequestStrategySupport implements WebRequestStrategy {
private final WebTestClient client;
WebTestClientRequestStrategy(WebTestClient client,
@Nullable Predicate<GraphQLError> errorFilter, Configuration jsonPathConfig, Duration responseTimeout) {
super(errorFilter, jsonPathConfig, responseTimeout);
this.client = client;
}
@Override
public WebGraphQlTester.WebResponseSpec execute(WebInput webInput) {
EntityExchangeResult<byte[]> result = this.client.post()
.contentType(MediaType.APPLICATION_JSON)
.headers(headers -> headers.putAll(webInput.getHeaders()))
.bodyValue(webInput.toMap())
.exchange()
.expectStatus()
.isOk()
.expectHeader()
.contentType(MediaType.APPLICATION_JSON)
.expectBody()
.returnResult();
byte[] bytes = result.getResponseBodyContent();
Assert.notNull(bytes, "Expected GraphQL response content");
String content = new String(bytes, StandardCharsets.UTF_8);
DocumentContext documentContext = JsonPath.parse(content, getJsonPathConfig());
GraphQlTester.ResponseSpec responseSpec = createResponseSpec(documentContext, result::assertWithDiagnostics);
return DefaultWebGraphQlTester.createResponseSpec(responseSpec, result.getResponseHeaders());
}
@Override
public WebGraphQlTester.WebSubscriptionSpec executeSubscription(WebInput webInput) {
FluxExchangeResult<TestExecutionResult> exchangeResult = this.client.post()
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.TEXT_EVENT_STREAM)
.headers(headers -> headers.putAll(webInput.getHeaders()))
.bodyValue(webInput.toMap())
.exchange()
.expectStatus()
.isOk()
.expectHeader()
.contentType(MediaType.TEXT_EVENT_STREAM)
.returnResult(TestExecutionResult.class);
Flux<GraphQlTester.ResponseSpec> flux = exchangeResult.getResponseBody()
.map((result) -> createResponseSpec(result, exchangeResult::assertWithDiagnostics));
return DefaultWebGraphQlTester.createSubscriptionSpec(() -> flux, exchangeResult.getResponseHeaders());
}
}