Introduce transport specific GraphQlTester extensions

See gh-317
This commit is contained in:
rstoyanchev
2022-03-04 11:41:19 +00:00
parent 1ae7eb742f
commit 29b1aa2cc7
19 changed files with 1047 additions and 921 deletions

View File

@@ -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.
*
* <p>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);
}
}

View File

@@ -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.
*
* <p>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<B extends AbstractGraphQlTesterBuilder<B>> implements GraphQlTester.Builder<B> {
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<GraphQLError> errorFilter;
private DocumentSource documentSource = new CachingDocumentSource(new ResourceDocumentSource());
private Duration responseTimeout = DEFAULT_RESPONSE_DURATION;
@Override
public B errorFilter(Predicate<GraphQLError> 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 extends B> 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<GraphQlTester.Builder<?>> 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();
}
}
}

View File

@@ -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<GraphQlTester.Builder<?>> builderInitializer;
DefaultGraphQlServiceTester(GraphQlTester tester, GraphQlServiceTransport transport,
Consumer<GraphQlTester.Builder<?>> 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<B extends Builder<B>> extends AbstractGraphQlTesterBuilder<B>
implements GraphQlServiceTester.Builder<B> {
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());
}
}
}

View File

@@ -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.
*
* <p>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<String, String> queryNameResolver;
@Nullable
private final Predicate<GraphQLError> errorFilter;
private final Configuration jsonPathConfig;
private final DocumentSource documentSource;
private final Duration responseTimeout;
private final Consumer<GraphQlTester.Builder<?>> builderInitializer;
DefaultGraphQlTester(RequestStrategy requestStrategy, Function<String, String> 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<GraphQLError> errorFilter,
Configuration jsonPathConfig, DocumentSource documentSource, Duration timeout,
Consumer<GraphQlTester.Builder<?>> 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<GraphQLError> errorFilter,
Consumer<Runnable> assertDecorator) {
static final class Builder extends AbstractGraphQlTesterBuilder<Builder> {
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<DefaultRequestSpec> {
private final class DefaultRequestSpec implements RequestSpec<DefaultRequestSpec> {
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<String, Object> 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<Runnable> assertDecorator) {
DocumentContext jsonDocument = JsonPath.parse(result.toSpecification(), jsonPathConfig);
return new DefaultResponseSpec(jsonDocument, errorFilter, assertDecorator);
}
private Consumer<Runnable> assertDecorator(GraphQlRequest request) {
return (assertion) -> {
try {
assertion.run();
}
catch (AssertionError ex) {
throw new AssertionError(ex.getMessage() + "\nRequest: " + request, ex);
}
};
}
}

View File

@@ -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<DefaultGraphQlTesterBuilder> {
private final GraphQlService service;
DefaultGraphQlTesterBuilder(GraphQlService service) {
Assert.notNull(service, "GraphQlService is required.");
this.service = service;
}
@Override
public DefaultGraphQlTesterBuilder errorFilter(Predicate<GraphQLError> predicate) {
addErrorFilter(predicate);
return this;
}
@Override
public DefaultGraphQlTesterBuilder jsonPathConfig(Configuration config) {
setJsonPathConfig(config);
return this;
}
@Override
public DefaultGraphQlTesterBuilder responseTimeout(Duration timeout) {
setResponseTimeout(timeout);
return this;
}
@Override
public GraphQlTester build() {
RequestStrategy strategy = new GraphQlServiceRequestStrategy(
this.service, getErrorFilter(), initJsonPathConfig(), initResponseTimeout());
return new DefaultGraphQlTester(strategy, getQueryNameResolver());
}
}

View File

@@ -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<GraphQlTester.Builder<?>> builderInitializer;
DefaultHttpGraphQlTester(GraphQlTester graphQlTester, WebTestClient webTestClient,
Consumer<GraphQlTester.Builder<?>> 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<Builder>
implements HttpGraphQlTester.Builder<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<HttpHeaders> headersConsumer) {
this.webTestClientBuilder.defaultHeaders(headersConsumer);
return this;
}
@Override
public Builder codecConfigurer(Consumer<CodecConfigurer> codecConsumer) {
this.webTestClientBuilder.codecs(codecConsumer::accept);
return this;
}
@Override
public Builder webTestClient(Consumer<WebTestClient.Builder> 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());
}
}
}

