WebGraphQlTester exposes HTTP response headers

Closes gh-74
This commit is contained in:
Rossen Stoyanchev
2021-09-30 22:04:51 +01:00
parent fb8e82dcc0
commit 65b5ff9a7c
4 changed files with 195 additions and 41 deletions

View File

@@ -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<GraphQLError> errorFilter;
@@ -164,7 +164,7 @@ class DefaultGraphQlTester implements GraphQlTester {
private final Duration responseTimeout;
protected AbstractDirectRequestStrategy(
protected DirectRequestStrategySupport(
@Nullable Predicate<GraphQLError> 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<GraphQLError> errors = result.getErrors();
Consumer<Runnable> assertDecorator = assertDecorator(input);
List<GraphQLError> 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<ExecutionResult> 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<Runnable> 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;

View File

@@ -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<RequestStrategy> requestStrategySupplier;
private final Supplier<WebRequestStrategy> 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<byte[]> 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<TestExecutionResult> 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<HttpHeaders> 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<HttpHeaders> consumer) {
consumer.accept(this.headers);
return this;
}
@Override
public <T> Flux<T> toFlux(String path, Class<T> entityType) {
return this.delegate.toFlux(path, entityType);
}
@Override
public Flux<ResponseSpec> toFlux() {
return this.delegate.toFlux();
}
}
}

View File

@@ -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<HttpHeaders> 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<HttpHeaders> 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<HttpHeaders> consumer);
}
}

View File

@@ -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;