From 65b5ff9a7c2024e3e7cd26a5aaf08f493710fd75 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 30 Sep 2021 22:04:51 +0100 Subject: [PATCH] WebGraphQlTester exposes HTTP response headers Closes gh-74 --- .../test/tester/DefaultGraphQlTester.java | 41 ++--- .../test/tester/DefaultWebGraphQlTester.java | 143 +++++++++++++++--- .../graphql/test/tester/WebGraphQlTester.java | 51 ++++++- .../test/tester/WebGraphQlTesterTests.java | 1 - 4 files changed, 195 insertions(+), 41 deletions(-) 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 e505bcce..6d4ea217 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 @@ -155,7 +155,7 @@ class DefaultGraphQlTester implements GraphQlTester { * without an underlying transport and where {@link RequestInput} provides * sufficient input. */ - protected abstract static class AbstractDirectRequestStrategy implements RequestStrategy { + protected static class DirectRequestStrategySupport { @Nullable private final Predicate errorFilter; @@ -164,7 +164,7 @@ class DefaultGraphQlTester implements GraphQlTester { private final Duration responseTimeout; - protected AbstractDirectRequestStrategy( + protected DirectRequestStrategySupport( @Nullable Predicate errorFilter, Configuration jsonPathConfig, Duration timeout) { this.errorFilter = errorFilter; @@ -176,32 +176,24 @@ class DefaultGraphQlTester implements GraphQlTester { return this.responseTimeout; } - @Override - public ResponseSpec execute(RequestInput input) { - ExecutionResult result = executeInternal(input); + protected ResponseSpec createResponseSpec(RequestInput input, ExecutionResult result) { DocumentContext context = JsonPath.parse(result.toSpecification(), this.jsonPathConfig); return new DefaultResponseSpec(context, this.errorFilter, assertDecorator(input)); } - @Override - public SubscriptionSpec executeSubscription(RequestInput input) { - ExecutionResult result = executeInternal(input); - AssertionErrors.assertTrue("Subscription did not return Publisher", result.getData() instanceof Publisher); + protected SubscriptionSpec createSubscriptionSpec(RequestInput input, ExecutionResult result) { + AssertionErrors.assertTrue( + "Subscription did not return Publisher", result.getData() instanceof Publisher); - List errors = result.getErrors(); Consumer assertDecorator = assertDecorator(input); + List errors = result.getErrors(); assertDecorator.accept(() -> AssertionErrors.assertTrue( "Response has " + errors.size() + " unexpected error(s).", CollectionUtils.isEmpty(errors))); - return new DefaultSubscriptionSpec( - result.getData(), this.errorFilter, this.jsonPathConfig, assertDecorator); + Publisher publisher = result.getData(); + return new DefaultSubscriptionSpec(publisher, this.errorFilter, this.jsonPathConfig, assertDecorator); } - /** - * Sub-classes implement this to actual perform the request. - */ - protected abstract ExecutionResult executeInternal(RequestInput input); - private Consumer assertDecorator(RequestInput input) { return (assertion) -> { try { @@ -218,7 +210,8 @@ class DefaultGraphQlTester implements GraphQlTester { /** * {@link RequestStrategy} that performs requests through a {@link GraphQlService}. */ - protected static class GraphQlServiceRequestStrategy extends AbstractDirectRequestStrategy { + protected static class GraphQlServiceRequestStrategy + extends DirectRequestStrategySupport implements RequestStrategy { private final GraphQlService graphQlService; @@ -230,7 +223,17 @@ class DefaultGraphQlTester implements GraphQlTester { this.graphQlService = service; } - protected ExecutionResult executeInternal(RequestInput input) { + @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(responseTimeout()); Assert.notNull(result, "Expected ExecutionResult"); return result; 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 dbeffcf6..2a069046 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 @@ -30,10 +30,12 @@ import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.JsonPath; import graphql.ExecutionResult; import graphql.GraphQLError; +import reactor.core.publisher.Flux; import org.springframework.graphql.RequestInput; 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; @@ -54,7 +56,7 @@ class DefaultWebGraphQlTester extends DefaultGraphQlTester implements WebGraphQl private final HttpHeaders defaultHeaders; - DefaultWebGraphQlTester(RequestStrategy requestStrategy, @Nullable HttpHeaders defaultHeaders) { + DefaultWebGraphQlTester(WebRequestStrategy requestStrategy, @Nullable HttpHeaders defaultHeaders) { super(requestStrategy); this.defaultHeaders = defaultHeaders; } @@ -62,7 +64,7 @@ class DefaultWebGraphQlTester extends DefaultGraphQlTester implements WebGraphQl @Override public WebRequestSpec query(String query) { - return new DefaultWebRequestSpec(getRequestStrategy(), query, this.defaultHeaders); + return new DefaultWebRequestSpec((WebRequestStrategy) getRequestStrategy(), query, this.defaultHeaders); } @@ -80,7 +82,7 @@ class DefaultWebGraphQlTester extends DefaultGraphQlTester implements WebGraphQl @Nullable private Duration responseTimeout; - private final Supplier requestStrategySupplier; + private final Supplier requestStrategySupplier; @Nullable private HttpHeaders headers; @@ -145,12 +147,37 @@ class DefaultWebGraphQlTester extends DefaultGraphQlTester implements WebGraphQl } } + + /** + * Extension of {@code RequestStrategy} for performing a GraphQL request + * in a web environment. + */ + interface WebRequestStrategy extends RequestStrategy { + + /** + * Perform a request with the given {@link RequestInput} container. + * @param input the request input + * @return the response spec + */ + WebResponseSpec execute(RequestInput input); + + /** + * Perform a subscription with the given {@link RequestInput} container. + * @param input the request input + * @return the subscription spec + */ + WebSubscriptionSpec executeSubscription(RequestInput input); + + } + + + /** * {@link RequestStrategy} 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 implements RequestStrategy { + private static class WebTestClientRequestStrategy implements WebRequestStrategy { private final WebTestClient client; @@ -168,7 +195,7 @@ class DefaultWebGraphQlTester extends DefaultGraphQlTester implements WebGraphQl } @Override - public ResponseSpec execute(RequestInput requestInput) { + public WebResponseSpec execute(RequestInput requestInput) { EntityExchangeResult result = this.client.post() .contentType(MediaType.APPLICATION_JSON) .headers(headers -> headers.putAll(getHeaders(requestInput))) @@ -186,11 +213,14 @@ class DefaultWebGraphQlTester extends DefaultGraphQlTester implements WebGraphQl String content = new String(bytes, StandardCharsets.UTF_8); DocumentContext documentContext = JsonPath.parse(content, this.jsonPathConfig); - return new DefaultResponseSpec(documentContext, this.errorFilter, result::assertWithDiagnostics); + ResponseSpec responseSpec = + new DefaultResponseSpec(documentContext, this.errorFilter, result::assertWithDiagnostics); + + return new DefaultWebResponseSpec(responseSpec, result.getResponseHeaders()); } @Override - public SubscriptionSpec executeSubscription(RequestInput requestInput) { + public WebSubscriptionSpec executeSubscription(RequestInput requestInput) { FluxExchangeResult exchangeResult = this.client.post() .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.TEXT_EVENT_STREAM) @@ -203,9 +233,11 @@ class DefaultWebGraphQlTester extends DefaultGraphQlTester implements WebGraphQl .contentType(MediaType.TEXT_EVENT_STREAM) .returnResult(TestExecutionResult.class); - return new DefaultSubscriptionSpec( + SubscriptionSpec subscriptionSpec = new DefaultSubscriptionSpec( exchangeResult.getResponseBody().cast(ExecutionResult.class), this.errorFilter, this.jsonPathConfig, exchangeResult::assertWithDiagnostics); + + return new DefaultWebSubscriptionSpec(subscriptionSpec, exchangeResult.getResponseHeaders()); } private HttpHeaders getHeaders(RequestInput requestInput) { @@ -219,7 +251,7 @@ class DefaultWebGraphQlTester extends DefaultGraphQlTester implements WebGraphQl * {@link RequestStrategy} that performs requests directly on * {@link WebGraphQlHandler}, i.e. Web request testing without a transport. */ - private static class WebGraphQlHandlerRequestStrategy extends AbstractDirectRequestStrategy { + private static class WebGraphQlHandlerRequestStrategy extends DirectRequestStrategySupport implements WebRequestStrategy { private final WebGraphQlHandler graphQlHandler; @@ -230,21 +262,34 @@ class DefaultWebGraphQlTester extends DefaultGraphQlTester implements WebGraphQl this.graphQlHandler = handler; } - protected ExecutionResult executeInternal(RequestInput input) { - Assert.isInstanceOf(WebInput.class, input); - WebInput webInput = (WebInput) input; - ExecutionResult result = this.graphQlHandler.handle(webInput).block(responseTimeout()); - Assert.notNull(result, "Expected ExecutionResult"); - return result; + @Override + public WebResponseSpec execute(RequestInput input) { + WebOutput webOutput = executeInternal(input); + ResponseSpec responseSpec = createResponseSpec(input, webOutput); + return new DefaultWebResponseSpec(responseSpec, webOutput.getResponseHeaders()); } + @Override + public WebSubscriptionSpec executeSubscription(RequestInput input) { + WebOutput webOutput = executeInternal(input); + SubscriptionSpec spec = createSubscriptionSpec(input, webOutput); + return new DefaultWebSubscriptionSpec(spec, webOutput.getResponseHeaders()); + } + + private WebOutput executeInternal(RequestInput input) { + Assert.isInstanceOf(WebInput.class, input); + WebInput webInput = (WebInput) input; + WebOutput webOutput = this.graphQlHandler.handle(webInput).block(responseTimeout()); + Assert.notNull(webOutput, "Expected WebOutput"); + return webOutput; + } } private static final class DefaultWebRequestSpec implements WebRequestSpec { private static final URI DEFAULT_URL = URI.create(""); - private final RequestStrategy requestStrategy; + private final WebRequestStrategy requestStrategy; private final String query; @@ -255,8 +300,8 @@ class DefaultWebGraphQlTester extends DefaultGraphQlTester implements WebGraphQl private final HttpHeaders headers = new HttpHeaders(); - DefaultWebRequestSpec(RequestStrategy requestStrategy, String query, @Nullable HttpHeaders headers) { - Assert.notNull(requestStrategy, "RequestStrategy is required"); + DefaultWebRequestSpec(WebRequestStrategy requestStrategy, String query, @Nullable HttpHeaders headers) { + Assert.notNull(requestStrategy, "WebRequestStrategy is required"); Assert.notNull(query, "`query` is required"); this.requestStrategy = requestStrategy; this.query = query; @@ -292,7 +337,7 @@ class DefaultWebGraphQlTester extends DefaultGraphQlTester implements WebGraphQl } @Override - public ResponseSpec execute() { + public WebResponseSpec execute() { return this.requestStrategy.execute(createRequestInput()); } @@ -302,7 +347,7 @@ class DefaultWebGraphQlTester extends DefaultGraphQlTester implements WebGraphQl } @Override - public SubscriptionSpec executeSubscription() { + public WebSubscriptionSpec executeSubscription() { return this.requestStrategy.executeSubscription(createRequestInput()); } @@ -319,4 +364,62 @@ class DefaultWebGraphQlTester extends DefaultGraphQlTester implements WebGraphQl } } + + private static final class DefaultWebResponseSpec implements WebResponseSpec { + + private final ResponseSpec delegate; + + private final HttpHeaders headers; + + public DefaultWebResponseSpec(ResponseSpec delegate, @Nullable HttpHeaders headers) { + this.delegate = delegate; + this.headers = (headers != null ? headers : new HttpHeaders()); + } + + @Override + public ResponseSpec httpHeadersSatisfy(Consumer consumer) { + consumer.accept(this.headers); + return this; + } + + @Override + public PathSpec path(String path) { + return this.delegate.path(path); + } + + @Override + public ErrorSpec errors() { + return this.delegate.errors(); + } + } + + + private static final class DefaultWebSubscriptionSpec implements WebSubscriptionSpec { + + private final SubscriptionSpec delegate; + + private final HttpHeaders headers; + + public DefaultWebSubscriptionSpec(SubscriptionSpec delegate, @Nullable HttpHeaders headers) { + this.delegate = delegate; + this.headers = (headers != null ? headers : new HttpHeaders()); + } + + @Override + public SubscriptionSpec httpHeadersSatisfy(Consumer consumer) { + consumer.accept(this.headers); + return this; + } + + @Override + public Flux toFlux(String path, Class entityType) { + return this.delegate.toFlux(path, entityType); + } + + @Override + public Flux toFlux() { + return this.delegate.toFlux(); + } + } + } 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 24230426..c441f782 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 @@ -155,9 +155,9 @@ public interface WebGraphQlTester extends GraphQlTester { */ @Override WebGraphQlTester build(); - } + /** * Extends {@link GraphQlTester.RequestSpec} with further input options * applicable to Web requests. @@ -183,6 +183,55 @@ public interface WebGraphQlTester extends GraphQlTester { */ WebRequestSpec headers(Consumer headersConsumer); + + /** + * Execute the GraphQL request and return a spec for further inspection of + * response data and errors. + * @return options for asserting the response + * @throws AssertionError if the request is performed over HTTP and the response + * status is not 200 (OK). + */ + WebResponseSpec execute(); + + /** + * Execute the GraphQL request as a subscription and return a spec with options to + * transform the result stream. + * @return spec with options to transform the subscription result stream + * @throws AssertionError if the request is performed over HTTP and the response + * status is not 200 (OK). + */ + WebSubscriptionSpec executeSubscription(); + + } + + + /** + * Extension of {@code ResponseSpec} with access to HTTP response headers. + */ + interface WebResponseSpec extends ResponseSpec { + + /** + * Perform any necessary assertions on the HTTP response headers. + * @param consumer the consumer to check the headers + * @return options for asserting the response + */ + ResponseSpec httpHeadersSatisfy(Consumer consumer); + + } + + + /** + * Extension of {@code SubscriptionSpec} with access to HTTP response headers. + */ + interface WebSubscriptionSpec extends SubscriptionSpec { + + /** + * Perform any necessary assertions on the HTTP response headers. + * @param consumer the consumer to check the headers + * @return options for asserting the response + */ + SubscriptionSpec httpHeadersSatisfy(Consumer consumer); + } } diff --git a/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/WebGraphQlTesterTests.java b/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/WebGraphQlTesterTests.java index b20b236c..21d1999c 100644 --- a/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/WebGraphQlTesterTests.java +++ b/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/WebGraphQlTesterTests.java @@ -33,7 +33,6 @@ import graphql.GraphqlErrorBuilder; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.mockito.ArgumentCaptor;