Refactoring in spring-graphql tests

Add GraphQlSetup that provides a unified workflow for test setup
including the creation of a GraphQsSource, a GraphQlService, a
WebGraphQlHandler, and/or HTTP handlers. The workflow also helps to
collect input (schema, data fetchers) in ways that are convenient
for testing purposes.
This commit is contained in:
Rossen Stoyanchev
2021-11-05 15:03:29 +00:00
parent 2da6bd3824
commit 98f3d4f6ed
18 changed files with 475 additions and 403 deletions

View File

@@ -0,0 +1,32 @@
/*
* 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;
import org.springframework.graphql.execution.DataLoaderRegistrar;
/**
* Workflow that results in the creation of a {@link GraphQlService} or a
* {@link org.springframework.graphql.web.WebGraphQlHandler}.
*
* @author Rossen Stoyanchev
*/
public interface GraphQlServiceSetup extends WebGraphQlSetup {
GraphQlServiceSetup dataLoaders(DataLoaderRegistrar... registrars);
GraphQlService toGraphQlService();
}

View File

@@ -0,0 +1,167 @@
/*
* 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;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import graphql.GraphQL;
import graphql.schema.DataFetcher;
import graphql.schema.GraphQLTypeVisitor;
import graphql.schema.TypeResolver;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
import org.springframework.graphql.execution.DataLoaderRegistrar;
import org.springframework.graphql.execution.ExecutionGraphQlService;
import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
import org.springframework.graphql.execution.ThreadLocalAccessor;
import org.springframework.graphql.web.WebGraphQlHandler;
import org.springframework.graphql.web.WebInterceptor;
/**
* Workflow for GraphQL tests setup that starts with {@link GraphQlSource.Builder}
* related input, and then optionally moving on to the creation of a
* {@link GraphQlService} or a {@link WebGraphQlHandler}.
*
* @author Rossen Stoyanchev
*/
@SuppressWarnings("unused")
public class GraphQlSetup implements GraphQlServiceSetup {
private final GraphQlSource.Builder graphQlSourceBuilder;
private final List<DataLoaderRegistrar> dataLoaderRegistrars = new ArrayList<>();
private final List<WebInterceptor> webInterceptors = new ArrayList<>();
private final List<ThreadLocalAccessor> accessors = new ArrayList<>();
private GraphQlSetup(Resource... schemaResources) {
this.graphQlSourceBuilder = GraphQlSource.builder().schemaResources(schemaResources);
}
public GraphQlSetup queryFetcher(String field, DataFetcher<?> dataFetcher) {
return dataFetcher("Query", field, dataFetcher);
}
public GraphQlSetup mutationFetcher(String field, DataFetcher<?> dataFetcher) {
return dataFetcher("Mutation", field, dataFetcher);
}
public GraphQlSetup subscriptionFetcher(String field, DataFetcher<?> dataFetcher) {
return dataFetcher("Subscription", field, dataFetcher);
}
public GraphQlSetup dataFetcher(String type, String field, DataFetcher<?> dataFetcher) {
return runtimeWiring(wiringBuilder ->
wiringBuilder.type(type, typeBuilder -> typeBuilder.dataFetcher(field, dataFetcher)));
}
public GraphQlSetup runtimeWiring(RuntimeWiringConfigurer configurer) {
this.graphQlSourceBuilder.configureRuntimeWiring(configurer);
return this;
}
public GraphQlSetup exceptionResolver(DataFetcherExceptionResolver... resolvers) {
this.graphQlSourceBuilder.exceptionResolvers(Arrays.asList(resolvers));
return this;
}
public GraphQlSetup typeResolver(TypeResolver typeResolver) {
this.graphQlSourceBuilder.defaultTypeResolver(typeResolver);
return this;
}
public GraphQlSetup typeVisitor(GraphQLTypeVisitor... visitors) {
this.graphQlSourceBuilder.typeVisitors(Arrays.asList(visitors));
return this;
}
public GraphQL toGraphQl() {
return this.graphQlSourceBuilder.build().graphQl();
}
public GraphQlSource toGraphQlSource() {
return this.graphQlSourceBuilder.build();
}
// GraphQlServiceSetup...
@Override
public GraphQlServiceSetup dataLoaders(DataLoaderRegistrar... registrars) {
this.dataLoaderRegistrars.addAll(Arrays.asList(registrars));
return this;
}
public ExecutionGraphQlService toGraphQlService() {
GraphQlSource source = graphQlSourceBuilder.build();
ExecutionGraphQlService service = new ExecutionGraphQlService(source);
this.dataLoaderRegistrars.forEach(service::addDataLoaderRegistrar);
return service;
}
// WebGraphQlSetup...
public WebGraphQlSetup webInterceptor(WebInterceptor... interceptors) {
this.webInterceptors.addAll(Arrays.asList(interceptors));
return this;
}
@Override
public WebGraphQlSetup threadLocalAccessor(ThreadLocalAccessor... accessors) {
this.accessors.addAll(Arrays.asList(accessors));
return this;
}
public WebGraphQlHandler toWebGraphQlHandler() {
ExecutionGraphQlService service = toGraphQlService();
return WebGraphQlHandler.builder(service)
.interceptors(webInterceptors)
.threadLocalAccessors(this.accessors)
.build();
}
@Override
public org.springframework.graphql.web.webmvc.GraphQlHttpHandler toHttpHandler() {
return new org.springframework.graphql.web.webmvc.GraphQlHttpHandler(toWebGraphQlHandler());
}
@Override
public org.springframework.graphql.web.webflux.GraphQlHttpHandler toHttpHandlerWebFlux() {
return new org.springframework.graphql.web.webflux.GraphQlHttpHandler(toWebGraphQlHandler());
}
// Factory methods
public static GraphQlSetup schemaContent(String schema) {
return new GraphQlSetup(new ByteArrayResource(schema.getBytes(StandardCharsets.UTF_8)));
}
public static GraphQlSetup schemaResource(Resource... resources) {
return new GraphQlSetup(resources);
}
}

View File

@@ -1,70 +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;
import java.nio.charset.StandardCharsets;
import graphql.schema.DataFetcher;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
/**
* Utility methods for GraphQL tests.
*/
public abstract class GraphQlTestUtils {
/**
* Initialize a {@link GraphQlSource.Builder} with a single {@link DataFetcher}.
*
* @param schema either String content or a {@link Resource}.
* @param typeName the parent type name (Query, Mutation, or Subscription).
* @param fieldName the name of the operation
* @param fetcher the fetcher to use
*
* @return the created builder
*/
public static GraphQlSource.Builder graphQlSource(
Object schema, String typeName, String fieldName, DataFetcher<?> fetcher) {
return graphQlSource(schema,
wiring -> wiring.type(typeName, builder -> builder.dataFetcher(fieldName, fetcher)));
}
/**
* Initialize a {@link GraphQlSource.Builder} with a {@link RuntimeWiringConfigurer},
* which may be useful for multiple {@link DataFetcher} registrations, or for a
* built-in implementation (e.g. for annotated handler methods).
*
* @param schema either String content or a {@link Resource}.
* @param configurer the configurer to apply to the RuntimeWiring
*
* @return the created builder
*/
public static GraphQlSource.Builder graphQlSource(Object schema, RuntimeWiringConfigurer configurer) {
Resource schemaResource = (schema instanceof String ?
new ByteArrayResource(((String) schema).getBytes(StandardCharsets.UTF_8)) :
(Resource) schema);
return GraphQlSource.builder()
.schemaResources(schemaResource)
.configureRuntimeWiring(configurer);
}
}

View File

