From 29b1aa2cc73a7919729d68b5a6890750be01c5d2 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 4 Mar 2022 11:41:19 +0000 Subject: [PATCH] Introduce transport specific GraphQlTester extensions See gh-317 --- .../AbstractDelegatingGraphQlTester.java | 52 +++++ .../tester/AbstractGraphQlTesterBuilder.java | 134 +++++++++++ .../tester/DefaultGraphQlServiceTester.java | 87 +++++++ .../test/tester/DefaultGraphQlTester.java | 154 ++++++++---- .../tester/DefaultGraphQlTesterBuilder.java | 71 ------ .../test/tester/DefaultHttpGraphQlTester.java | 119 ++++++++++ .../test/tester/DefaultWebGraphQlTester.java | 219 +++++------------- .../DefaultWebGraphQlTesterBuilder.java | 120 ---------- .../tester/DefaultWebSocketGraphQlTester.java | 170 ++++++++++++++ .../test/tester/GraphQlServiceTester.java | 63 +++++ .../graphql/test/tester/GraphQlTester.java | 100 ++++---- .../tester/GraphQlTesterBuilderSupport.java | 157 ------------- .../GraphQlTesterRequestSpecSupport.java | 76 ------ .../test/tester/HttpGraphQlTester.java | 77 ++++++ .../test/tester/TestExecutionResult.java | 94 -------- .../graphql/test/tester/TestGraphQlError.java | 3 +- .../graphql/test/tester/TypeRefAdapter.java | 9 +- .../graphql/test/tester/WebGraphQlTester.java | 181 ++++----------- .../test/tester/WebSocketGraphQlTester.java | 82 +++++++ 19 files changed, 1047 insertions(+), 921 deletions(-) create mode 100644 spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/AbstractDelegatingGraphQlTester.java create mode 100644 spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/AbstractGraphQlTesterBuilder.java create mode 100644 spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlServiceTester.java delete mode 100644 spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlTesterBuilder.java create mode 100644 spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultHttpGraphQlTester.java delete mode 100644 spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebGraphQlTesterBuilder.java create mode 100644 spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebSocketGraphQlTester.java create mode 100644 spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlServiceTester.java delete mode 100644 spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlTesterBuilderSupport.java delete mode 100644 spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlTesterRequestSpecSupport.java create mode 100644 spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/HttpGraphQlTester.java delete mode 100644 spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/TestExecutionResult.java create mode 100644 spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/WebSocketGraphQlTester.java diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/AbstractDelegatingGraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/AbstractDelegatingGraphQlTester.java new file mode 100644 index 00000000..b1e0248f --- /dev/null +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/AbstractDelegatingGraphQlTester.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2022 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; + + +/** + * Base class for extensions of {@link GraphQlTester} that mainly assist with + * building the underlying transport, but otherwise delegate to the default + * {@link GraphQlTester} implementation for actual request execution. + * + *

Subclasses must implement {@link GraphQlTester#mutate()} to allow mutation + * of both {@code GraphQlTester} and {@code GraphQlTransport} configuration. + * + * @author Rossen Stoyanchev + * @since 1.0.0 + * @see AbstractGraphQlTesterBuilder + */ +public abstract class AbstractDelegatingGraphQlTester implements GraphQlTester { + + private final GraphQlTester delegate; + + + protected AbstractDelegatingGraphQlTester(GraphQlTester delegate) { + this.delegate = delegate; + } + + + @Override + public RequestSpec document(String document) { + return this.delegate.document(document); + } + + @Override + public RequestSpec documentName(String documentName) { + return this.delegate.documentName(documentName); + } + +} diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/AbstractGraphQlTesterBuilder.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/AbstractGraphQlTesterBuilder.java new file mode 100644 index 00000000..2bdb0e53 --- /dev/null +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/AbstractGraphQlTesterBuilder.java @@ -0,0 +1,134 @@ +/* + * Copyright 2002-2022 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.spi.json.JacksonJsonProvider; +import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider; +import graphql.GraphQLError; + +import org.springframework.graphql.client.GraphQlTransport; +import org.springframework.graphql.support.CachingDocumentSource; +import org.springframework.graphql.support.DocumentSource; +import org.springframework.graphql.support.ResourceDocumentSource; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Abstract, base class for transport specific {@link GraphQlTester.Builder} + * implementations. + * + *

Subclasses must implement {@link #build()} and call + * {@link #buildGraphQlTester(GraphQlTransport)} to obtain a default, transport + * agnostic {@code GraphQlTester}. A transport specific extension can then wrap + * this default tester by extending {@link AbstractDelegatingGraphQlTester}. + * + * @author Rossen Stoyanchev + * @since 1.0.0 + * @see AbstractDelegatingGraphQlTester + */ +public abstract class AbstractGraphQlTesterBuilder> implements GraphQlTester.Builder { + + private static final boolean jackson2Present; + + static { + ClassLoader classLoader = AbstractGraphQlTesterBuilder.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; + + private DocumentSource documentSource = new CachingDocumentSource(new ResourceDocumentSource()); + + private Duration responseTimeout = DEFAULT_RESPONSE_DURATION; + + + @Override + public B errorFilter(Predicate predicate) { + this.errorFilter = (this.errorFilter != null ? errorFilter.and(predicate) : predicate); + return self(); + } + + @Override + public B documentSource(DocumentSource documentSource) { + this.documentSource = documentSource; + return self(); + } + + @Override + public B responseTimeout(Duration timeout) { + Assert.notNull(timeout, "'timeout' is required"); + this.responseTimeout = timeout; + return self(); + } + + @SuppressWarnings("unchecked") + private T self() { + return (T) this; + } + + /** + * Subclasses call this from {@link #build()} to provide the transport and get + * the default {@code GraphQlTester} to delegate to for request execution. + */ + protected GraphQlTester buildGraphQlTester(GraphQlTransport transport) { + Assert.notNull(transport, "GraphQlTransport is required"); + return new DefaultGraphQlTester( + transport, this.errorFilter, initJsonPathConfig(), this.documentSource, this.responseTimeout, + getBuilderInitializer()); + } + + private Configuration initJsonPathConfig() { + // Allow configuring JSONPath with codecs from transport subclasses + return (jackson2Present ? Jackson2Configuration.create() : Configuration.builder().build()); + } + + /** + * Subclasses call this from {@link #build()} to obtain a {@code Consumer} to + * initialize new builder instances with, based on "this" builder. + */ + protected Consumer> getBuilderInitializer() { + return builder -> { + if (this.errorFilter != null) { + builder.errorFilter(this.errorFilter); + } + builder.documentSource(this.documentSource); + builder.responseTimeout(this.responseTimeout); + }; + } + + + 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/DefaultGraphQlServiceTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlServiceTester.java new file mode 100644 index 00000000..151afcce --- /dev/null +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlServiceTester.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2022 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.function.Consumer; + +import org.springframework.graphql.GraphQlService; +import org.springframework.util.Assert; + + +/** + * Default {@link GraphQlServiceTester} that uses a {@link GraphQlService} for + * request execution. + * + * @author Rossen Stoyanchev + * @since 1.0.0 + */ +final class DefaultGraphQlServiceTester extends AbstractDelegatingGraphQlTester implements GraphQlServiceTester { + + private final GraphQlServiceTransport transport; + + private final Consumer> builderInitializer; + + + DefaultGraphQlServiceTester(GraphQlTester tester, GraphQlServiceTransport transport, + Consumer> builderInitializer) { + + super(tester); + + Assert.notNull(transport, "GraphQlServiceTransport is required"); + Assert.notNull(builderInitializer, "`builderInitializer` is required"); + + this.transport = transport; + this.builderInitializer = builderInitializer; + } + + + @Override + public Builder mutate() { + Builder builder = new Builder<>(this.transport); + this.builderInitializer.accept(builder); + return builder; + } + + + /** + * Default {@link GraphQlServiceTester.Builder} implementation. + */ + static class Builder> extends AbstractGraphQlTesterBuilder + implements GraphQlServiceTester.Builder { + + private final GraphQlService service; + + Builder(GraphQlService service) { + Assert.notNull(service, "GraphQlService is required"); + this.service = service; + } + + Builder(GraphQlServiceTransport transport) { + this.service = transport.getGraphQlService(); + } + + @Override + public GraphQlServiceTester build() { + GraphQlServiceTransport transport = new GraphQlServiceTransport(this.service); + GraphQlTester tester = super.buildGraphQlTester(transport); + return new DefaultGraphQlServiceTester(tester, transport, getBuilderInitializer()); + } + + } + +} 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 9ee6f63c..f21d9ad9 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,23 +16,29 @@ 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.Locale; +import java.util.Map; import java.util.function.Consumer; -import java.util.function.Function; 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 graphql.ExecutionResult; import graphql.GraphQLError; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.graphql.GraphQlRequest; +import org.springframework.graphql.client.GraphQlTransport; +import org.springframework.graphql.support.DocumentSource; import org.springframework.lang.Nullable; import org.springframework.test.util.AssertionErrors; import org.springframework.test.util.JsonExpectationsHelper; @@ -42,98 +48,158 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; /** - * Default implementation of {@link GraphQlTester}. + * Default {@link GraphQlTester} implementation with the logic to initialize + * requests and handle responses. It is transport agnostic and depends on a + * {@link GraphQlTransport} to execute requests with. + * + *

This class is final but works with any transport. * * @author Rossen Stoyanchev */ -class DefaultGraphQlTester implements GraphQlTester { +final class DefaultGraphQlTester implements GraphQlTester { - private final RequestStrategy requestStrategy; + private final GraphQlTransport transport; - private final Function queryNameResolver; + @Nullable + private final Predicate errorFilter; + + private final Configuration jsonPathConfig; + + private final DocumentSource documentSource; + + private final Duration responseTimeout; + + private final Consumer> builderInitializer; - DefaultGraphQlTester(RequestStrategy requestStrategy, Function queryNameResolver) { - Assert.notNull(requestStrategy, "RequestStrategy is required."); - Assert.notNull(queryNameResolver, "'queryNameResolver' is required."); - this.requestStrategy = requestStrategy; - this.queryNameResolver = queryNameResolver; + /** + * Package private constructor for use from {@link AbstractGraphQlTesterBuilder}. + */ + DefaultGraphQlTester( + GraphQlTransport transport, @Nullable Predicate errorFilter, + Configuration jsonPathConfig, DocumentSource documentSource, Duration timeout, + Consumer> builderInitializer) { + + this.transport = transport; + this.errorFilter = errorFilter; + this.jsonPathConfig = jsonPathConfig; + this.documentSource = documentSource; + this.responseTimeout = timeout; + this.builderInitializer = builderInitializer; } @Override - public RequestSpec query(String query) { - return new DefaultRequestSpec(this.requestStrategy, query); + public RequestSpec document(String document) { + return new DefaultRequestSpec(document); } @Override - public RequestSpec queryName(String queryName) { - return query(this.queryNameResolver.apply(queryName)); + public RequestSpec documentName(String documentName) { + String document = this.documentSource.getDocument(documentName).block(this.responseTimeout); + Assert.notNull(document, "Expected document content or an error"); + return document(document); + } + + @Override + public Builder mutate() { + Builder builder = new Builder(this.transport); + this.builderInitializer.accept(builder); + return 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 + * Default {@link GraphQlTester.Builder} with a given transport. */ - static GraphQlTester.ResponseSpec createResponseSpec( - DocumentContext documentContext, @Nullable Predicate errorFilter, - Consumer assertDecorator) { + static final class Builder extends AbstractGraphQlTesterBuilder { + + private final GraphQlTransport transport; + + Builder(GraphQlTransport transport) { + this.transport = transport; + } + + @Override + public GraphQlTester build() { + return super.buildGraphQlTester(this.transport); + } - return new DefaultResponseSpec(documentContext, errorFilter, assertDecorator); } /** - * {@link RequestSpec} that collects the query, operationName, and variables. + * {@link RequestSpec} that gathers the document, operationName, and variables. */ - private static final class DefaultRequestSpec - extends GraphQlTesterRequestSpecSupport implements RequestSpec { + private final class DefaultRequestSpec implements RequestSpec { - private final RequestStrategy requestStrategy; + private final String document; - private DefaultRequestSpec(RequestStrategy requestStrategy, String query) { - super(query); - Assert.notNull(requestStrategy, "RequestStrategy is required"); - this.requestStrategy = requestStrategy; + @Nullable + private String operationName; + + private final Map variables = new LinkedHashMap<>(); + + private DefaultRequestSpec(String document) { + Assert.notNull(document, "`document` is required"); + this.document = document; } @Override public DefaultRequestSpec operationName(@Nullable String name) { - setOperationName(name); + this.operationName = name; return this; } @Override public DefaultRequestSpec variable(String name, @Nullable Object value) { - addVariable(name, value); - return this; - } - - @Override - public DefaultRequestSpec locale(Locale locale) { - setLocale(locale); + this.variables.put(name, value); return this; } + @SuppressWarnings("ConstantConditions") @Override public ResponseSpec execute() { - return this.requestStrategy.execute(createRequestInput()); + GraphQlRequest request = createRequest(); + return transport.execute(request) + .map(result -> createResponseSpec(result, assertDecorator(request))) + .block(responseTimeout); } @Override public void executeAndVerify() { - verify(execute()); + execute().path("$.errors").valueIsEmpty(); } @Override public SubscriptionSpec executeSubscription() { - return this.requestStrategy.executeSubscription(createRequestInput()); + GraphQlRequest request = createRequest(); + return () -> transport.executeSubscription(request) + .map(result -> createResponseSpec(result, assertDecorator(request))); } + + private GraphQlRequest createRequest() { + return new GraphQlRequest(this.document, this.operationName, this.variables); + } + + private GraphQlTester.ResponseSpec createResponseSpec( + ExecutionResult result, Consumer assertDecorator) { + + DocumentContext jsonDocument = JsonPath.parse(result.toSpecification(), jsonPathConfig); + return new DefaultResponseSpec(jsonDocument, errorFilter, assertDecorator); + } + + private Consumer assertDecorator(GraphQlRequest request) { + return (assertion) -> { + try { + assertion.run(); + } + catch (AssertionError ex) { + throw new AssertionError(ex.getMessage() + "\nRequest: " + request, ex); + } + }; + } + } 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 deleted file mode 100644 index 89be89d2..00000000 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlTesterBuilder.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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, getQueryNameResolver()); - } - -} diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultHttpGraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultHttpGraphQlTester.java new file mode 100644 index 00000000..46e6d708 --- /dev/null +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultHttpGraphQlTester.java @@ -0,0 +1,119 @@ +/* + * Copyright 2002-2022 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.net.URI; +import java.util.function.Consumer; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.codec.CodecConfigurer; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.util.DefaultUriBuilderFactory; +import org.springframework.web.util.UriBuilderFactory; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * Default {@link HttpGraphQlTester} that builds and uses a {@link WebTestClient} + * for request execution. + * + * @author Rossen Stoyanchev + * @since 1.0.0 + */ +final class DefaultHttpGraphQlTester extends AbstractDelegatingGraphQlTester implements HttpGraphQlTester { + + private final WebTestClient webTestClient; + + private final Consumer> builderInitializer; + + + DefaultHttpGraphQlTester(GraphQlTester graphQlTester, WebTestClient webTestClient, + Consumer> builderInitializer) { + + super(graphQlTester); + this.webTestClient = webTestClient; + this.builderInitializer = builderInitializer; + } + + + @Override + public Builder mutate() { + Builder builder = new Builder(this.webTestClient.mutate()); + this.builderInitializer.accept(builder); + return builder; + } + + + /** + * Default {@link HttpGraphQlTester.Builder} implementation. + */ + static final class Builder extends AbstractGraphQlTesterBuilder + implements HttpGraphQlTester.Builder { + + private final WebTestClient.Builder webTestClientBuilder; + + Builder(WebTestClient.Builder clientBuilder) { + this.webTestClientBuilder = clientBuilder; + } + + @Override + public Builder url(String url) { + this.webTestClientBuilder.baseUrl(url); + return this; + } + + @Override + public Builder url(URI url) { + UriBuilderFactory factory = new DefaultUriBuilderFactory(UriComponentsBuilder.fromUri(url)); + this.webTestClientBuilder.uriBuilderFactory(factory); + return this; + } + + @Override + public Builder header(String name, String... values) { + this.webTestClientBuilder.defaultHeader(name, values); + return this; + } + + @Override + public Builder headers(Consumer headersConsumer) { + this.webTestClientBuilder.defaultHeaders(headersConsumer); + return this; + } + + @Override + public Builder codecConfigurer(Consumer codecConsumer) { + this.webTestClientBuilder.codecs(codecConsumer::accept); + return this; + } + + @Override + public Builder webTestClient(Consumer configurer) { + configurer.accept(this.webTestClientBuilder); + return this; + } + + @Override + public HttpGraphQlTester build() { + WebTestClient client = this.webTestClientBuilder.build(); + GraphQlTester tester = super.buildGraphQlTester(new WebTestClientTransport(client)); + return new DefaultHttpGraphQlTester(tester, client, getBuilderInitializer()); + } + + } + +} 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 9140d409..86a52656 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -16,221 +16,108 @@ package org.springframework.graphql.test.tester; + import java.net.URI; -import java.util.Locale; +import java.util.Arrays; import java.util.function.Consumer; -import java.util.function.Function; -import reactor.core.publisher.Flux; - -import org.springframework.graphql.RequestInput; -import org.springframework.graphql.web.WebInput; +import org.springframework.graphql.web.WebGraphQlHandler; import org.springframework.http.HttpHeaders; -import org.springframework.lang.Nullable; +import org.springframework.http.codec.CodecConfigurer; import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; -import org.springframework.util.ObjectUtils; +import org.springframework.web.util.DefaultUriBuilderFactory; /** - * Default implementation of {@link WebGraphQlTester}. + * Default {@link WebGraphQlTester} that uses {@link WebGraphQlHandler} for + * request execution. * * @author Rossen Stoyanchev + * @since 1.0.0 */ -class DefaultWebGraphQlTester implements WebGraphQlTester { +final class DefaultWebGraphQlTester extends AbstractDelegatingGraphQlTester implements WebGraphQlTester { - private final WebRequestStrategy requestStrategy; + private final WebGraphQlHandlerTransport transport; - @Nullable - private final HttpHeaders defaultHeaders; - - private final Function queryNameResolver; + private final Consumer> builderInitializer; - DefaultWebGraphQlTester( - WebRequestStrategy requestStrategy, @Nullable HttpHeaders defaultHeaders, - Function queryNameResolver) { + DefaultWebGraphQlTester(GraphQlTester tester, WebGraphQlHandlerTransport transport, + Consumer> builderInitializer) { - Assert.notNull(requestStrategy, "WebRequestStrategy is required."); - this.requestStrategy = requestStrategy; - this.defaultHeaders = defaultHeaders; - this.queryNameResolver = queryNameResolver; + super(tester); + this.transport = transport; + this.builderInitializer = builderInitializer; } @Override - public WebRequestSpec query(String query) { - return new DefaultWebRequestSpec(this.requestStrategy, this.defaultHeaders, query); - } - - @Override - public WebRequestSpec queryName(String queryName) { - return query(this.queryNameResolver.apply(queryName)); + public Builder mutate() { + Builder builder = new Builder<>(this.transport.getGraphQlHandler()); + builder.url(this.transport.getUrl()); + builder.headers(headers -> headers.putAll(this.transport.getHeaders())); + this.builderInitializer.accept(builder); + return builder; } /** - * Factory for {@link WebGraphQlTester.ResponseSpec}, for use from - * {@link WebRequestStrategy} implementations. + * Base builder implementation for all Web transport extensions. */ - static WebResponseSpec createResponseSpec( - ResponseSpec responseSpec, @Nullable HttpHeaders responseHeaders) { + static class Builder> extends AbstractGraphQlTesterBuilder + implements WebGraphQlTester.Builder { - return new DefaultWebResponseSpec(responseSpec, responseHeaders); - } - - /** - * Factory for {@link WebGraphQlTester.SubscriptionSpec}, for use from - * {@link WebRequestStrategy} implementations. - */ - static WebSubscriptionSpec createSubscriptionSpec( - SubscriptionSpec subscriptionSpec, @Nullable HttpHeaders responseHeaders) { - - return new DefaultWebSubscriptionSpec(subscriptionSpec, responseHeaders); - } - - - /** - * {@link WebRequestSpec} that also collects HTTP request headers, in - * addition to the query, operationName, and variables. - */ - private static final class DefaultWebRequestSpec - extends GraphQlTesterRequestSpecSupport implements WebRequestSpec { - - private static final URI DEFAULT_URL = URI.create(""); - - private final WebRequestStrategy requestStrategy; + private URI url = URI.create(""); private final HttpHeaders headers = new HttpHeaders(); - private DefaultWebRequestSpec( - WebRequestStrategy requestStrategy, @Nullable HttpHeaders defaultHeaders, String query) { + private final WebGraphQlHandler handler; - super(query); - Assert.notNull(requestStrategy, "WebRequestStrategy is required"); - this.requestStrategy = requestStrategy; - if (!CollectionUtils.isEmpty(defaultHeaders)) { - this.headers.putAll(defaultHeaders); - } + Builder(WebGraphQlHandler handler) { + Assert.notNull(handler, "WebGraphQlHandler is required"); + this.handler = handler; } @Override - public WebRequestSpec operationName(@Nullable String name) { - setOperationName(name); - return this; + public B url(String url) { + return url(new DefaultUriBuilderFactory().uriString(url).build()); } @Override - public WebRequestSpec variable(String name, @Nullable Object value) { - addVariable(name, value); - return this; + public B url(URI url) { + this.url = url; + return self(); } @Override - public WebRequestSpec locale(Locale locale) { - setLocale(locale); - return this; + public B header(String name, String... values) { + this.headers.put(name, Arrays.asList(values)); + return self(); } @Override - public WebRequestSpec httpHeader(String headerName, String... headerValues) { - for (String headerValue : headerValues) { - this.headers.add(headerName, headerValue); - } - return this; - } - - @Override - public WebRequestSpec httpHeaders(Consumer headersConsumer) { + public B headers(Consumer headersConsumer) { headersConsumer.accept(this.headers); - return this; + return self(); } @Override - public WebResponseSpec execute() { - return this.requestStrategy.execute(createWebInput()); + public B codecConfigurer(Consumer codecConsumer) { + // Ignore, no serialization needs at this level + return self(); + } + + @SuppressWarnings("unchecked") + protected T self() { + return (T) this; } @Override - public void executeAndVerify() { - verify(execute()); + public WebGraphQlTester build() { + WebGraphQlHandlerTransport transport = new WebGraphQlHandlerTransport(this.url, this.headers, this.handler); + GraphQlTester tester = super.buildGraphQlTester(transport); + return new DefaultWebGraphQlTester(tester, transport, getBuilderInitializer()); } - @Override - public WebSubscriptionSpec executeSubscription() { - return this.requestStrategy.executeSubscription(createWebInput()); - } - - private WebInput createWebInput() { - RequestInput input = createRequestInput(); - return new WebInput(DEFAULT_URL, this.headers, input.toMap(), input.getLocale(), - (input.getId() != null) ? input.getId() : ObjectUtils.getIdentityHexString(input)); - } - } - - - /** - * {@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 responseHeaders; - - public DefaultWebResponseSpec(ResponseSpec responseSpec, @Nullable HttpHeaders responseHeaders) { - this.responseSpec = responseSpec; - this.responseHeaders = (responseHeaders != null ? responseHeaders : new HttpHeaders()); - } - - @Override - public ResponseSpec httpHeadersSatisfy(Consumer consumer) { - consumer.accept(this.responseHeaders); - return this; - } - - @Override - public PathSpec path(String path) { - return this.responseSpec.path(path); - } - - @Override - public ErrorSpec errors() { - return this.responseSpec.errors(); - } - } - - - /** - * {@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; - - private 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/DefaultWebGraphQlTesterBuilder.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebGraphQlTesterBuilder.java deleted file mode 100644 index 0fc76e65..00000000 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebGraphQlTesterBuilder.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * 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 defaultHttpHeader(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 defaultHttpHeaders(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, getQueryNameResolver()); - } - - 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/DefaultWebSocketGraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebSocketGraphQlTester.java new file mode 100644 index 00000000..00735f1c --- /dev/null +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebSocketGraphQlTester.java @@ -0,0 +1,170 @@ +/* + * Copyright 2002-2022 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.net.URI; +import java.util.function.Consumer; + +import graphql.ExecutionResult; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.graphql.GraphQlRequest; +import org.springframework.graphql.client.GraphQlClient; +import org.springframework.graphql.client.GraphQlTransport; +import org.springframework.graphql.client.WebSocketGraphQlClient; +import org.springframework.http.HttpHeaders; +import org.springframework.http.codec.CodecConfigurer; +import org.springframework.util.Assert; +import org.springframework.web.reactive.socket.client.WebSocketClient; + +/** + * Default {@link WebSocketGraphQlTester} that builds and uses a + * {@link WebSocketGraphQlClient} for request execution. + * + * @author Rossen Stoyanchev + * @since 1.0.0 + */ +final class DefaultWebSocketGraphQlTester extends AbstractDelegatingGraphQlTester implements WebSocketGraphQlTester { + + private final WebSocketGraphQlClient webSocketGraphQlClient; + + private final Consumer> builderInitializer; + + + DefaultWebSocketGraphQlTester( + GraphQlTester graphQlTester, WebSocketGraphQlClient webSocketGraphQlClient, + Consumer> builderInitializer) { + + super(graphQlTester); + this.webSocketGraphQlClient = webSocketGraphQlClient; + this.builderInitializer = builderInitializer; + } + + + @Override + public Mono start() { + return this.webSocketGraphQlClient.start(); + } + + @Override + public Mono stop() { + return this.webSocketGraphQlClient.stop(); + } + + @Override + public Builder mutate() { + Builder builder = new Builder(this.webSocketGraphQlClient); + this.builderInitializer.accept(builder); + return builder; + } + + + /** + * Default {@link WebSocketGraphQlTester.Builder} implementation. + */ + static final class Builder extends AbstractGraphQlTesterBuilder implements WebSocketGraphQlTester.Builder { + + private final WebSocketGraphQlClient.Builder graphQlClientBuilder; + + /** + * Constructor to start via {@link WebSocketGraphQlTester#builder(URI, WebSocketClient)}. + */ + Builder(URI url, WebSocketClient webSocketClient) { + Assert.notNull(webSocketClient, "WebSocketClient is required"); + this.graphQlClientBuilder = WebSocketGraphQlClient.builder(url, webSocketClient); + } + + /** + * Constructor to mutate. + * @param client the underlying client with the current state + */ + Builder(WebSocketGraphQlClient client) { + Assert.notNull(client, "WebSocketGraphQlClient is required"); + this.graphQlClientBuilder = client.mutate(); + } + + + @Override + public Builder url(String url) { + this.graphQlClientBuilder.url(url); + return this; + } + + @Override + public Builder url(URI url) { + this.graphQlClientBuilder.url(url); + return this; + } + + @Override + public Builder header(String name, String... values) { + this.graphQlClientBuilder.header(name, values); + return this; + } + + @Override + public Builder headers(Consumer headersConsumer) { + this.graphQlClientBuilder.headers(headersConsumer); + return this; + } + + @Override + public Builder codecConfigurer(Consumer codecsConsumer) { + this.graphQlClientBuilder.codecConfigurer(codecsConsumer); + return this; + } + + @Override + public WebSocketGraphQlTester build() { + WebSocketGraphQlClient client = this.graphQlClientBuilder.build(); + GraphQlTester graphQlTester = super.buildGraphQlTester(asTransport(client)); + return new DefaultWebSocketGraphQlTester(graphQlTester, client, getBuilderInitializer()); + } + + /** + * GraphQlTransport implementations are private, but we can create the + * GraphQlClient for it and adapt it. + */ + private static GraphQlTransport asTransport(GraphQlClient client) { + return new GraphQlTransport() { + + @Override + public Mono execute(GraphQlRequest request) { + return client + .document(request.getDocument()) + .operationName(request.getOperationName()) + .variables(request.getVariables()) + .execute() + .map(GraphQlClient.ResponseSpec::andReturn); + } + + @Override + public Flux executeSubscription(GraphQlRequest request) { + return client + .document(request.getDocument()) + .operationName(request.getOperationName()) + .variables(request.getVariables()) + .executeSubscription().map(GraphQlClient.ResponseSpec::andReturn); + } + }; + } + + } + +} diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlServiceTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlServiceTester.java new file mode 100644 index 00000000..d95c21c4 --- /dev/null +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlServiceTester.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2022 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.GraphQlService; + +/** + * {@link GraphQlTester} that executes requests through a {@link GraphQlService} + * Use it for server-side tests, without a client. + * + * @author Rossen Stoyanchev + * @since 1.0.0 + */ +public interface GraphQlServiceTester extends GraphQlTester { + + + @Override + Builder mutate(); + + + /** + * Create a {@link GraphQlServiceTester} instance. + */ + static GraphQlServiceTester create(GraphQlService service) { + return builder(service).build(); + } + + /** + * Return a builder for {@link GraphQlServiceTester}. + */ + static GraphQlServiceTester.Builder builder(GraphQlService service) { + return new DefaultGraphQlServiceTester.Builder<>(service); + } + + + /** + * Default {@link GraphQlServiceTester.Builder} implementation. + */ + interface Builder> extends GraphQlTester.Builder { + + /** + * Build a {@link GraphQlServiceTester} instance. + */ + @Override + GraphQlServiceTester build(); + + } + +} 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 080e0355..72aa413e 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 @@ -18,26 +18,34 @@ package org.springframework.graphql.test.tester; import java.time.Duration; import java.util.List; -import java.util.Locale; import java.util.function.Consumer; import java.util.function.Predicate; -import com.jayway.jsonpath.Configuration; import graphql.GraphQLError; import reactor.core.publisher.Flux; import org.springframework.core.ParameterizedTypeReference; -import org.springframework.graphql.GraphQlService; +import org.springframework.graphql.client.GraphQlTransport; +import org.springframework.graphql.support.DocumentSource; +import org.springframework.graphql.support.ResourceDocumentSource; import org.springframework.lang.Nullable; /** - * Contract for testing GraphQL requests. + * Define a workflow to test GraphQL requests that is independent of the + * underlying transport. * - *

The workflow declared to prepare, execute, and verify requests is not tied - * to any specific underlying transport. Use {@link WebGraphQlTester} to test - * GraphQL requests over a Web transport. This class can also be used to perform - * calls directly on {@link graphql.GraphQL}, without a transport, via - * {@link GraphQlService}. + *

To test using a client that connects to a server, with or without a live + * server, see {@code GraphQlTester} extensions: + *

+ * + *

To test on the server side, without a client, see the following: + *

    + *
  • {@link GraphQlServiceTester} + *
  • {@link WebGraphQlTester} + *
* * @author Rossen Stoyanchev * @since 1.0.0 @@ -45,49 +53,49 @@ import org.springframework.lang.Nullable; public interface GraphQlTester { /** - * Prepare to perform a GraphQL request with the given operation which may - * be a query, mutation, or a subscription. - * @param query the operation to be performed + * Start defining a GraphQL request with the given document, which is the + * textual representation of an operation (or operations) to perform, + * including selection sets and fragments. + * @param document the document for the request * @return spec for response assertions * @throws AssertionError if the response status is not 200 (OK) */ - RequestSpec query(String query); + RequestSpec document(String document); /** - * Refer to a query by name where the given name is to look for a file with - * the same name and extension {@code ".graphql"} or {@code ".gql"} under - * classpath location {@code "graphql/"}. + * Variant of {@link #document(String)} that uses the given key to resolve + * the GraphQL document from a file, or in another way with the help of the + * {@link DocumentSource} that the client is configured with. * @return spec for response assertions - * @throws IllegalArgumentException if the queryName cannot be resolved + * @throws IllegalArgumentException if the documentName cannot be resolved * @throws AssertionError if the response status is not 200 (OK) */ - RequestSpec queryName(String queryName); + RequestSpec documentName(String documentName); + + /** + * Create a builder initialized from the configuration of "this" tester. + * Use it to build a new, independently configured instance. + */ + Builder mutate(); /** - * Create a {@code GraphQlTester} that performs GraphQL requests through the - * given {@link GraphQlService}. - * @param service the service to execute requests with - * @return the created {@code GraphQlTester} + * Create a builder with a custom {@code GraphQlTransport}. + *

For most cases, use a transport specific extension such as + * {@link HttpGraphQlTester} or {@link WebSocketGraphQlTester}. This method + * is for use with a custom {@code GraphQlTransport}. + * @param transport the transport to execute requests with + * @return the builder for further initialization */ - static GraphQlTester create(GraphQlService service) { - return builder(service).build(); - } - - /** - * Return a builder with options to initialize a {@code GraphQlTester}. - * @param service the service to execute requests with - * @return the builder to use - */ - static Builder builder(GraphQlService service) { - return new DefaultGraphQlTesterBuilder(service); + static GraphQlTester.Builder builder(GraphQlTransport transport) { + return new DefaultGraphQlTester.Builder(transport); } /** * A builder to create a {@link GraphQlTester} instance. */ - interface Builder> { + interface Builder> { /** * Configure a global {@link ErrorSpec#filter(Predicate) filter} that @@ -95,26 +103,21 @@ public interface GraphQlTester { * @param predicate the error filter to add * @return the same builder instance */ - T errorFilter(Predicate predicate); + B errorFilter(Predicate predicate); /** - * Provide JSONPath configuration settings, including a - * {@link com.jayway.jsonpath.spi.json.JsonProvider} as well as a - * {@link com.jayway.jsonpath.spi.mapper.MappingProvider} that are used - * to serialize and deserialize GraphQL JSON content. - *

By default the configuration is to use Jackson JSON if it is - * present on the classpath. - * @param config the JSONPath configuration to use - * @return the same builder instance + * Configure a {@link DocumentSource} for use with + * {@link #documentName(String)} for resolving a document by name. + *

By default, {@link ResourceDocumentSource} is used. */ - T jsonPathConfig(Configuration config); + B documentSource(DocumentSource contentLoader); /** * Max amount of time to wait for a GraphQL response. *

By default this is set to 5 seconds. * @param timeout the response timeout value */ - T responseTimeout(Duration timeout); + B responseTimeout(Duration timeout); /** * Build the {@code GraphQlTester}. @@ -174,13 +177,6 @@ public interface GraphQlTester { */ T variable(String name, @Nullable Object value); - /** - * Set the locale to associate with the request. - * @param locale the locale to use - * @return this request spec - */ - T locale(Locale locale); - } /** 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 deleted file mode 100644 index 8f1e2a0f..00000000 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlTesterBuilderSupport.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * 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.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.util.Arrays; -import java.util.function.Function; -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.core.io.ClassPathResource; -import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; -import org.springframework.util.FileCopyUtils; - -/** - * 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; - - private final Function queryNameResolver = new QueryNameResolver(); - - - 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 Function getQueryNameResolver() { - return this.queryNameResolver; - } - - 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 QueryNameResolver implements Function { - - private static final ClassPathResource LOCATION = new ClassPathResource("graphql/"); - - private static final String[] EXTENSIONS = new String[] {".graphql", ".gql"}; - - @Override - public String apply(String queryName) { - Resource queryResource = getQueryResource(queryName); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - try { - FileCopyUtils.copy(queryResource.getInputStream(), outputStream); - } - catch (IOException ex) { - throw new IllegalArgumentException("Failed to read query from: " + LOCATION.getPath()); - } - return new String(outputStream.toByteArray(), StandardCharsets.UTF_8); - } - - private Resource getQueryResource(String queryName) { - for (String extension : EXTENSIONS) { - Resource resource = LOCATION.createRelative(queryName + extension); - if (resource.exists()) { - return resource; - } - } - throw new IllegalArgumentException( - "Could not find file '" + queryName + "' with extensions " + Arrays.toString(EXTENSIONS) + - " under " + LOCATION.getDescription()); - } - } - - - 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 deleted file mode 100644 index b380817b..00000000 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlTesterRequestSpecSupport.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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.Locale; -import java.util.Map; - -import org.springframework.graphql.RequestInput; -import org.springframework.lang.Nullable; -import org.springframework.util.AlternativeJdkIdGenerator; -import org.springframework.util.Assert; -import org.springframework.util.IdGenerator; - -/** - * Base class support for implementations of - * {@link GraphQlTester.RequestSpec} and {@link WebGraphQlTester.RequestSpec}. - * - * @author Rossen Stoyanchev - * @since 1.0.0 - */ -class GraphQlTesterRequestSpecSupport { - - private static final IdGenerator idGenerator = new AlternativeJdkIdGenerator(); - - private final String query; - - @Nullable - private String operationName; - - private final Map variables = new LinkedHashMap<>(); - - @Nullable - private Locale locale; - - - 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, @Nullable Object value) { - this.variables.put(name, value); - } - - protected void setLocale(Locale locale) { - this.locale = locale; - } - - protected void verify(GraphQlTester.ResponseSpec responseSpec) { - responseSpec.path("$.errors").valueIsEmpty(); - } - - protected RequestInput createRequestInput() { - return new RequestInput(this.query, this.operationName, this.variables, idGenerator.generateId().toString(), this.locale); - } - -} diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/HttpGraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/HttpGraphQlTester.java new file mode 100644 index 00000000..8a795daf --- /dev/null +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/HttpGraphQlTester.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2022 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.function.Consumer; + +import org.springframework.test.web.reactive.server.WebTestClient; + +/** + * GraphQL over HTTP tester that uses {@link WebTestClient} and supports tests + * with or without a running server, depending on how {@code WebTestClient} is + * configured. + * + * @author Rossen Stoyanchev + * @since 1.0.0 + */ +public interface HttpGraphQlTester extends WebGraphQlTester { + + + @Override + Builder mutate(); + + + /** + * Create an {@link HttpGraphQlTester} that uses the given {@link WebTestClient}. + */ + static HttpGraphQlTester create(WebTestClient webTestClient) { + return builder(webTestClient.mutate()).build(); + } + + /** + * Return a builder to initialize an {@link HttpGraphQlTester} by creating + * the underlying {@link WebTestClient} through the given builder. + */ + static HttpGraphQlTester.Builder builder(WebTestClient.Builder webTestClientBuilder) { + return new DefaultHttpGraphQlTester.Builder(webTestClientBuilder); + } + + + /** + * Builder for the GraphQL over HTTP tester. + */ + interface Builder> extends WebGraphQlTester.Builder { + + /** + * Customize the {@code WebTestClient} to use. + *

Note that some properties of {@code WebTestClient.Builder} like the + * base URL, headers, and codecs can be customized through this builder. + * @see #url(String) + * @see #header(String, String...) + * @see #codecConfigurer(Consumer) + */ + B webTestClient(Consumer webClient); + + /** + * Build the {@code HttpGraphQlTester} instance. + */ + @Override + HttpGraphQlTester build(); + + } + +} diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/TestExecutionResult.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/TestExecutionResult.java deleted file mode 100644 index b9949166..00000000 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/TestExecutionResult.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * 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.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import graphql.ExecutionResult; -import graphql.ExecutionResultImpl; -import graphql.GraphQLError; - -import org.springframework.lang.Nullable; - -/** - * {@link GraphQLError} with setters, for internal use to use to deserialize - * from a response. - * - * @author Rossen Stoyanchev - * @since 1.0.0 - */ -final class TestExecutionResult implements ExecutionResult { - - @Nullable - private Object data; - - private List errors = Collections.emptyList(); - - private Map extensions = Collections.emptyMap(); - - public void setData(Object data) { - this.data = data; - } - - @Override - @SuppressWarnings("unchecked") - @Nullable - public T getData() { - return (T) this.data; - } - - public void setErrors(List errors) { - this.errors = new ArrayList<>(errors); - } - - @Override - public List getErrors() { - return this.errors; - } - - @Override - public boolean isDataPresent() { - return getData() != null; - } - - public void setExtensions(Map extensions) { - this.extensions = new LinkedHashMap<>(extensions); - } - - @Override - public Map getExtensions() { - return this.extensions; - } - - @Override - public Map toSpecification() { - ExecutionResultImpl.Builder builder = ExecutionResultImpl.newExecutionResult() - .addErrors(this.errors) - .extensions(this.extensions); - - if (isDataPresent()) { - builder.data(this.data); - } - - return builder.build().toSpecification(); - } - -} diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/TestGraphQlError.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/TestGraphQlError.java index ae758ceb..d7359e55 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/TestGraphQlError.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/TestGraphQlError.java @@ -30,8 +30,7 @@ import graphql.language.SourceLocation; import org.springframework.lang.Nullable; /** - * {@link GraphQLError} with setters, for internal use to use to deserialize - * from a response. + * {@link GraphQLError} with setters, for internal use to deserialize from a response. * * @author Rossen Stoyanchev */ diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/TypeRefAdapter.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/TypeRefAdapter.java index 5ca0f3a6..b150f366 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/TypeRefAdapter.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/TypeRefAdapter.java @@ -24,17 +24,17 @@ import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ResolvableType; /** - * Adapter for a JSONPath {@link TypeRef} with a {@link #getType() type} that - * returns fixed type information rather than obtained from the generic type - * declaration. + * Adapt a JSONPath {@link TypeRef} to {@link ParameterizedTypeReference} and + * {@link ResolvableType} for classes with generics. * - * @param the referenced type * @author Rossen Stoyanchev + * @since 1.0.0 */ final class TypeRefAdapter extends TypeRef { private final Type type; + TypeRefAdapter(Class clazz) { this.type = clazz; } @@ -51,6 +51,7 @@ final class TypeRefAdapter extends TypeRef { this.type = ResolvableType.forClassWithGenerics(clazz, ResolvableType.forType(generic)).getType(); } + @Override public Type getType() { return this.type; 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 549d6383..4bf91432 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -13,180 +13,91 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql.test.tester; +import java.net.URI; import java.util.function.Consumer; import org.springframework.graphql.web.WebGraphQlHandler; import org.springframework.http.HttpHeaders; -import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.http.codec.CodecConfigurer; /** - * Main entry point for testing GraphQL over a Web transport with requests - * executed {@link #create(WebTestClient) via WebTestClient} or - * {@link #create(WebGraphQlHandler) via WebGraphQlHandler}. + * Server-side tester, without a client, that executes requests through a + * {@link WebGraphQlHandler}. Similar to {@link GraphQlServiceTester} but also + * adding a web processing layer with a {@code WebInterceptor} chain. * * @author Rossen Stoyanchev * @since 1.0.0 + * @see HttpGraphQlTester + * @see WebSocketGraphQlTester */ public interface WebGraphQlTester extends GraphQlTester { - /** - * {@inheritDoc} - *

The returned spec for Web request input also allows adding HTTP headers. - */ - WebRequestSpec query(String query); - - /** - * {@inheritDoc} - *

The returned spec for Web request input also allows adding HTTP headers. - */ - WebRequestSpec queryName(String queryName); + @Override + Builder mutate(); /** - * Create a {@code WebGraphQlTester} that performs GraphQL requests as an - * HTTP client through the given {@link WebTestClient}. Depending on how the - * {@code WebTestClient} is set up, tests may be with or without a server. - * See setup examples in class-level Javadoc. - * @param client the web client to perform requests with - * @return the created {@code WebGraphQlTester} + * Create a {@link WebGraphQlTester} instance. */ - static WebGraphQlTester create(WebTestClient client) { - return builder(client).build(); + static WebGraphQlTester create(WebGraphQlHandler graphQlHandler) { + return builder(graphQlHandler).build(); } /** - * Create a {@code WebGraphQlTester} that performs GraphQL requests through - * the given {@link WebGraphQlHandler}. - * @param handler the handler to execute requests with - * @return the created {@code WebGraphQlTester} + * Return a builder for a {@link WebGraphQlTester}. + * @param graphQlHandler the handler to execute requests */ - static WebGraphQlTester create(WebGraphQlHandler handler) { - return builder(handler).build(); - } - - /** - * Return a builder with options to initialize a {@code WebGraphQlTester}. - * @param client the client to execute requests with - * @return the builder to use - */ - static Builder builder(WebTestClient client) { - return new DefaultWebGraphQlTesterBuilder(client); - } - - /** - * Return a builder with options to initialize a {@code WebGraphQlHandler}. - * @param handler the handler to execute requests with - * @return the builder to use - */ - static Builder builder(WebGraphQlHandler handler) { - return new DefaultWebGraphQlTesterBuilder(handler); + static WebGraphQlTester.Builder builder(WebGraphQlHandler graphQlHandler) { + return new DefaultWebGraphQlTester.Builder<>(graphQlHandler); } /** - * A builder to create a {@link WebGraphQlTester} instance. + * Common builder for Web {@code GraphQlTester} extensions. */ - interface Builder extends GraphQlTester.Builder { + interface Builder> extends GraphQlTester.Builder { /** - * Add the given header to all requests that haven't added it. - * @param headerName the header name - * @param headerValues the header values + * Set the GraphQL endpoint URL as a String. + * @param url the url to send HTTP requests to or connect over WebSocket */ - Builder defaultHttpHeader(String headerName, String... headerValues); + B url(String url); /** - * Variant of {@link #defaultHttpHeader(String, String...)} that provides - * access to the underlying headers to inspect or modify directly. + * Set the GraphQL endpoint URL. + * @param url the url to send HTTP requests to or connect over WebSocket + */ + B url(URI url); + + /** + * Add the given header to HTTP requests or to the WebSocket handshake request. + * @param name the header name + * @param values the header values + */ + B header(String name, String... values); + + /** + * Variant of {@link #header(String, String...)} that provides access + * to the underlying headers to inspect or modify directly. * @param headersConsumer a function that consumes the {@code HttpHeaders} */ - Builder defaultHttpHeaders(Consumer headersConsumer); + B headers(Consumer headersConsumer); /** - * Build the {@code WebGraphQlTester}. - * @return the created instance + * Configure the underlying {@code CodecConfigurer} to use for all JSON + * encoding and decoding needs. + */ + B codecConfigurer(Consumer codecsConsumer); + + /** + * Build a {@link WebGraphQlTester} instance. */ @Override WebGraphQlTester build(); - } - - - /** - * Extends {@link GraphQlTester.RequestSpec} with further input options - * applicable to Web requests. - */ - interface WebRequestSpec extends RequestSpec { - - /** - * Add the given, single header value under the given name. - * @param headerName the header name - * @param headerValues the header value(s) - * @return the same instance - */ - WebRequestSpec httpHeader(String headerName, String... headerValues); - - /** - * Manipulate the request's headers with the given consumer. The - * headers provided to the consumer are "live", so that the consumer can - * be used to {@linkplain HttpHeaders#set(String, String) overwrite} - * existing header values, {@linkplain HttpHeaders#remove(Object) remove} - * values, or use any of the other {@link HttpHeaders} methods. - * @param headersConsumer a function that consumes the {@code HttpHeaders} - * @return this builder - */ - WebRequestSpec httpHeaders(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} to expose 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} to expose 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/main/java/org/springframework/graphql/test/tester/WebSocketGraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/WebSocketGraphQlTester.java new file mode 100644 index 00000000..e4e8a1b3 --- /dev/null +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/WebSocketGraphQlTester.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2022 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.net.URI; + +import reactor.core.publisher.Mono; + +import org.springframework.graphql.client.WebSocketGraphQlClient; +import org.springframework.web.reactive.socket.client.WebSocketClient; + +/** + * GraphQL over WebSocket client that uses {@link WebSocketClient}. + * + * @author Rossen Stoyanchev + * @since 1.0.0 + */ +public interface WebSocketGraphQlTester extends WebGraphQlTester { + + /** + * This is delegated to the {@code start()} method of the underlying + * {@link WebSocketGraphQlClient}. + */ + Mono start(); + + /** + * This is delegated to the {@code stop()} method of the underlying + * {@link WebSocketGraphQlClient}. + */ + Mono stop(); + + @Override + Builder mutate(); + + + /** + * Create a {@link WebSocketGraphQlTester}. + * @param url the GraphQL endpoint URL + * @param webSocketClient the underlying transport client to use + */ + static WebSocketGraphQlTester create(URI url, WebSocketClient webSocketClient) { + return builder(url, webSocketClient).build(); + } + + /** + * Return a builder for a {@link WebSocketGraphQlClient}. + * @param url the GraphQL endpoint URL + * @param webSocketClient the underlying transport client to use + */ + static WebSocketGraphQlTester.Builder builder(URI url, WebSocketClient webSocketClient) { + return new DefaultWebSocketGraphQlTester.Builder(url, webSocketClient); + } + + + /** + * Builder for a GraphQL over WebSocket tester. + */ + interface Builder> extends WebGraphQlTester.Builder { + + /** + * Build the {@code WebSocketGraphQlTester}. + */ + @Override + WebSocketGraphQlTester build(); + + } + +}