View File

@@ -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<String, String> queryNameResolver;
private final Consumer<GraphQlTester.Builder<?>> builderInitializer;
DefaultWebGraphQlTester(
WebRequestStrategy requestStrategy, @Nullable HttpHeaders defaultHeaders,
Function<String, String> queryNameResolver) {
DefaultWebGraphQlTester(GraphQlTester tester, WebGraphQlHandlerTransport transport,
Consumer<GraphQlTester.Builder<?>> 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<B extends Builder<B>> extends AbstractGraphQlTesterBuilder<B>
implements WebGraphQlTester.Builder<B> {
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<HttpHeaders> headersConsumer) {
public B headers(Consumer<HttpHeaders> headersConsumer) {
headersConsumer.accept(this.headers);
return this;
return self();
}
@Override
public WebResponseSpec execute() {
return this.requestStrategy.execute(createWebInput());
public B codecConfigurer(Consumer<CodecConfigurer> codecConsumer) {
// Ignore, no serialization needs at this level
return self();
}
@SuppressWarnings("unchecked")
protected <T extends B> 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<HttpHeaders> 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<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

@@ -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<GraphQLError> predicate) {
addErrorFilter(predicate);
return this;
}
@Override
public DefaultWebGraphQlTesterBuilder jsonPathConfig(Configuration config) {
setJsonPathConfig(config);
return this;
}
@Override
public DefaultWebGraphQlTesterBuilder responseTimeout(Duration timeout) {
setResponseTimeout(timeout);
return this;
}
@Override
public DefaultWebGraphQlTesterBuilder 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<HttpHeaders> headersConsumer) {
this.headers = (this.headers != null ? this.headers : new HttpHeaders());
headersConsumer.accept(this.headers);
return this;
}
@Override
public WebGraphQlTester build() {
return new DefaultWebGraphQlTester(initRequestStrategy(), this.headers, 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");
}
}

View File

@@ -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<GraphQlTester.Builder<?>> builderInitializer;
DefaultWebSocketGraphQlTester(
GraphQlTester graphQlTester, WebSocketGraphQlClient webSocketGraphQlClient,
Consumer<GraphQlTester.Builder<?>> builderInitializer) {
super(graphQlTester);
this.webSocketGraphQlClient = webSocketGraphQlClient;
this.builderInitializer = builderInitializer;
}
@Override
public Mono<Void> start() {
return this.webSocketGraphQlClient.start();
}
@Override
public Mono<Void> 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<Builder> implements WebSocketGraphQlTester.Builder<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<HttpHeaders> headersConsumer) {
this.graphQlClientBuilder.headers(headersConsumer);
return this;
}
@Override
public Builder codecConfigurer(Consumer<CodecConfigurer> 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<ExecutionResult> execute(GraphQlRequest request) {
return client
.document(request.getDocument())
.operationName(request.getOperationName())
.variables(request.getVariables())
.execute()
.map(GraphQlClient.ResponseSpec::andReturn);
}
@Override
public Flux<ExecutionResult> executeSubscription(GraphQlRequest request) {
return client
.document(request.getDocument())
.operationName(request.getOperationName())
.variables(request.getVariables())
.executeSubscription().map(GraphQlClient.ResponseSpec::andReturn);
}
};
}
}
}

View File

@@ -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<B extends Builder<B>> extends GraphQlTester.Builder<B> {
/**
* Build a {@link GraphQlServiceTester} instance.
*/
@Override
GraphQlServiceTester build();
}
}

View File

