diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlTester.java index 08bfdd83..d8b1f6c9 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlTester.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlTester.java @@ -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 errorFilter, + Consumer assertDecorator) { - @Nullable - private Predicate errorFilter; - - @Nullable - private Configuration jsonPathConfig; - - @Nullable - private Duration responseTimeout = Duration.ofSeconds(5); - - protected void addErrorFilter(Predicate predicate) { - this.errorFilter = (this.errorFilter != null ? errorFilter.and(predicate) : predicate); - } - - @Nullable - protected Predicate 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 { - - private final GraphQlService service; - - DefaultBuilder(GraphQlService service) { - Assert.notNull(service, "GraphQlService is required."); - this.service = service; - } - - @Override - public DefaultBuilder errorFilter(Predicate 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 errorFilter; - - private final Configuration jsonPathConfig; - - private final Duration responseTimeout; - - protected RequestStrategySupport( - @Nullable Predicate 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 assertDecorator) { - DocumentContext context = JsonPath.parse(result.toSpecification(), this.jsonPathConfig); - return createResponseSpec(context, assertDecorator); - } - - protected ResponseSpec createResponseSpec(DocumentContext context, Consumer 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 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 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 publisher = result.getData(); - return Flux.from(publisher).map((current) -> createResponseSpec(current, assertDecorator)); - }; - } - - private Consumer 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 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 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 { + private static final class DefaultRequestSpec + extends GraphQlTesterRequestSpecSupport implements RequestSpec { 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> ERROR_LIST_TYPE = new TypeRef>() {}; @@ -403,7 +141,7 @@ class DefaultGraphQlTester implements GraphQlTester { private final Consumer assertDecorator; - ResponseContainer( + private ResponseContainer( DocumentContext documentContext, @Nullable Predicate errorFilter, Consumer 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 errorFilter, Consumer 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 extends DefaultEntitySpec, ListEntitySpec> + private static final class DefaultListEntitySpec extends DefaultEntitySpec, ListEntitySpec> implements ListEntitySpec { - DefaultListEntitySpec(List entity, ResponseContainer responseContainer, String path) { + private DefaultListEntitySpec(List entity, ResponseContainer responseContainer, String path) { super(entity, responseContainer, path); } @@ -766,7 +498,7 @@ class DefaultGraphQlTester implements GraphQlTester { doAssert(() -> { List 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 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 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 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 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 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(); - } - } - } diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlTesterBuilder.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlTesterBuilder.java new file mode 100644 index 00000000..e9bf883d --- /dev/null +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlTesterBuilder.java @@ -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 { + + private final GraphQlService service; + + + DefaultGraphQlTesterBuilder(GraphQlService service) { + Assert.notNull(service, "GraphQlService is required."); + this.service = service; + } + + + @Override + public DefaultGraphQlTesterBuilder errorFilter(Predicate 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); + } + +} diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebGraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebGraphQlTester.java index f2587ee5..173c5222 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebGraphQlTester.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebGraphQlTester.java @@ -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 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 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 errorFilter, Configuration jsonPathConfig, Duration responseTimeout) { - - super(errorFilter, jsonPathConfig, responseTimeout); - this.client = client; - } - - @Override - public WebResponseSpec execute(WebInput webInput) { - EntityExchangeResult 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 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 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 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 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()); } diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebGraphQlTesterBuilder.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebGraphQlTesterBuilder.java new file mode 100644 index 00000000..f6b2a8e5 --- /dev/null +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebGraphQlTesterBuilder.java @@ -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 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 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"); + } + +} diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DirectRequestStrategySupport.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DirectRequestStrategySupport.java new file mode 100644 index 00000000..85286bb3 --- /dev/null +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DirectRequestStrategySupport.java @@ -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 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 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 publisher = result.getData(); + return Flux.from(publisher).map((current) -> createResponseSpec(current, assertDecorator)); + }; + } + + private Consumer assertDecorator(RequestInput input) { + return (assertion) -> { + try { + assertion.run(); + } + catch (AssertionError ex) { + throw new AssertionError(ex.getMessage() + "\nRequest: " + input, ex); + } + }; + } + +} diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlServiceRequestStrategy.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlServiceRequestStrategy.java new file mode 100644 index 00000000..bea51b66 --- /dev/null +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlServiceRequestStrategy.java @@ -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 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; + } + +} diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlTester.java index af73acf4..28cb968f 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlTester.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlTester.java @@ -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); } diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlTesterBuilderSupport.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlTesterBuilderSupport.java new file mode 100644 index 00000000..07f8446d --- /dev/null +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlTesterBuilderSupport.java @@ -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 errorFilter; + + @Nullable + private Configuration jsonPathConfig; + + @Nullable + private Duration responseTimeout; + + + protected void addErrorFilter(Predicate predicate) { + this.errorFilter = (this.errorFilter != null ? errorFilter.and(predicate) : predicate); + } + + @Nullable + protected Predicate 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(); + } + } + +} diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlTesterRequestSpecSupport.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlTesterRequestSpecSupport.java new file mode 100644 index 00000000..7e973136 --- /dev/null +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlTesterRequestSpecSupport.java @@ -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 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); + } + +} diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/RequestStrategy.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/RequestStrategy.java new file mode 100644 index 00000000..8f4ba4c9 --- /dev/null +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/RequestStrategy.java @@ -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. + * + *

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); + +} diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/RequestStrategySupport.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/RequestStrategySupport.java new file mode 100644 index 00000000..0b1a6e13 --- /dev/null +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/RequestStrategySupport.java @@ -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 errorFilter; + + private final Configuration jsonPathConfig; + + private final Duration responseTimeout; + + + protected RequestStrategySupport( + @Nullable Predicate 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 assertDecorator) { + + DocumentContext context = JsonPath.parse(result.toSpecification(), this.jsonPathConfig); + return createResponseSpec(context, assertDecorator); + } + + protected GraphQlTester.ResponseSpec createResponseSpec( + DocumentContext context, Consumer assertDecorator) { + + return DefaultGraphQlTester.createResponseSpec(context, this.errorFilter, assertDecorator); + } + +} diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/WebGraphQlHandlerRequestStrategy.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/WebGraphQlHandlerRequestStrategy.java new file mode 100644 index 00000000..ad94b044 --- /dev/null +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/WebGraphQlHandlerRequestStrategy.java @@ -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 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; + } + +} diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/WebGraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/WebGraphQlTester.java index c441f782..2a86b0e3 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/WebGraphQlTester.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/WebGraphQlTester.java @@ -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 { diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/WebRequestStrategy.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/WebRequestStrategy.java new file mode 100644 index 00000000..3f758f73 --- /dev/null +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/WebRequestStrategy.java @@ -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. + * + *

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); + +} diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/WebTestClientRequestStrategy.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/WebTestClientRequestStrategy.java new file mode 100644 index 00000000..6172a4e3 --- /dev/null +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/WebTestClientRequestStrategy.java @@ -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 errorFilter, Configuration jsonPathConfig, Duration responseTimeout) { + + super(errorFilter, jsonPathConfig, responseTimeout); + this.client = client; + } + + + @Override + public WebGraphQlTester.WebResponseSpec execute(WebInput webInput) { + EntityExchangeResult 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 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 flux = exchangeResult.getResponseBody() + .map((result) -> createResponseSpec(result, exchangeResult::assertWithDiagnostics)); + + return DefaultWebGraphQlTester.createSubscriptionSpec(() -> flux, exchangeResult.getResponseHeaders()); + } + +}