@@ -34,7 +34,7 @@ public class TestThreadLocalAccessor<T> implements ThreadLocalAccessor {
@Nullable
private Long threadId;
private boolean suppressThreadIdCheck;
private final boolean suppressThreadIdCheck;
public TestThreadLocalAccessor(ThreadLocal<T> threadLocal) {
this(threadLocal, false);

View File

@@ -0,0 +1,40 @@
/*
* 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;
import org.springframework.graphql.execution.ThreadLocalAccessor;
import org.springframework.graphql.web.WebGraphQlHandler;
import org.springframework.graphql.web.WebInterceptor;
/**
* Workflow that results in the creation of a {@link WebGraphQlHandler} or
* an HTTP handler for WebMvc or WebFlux.
*
* @author Rossen Stoyanchev
*/
public interface WebGraphQlSetup {
WebGraphQlSetup webInterceptor(WebInterceptor... interceptors);
WebGraphQlSetup threadLocalAccessor(ThreadLocalAccessor... accessors);
WebGraphQlHandler toWebGraphQlHandler();
org.springframework.graphql.web.webmvc.GraphQlHttpHandler toHttpHandler();
org.springframework.graphql.web.webflux.GraphQlHttpHandler toHttpHandlerWebFlux();
}

View File

@@ -40,14 +40,13 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext
import org.springframework.context.annotation.Bean;
import org.springframework.graphql.GraphQlResponse;
import org.springframework.graphql.GraphQlService;
import org.springframework.graphql.GraphQlTestUtils;
import org.springframework.graphql.GraphQlSetup;
import org.springframework.graphql.RequestInput;
import org.springframework.graphql.data.method.annotation.BatchMapping;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.execution.BatchLoaderRegistry;
import org.springframework.graphql.execution.DefaultBatchLoaderRegistry;
import org.springframework.graphql.execution.ExecutionGraphQlService;
import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Controller;
@@ -258,10 +257,10 @@ public class BatchMappingInvocationTests {
@Bean
public GraphQlService graphQlService(AnnotatedControllerConfigurer configurer, BatchLoaderRegistry registry) {
GraphQlSource source = GraphQlTestUtils.graphQlSource(schema, configurer).build();
ExecutionGraphQlService service = new ExecutionGraphQlService(source);
service.addDataLoaderRegistrar(registry);
return service;
return GraphQlSetup.schemaContent(schema)
.runtimeWiring(configurer)
.dataLoaders(registry)
.toGraphQlService();
}
@Bean

View File

@@ -37,7 +37,7 @@ import org.springframework.graphql.BookCriteria;
import org.springframework.graphql.BookSource;
import org.springframework.graphql.GraphQlResponse;
import org.springframework.graphql.GraphQlService;
import org.springframework.graphql.GraphQlTestUtils;
import org.springframework.graphql.GraphQlSetup;
import org.springframework.graphql.RequestInput;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.MutationMapping;
@@ -47,7 +47,6 @@ import org.springframework.graphql.data.method.annotation.SubscriptionMapping;
import org.springframework.graphql.execution.BatchLoaderRegistry;
import org.springframework.graphql.execution.DefaultBatchLoaderRegistry;
import org.springframework.graphql.execution.ExecutionGraphQlService;
import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.stereotype.Controller;
import static org.assertj.core.api.Assertions.assertThat;
@@ -192,11 +191,11 @@ public class SchemaMappingInvocationTests {
}
@Bean
public GraphQlService graphQlService(AnnotatedControllerConfigurer configurer) {
GraphQlSource source = GraphQlTestUtils.graphQlSource(BookSource.schema, configurer).build();
ExecutionGraphQlService service = new ExecutionGraphQlService(source);
service.addDataLoaderRegistrar(batchLoaderRegistry());
return service;
public GraphQlService graphQlService(AnnotatedControllerConfigurer configurer, BatchLoaderRegistry registry) {
return GraphQlSetup.schemaResource(BookSource.schema)
.runtimeWiring(configurer)
.dataLoaders(registry)
.toGraphQlService();
}
@Bean

View File

@@ -44,11 +44,8 @@ import org.springframework.data.repository.Repository;
import org.springframework.graphql.Author;
import org.springframework.graphql.BookSource;
import org.springframework.graphql.GraphQlResponse;
import org.springframework.graphql.GraphQlTestUtils;
import org.springframework.graphql.GraphQlSetup;
import org.springframework.graphql.data.GraphQlRepository;
import org.springframework.graphql.execution.ExecutionGraphQlService;
import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
import org.springframework.graphql.web.WebGraphQlHandler;
import org.springframework.graphql.web.WebInput;
import org.springframework.graphql.web.WebOutput;
@@ -77,18 +74,18 @@ class QuerydslDataFetcherTests {
Book book = new Book(42L, "Hitchhiker's Guide to the Galaxy", new Author(0L, "Douglas", "Adams"));
mockRepository.save(book);
Consumer<WebGraphQlHandler> tester = (handler) -> {
Mono<WebOutput> outputMono = handler.handleRequest(input("{ bookById(id: 42) {name}}"));
Consumer<GraphQlSetup> tester = setup -> {
Mono<WebOutput> output = setup.toWebGraphQlHandler().handleRequest(input("{ bookById(id: 42) {name}}"));
Book actualBook = GraphQlResponse.from(output).toEntity("bookById", Book.class);
Book actualBook = GraphQlResponse.from(outputMono).toEntity("bookById", Book.class);
assertThat(actualBook.getName()).isEqualTo(book.getName());
};
// explicit wiring
tester.accept(initHandler("bookById", QuerydslDataFetcher.builder(mockRepository).single()));
tester.accept(graphQlSetup("bookById", QuerydslDataFetcher.builder(mockRepository).single()));
// auto registration
tester.accept(initHandler(builder -> {}, mockRepository, null));
tester.accept(graphQlSetup(mockRepository));
}
@Test
@@ -97,20 +94,20 @@ class QuerydslDataFetcherTests {
Book book2 = new Book(53L, "Breaking Bad", new Author(0L, "", "Heisenberg"));
mockRepository.saveAll(Arrays.asList(book1, book2));
Consumer<WebGraphQlHandler> tester = (handler) -> {
Mono<WebOutput> outputMono = handler.handleRequest(input("{ books {name}}"));
Consumer<GraphQlSetup> tester = graphQlSetup -> {
Mono<WebOutput> output = graphQlSetup.toWebGraphQlHandler().handleRequest(input("{ books {name}}"));
List<String> names = GraphQlResponse.from(outputMono).toList("books", Book.class)
List<String> names = GraphQlResponse.from(output).toList("books", Book.class)
.stream().map(Book::getName).collect(Collectors.toList());
assertThat(names).containsExactlyInAnyOrder(book1.getName(), book2.getName());
};
// explicit wiring
tester.accept(initHandler("books", QuerydslDataFetcher.builder(mockRepository).many()));
tester.accept(graphQlSetup("books", QuerydslDataFetcher.builder(mockRepository).many()));
// auto registration
tester.accept(initHandler(builder -> {}, mockRepository, null));
tester.accept(graphQlSetup(mockRepository));
}
@Test
@@ -120,16 +117,16 @@ class QuerydslDataFetcherTests {
when(mockRepository.findBy(any(), any())).thenReturn(Optional.of(book));
// 1) Automatic registration only
WebGraphQlHandler handler = initHandler(builder -> {}, mockRepository, null);
WebGraphQlHandler handler = graphQlSetup(mockRepository).toWebGraphQlHandler();
Mono<WebOutput> outputMono = handler.handleRequest(input("{ bookById(id: 1) {name}}"));
Book actualBook = GraphQlResponse.from(outputMono).toEntity("bookById", Book.class);
assertThat(actualBook.getName()).isEqualTo("Hitchhiker's Guide to the Galaxy");
// 2) Automatic registration and explicit wiring
handler = initHandler(
"bookById", env -> new Book(53L, "Breaking Bad", new Author(0L, "", "Heisenberg")),
mockRepository);
handler = graphQlSetup(mockRepository)
.queryFetcher("bookById", env -> new Book(53L, "Breaking Bad", new Author(0L, "", "Heisenberg")))
.toWebGraphQlHandler();
outputMono = handler.handleRequest(input("{ bookById(id: 1) {name}}"));
@@ -142,8 +139,8 @@ class QuerydslDataFetcherTests {
Book book = new Book(42L, "Hitchhiker's Guide to the Galaxy", new Author(0L, "Douglas", "Adams"));
mockRepository.save(book);
WebGraphQlHandler handler = initHandler("bookById",
QuerydslDataFetcher.builder(mockRepository).projectAs(BookProjection.class).single());
DataFetcher<?> fetcher = QuerydslDataFetcher.builder(mockRepository).projectAs(BookProjection.class).single();
WebGraphQlHandler handler = graphQlSetup("bookById", fetcher).toWebGraphQlHandler();
Mono<WebOutput> outputMono = handler.handleRequest(input("{ bookById(id: 42) {name}}"));
@@ -156,8 +153,8 @@ class QuerydslDataFetcherTests {
Book book = new Book(42L, "Hitchhiker's Guide to the Galaxy", new Author(0L, "Douglas", "Adams"));
mockRepository.save(book);
WebGraphQlHandler handler = initHandler("bookById",
QuerydslDataFetcher.builder(mockRepository).projectAs(BookDto.class).single());
DataFetcher<?> fetcher = QuerydslDataFetcher.builder(mockRepository).projectAs(BookDto.class).single();
WebGraphQlHandler handler = graphQlSetup("bookById", fetcher).toWebGraphQlHandler();
Mono<WebOutput> outputMono = handler.handleRequest(input("{ bookById(id: 42) {name}}"));
@@ -169,11 +166,12 @@ class QuerydslDataFetcherTests {
void shouldConstructPredicateProperly() {
MockRepository mockRepository = mock(MockRepository.class);
WebGraphQlHandler handler = initHandler("books",
QuerydslDataFetcher.builder(mockRepository)
.customizer((QuerydslBinderCustomizer<QBook>) (bindings, book) ->
bindings.bind(book.name).firstOptional((path, value) -> value.map(path::startsWith)))
.many());
DataFetcher<Iterable<Book>> fetcher = QuerydslDataFetcher.builder(mockRepository)
.customizer((QuerydslBinderCustomizer<QBook>) (bindings, book) ->
bindings.bind(book.name).firstOptional((path, value) -> value.map(path::startsWith)))
.many();
WebGraphQlHandler handler = graphQlSetup("books", fetcher).toWebGraphQlHandler();
handler.handleRequest(input("{ books(name: \"H\", author: \"Doug\") {name}}")).block();
@@ -190,18 +188,18 @@ class QuerydslDataFetcherTests {
Book book = new Book(42L, "Hitchhiker's Guide to the Galaxy", new Author(0L, "Douglas", "Adams"));
when(mockRepository.findBy(any(), any())).thenReturn(Mono.just(book));
Consumer<WebGraphQlHandler> tester = (handler) -> {
Mono<WebOutput> outputMono = handler.handleRequest(input("{ bookById(id: 1) {name}}"));
Consumer<GraphQlSetup> tester = setup -> {
Mono<WebOutput> outputMono = setup.toWebGraphQlHandler().handleRequest(input("{ bookById(id: 1) {name}}"));
Book actualBook = GraphQlResponse.from(outputMono).toEntity("bookById", Book.class);
assertThat(actualBook.getName()).isEqualTo(book.getName());
};
// explicit wiring
tester.accept(initHandler("bookById", QuerydslDataFetcher.builder(mockRepository).single()));
tester.accept(graphQlSetup("bookById", QuerydslDataFetcher.builder(mockRepository).single()));
// auto registration
tester.accept(initHandler(builder -> {}, null, mockRepository));
tester.accept(graphQlSetup(mockRepository));
}
@Test
@@ -211,8 +209,8 @@ class QuerydslDataFetcherTests {
Book book2 = new Book(53L, "Breaking Bad", new Author(0L, "", "Heisenberg"));
when(mockRepository.findBy(any(), any())).thenReturn(Flux.just(book1, book2));
Consumer<WebGraphQlHandler> tester = (handler) -> {
Mono<WebOutput> outputMono = handler.handleRequest(input("{ books {name}}"));
Consumer<GraphQlSetup> tester = setup -> {
Mono<WebOutput> outputMono = setup.toWebGraphQlHandler().handleRequest(input("{ books {name}}"));
List<String> names = GraphQlResponse.from(outputMono).toList("books", Book.class)
.stream().map(Book::getName).collect(Collectors.toList());
@@ -221,10 +219,37 @@ class QuerydslDataFetcherTests {
};
// explicit wiring
tester.accept(initHandler("books", QuerydslDataFetcher.builder(mockRepository).many()));
tester.accept(graphQlSetup("books", QuerydslDataFetcher.builder(mockRepository).many()));
// auto registration
tester.accept(initHandler(builder -> {}, null, mockRepository));
tester.accept(graphQlSetup(mockRepository));
}
static GraphQlSetup graphQlSetup(String fieldName, DataFetcher<?> fetcher) {
return initGraphQlSetup(null, null).queryFetcher(fieldName, fetcher);
}
static GraphQlSetup graphQlSetup(@Nullable QuerydslPredicateExecutor<?> executor) {
return initGraphQlSetup(executor, null);
}
static GraphQlSetup graphQlSetup(@Nullable ReactiveQuerydslPredicateExecutor<?> executor) {
return initGraphQlSetup(null, executor);
}
private static GraphQlSetup initGraphQlSetup(
@Nullable QuerydslPredicateExecutor<?> executor,
@Nullable ReactiveQuerydslPredicateExecutor<?> reactiveExecutor) {
GraphQLTypeVisitor visitor = QuerydslDataFetcher.registrationTypeVisitor(
(executor != null ? Collections.singletonList(executor) : Collections.emptyList()),
(reactiveExecutor != null ? Collections.singletonList(reactiveExecutor) : Collections.emptyList()));
return GraphQlSetup.schemaResource(BookSource.schema).typeVisitor(visitor);
}
private WebInput input(String query) {
return new WebInput(URI.create("/"), new HttpHeaders(), Collections.singletonMap("query", query), null, "1");
}
@@ -233,52 +258,12 @@ class QuerydslDataFetcherTests {
}
@GraphQlRepository
interface ReactiveMockRepository extends Repository<Book, Long>, ReactiveQuerydslPredicateExecutor<Book> {
}
static WebGraphQlHandler initHandler(String fieldName, DataFetcher<?> fetcher) {
return initHandler(fieldName, fetcher, null);
}
static WebGraphQlHandler initHandler(
String fieldName, DataFetcher<?> fetcher, @Nullable QuerydslPredicateExecutor<?> executor) {
return initHandler(
GraphQlTestUtils.graphQlSource(BookSource.schema, "Query", fieldName, fetcher),
executor, null);
}
static WebGraphQlHandler initHandler(
RuntimeWiringConfigurer configurer,
@Nullable QuerydslPredicateExecutor<?> executor,
@Nullable ReactiveQuerydslPredicateExecutor<?> reactiveExecutor) {
return initHandler(
GraphQlTestUtils.graphQlSource(BookSource.schema, configurer),
executor,
reactiveExecutor);
}
private static WebGraphQlHandler initHandler(
GraphQlSource.Builder sourceBuilder,
@Nullable QuerydslPredicateExecutor<?> executor,
@Nullable ReactiveQuerydslPredicateExecutor<?> reactiveExecutor) {
GraphQLTypeVisitor visitor = QuerydslDataFetcher.registrationTypeVisitor(
(executor != null ? Collections.singletonList(executor) : Collections.emptyList()),
(reactiveExecutor != null ? Collections.singletonList(reactiveExecutor) : Collections.emptyList()));
GraphQlSource source = sourceBuilder.typeVisitors(Collections.singletonList(visitor)).build();
ExecutionGraphQlService service = new ExecutionGraphQlService(source);
return WebGraphQlHandler.builder(service).build();
}
private WebInput input(String query) {
return new WebInput(URI.create("/"), new HttpHeaders(), Collections.singletonMap("query", query), null, "1");
}
interface BookProjection {

View File

@@ -29,7 +29,8 @@ import org.springframework.graphql.Author;
import org.springframework.graphql.Book;
import org.springframework.graphql.BookSource;
import org.springframework.graphql.GraphQlResponse;
import org.springframework.graphql.GraphQlTestUtils;
import org.springframework.graphql.GraphQlService;
import org.springframework.graphql.GraphQlSetup;
import org.springframework.graphql.RequestInput;
import static org.assertj.core.api.Assertions.assertThat;
@@ -59,20 +60,21 @@ public class BatchLoadingTests {
this.registry.forTypePair(Long.class, Author.class)
.registerBatchLoader((ids, env) -> Flux.fromIterable(ids).map(BookSource::getAuthor));
ExecutionGraphQlService service = initExecutionGraphQlService(wiring -> {
wiring.type("Query", builder -> builder.dataFetcher("booksByCriteria", env -> {
Map<String, Object> criteria = env.getArgument("criteria");
String authorName = (String) criteria.get("author");
return BookSource.findBooksByAuthor(authorName).stream()
.map(book -> new Book(book.getId(), book.getName(), book.getAuthorId()))
.collect(Collectors.toList());
}));
wiring.type("Book", builder -> builder.dataFetcher("author", env -> {
Book book = env.getSource();
DataLoader<Long, Author> dataLoader = env.getDataLoader(Author.class.getName());
return dataLoader.load(book.getAuthorId());
}));
});
GraphQlService service = GraphQlSetup.schemaResource(BookSource.schema)
.queryFetcher("booksByCriteria", env -> {
Map<String, Object> criteria = env.getArgument("criteria");
String authorName = (String) criteria.get("author");
return BookSource.findBooksByAuthor(authorName).stream()
.map(book -> new Book(book.getId(), book.getName(), book.getAuthorId()))
.collect(Collectors.toList());
})
.dataFetcher("Book", "author", env -> {
Book book = env.getSource();
DataLoader<Long, Author> dataLoader = env.getDataLoader(Author.class.getName());
return dataLoader.load(book.getAuthorId());
})
.dataLoaders(this.registry)
.toGraphQlService();
Mono<ExecutionResult> resultMono = service.execute(new RequestInput(query, null, null, null));
@@ -85,11 +87,4 @@ public class BatchLoadingTests {
assertThat(author.getLastName()).isEqualTo("Orwell");
}
private ExecutionGraphQlService initExecutionGraphQlService(RuntimeWiringConfigurer configurer) {
GraphQlSource source = GraphQlTestUtils.graphQlSource(BookSource.schema, configurer).build();
ExecutionGraphQlService service = new ExecutionGraphQlService(source);
service.addDataLoaderRegistrar(this.registry);
return service;
}
}

View File

@@ -24,7 +24,7 @@ import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.graphql.GraphQlResponse;
import org.springframework.graphql.GraphQlTestUtils;
import org.springframework.graphql.GraphQlSetup;
import org.springframework.graphql.RequestInput;
import static org.assertj.core.api.Assertions.assertThat;
@@ -64,13 +64,11 @@ public class ClassNameTypeResolverTests {
"}" +
"union Sighting = Bird | Mammal | Plant | Vegetable ";
private final GraphQlSetup graphQlSetup = GraphQlSetup.schemaContent(schema);
@Test
void typeResolutionViaSuperHierarchy() {
GraphQlSource graphQlSource =
GraphQlTestUtils.graphQlSource(schema, "Query", "animals", env -> animalList).build();
String query = "" +
"query Animals {" +
" animals {" +
@@ -85,7 +83,8 @@ public class ClassNameTypeResolverTests {
" }" +
"}";
Mono<ExecutionResult> resultMono = new ExecutionGraphQlService(graphQlSource)
Mono<ExecutionResult> resultMono = graphQlSetup.queryFetcher("animals", env -> animalList)
.toGraphQlService()
.execute(new RequestInput(query, null, null, null));
GraphQlResponse response = GraphQlResponse.from(resultMono);
@@ -107,15 +106,6 @@ public class ClassNameTypeResolverTests {
@Test
void typeResolutionViaMapping() {
ClassNameTypeResolver typeResolver = new ClassNameTypeResolver();
typeResolver.addMapping(Tree.class, "Plant");
GraphQlSource graphQlSource =
GraphQlTestUtils.graphQlSource(schema, "Query", "sightings", env -> animalAndPlantList)
.defaultTypeResolver(typeResolver)
.build();
String query = "" +
"query Sightings {" +
" sightings {" +
@@ -132,10 +122,15 @@ public class ClassNameTypeResolverTests {
" }" +
"}";
Mono<ExecutionResult> resultMono = new ExecutionGraphQlService(graphQlSource)
ClassNameTypeResolver typeResolver = new ClassNameTypeResolver();
typeResolver.addMapping(Tree.class, "Plant");
Mono<ExecutionResult> result = graphQlSetup.queryFetcher("sightings", env -> animalAndPlantList)
.typeResolver(typeResolver)
.toGraphQlService()
.execute(new RequestInput(query, null, null, null));
GraphQlResponse response = GraphQlResponse.from(resultMono);
GraphQlResponse response = GraphQlResponse.from(result);
for (int i = 0; i < animalAndPlantList.size(); i++) {
Object sighting = animalAndPlantList.get(i);
if (sighting instanceof Animal) {

View File

@@ -22,7 +22,6 @@ import java.util.List;
import graphql.ExecutionInput;
import graphql.ExecutionResult;
import graphql.GraphQL;
import graphql.schema.DataFetcher;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -31,7 +30,7 @@ import reactor.util.context.Context;
import reactor.util.context.ContextView;
import org.springframework.graphql.GraphQlResponse;
import org.springframework.graphql.GraphQlTestUtils;
import org.springframework.graphql.GraphQlSetup;
import org.springframework.graphql.TestThreadLocalAccessor;
import static org.assertj.core.api.Assertions.assertThat;
@@ -44,12 +43,13 @@ public class ContextDataFetcherDecoratorTests {
@Test
void monoDataFetcher() throws Exception {
GraphQL graphQl = initGraphQl(
"type Query { greeting: String }",
"Query", "greeting", (env) -> Mono.deferContextual((context) -> {
Object name = context.get("name");
return Mono.delay(Duration.ofMillis(50)).map((aLong) -> "Hello " + name);
}));
GraphQL graphQl = GraphQlSetup.schemaContent("type Query { greeting: String }")
.queryFetcher("greeting", (env) ->
Mono.deferContextual((context) -> {
Object name = context.get("name");
return Mono.delay(Duration.ofMillis(50)).map((aLong) -> "Hello " + name);
}))
.toGraphQl();
ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build();
ReactorContextManager.setReactorContext(Context.of("name", "007"), input);
@@ -62,33 +62,34 @@ public class ContextDataFetcherDecoratorTests {
@Test
void fluxDataFetcher() throws Exception {
GraphQL graphQl = initGraphQl(
"type Query { greetings: [String] }",
"Query", "greetings",
(env) -> Mono.delay(Duration.ofMillis(50))
.flatMapMany((aLong) -> Flux.deferContextual((context) -> {
String name = context.get("name");
return Flux.just("Hi", "Bonjour", "Hola").map((s) -> s + " " + name);
})));
GraphQL graphQl = GraphQlSetup.schemaContent("type Query { greetings: [String] }")
.queryFetcher("greetings", (env) ->
Mono.delay(Duration.ofMillis(50))
.flatMapMany((aLong) -> Flux.deferContextual((context) -> {
String name = context.get("name");
return Flux.just("Hi", "Bonjour", "Hola").map((s) -> s + " " + name);
})))
.toGraphQl();
ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greetings }").build();
ReactorContextManager.setReactorContext(Context.of("name", "007"), input);
ExecutionResult result = graphQl.executeAsync(input).get();
List<String> data = GraphQlResponse.from(result).toList("greetings", String.class);;
List<String> data = GraphQlResponse.from(result).toList("greetings", String.class);
assertThat(data).containsExactly("Hi 007", "Bonjour 007", "Hola 007");
}
@Test
void fluxDataFetcherSubscription() throws Exception {
GraphQL graphQl = initGraphQl(
"type Query { greeting: String } type Subscription { greetings: String }",
"Subscription", "greetings", (env) -> Mono.delay(Duration.ofMillis(50))
.flatMapMany((aLong) -> Flux.deferContextual((context) -> {
String name = context.get("name");
return Flux.just("Hi", "Bonjour", "Hola").map((s) -> s + " " + name);
})));
GraphQL graphQl = GraphQlSetup.schemaContent("type Query { greeting: String } type Subscription { greetings: String }")
.subscriptionFetcher("greetings", (env) ->
Mono.delay(Duration.ofMillis(50))
.flatMapMany((aLong) -> Flux.deferContextual((context) -> {
String name = context.get("name");
return Flux.just("Hi", "Bonjour", "Hola").map((s) -> s + " " + name);
})))
.toGraphQl();
ExecutionInput input = ExecutionInput.newExecutionInput().query("subscription { greetings }").build();
ReactorContextManager.setReactorContext(Context.of("name", "007"), input);
@@ -109,9 +110,9 @@ public class ContextDataFetcherDecoratorTests {
nameThreadLocal.set("007");
TestThreadLocalAccessor<String> accessor = new TestThreadLocalAccessor<>(nameThreadLocal);
try {
GraphQL graphQl = initGraphQl(
"type Query { greeting: String }",
"Query", "greeting", (env) -> "Hello " + nameThreadLocal.get());
GraphQL graphQl = GraphQlSetup.schemaContent("type Query { greeting: String }")
.queryFetcher("greeting", (env) -> "Hello " + nameThreadLocal.get())
.toGraphQl();
ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build();
ContextView view = ReactorContextManager.extractThreadLocalValues(accessor, Context.empty());
@@ -128,11 +129,4 @@ public class ContextDataFetcherDecoratorTests {
}
}
private static GraphQL initGraphQl(
String schemaContent, String typeName, String fieldName, DataFetcher<?> fetcher) {
return GraphQlTestUtils.graphQlSource(schemaContent, typeName, fieldName, fetcher)
.build().graphQl();
}
}

View File

@@ -32,7 +32,7 @@ import reactor.util.context.Context;
import reactor.util.context.ContextView;
import org.springframework.graphql.GraphQlResponse;
import org.springframework.graphql.GraphQlTestUtils;
import org.springframework.graphql.GraphQlSetup;
import org.springframework.graphql.TestThreadLocalAccessor;
import static org.assertj.core.api.Assertions.assertThat;
@@ -45,7 +45,7 @@ public class ExceptionResolversExceptionHandlerTests {
@Test
void resolveException() throws Exception {
GraphQL graphQl = initGraphQl((ex, env) ->
GraphQL graphQl = graphQl((ex, env) ->
Mono.just(Collections.singletonList(
GraphqlErrorBuilder.newError(env)
.message("Resolved error: " + ex.getMessage())
@@ -65,7 +65,7 @@ public class ExceptionResolversExceptionHandlerTests {
@Test
void resolveExceptionWithReactorContext() throws Exception {
GraphQL graphQl = initGraphQl((ex, env) ->
GraphQL graphQl = graphQl((ex, env) ->
Mono.deferContextual((view) -> Mono.just(Collections.singletonList(
GraphqlErrorBuilder.newError(env)
.message("Resolved error: " + ex.getMessage() + ", name=" + view.get("name"))
@@ -87,7 +87,7 @@ public class ExceptionResolversExceptionHandlerTests {
nameThreadLocal.set("007");
TestThreadLocalAccessor<String> accessor = new TestThreadLocalAccessor<>(nameThreadLocal);
try {
GraphQL graphQl = initGraphQl(threadLocalContextAwareExceptionResolver((ex, env) ->
GraphQL graphQl = graphQl(threadLocalContextAwareResolver((ex, env) ->
GraphqlErrorBuilder.newError(env)
.message("Resolved error: " + ex.getMessage() + ", name=" + nameThreadLocal.get())
.errorType(ErrorType.BAD_REQUEST)
@@ -111,7 +111,7 @@ public class ExceptionResolversExceptionHandlerTests {
@Test
void unresolvedException() throws Exception {
GraphQL graphQl = initGraphQl((exception, environment) -> Mono.empty());
GraphQL graphQl = graphQl((exception, environment) -> Mono.empty());
ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build();
ExecutionResult result = graphQl.executeAsync(input).get();
@@ -127,7 +127,7 @@ public class ExceptionResolversExceptionHandlerTests {
@Test
void suppressedException() throws Exception {
GraphQL graphQl = initGraphQl((ex, env) -> Mono.just(Collections.emptyList()));
GraphQL graphQl = graphQl((ex, env) -> Mono.just(Collections.emptyList()));
ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build();
ExecutionResult result = graphQl.executeAsync(input).get();
@@ -136,18 +136,16 @@ public class ExceptionResolversExceptionHandlerTests {
assertThat(greeting).isNull();
}
private static GraphQL initGraphQl(DataFetcherExceptionResolver exceptionResolver) {
return GraphQlTestUtils.graphQlSource(
"type Query { greeting: String }",
"Query", "greeting", (env) -> {
throw new IllegalArgumentException("Invalid greeting");
})
.exceptionResolvers(Collections.singletonList(exceptionResolver))
.build()
.graphQl();
private static GraphQL graphQl(DataFetcherExceptionResolver exceptionResolver) {
return GraphQlSetup.schemaContent("type Query { greeting: String }")
.queryFetcher("greeting", (env) -> {
throw new IllegalArgumentException("Invalid greeting");
})
.exceptionResolver(exceptionResolver)
.toGraphQl();
}
private static DataFetcherExceptionResolver threadLocalContextAwareExceptionResolver(
private static DataFetcherExceptionResolver threadLocalContextAwareResolver(
BiFunction<Throwable, DataFetchingEnvironment, GraphQLError> resolver) {
DataFetcherExceptionResolverAdapter adapter = new DataFetcherExceptionResolverAdapter() {

View File

@@ -28,14 +28,11 @@ import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.graphql.GraphQlResponse;
import org.springframework.graphql.GraphQlService;
import org.springframework.graphql.GraphQlTestUtils;
import org.springframework.graphql.GraphQlSetup;
import org.springframework.graphql.TestThreadLocalAccessor;
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
import org.springframework.graphql.execution.DataFetcherExceptionResolverAdapter;
import org.springframework.graphql.execution.ErrorType;
import org.springframework.graphql.execution.ExecutionGraphQlService;
import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.http.HttpHeaders;
import static org.assertj.core.api.Assertions.assertThat;
@@ -50,15 +47,13 @@ public class WebGraphQlHandlerTests {
@Test
void reactorContextPropagation() {
GraphQlSource graphQlSource = GraphQlTestUtils.graphQlSource(
"type Query { greeting: String }",
"Query", "greeting", (env) -> Mono.deferContextual((context) -> {
Object name = context.get("name");
return Mono.delay(Duration.ofMillis(50)).map((aLong) -> "Hello " + name);
})).build();
GraphQlService service = new ExecutionGraphQlService(graphQlSource);
WebGraphQlHandler handler = WebGraphQlHandler.builder(service).build();
WebGraphQlHandler handler = GraphQlSetup.schemaContent("type Query { greeting: String }")
.queryFetcher("greeting", (env) ->
Mono.deferContextual((context) -> {
Object name = context.get("name");
return Mono.delay(Duration.ofMillis(50)).map((aLong) -> "Hello " + name);
}))
.toWebGraphQlHandler();
Mono<WebOutput> outputMono = handler.handleRequest(webInput)
.contextWrite((context) -> context.put("name", "007"));
@@ -69,25 +64,19 @@ public class WebGraphQlHandlerTests {
@Test
void reactorContextPropagationToExceptionResolver() {
GraphQlSource graphQlSource = GraphQlTestUtils.graphQlSource(
"type Query { greeting: String }",
"Query", "greeting", (env) -> {
throw new IllegalArgumentException("Invalid greeting");
})
.exceptionResolvers(Collections.singletonList(
(ex, env) -> Mono.deferContextual((view) -> Mono.just(Collections.singletonList(
WebGraphQlHandler handler = GraphQlSetup.schemaContent("type Query { greeting: String }")
.queryFetcher("greeting", (env) -> {
throw new IllegalArgumentException("Invalid greeting");
})
.exceptionResolver((ex, env) -> Mono.deferContextual((view) ->
Mono.just(Collections.singletonList(
GraphqlErrorBuilder.newError(env)
.message("Resolved error: " + ex.getMessage() + ", name=" + view.get("name"))
.errorType(ErrorType.BAD_REQUEST)
.build())))))
.build();
.build()))))
.toWebGraphQlHandler();
GraphQlService service = new ExecutionGraphQlService(graphQlSource);
WebGraphQlHandler handler = WebGraphQlHandler.builder(service).build();
Mono<WebOutput> outputMono = handler.handleRequest(webInput)
.contextWrite((context) -> context.put("name", "007"));
Mono<WebOutput> outputMono = handler.handleRequest(webInput).contextWrite((cxt) -> cxt.put("name", "007"));
GraphQlResponse response = GraphQlResponse.from(outputMono);
assertThat(response.errorCount()).isEqualTo(1);
@@ -103,17 +92,11 @@ public class WebGraphQlHandlerTests {
nameThreadLocal.set("007");
TestThreadLocalAccessor<String> threadLocalAccessor = new TestThreadLocalAccessor<>(nameThreadLocal);
try {
GraphQlSource graphQlSource = GraphQlTestUtils.graphQlSource(
"type Query { greeting: String }",
"Query", "greeting", env -> "Hello " + nameThreadLocal.get())
.build();
GraphQlService service = new ExecutionGraphQlService(graphQlSource);
WebGraphQlHandler handler = WebGraphQlHandler.builder(service)
.interceptor((input, next) -> Mono.delay(Duration.ofMillis(10)).flatMap((aLong) -> next.next(input)))
WebGraphQlHandler handler = GraphQlSetup.schemaContent("type Query { greeting: String }")
.queryFetcher("greeting", env -> "Hello " + nameThreadLocal.get())
.webInterceptor((input, next) -> Mono.delay(Duration.ofMillis(10)).flatMap((aLong) -> next.next(input)))
.threadLocalAccessor(threadLocalAccessor)
.build();
.toWebGraphQlHandler();
Mono<WebOutput> outputMono = handler.handleRequest(webInput);
@@ -131,25 +114,18 @@ public class WebGraphQlHandlerTests {
nameThreadLocal.set("007");
TestThreadLocalAccessor<String> threadLocalAccessor = new TestThreadLocalAccessor<>(nameThreadLocal);
try {
GraphQlSource graphQlSource = GraphQlTestUtils.graphQlSource(
"type Query { greeting: String }",
"Query", "greeting", env -> {
WebGraphQlHandler handler = GraphQlSetup.schemaContent("type Query { greeting: String }")
.queryFetcher("greeting", env -> {
throw new IllegalArgumentException("Invalid greeting");
})
.exceptionResolvers(Collections.singletonList(
threadLocalContextAwareExceptionResolver((ex, env) ->
GraphqlErrorBuilder.newError(env)
.message("Resolved error: " + ex.getMessage() + ", name=" + nameThreadLocal.get())
.errorType(ErrorType.BAD_REQUEST).build())
))
.build();
GraphQlService service = new ExecutionGraphQlService(graphQlSource);
WebGraphQlHandler handler = WebGraphQlHandler.builder(service)
.interceptor((input, next) -> Mono.delay(Duration.ofMillis(10)).flatMap((aLong) -> next.next(input)))
.exceptionResolver(threadLocalContextAwareExceptionResolver((ex, env) ->
GraphqlErrorBuilder.newError(env)
.message("Resolved error: " + ex.getMessage() + ", name=" + nameThreadLocal.get())
.errorType(ErrorType.BAD_REQUEST).build())
)
.webInterceptor((input, next) -> Mono.delay(Duration.ofMillis(10)).flatMap((aLong) -> next.next(input)))
.threadLocalAccessor(threadLocalAccessor)
.build();
.toWebGraphQlHandler();
Mono<WebOutput> outputMono = handler.handleRequest(webInput);

View File

@@ -16,21 +16,17 @@
package org.springframework.graphql.web;
import java.util.Arrays;
import reactor.core.publisher.Flux;
import org.springframework.graphql.BookSource;
import org.springframework.graphql.GraphQlTestUtils;
import org.springframework.graphql.execution.ExecutionGraphQlService;
import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.graphql.GraphQlSetup;
public abstract class BookTestUtils {
public abstract class WebSocketHandlerTestSupport {
public static final String SUBSCRIPTION_ID = "1";
protected static final String SUBSCRIPTION_ID = "1";
public static final String BOOK_QUERY = "{" +
"\"id\":\"" + BookTestUtils.SUBSCRIPTION_ID + "\"," +
protected static final String BOOK_QUERY = "{" +
"\"id\":\"" + WebSocketHandlerTestSupport.SUBSCRIPTION_ID + "\"," +
"\"type\":\"subscribe\"," +
"\"payload\":{\"query\": \"" +
" query TestQuery {" +
@@ -44,7 +40,7 @@ public abstract class BookTestUtils {
" }}\"}" +
"}";
public static final String BOOK_SUBSCRIPTION = "{" +
protected static final String BOOK_SUBSCRIPTION = "{" +
"\"id\":\"" + SUBSCRIPTION_ID + "\"," +
"\"type\":\"subscribe\"," +
"\"payload\":{\"query\": \"" +
@@ -59,24 +55,20 @@ public abstract class BookTestUtils {
" }}\"}" +
"}";
public static WebGraphQlHandler initWebGraphQlHandler(WebInterceptor... interceptors) {
GraphQlSource source = GraphQlTestUtils.graphQlSource(BookSource.schema,
wiringBuilder -> {
wiringBuilder.type("Query", builder -> builder.dataFetcher("bookById", (env) -> {
Long id = Long.parseLong(env.getArgument("id"));
return BookSource.getBook(id);
}));
wiringBuilder.type("Subscription", builder -> builder.dataFetcher("bookSearch", (env) -> {
String author = env.getArgument("author");
return Flux.fromIterable(BookSource.books())
.filter((book) -> book.getAuthor().getFullName().contains(author));
}));
}).build();
return WebGraphQlHandler.builder(new ExecutionGraphQlService(source))
.interceptors(Arrays.asList(interceptors))
.build();
protected WebGraphQlHandler initHandler(WebInterceptor... interceptors) {
return GraphQlSetup.schemaResource(BookSource.schema)
.queryFetcher("bookById", environment -> {
Long id = Long.parseLong(environment.getArgument("id"));
return BookSource.getBook(id);
})
.subscriptionFetcher("bookSearch", environment -> {
String author = environment.getArgument("author");
return Flux.fromIterable(BookSource.books())
.filter((book) -> book.getAuthor().getFullName().contains(author));
})
.webInterceptor(interceptors)
.toWebGraphQlHandler();
}
}

View File

@@ -20,15 +20,10 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import graphql.schema.DataFetcher;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.graphql.GraphQlService;
import org.springframework.graphql.GraphQlTestUtils;
import org.springframework.graphql.execution.ExecutionGraphQlService;
import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.graphql.web.WebGraphQlHandler;
import org.springframework.graphql.GraphQlSetup;
import org.springframework.http.codec.EncoderHttpMessageWriter;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.http.codec.json.Jackson2JsonEncoder;
@@ -50,9 +45,9 @@ public class GraphQlHttpHandlerTests {
@Test
void locale() {
GraphQlHttpHandler handler = createHttpHandler(
"type Query { greeting: String }", "Query", "greeting",
(env) -> "Hello in " + env.getLocale());
GraphQlHttpHandler handler = GraphQlSetup.schemaContent("type Query { greeting: String }")
.queryFetcher("greeting", (env) -> "Hello in " + env.getLocale())
.toHttpHandlerWebFlux();
MockServerHttpRequest httpRequest =
MockServerHttpRequest.post("/").acceptLanguageAsLocales(Locale.FRENCH).build();
@@ -64,14 +59,6 @@ public class GraphQlHttpHandlerTests {
.isEqualTo("{\"data\":{\"greeting\":\"Hello in fr\"}}");
}
private GraphQlHttpHandler createHttpHandler(
String schemaContent, String type, String field, DataFetcher<Object> dataFetcher) {
GraphQlSource source = GraphQlTestUtils.graphQlSource(schemaContent, type, field, dataFetcher).build();
GraphQlService service = new ExecutionGraphQlService(source);
return new GraphQlHttpHandler(WebGraphQlHandler.builder(service).build());
}
private MockServerHttpResponse handleRequest(
MockServerHttpRequest httpRequest, GraphQlHttpHandler handler, Map<String, String> body) {

View File

@@ -34,7 +34,7 @@ import reactor.test.StepVerifier;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.graphql.web.BookTestUtils;
import org.springframework.graphql.web.WebSocketHandlerTestSupport;
import org.springframework.graphql.web.ConsumeOneAndNeverCompleteInterceptor;
import org.springframework.graphql.web.WebInterceptor;
import org.springframework.graphql.web.WebSocketInterceptor;
@@ -49,7 +49,7 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit tests for {@link GraphQlWebSocketHandler}.
*/
public class GraphQlWebSocketHandlerTests {
public class GraphQlWebSocketHandlerTests extends WebSocketHandlerTestSupport {
private static final Jackson2JsonDecoder decoder = new Jackson2JsonDecoder();
@@ -57,12 +57,12 @@ public class GraphQlWebSocketHandlerTests {
void query() {
TestWebSocketSession session = handle(Flux.just(
toWebSocketMessage("{\"type\":\"connection_init\"}"),
toWebSocketMessage(BookTestUtils.BOOK_QUERY)));
toWebSocketMessage(BOOK_QUERY)));
StepVerifier.create(session.getOutput())
.consumeNextWith((message) -> assertMessageType(message, "connection_ack"))
.consumeNextWith((message) -> assertThat(decode(message)).hasSize(3)
.containsEntry("id", BookTestUtils.SUBSCRIPTION_ID).containsEntry("type", "next")
.containsEntry("id", SUBSCRIPTION_ID).containsEntry("type", "next")
.extractingByKey("payload", as(InstanceOfAssertFactories.map(String.class, Object.class)))
.extractingByKey("data", as(InstanceOfAssertFactories.map(String.class, Object.class)))
.extractingByKey("bookById", as(InstanceOfAssertFactories.map(String.class, Object.class)))
@@ -75,11 +75,11 @@ public class GraphQlWebSocketHandlerTests {
void subscription() {
TestWebSocketSession session = handle(Flux.just(
toWebSocketMessage("{\"type\":\"connection_init\"}"),
toWebSocketMessage(BookTestUtils.BOOK_SUBSCRIPTION)));
toWebSocketMessage(BOOK_SUBSCRIPTION)));
BiConsumer<WebSocketMessage, String> bookPayloadAssertion = (message, bookId) ->
assertThat(decode(message))
.hasSize(3).containsEntry("id", BookTestUtils.SUBSCRIPTION_ID).containsEntry("type", "next")
.hasSize(3).containsEntry("id", SUBSCRIPTION_ID).containsEntry("type", "next")
.extractingByKey("payload", as(InstanceOfAssertFactories.map(String.class, Object.class)))
.extractingByKey("data", as(InstanceOfAssertFactories.map(String.class, Object.class)))
.extractingByKey("bookSearch", as(InstanceOfAssertFactories.map(String.class, Object.class)))
@@ -97,7 +97,7 @@ public class GraphQlWebSocketHandlerTests {
void unauthorizedWithoutMessageType() {
TestWebSocketSession session = handle(Flux.just(
toWebSocketMessage("{\"type\":\"connection_init\"}"),
toWebSocketMessage("{\"id\":\"" + BookTestUtils.SUBSCRIPTION_ID + "\"}")));
toWebSocketMessage("{\"id\":\"" + SUBSCRIPTION_ID + "\"}")));
StepVerifier.create(session.getOutput())
.consumeNextWith((message) -> assertMessageType(message, "connection_ack"))
@@ -168,7 +168,7 @@ public class GraphQlWebSocketHandlerTests {
@Test
void unauthorizedWithoutConnectionInit() {
TestWebSocketSession session = handle(Flux.just(toWebSocketMessage(BookTestUtils.BOOK_SUBSCRIPTION)));
TestWebSocketSession session = handle(Flux.just(toWebSocketMessage(BOOK_SUBSCRIPTION)));
StepVerifier.create(session.getOutput()).verifyComplete();
StepVerifier.create(session.closeStatus()).expectNext(new CloseStatus(4401, "Unauthorized")).verifyComplete();
@@ -192,7 +192,7 @@ public class GraphQlWebSocketHandlerTests {
@Test
void connectionInitTimeout() {
GraphQlWebSocketHandler handler = new GraphQlWebSocketHandler(
BookTestUtils.initWebGraphQlHandler(), ServerCodecConfigurer.create(), Duration.ofMillis(50));
initHandler(), ServerCodecConfigurer.create(), Duration.ofMillis(50));
TestWebSocketSession session = new TestWebSocketSession(Flux.empty());
handler.handle(session).block();
@@ -206,8 +206,8 @@ public class GraphQlWebSocketHandlerTests {
void subscriptionExists() {
Flux<WebSocketMessage> messageFlux = Flux.just(
toWebSocketMessage("{\"type\":\"connection_init\"}"),
toWebSocketMessage(BookTestUtils.BOOK_SUBSCRIPTION),
toWebSocketMessage(BookTestUtils.BOOK_SUBSCRIPTION));
toWebSocketMessage(BOOK_SUBSCRIPTION),
toWebSocketMessage(BOOK_SUBSCRIPTION));
TestWebSocketSession session = handle(messageFlux, new ConsumeOneAndNeverCompleteInterceptor());
@@ -216,7 +216,7 @@ public class GraphQlWebSocketHandlerTests {
session.getOutput().subscribe((message) -> messages.add(decode(message)));
StepVerifier.create(session.closeStatus())
.expectNext(new CloseStatus(4409, "Subscriber for " + BookTestUtils.SUBSCRIPTION_ID + " already exists"))
.expectNext(new CloseStatus(4409, "Subscriber for " + SUBSCRIPTION_ID + " already exists"))
.verifyComplete();
assertThat(messages.size()).isEqualTo(2);
@@ -228,18 +228,18 @@ public class GraphQlWebSocketHandlerTests {
void clientCompletion() {
Sinks.Many<WebSocketMessage> input = Sinks.many().unicast().onBackpressureBuffer();
input.tryEmitNext(toWebSocketMessage("{\"type\":\"connection_init\"}"));
input.tryEmitNext(toWebSocketMessage(BookTestUtils.BOOK_SUBSCRIPTION));
input.tryEmitNext(toWebSocketMessage(BOOK_SUBSCRIPTION));
TestWebSocketSession session = handle(input.asFlux(), new ConsumeOneAndNeverCompleteInterceptor());
String completeMessage = "{\"id\":\"" + BookTestUtils.SUBSCRIPTION_ID + "\",\"type\":\"complete\"}";
String completeMessage = "{\"id\":\"" + SUBSCRIPTION_ID + "\",\"type\":\"complete\"}";
StepVerifier.create(session.getOutput())
.consumeNextWith((message) -> assertMessageType(message, "connection_ack"))
.consumeNextWith((message) -> assertMessageType(message, "next"))
.then(() -> input.tryEmitNext(toWebSocketMessage(completeMessage)))
.as("Second subscription with same id is possible only if the first was properly removed")
.then(() -> input.tryEmitNext(toWebSocketMessage(BookTestUtils.BOOK_SUBSCRIPTION)))
.then(() -> input.tryEmitNext(toWebSocketMessage(BOOK_SUBSCRIPTION)))
.consumeNextWith((message) -> assertMessageType(message, "next"))
.then(() -> input.tryEmitNext(toWebSocketMessage(completeMessage)))
.verifyTimeout(Duration.ofMillis(500));
@@ -247,7 +247,7 @@ public class GraphQlWebSocketHandlerTests {
private TestWebSocketSession handle(Flux<WebSocketMessage> input, WebInterceptor... interceptors) {
GraphQlWebSocketHandler handler = new GraphQlWebSocketHandler(
BookTestUtils.initWebGraphQlHandler(interceptors),
initHandler(interceptors),
ServerCodecConfigurer.create(),
Duration.ofSeconds(60));
@@ -271,7 +271,7 @@ public class GraphQlWebSocketHandlerTests {
Map<String, Object> map = decode(message);
assertThat(map).containsEntry("type", messageType);
if (!messageType.equals("connection_ack")) {
assertThat(map).containsEntry("id", BookTestUtils.SUBSCRIPTION_ID);
assertThat(map).containsEntry("id", SUBSCRIPTION_ID);
}
}

View File

@@ -23,15 +23,10 @@ import java.util.Locale;
import javax.servlet.ServletException;
import graphql.schema.DataFetcher;
import org.junit.jupiter.api.Test;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.graphql.GraphQlService;
import org.springframework.graphql.GraphQlTestUtils;
import org.springframework.graphql.execution.ExecutionGraphQlService;
import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.graphql.web.WebGraphQlHandler;
import org.springframework.graphql.GraphQlSetup;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.mock.web.MockHttpServletRequest;
@@ -54,8 +49,9 @@ public class GraphQlHttpHandlerTests {
@Test
void locale() throws Exception {
GraphQlHttpHandler handler = createHttpHandler(
"type Query { greeting: String }", "Query", "greeting", (env) -> "Hello in " + env.getLocale());
GraphQlHttpHandler handler = GraphQlSetup.schemaContent("type Query { greeting: String }")
.queryFetcher("greeting", (env) -> "Hello in " + env.getLocale())
.toHttpHandler();
MockHttpServletRequest servletRequest = new MockHttpServletRequest("POST", "/");
servletRequest.setContentType("application/json");
@@ -74,14 +70,6 @@ public class GraphQlHttpHandlerTests {
}
}
private GraphQlHttpHandler createHttpHandler(
String schemaContent, String type, String field, DataFetcher<Object> dataFetcher) {
GraphQlSource source = GraphQlTestUtils.graphQlSource(schemaContent, type, field, dataFetcher).build();
GraphQlService service = new ExecutionGraphQlService(source);
return new GraphQlHttpHandler(WebGraphQlHandler.builder(service).build());
}
private MockHttpServletResponse handleRequest(
MockHttpServletRequest servletRequest, GraphQlHttpHandler handler) throws ServletException, IOException {

View File

@@ -29,11 +29,10 @@ import java.util.function.Consumer;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.graphql.web.BookTestUtils;
import org.springframework.graphql.web.WebSocketHandlerTestSupport;
import org.springframework.graphql.web.ConsumeOneAndNeverCompleteInterceptor;
import org.springframework.graphql.web.WebInterceptor;
import org.springframework.graphql.web.WebSocketInterceptor;
@@ -51,7 +50,7 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit tests for {@link GraphQlWebSocketHandler}.
*/
public class GraphQlWebSocketHandlerTests {
public class GraphQlWebSocketHandlerTests extends WebSocketHandlerTestSupport {
private static final HttpMessageConverter<?> converter = new MappingJackson2HttpMessageConverter();
@@ -63,12 +62,12 @@ public class GraphQlWebSocketHandlerTests {
void query() throws Exception {
handle(this.handler,
new TextMessage("{\"type\":\"connection_init\"}"),
new TextMessage(BookTestUtils.BOOK_QUERY));
new TextMessage(BOOK_QUERY));
StepVerifier.create(this.session.getOutput())
.consumeNextWith((message) -> assertMessageType(message, "connection_ack"))
.consumeNextWith((message) -> assertThat(decode(message)).hasSize(3)
.containsEntry("id", BookTestUtils.SUBSCRIPTION_ID).containsEntry("type", "next")
.containsEntry("id", SUBSCRIPTION_ID).containsEntry("type", "next")
.extractingByKey("payload", as(InstanceOfAssertFactories.map(String.class, Object.class)))
.extractingByKey("data", as(InstanceOfAssertFactories.map(String.class, Object.class)))
.extractingByKey("bookById", as(InstanceOfAssertFactories.map(String.class, Object.class)))
@@ -80,12 +79,11 @@ public class GraphQlWebSocketHandlerTests {
@Test
void subscription() throws Exception {
handle(this.handler, new TextMessage("{\"type\":\"connection_init\"}"),
new TextMessage(BookTestUtils.BOOK_SUBSCRIPTION));
handle(this.handler, new TextMessage("{\"type\":\"connection_init\"}"), new TextMessage(BOOK_SUBSCRIPTION));
BiConsumer<WebSocketMessage<?>, String> bookPayloadAssertion = (message, bookId) ->
assertThat(decode(message))
.hasSize(3).containsEntry("id", BookTestUtils.SUBSCRIPTION_ID).containsEntry("type", "next")
.hasSize(3).containsEntry("id", SUBSCRIPTION_ID).containsEntry("type", "next")
.extractingByKey("payload", as(InstanceOfAssertFactories.map(String.class, Object.class)))
.extractingByKey("data", as(InstanceOfAssertFactories.map(String.class, Object.class)))
.extractingByKey("bookSearch", as(InstanceOfAssertFactories.map(String.class, Object.class)))
@@ -104,7 +102,7 @@ public class GraphQlWebSocketHandlerTests {
void unauthorizedWithoutMessageType() throws Exception {
handle(this.handler,
new TextMessage("{\"type\":\"connection_init\"}"),
new TextMessage("{\"id\":\"" + BookTestUtils.SUBSCRIPTION_ID + "\"}"));
new TextMessage("{\"id\":\"" + SUBSCRIPTION_ID + "\"}"));
// No message type
StepVerifier.create(this.session.getOutput())
@@ -173,7 +171,7 @@ public class GraphQlWebSocketHandlerTests {
@Test
void unauthorizedWithoutConnectionInit() throws Exception {
handle(this.handler, new TextMessage(BookTestUtils.BOOK_SUBSCRIPTION));
handle(this.handler, new TextMessage(BOOK_SUBSCRIPTION));
StepVerifier.create(this.session.getOutput()).verifyComplete();
assertThat(this.session.getCloseStatus()).isEqualTo(new CloseStatus(4401, "Unauthorized"));
@@ -194,9 +192,7 @@ public class GraphQlWebSocketHandlerTests {
@Test
void connectionInitTimeout() {
GraphQlWebSocketHandler handler = new GraphQlWebSocketHandler(
BookTestUtils.initWebGraphQlHandler(), converter, Duration.ofMillis(50));
GraphQlWebSocketHandler handler = new GraphQlWebSocketHandler(initHandler(), converter, Duration.ofMillis(50));
handler.afterConnectionEstablished(this.session);
StepVerifier.create(this.session.closeStatus())
@@ -208,15 +204,15 @@ public class GraphQlWebSocketHandlerTests {
void subscriptionExists() throws Exception {
handle(initWebSocketHandler(new ConsumeOneAndNeverCompleteInterceptor()),
new TextMessage("{\"type\":\"connection_init\"}"),
new TextMessage(BookTestUtils.BOOK_SUBSCRIPTION),
new TextMessage(BookTestUtils.BOOK_SUBSCRIPTION));
new TextMessage(BOOK_SUBSCRIPTION),
new TextMessage(BOOK_SUBSCRIPTION));
// Collect messages until session closed
List<Map<String, Object>> messages = new ArrayList<>();
this.session.getOutput().subscribe((message) -> messages.add(decode(message)));
StepVerifier.create(this.session.closeStatus())
.expectNext(new CloseStatus(4409, "Subscriber for " + BookTestUtils.SUBSCRIPTION_ID + " already exists"))
.expectNext(new CloseStatus(4409, "Subscriber for " + SUBSCRIPTION_ID + " already exists"))
.verifyComplete();
assertThat(messages.size()).isEqualTo(2);
@@ -230,9 +226,9 @@ public class GraphQlWebSocketHandlerTests {
handle(handler,
new TextMessage("{\"type\":\"connection_init\"}"),
new TextMessage(BookTestUtils.BOOK_SUBSCRIPTION));
new TextMessage(BOOK_SUBSCRIPTION));
String completeMessage = "{\"id\":\"" + BookTestUtils.SUBSCRIPTION_ID + "\",\"type\":\"complete\"}";
String completeMessage = "{\"id\":\"" + SUBSCRIPTION_ID + "\",\"type\":\"complete\"}";
Consumer<String> messageSender = (body) -> {
try {
handler.handleTextMessage(this.session, new TextMessage(body));
@@ -247,7 +243,7 @@ public class GraphQlWebSocketHandlerTests {
.consumeNextWith((message) -> assertMessageType(message, "next"))
.then(() -> messageSender.accept(completeMessage))
.as("Second subscription with same id is possible only if the first was properly removed")
.then(() -> messageSender.accept(BookTestUtils.BOOK_SUBSCRIPTION))
.then(() -> messageSender.accept(BOOK_SUBSCRIPTION))
.consumeNextWith((message) -> assertMessageType(message, "next"))
.then(() -> messageSender.accept(completeMessage))
.verifyTimeout(Duration.ofMillis(500));
@@ -262,8 +258,7 @@ public class GraphQlWebSocketHandlerTests {
private GraphQlWebSocketHandler initWebSocketHandler(WebInterceptor... interceptors) {
try {
return new GraphQlWebSocketHandler(
BookTestUtils.initWebGraphQlHandler(interceptors), converter, Duration.ofSeconds(60));
return new GraphQlWebSocketHandler(initHandler(interceptors), converter, Duration.ofSeconds(60));
}
catch (Exception ex) {
throw new IllegalStateException(ex);
@@ -274,7 +269,7 @@ public class GraphQlWebSocketHandlerTests {
Map<String, Object> map = decode(message, Map.class);
assertThat(map).containsEntry("type", messageType);
if (!messageType.equals("connection_ack")) {
assertThat(map).containsEntry("id", BookTestUtils.SUBSCRIPTION_ID);
assertThat(map).containsEntry("id", SUBSCRIPTION_ID);
}
}