@@ -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.
*
* <p>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}.
* <p>To test using a client that connects to a server, with or without a live
* server, see {@code GraphQlTester} extensions:
* <ul>
* <li>{@link HttpGraphQlTester}
* <li>{@link WebSocketGraphQlTester}
* </ul>
*
* <p>To test on the server side, without a client, see the following:
* <ul>
* <li>{@link GraphQlServiceTester}
* <li>{@link WebGraphQlTester}
* </ul>
*
* @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}.
* <p>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<T extends Builder<T>> {
interface Builder<B extends Builder<B>> {
/**
* 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<GraphQLError> predicate);
B errorFilter(Predicate<GraphQLError> 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.
* <p>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.
* <p>By default, {@link ResourceDocumentSource} is used.
*/
T jsonPathConfig(Configuration config);
B documentSource(DocumentSource contentLoader);
/**
* Max amount of time to wait for a GraphQL response.
* <p>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);
}
/**

View File

@@ -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<GraphQLError> errorFilter;
@Nullable
private Configuration jsonPathConfig;
@Nullable
private Duration responseTimeout;
private final Function<String, String> queryNameResolver = new QueryNameResolver();
protected void addErrorFilter(Predicate<GraphQLError> predicate) {
this.errorFilter = (this.errorFilter != null ? errorFilter.and(predicate) : predicate);
}
@Nullable
protected Predicate<GraphQLError> getErrorFilter() {
return errorFilter;
}
protected void setJsonPathConfig(Configuration config) {
this.jsonPathConfig = config;
}
protected void setResponseTimeout(Duration timeout) {
Assert.notNull(timeout, "'timeout' is required");
this.responseTimeout = timeout;
}
@Nullable
protected Duration getResponseTimeout() {
return this.responseTimeout;
}
protected Function<String, String> 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<String, String> {
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();
}
}
}

View File

@@ -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<String, Object> 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);
}
}

View File

@@ -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<B extends Builder<B>> extends WebGraphQlTester.Builder<B> {
/**
* Customize the {@code WebTestClient} to use.
* <p>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<WebTestClient.Builder> webClient);
/**
* Build the {@code HttpGraphQlTester} instance.
*/
@Override
HttpGraphQlTester build();
}
}

View File

@@ -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<GraphQLError> errors = Collections.emptyList();
private Map<Object, Object> extensions = Collections.emptyMap();
public void setData(Object data) {
this.data = data;
}
@Override
@SuppressWarnings("unchecked")
@Nullable
public <T> T getData() {
return (T) this.data;
}
public void setErrors(List<TestGraphQlError> errors) {
this.errors = new ArrayList<>(errors);
}
@Override
public List<GraphQLError> getErrors() {
return this.errors;
}
@Override
public boolean isDataPresent() {
return getData() != null;
}
public void setExtensions(Map<Object, Object> extensions) {
this.extensions = new LinkedHashMap<>(extensions);
}
@Override
public Map<Object, Object> getExtensions() {
return this.extensions;
}
@Override
public Map<String, Object> toSpecification() {
ExecutionResultImpl.Builder builder = ExecutionResultImpl.newExecutionResult()
.addErrors(this.errors)
.extensions(this.extensions);
if (isDataPresent()) {
builder.data(this.data);
}
return builder.build().toSpecification();
}
}

View File

@@ -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
*/

View File

@@ -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 <T> the referenced type
* @author Rossen Stoyanchev
* @since 1.0.0
*/
final class TypeRefAdapter<T> extends TypeRef<T> {
private final Type type;
TypeRefAdapter(Class<T> clazz) {
this.type = clazz;
}
@@ -51,6 +51,7 @@ final class TypeRefAdapter<T> extends TypeRef<T> {
this.type = ResolvableType.forClassWithGenerics(clazz, ResolvableType.forType(generic)).getType();
}
@Override
public Type getType() {
return this.type;

View File

@@ -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}
* <p>The returned spec for Web request input also allows adding HTTP headers.
*/
WebRequestSpec query(String query);
/**
* {@inheritDoc}
* <p>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<Builder> {
interface Builder<B extends Builder<B>> extends GraphQlTester.Builder<B> {
/**
* 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<HttpHeaders> headersConsumer);
B headers(Consumer<HttpHeaders> 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<CodecConfigurer> 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<WebRequestSpec> {
/**
* 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<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} 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<HttpHeaders> 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<HttpHeaders> consumer);
}

View File

@@ -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<Void> start();
/**
* This is delegated to the {@code stop()} method of the underlying
* {@link WebSocketGraphQlClient}.
*/
Mono<Void> 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<B extends Builder<B>> extends WebGraphQlTester.Builder<B> {
/**
* Build the {@code WebSocketGraphQlTester}.
*/
@Override
WebSocketGraphQlTester build();
}
}