Refactoring in spring-graphql tests

Add GraphQlResponse wrapper around ExecutionResult to provide access to
data with JSONPath and type conversion, along with convenience methods
to verify errors.
This commit is contained in:
Rossen Stoyanchev
2021-11-05 11:20:15 +00:00
parent 3318e81885
commit 2da6bd3824
12 changed files with 413 additions and 222 deletions

View File

@@ -35,6 +35,7 @@ dependencies {
testImplementation 'com.querydsl:querydsl-core'
testImplementation 'com.querydsl:querydsl-collections'
testImplementation 'javax.servlet:javax.servlet-api'
testImplementation 'com.jayway.jsonpath:json-path'
testImplementation 'com.fasterxml.jackson.core:jackson-databind'
testRuntimeOnly 'org.apache.logging.log4j:log4j-core'

View File

@@ -0,0 +1,204 @@
/*
* 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.lang.reflect.Type;
import java.util.List;
import java.util.Map;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.TypeRef;
import com.jayway.jsonpath.spi.json.JacksonJsonProvider;
import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
import graphql.ExecutionResult;
import graphql.GraphQLError;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ResolvableType;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Wrap an {@link ExecutionResult} for testing purposes. Provide data access
* methods with JSONPath and type conversion, transparently check for no errors,
* and provide convenience methods to verify errors.
*
* @author Rossen Stoyanchev
*/
public class GraphQlResponse {
private static final Log logger = LogFactory.getLog(GraphQlResponse.class);
private final DocumentContext documentContext;
private final List<GraphQLError> errors;
private boolean errorsChecked;
private GraphQlResponse(ExecutionResult result) {
this.documentContext = JsonPath.parse(result.toSpecification(), initJsonPathConfig());
this.errors = result.getErrors();
}
private static Configuration initJsonPathConfig() {
return Configuration.builder()
.jsonProvider(new JacksonJsonProvider())
.mappingProvider(new JacksonMappingProvider())
.build();
}
public GraphQlResponse log() {
logger.debug("GraphQlResponse: " + this.documentContext.jsonString());
return this;
}
public <T> T toEntity(String path, Class<T> targetClass) {
assertNoErrors();
return this.documentContext.read(jsonPath(path), targetClass);
}
public <T> T toEntity(String path, ParameterizedTypeReference<T> targetType) {
assertNoErrors();
return this.documentContext.read(jsonPath(path), new TypeRefAdapter<>(targetType));
}
public <T> List<T> toList(String path, Class<T> elementClass) {
assertNoErrors();
return this.documentContext.read(jsonPath(path), new TypeRefAdapter<>(List.class, elementClass));
}
public <T> List<T> toList(String path, ParameterizedTypeReference<T> elementType) {
assertNoErrors();
return this.documentContext.read(jsonPath(path), new TypeRefAdapter<>(List.class, elementType));
}
@Nullable
public <T> T rawValue(String path) {
assertNoErrors();
return this.documentContext.read(jsonPath(path));
}
private void assertNoErrors() {
if (!this.errorsChecked) {
assertThat(this.errors).as("Errors present in GraphQL response").isEmpty();
this.errorsChecked = true;
}
}
private static JsonPath jsonPath(String path) {
if (!StringUtils.hasText(path)) {
path = "$.data";
}
else if (!path.startsWith("$") && !path.startsWith("data.")) {
path = "$.data." + path;
}
return JsonPath.compile(path);
}
public int errorCount() {
this.errorsChecked = true;
return this.errors.size();
}
public Error error(int index) {
this.errorsChecked = true;
return new Error(index);
}
public static GraphQlResponse from(ExecutionResult result) {
return new GraphQlResponse(result);
}
public static GraphQlResponse from(Mono<? extends ExecutionResult> resultMono) {
ExecutionResult result = resultMono.block();
assertThat(result).isNotNull();
return from(result);
}
public static Flux<GraphQlResponse> forSubscription(ExecutionResult result) {
assertThat(result.getErrors()).as("Errors present in GraphQL response").isEmpty();
Publisher<ExecutionResult> publisher = result.getData();
return Flux.from(publisher).map(GraphQlResponse::from);
}
@SuppressWarnings("BlockingMethodInNonBlockingContext")
public static Flux<GraphQlResponse> forSubscription(Mono<ExecutionResult> resultMono) {
ExecutionResult result = resultMono.block();
assertThat(result).isNotNull();
return forSubscription(result);
}
public class Error {
private final int index;
public Error(int index) {
this.index = index;
}
public String message() {
return GraphQlResponse.this.errors.get(index).getMessage();
}
public String errorType() {
return GraphQlResponse.this.errors.get(index).getErrorType().toString();
}
public Map<String, Object> extensions() {
return GraphQlResponse.this.errors.get(index).getExtensions();
}
}
private static final class TypeRefAdapter<T> extends TypeRef<T> {
private final Type type;
TypeRefAdapter(ParameterizedTypeReference<T> typeReference) {
this.type = typeReference.getType();
}
TypeRefAdapter(Class<?> clazz, Class<?> generic) {
this.type = ResolvableType.forClassWithGenerics(clazz, generic).getType();
}
TypeRefAdapter(Class<?> clazz, ParameterizedTypeReference<?> generic) {
this.type = ResolvableType.forClassWithGenerics(clazz, ResolvableType.forType(generic)).getType();
}
@Override
public Type getType() {
return this.type;
}
}
}

View File

@@ -17,18 +17,13 @@
package org.springframework.graphql;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import graphql.ExecutionResult;
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;
import org.springframework.lang.Nullable;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Utility methods for GraphQL tests.
@@ -36,7 +31,7 @@ import static org.assertj.core.api.Assertions.assertThat;
public abstract class GraphQlTestUtils {
/**
* Convenience method for a {@link GraphQlSource.Builder} with a single {@link DataFetcher}.
* 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).
@@ -53,8 +48,9 @@ public abstract class GraphQlTestUtils {
}
/**
* Convenience method for a {@link GraphQlSource.Builder} when multiple
* {@link DataFetcher} registrations might be needed.
* 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
@@ -71,18 +67,4 @@ public abstract class GraphQlTestUtils {
.configureRuntimeWiring(configurer);
}
@SuppressWarnings("unchecked")
public static <T> T getData(@Nullable ExecutionResult result, String key) {
Map<String, Object> map = getData(result);
return (T) map.get(key);
}
public static <T> T getData(@Nullable ExecutionResult result) {
assertThat(result).isNotNull();
assertThat(result.getErrors()).as("Errors present in GraphQL response").isEmpty();
T data = result.getData();
assertThat(data).isNotNull();
return data;
}
}

View File

@@ -18,6 +18,7 @@ package org.springframework.graphql.data.method.annotation.support;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -26,6 +27,8 @@ import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import graphql.ExecutionResult;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
@@ -35,6 +38,7 @@ import reactor.core.publisher.Mono;
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.RequestInput;
@@ -44,6 +48,7 @@ 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;
import static org.assertj.core.api.Assertions.assertThat;
@@ -55,7 +60,7 @@ import static org.junit.jupiter.params.provider.Arguments.arguments;
*
* @author Rossen Stoyanchev
*/
@SuppressWarnings({"unchecked", "unused"})
@SuppressWarnings("unused")
public class BatchMappingInvocationTests {
private static final Map<Long, Course> courseMap = new HashMap<>();
@@ -107,30 +112,31 @@ public class BatchMappingInvocationTests {
void oneToOne(Class<?> controllerClass) {
String query = "{ " +
" courses { " +
" id" +
" name" +
" instructor {" +
" id" +
" firstName" +
" lastName" +
" }" +
" }" +
"}";
ExecutionResult result = initGraphQlService(controllerClass, CourseConfig.class)
.execute(new RequestInput(query, null, null, null))
.block();
Mono<ExecutionResult> resultMono = graphQlService(controllerClass, CourseConfig.class)
.execute(new RequestInput(query, null, null, null));
List<Map<String, Object>> actualCourses = GraphQlTestUtils.getData(result, "courses");
List<Course> actualCourses = GraphQlResponse.from(resultMono).toList("courses", Course.class);
List<Course> courses = Course.allCourses();
assertThat(actualCourses).hasSize(courses.size());
for (int i = 0; i < courses.size(); i++) {
Map<String, Object> actualCourse = actualCourses.get(i);
Course actualCourse = actualCourses.get(i);
Course course = courses.get(i);
assertThat(actualCourse.get("name")).isEqualTo(course.name());
assertThat(actualCourse).isEqualTo(course);
Map<String, Object> actualInstructor = (Map<String, Object>) actualCourse.get("instructor");
assertThat(actualInstructor.get("firstName")).isEqualTo(course.instructor().firstName());
assertThat(actualInstructor.get("lastName")).isEqualTo(course.instructor().lastName());
Person actualInstructor = actualCourse.instructor();
assertThat(actualInstructor.firstName()).isEqualTo(course.instructor().firstName());
assertThat(actualInstructor.lastName()).isEqualTo(course.instructor().lastName());
}
}
@@ -139,40 +145,40 @@ public class BatchMappingInvocationTests {
void oneToMany(Class<?> controllerClass) {
String query = "{ " +
" courses { " +
" id" +
" name" +
" students {" +
" id" +
" firstName" +
" lastName" +
" }" +
" }" +
"}";
ExecutionResult result = initGraphQlService(controllerClass, CourseConfig.class)
.execute(new RequestInput(query, null, null, null))
.block();
List<Map<String, Object>> actualCourses = GraphQlTestUtils.getData(result, "courses");
Mono<ExecutionResult> resultMono = graphQlService(controllerClass, CourseConfig.class)
.execute(new RequestInput(query, null, null, null));
List<Course> actualCourses = GraphQlResponse.from(resultMono).toList("courses", Course.class);
List<Course> courses = Course.allCourses();
assertThat(actualCourses).hasSize(courses.size());
for (int i = 0; i < courses.size(); i++) {
Map<String, Object> actualCourse = actualCourses.get(i);
Course actualCourse = actualCourses.get(i);
Course course = courses.get(i);
assertThat(actualCourse.get("name")).isEqualTo(course.name());
assertThat(actualCourse.name()).isEqualTo(course.name());
List<Map<String, Object>> actualStudents = (List<Map<String, Object>>) actualCourse.get("students");
List<Person> actualStudents = actualCourse.students();
List<Person> students = course.students();
assertThat(actualStudents).hasSize(students.size());
for (int j = 0; j < actualStudents.size(); j++) {
assertThat(actualStudents.get(i).get("firstName")).isEqualTo(students.get(i).firstName());
assertThat(actualStudents.get(i).get("lastName")).isEqualTo(students.get(i).lastName());
assertThat(actualStudents.get(i).firstName()).isEqualTo(students.get(i).firstName());
assertThat(actualStudents.get(i).lastName()).isEqualTo(students.get(i).lastName());
}
}
}
private ExecutionGraphQlService initGraphQlService(Class<?>... configClasses) {
private ExecutionGraphQlService graphQlService(Class<?>... configClasses) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.register(configClasses);
applicationContext.refresh();
@@ -280,6 +286,20 @@ public class BatchMappingInvocationTests {
private final List<Long> studentIds;
@JsonCreator
public Course(
@JsonProperty("id") Long id, @JsonProperty("name") String name,
@JsonProperty("instructor") @Nullable Person instructor,
@JsonProperty("students") @Nullable List<Person> students) {
this.id = id;
this.name = name;
this.instructorId = (instructor != null ? instructor.id() : -1);
this.studentIds = (students != null ?
students.stream().map(Person::id).collect(Collectors.toList()) :
Collections.emptyList());
}
public Course(Long id, String name, Long instructorId, List<Long> studentIds) {
this.id = id;
this.name = name;
@@ -343,7 +363,12 @@ public class BatchMappingInvocationTests {
private final String lastName;
public Person(Long id, String firstName, String lastName) {
@JsonCreator
public Person(
@JsonProperty("id") Long id,
@JsonProperty("firstName") String firstName,
@JsonProperty("lastName") String lastName) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;

View File

@@ -16,7 +16,6 @@
package org.springframework.graphql.data.method.annotation.support;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
@@ -25,8 +24,8 @@ import graphql.GraphQLContext;
import graphql.schema.DataFetchingEnvironment;
import org.dataloader.DataLoader;
import org.junit.jupiter.api.Test;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
@@ -36,6 +35,7 @@ import org.springframework.graphql.Author;
import org.springframework.graphql.Book;
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.RequestInput;
@@ -72,17 +72,15 @@ public class SchemaMappingInvocationTests {
" }" +
"}";
ExecutionResult result = initGraphQlService()
.execute(new RequestInput(query, null, null, null))
.block();
Mono<ExecutionResult> resultMono = graphQlService().execute(new RequestInput(query, null, null, null));
Map<String, Object> book = GraphQlTestUtils.getData(result, "bookById");
assertThat(book.get("id")).isEqualTo("1");
assertThat(book.get("name")).isEqualTo("Nineteen Eighty-Four");
Book book = GraphQlResponse.from(resultMono).toEntity("bookById", Book.class);
assertThat(book.getId()).isEqualTo(1);
assertThat(book.getName()).isEqualTo("Nineteen Eighty-Four");
Map<String, Object> author = (Map<String, Object>) book.get("author");
assertThat(author.get("firstName")).isEqualTo("George");
assertThat(author.get("lastName")).isEqualTo("Orwell");
Author author = book.getAuthor();
assertThat(author.getFirstName()).isEqualTo("George");
assertThat(author.getLastName()).isEqualTo("Orwell");
}
@Test
@@ -94,15 +92,12 @@ public class SchemaMappingInvocationTests {
" }" +
"}";
ExecutionResult result = initGraphQlService()
.execute(new RequestInput(query, null, null, null))
.block();
List<Map<String, Object>> bookList = GraphQlTestUtils.getData(result, "booksByCriteria");
Mono<ExecutionResult> resultMono = graphQlService().execute(new RequestInput(query, null, null, null));
List<Book> bookList = GraphQlResponse.from(resultMono).toList("booksByCriteria", Book.class);
assertThat(bookList).hasSize(2);
assertThat(bookList.get(0).get("name")).isEqualTo("Nineteen Eighty-Four");
assertThat(bookList.get(1).get("name")).isEqualTo("Animal Farm");
assertThat(bookList.get(0).getName()).isEqualTo("Nineteen Eighty-Four");
assertThat(bookList.get(1).getName()).isEqualTo("Animal Farm");
}
@Test
@@ -122,15 +117,12 @@ public class SchemaMappingInvocationTests {
return executionInput;
});
ExecutionResult result = initGraphQlService()
.execute(requestInput)
.block();
Mono<ExecutionResult> resultMono = graphQlService().execute(requestInput);
Map<String, Object> author = GraphQlTestUtils.getData(result, "authorById");
assertThat(author.get("id")).isEqualTo("101");
assertThat(author.get("firstName")).isEqualTo("George");
assertThat(author.get("lastName")).isEqualTo("Orwell");
Author author = GraphQlResponse.from(resultMono).toEntity("authorById", Author.class);
assertThat(author.getId()).isEqualTo(101);
assertThat(author.getFirstName()).isEqualTo("George");
assertThat(author.getLastName()).isEqualTo("Orwell");
assertThat(contextRef.get().<String>get("key")).isEqualTo("value");
}
@@ -145,14 +137,13 @@ public class SchemaMappingInvocationTests {
" }" +
"}";
ExecutionResult result = initGraphQlService()
.execute(new RequestInput(operation, null, null, null))
.block();
Mono<ExecutionResult> resultMono = graphQlService()
.execute(new RequestInput(operation, null, null, null));
Map<String, Object> author = GraphQlTestUtils.getData(result, "addAuthor");
assertThat(author.get("id")).isEqualTo("99");
assertThat(author.get("firstName")).isEqualTo("James");
assertThat(author.get("lastName")).isEqualTo("Joyce");
Author author = GraphQlResponse.from(resultMono).toEntity("addAuthor", Author.class);
assertThat(author.getId()).isEqualTo(99);
assertThat(author.getFirstName()).isEqualTo("James");
assertThat(author.getLastName()).isEqualTo("Joyce");
}
@Test
@@ -164,31 +155,26 @@ public class SchemaMappingInvocationTests {
" }" +
"}";
ExecutionResult result = initGraphQlService()
.execute(new RequestInput(operation, null, null, null))
.block();
Mono<ExecutionResult> resultMono = graphQlService()
.execute(new RequestInput(operation, null, null, null));
Publisher<ExecutionResult> publisher = GraphQlTestUtils.getData(result);
Flux<Map<String, Object>> bookFlux = Flux.from(publisher).map(executionResult -> {
Map<String, Object> map = executionResult.getData();
return (Map<String, Object>) map.get("bookSearch");
});
Flux<Book> bookFlux = GraphQlResponse.forSubscription(resultMono)
.map(response -> response.toEntity("bookSearch", Book.class));
StepVerifier.create(bookFlux)
.consumeNextWith(book -> {
assertThat(book.get("id")).isEqualTo("1");
assertThat(book.get("name")).isEqualTo("Nineteen Eighty-Four");
assertThat(book.getId()).isEqualTo(1);
assertThat(book.getName()).isEqualTo("Nineteen Eighty-Four");
})
.consumeNextWith(book -> {
assertThat(book.get("id")).isEqualTo("5");
assertThat(book.get("name")).isEqualTo("Animal Farm");
assertThat(book.getId()).isEqualTo(5);
assertThat(book.getName()).isEqualTo("Animal Farm");
})
.verifyComplete();
}
private ExecutionGraphQlService initGraphQlService() {
private ExecutionGraphQlService graphQlService() {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.register(TestConfig.class);
applicationContext.refresh();

View File

@@ -20,9 +20,9 @@ import java.net.URI;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import com.querydsl.core.types.Predicate;
import graphql.schema.DataFetcher;
@@ -43,6 +43,7 @@ import org.springframework.data.repository.CrudRepository;
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.data.GraphQlRepository;
import org.springframework.graphql.execution.ExecutionGraphQlService;
@@ -77,10 +78,10 @@ class QuerydslDataFetcherTests {
mockRepository.save(book);
Consumer<WebGraphQlHandler> tester = (handler) -> {
WebOutput output = handler.handleRequest(input("{ bookById(id: 42) {name}}")).block();
Mono<WebOutput> outputMono = handler.handleRequest(input("{ bookById(id: 42) {name}}"));
Map<String, Object> map = GraphQlTestUtils.getData(output, "bookById");
assertThat(map).hasSize(1).containsEntry("name", book.getName());
Book actualBook = GraphQlResponse.from(outputMono).toEntity("bookById", Book.class);
assertThat(actualBook.getName()).isEqualTo(book.getName());
};
// explicit wiring
@@ -97,12 +98,12 @@ class QuerydslDataFetcherTests {
mockRepository.saveAll(Arrays.asList(book1, book2));
Consumer<WebGraphQlHandler> tester = (handler) -> {
WebOutput output = handler.handleRequest(input("{ books {name}}")).block();
Mono<WebOutput> outputMono = handler.handleRequest(input("{ books {name}}"));
List<Map<String, Object>> data = GraphQlTestUtils.getData(output, "books");
assertThat(data).containsExactlyInAnyOrder(
Collections.singletonMap("name", "Breaking Bad"),
Collections.singletonMap("name", "Hitchhiker's Guide to the Galaxy"));
List<String> names = GraphQlResponse.from(outputMono).toList("books", Book.class)
.stream().map(Book::getName).collect(Collectors.toList());
assertThat(names).containsExactlyInAnyOrder(book1.getName(), book2.getName());
};
// explicit wiring
@@ -120,20 +121,20 @@ class QuerydslDataFetcherTests {
// 1) Automatic registration only
WebGraphQlHandler handler = initHandler(builder -> {}, mockRepository, null);
WebOutput output = handler.handleRequest(input("{ bookById(id: 1) {name}}")).block();
Mono<WebOutput> outputMono = handler.handleRequest(input("{ bookById(id: 1) {name}}"));
Map<String, Object> map = GraphQlTestUtils.getData(output, "bookById");
assertThat(map).hasSize(1).containsEntry("name", "Hitchhiker's Guide to the Galaxy");
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);
output = handler.handleRequest(input("{ bookById(id: 1) {name}}")).block();
outputMono = handler.handleRequest(input("{ bookById(id: 1) {name}}"));
map = GraphQlTestUtils.getData(output, "bookById");
assertThat(map).hasSize(1).containsEntry("name", "Breaking Bad");
actualBook = GraphQlResponse.from(outputMono).toEntity("bookById", Book.class);
assertThat(actualBook.getName()).isEqualTo("Breaking Bad");
}
@Test
@@ -144,10 +145,10 @@ class QuerydslDataFetcherTests {
WebGraphQlHandler handler = initHandler("bookById",
QuerydslDataFetcher.builder(mockRepository).projectAs(BookProjection.class).single());
WebOutput output = handler.handleRequest(input("{ bookById(id: 42) {name}}")).block();
Mono<WebOutput> outputMono = handler.handleRequest(input("{ bookById(id: 42) {name}}"));
Map<String, Object> map = GraphQlTestUtils.getData(output, "bookById");
assertThat(map).hasSize(1).containsEntry("name", "Hitchhiker's Guide to the Galaxy by Douglas Adams");
Book actualBook = GraphQlResponse.from(outputMono).toEntity("bookById", Book.class);
assertThat(actualBook.getName()).isEqualTo("Hitchhiker's Guide to the Galaxy by Douglas Adams");
}
@Test
@@ -158,10 +159,10 @@ class QuerydslDataFetcherTests {
WebGraphQlHandler handler = initHandler("bookById",
QuerydslDataFetcher.builder(mockRepository).projectAs(BookDto.class).single());
WebOutput output = handler.handleRequest(input("{ bookById(id: 42) {name}}")).block();
Mono<WebOutput> outputMono = handler.handleRequest(input("{ bookById(id: 42) {name}}"));
Map<String, Object> map = GraphQlTestUtils.getData(output, "bookById");
assertThat(map).hasSize(1).containsEntry("name", "The book is: Hitchhiker's Guide to the Galaxy");
Book actualBook = GraphQlResponse.from(outputMono).toEntity("bookById", Book.class);
assertThat(actualBook.getName()).isEqualTo("The book is: Hitchhiker's Guide to the Galaxy");
}
@Test
@@ -190,10 +191,10 @@ class QuerydslDataFetcherTests {
when(mockRepository.findBy(any(), any())).thenReturn(Mono.just(book));
Consumer<WebGraphQlHandler> tester = (handler) -> {
WebOutput output = handler.handleRequest(input("{ bookById(id: 1) {name}}")).block();
Mono<WebOutput> outputMono = handler.handleRequest(input("{ bookById(id: 1) {name}}"));
Map<String, Object> map = GraphQlTestUtils.getData(output, "bookById");
assertThat(map).hasSize(1).containsEntry("name", book.getName());
Book actualBook = GraphQlResponse.from(outputMono).toEntity("bookById", Book.class);
assertThat(actualBook.getName()).isEqualTo(book.getName());
};
// explicit wiring
@@ -211,12 +212,12 @@ class QuerydslDataFetcherTests {
when(mockRepository.findBy(any(), any())).thenReturn(Flux.just(book1, book2));
Consumer<WebGraphQlHandler> tester = (handler) -> {
WebOutput output = handler.handleRequest(input("{ books {name}}")).block();
Mono<WebOutput> outputMono = handler.handleRequest(input("{ books {name}}"));
List<Map<String, Object>> data = GraphQlTestUtils.getData(output, "books");
assertThat(data).containsExactlyInAnyOrder(
Collections.singletonMap("name", "Breaking Bad"),
Collections.singletonMap("name", "Hitchhiker's Guide to the Galaxy"));
List<String> names = GraphQlResponse.from(outputMono).toList("books", Book.class)
.stream().map(Book::getName).collect(Collectors.toList());
assertThat(names).containsExactlyInAnyOrder("Breaking Bad", "Hitchhiker's Guide to the Galaxy");
};
// explicit wiring

View File

@@ -23,10 +23,12 @@ import graphql.ExecutionResult;
import org.dataloader.DataLoader;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
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.RequestInput;
@@ -72,18 +74,15 @@ public class BatchLoadingTests {
}));
});
ExecutionResult result = service.execute(new RequestInput(query, null, null, null)).block();
Mono<ExecutionResult> resultMono = service.execute(new RequestInput(query, null, null, null));
assertThat(result.getErrors()).isEmpty();
Map<String, Object> data = result.getData();
assertThat(data).isNotNull();
List<Book> books = GraphQlResponse.from(resultMono).toList("booksByCriteria", Book.class);
assertThat(books).hasSize(2);
List<Map<String, Object>> bookList = getValue(data, "booksByCriteria");
assertThat(bookList).hasSize(2);
Map<String, Object> authorMap = (Map<String, Object>) bookList.get(0).get("author");
assertThat(authorMap).isNotNull();
assertThat(authorMap).containsEntry("firstName", "George");
assertThat(authorMap).containsEntry("lastName", "Orwell");
Author author = books.get(0).getAuthor();
assertThat(author).isNotNull();
assertThat(author.getFirstName()).isEqualTo("George");
assertThat(author.getLastName()).isEqualTo("Orwell");
}
private ExecutionGraphQlService initExecutionGraphQlService(RuntimeWiringConfigurer configurer) {
@@ -93,9 +92,4 @@ public class BatchLoadingTests {
return service;
}
@SuppressWarnings("unchecked")
private <T> T getValue(Map<String, Object> data, String key) {
return (T) data.get(key);
}
}

View File

@@ -17,11 +17,13 @@ package org.springframework.graphql.execution;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import graphql.ExecutionResult;
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.RequestInput;
@@ -83,22 +85,19 @@ public class ClassNameTypeResolverTests {
" }" +
"}";
ExecutionResult result = new ExecutionGraphQlService(graphQlSource)
.execute(new RequestInput(query, null, null, null))
.block();
List<Map<String, Object>> actualAnimals = GraphQlTestUtils.getData(result, "animals");
Mono<ExecutionResult> resultMono = new ExecutionGraphQlService(graphQlSource)
.execute(new RequestInput(query, null, null, null));
GraphQlResponse response = GraphQlResponse.from(resultMono);
for (int i = 0; i < animalList.size(); i++) {
Map<String, Object> actualAnimal = actualAnimals.get(i);
Animal animal = animalList.get(i);
assertThat(actualAnimal.get("name")).isEqualTo(animal.getName());
if (animal instanceof Bird) {
assertThat(actualAnimal.get("flightless")).isEqualTo(((Bird) animal).isFlightless());
Bird bird = (Bird) response.toEntity("animals[" + i + "]", animal.getClass());
assertThat(bird.isFlightless()).isEqualTo(((Bird) animal).isFlightless());
}
else if (animal instanceof Mammal) {
assertThat(actualAnimal.get("herbivore")).isEqualTo(((Mammal) animal).isHerbivore());
Mammal mammal = (Mammal) response.toEntity("animals[" + i + "]", animal.getClass());
assertThat(mammal.isHerbivore()).isEqualTo(((Mammal) animal).isHerbivore());
}
else {
throw new IllegalStateException();
@@ -133,21 +132,19 @@ public class ClassNameTypeResolverTests {
" }" +
"}";
ExecutionResult result = new ExecutionGraphQlService(graphQlSource)
.execute(new RequestInput(query, null, null, null))
.block();
List<Map<String, Object>> actualSightings = GraphQlTestUtils.getData(result, "sightings");
Mono<ExecutionResult> resultMono = new ExecutionGraphQlService(graphQlSource)
.execute(new RequestInput(query, null, null, null));
GraphQlResponse response = GraphQlResponse.from(resultMono);
for (int i = 0; i < animalAndPlantList.size(); i++) {
Map<String, Object> actualSighting = actualSightings.get(i);
Object sighting = animalAndPlantList.get(i);
if (sighting instanceof Animal) {
assertThat(actualSighting.get("name")).isEqualTo(((Animal) sighting).getName());
Animal animal = (Animal) response.toEntity("sightings[" + i + "]", sighting.getClass());
assertThat(animal.getName()).isEqualTo(((Animal) sighting).getName());
}
else if (sighting instanceof Tree) {
assertThat(actualSighting.get("family")).isEqualTo(((Tree) sighting).getFamily());
Tree tree = (Tree) response.toEntity("sightings[" + i + "]", sighting.getClass());
assertThat(tree.getFamily()).isEqualTo(((Tree) sighting).getFamily());
}
else {
throw new IllegalStateException();
@@ -227,6 +224,7 @@ public class ClassNameTypeResolverTests {
}
@JsonIgnoreProperties(ignoreUnknown = true)
static class Penguin extends BaseBird {
Penguin() {
@@ -236,6 +234,7 @@ public class ClassNameTypeResolverTests {
}
@JsonIgnoreProperties(ignoreUnknown = true)
static class Dog extends BaseMammal {
Dog() {
@@ -245,6 +244,7 @@ public class ClassNameTypeResolverTests {
}
@JsonIgnoreProperties(ignoreUnknown = true)
static class GrayWolf extends BaseMammal {
GrayWolf() {
@@ -268,6 +268,7 @@ public class ClassNameTypeResolverTests {
}
@JsonIgnoreProperties(ignoreUnknown = true)
static class GiantRedwood extends Tree {
GiantRedwood() {

View File

@@ -18,19 +18,19 @@ package org.springframework.graphql.execution;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import graphql.ExecutionInput;
import graphql.ExecutionResult;
import graphql.GraphQL;
import graphql.schema.DataFetcher;
import org.junit.jupiter.api.Test;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import reactor.util.context.Context;
import reactor.util.context.ContextView;
import org.springframework.graphql.GraphQlResponse;
import org.springframework.graphql.GraphQlTestUtils;
import org.springframework.graphql.TestThreadLocalAccessor;
@@ -54,9 +54,10 @@ public class ContextDataFetcherDecoratorTests {
ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build();
ReactorContextManager.setReactorContext(Context.of("name", "007"), input);
Map<String, Object> data = graphQl.executeAsync(input).get().getData();
ExecutionResult executionResult = graphQl.executeAsync(input).get();
assertThat(data).hasSize(1).containsEntry("greeting", "Hello 007");
String greeting = GraphQlResponse.from(executionResult).toEntity("greeting", String.class);
assertThat(greeting).isEqualTo("Hello 007");
}
@Test
@@ -75,7 +76,7 @@ public class ContextDataFetcherDecoratorTests {
ExecutionResult result = graphQl.executeAsync(input).get();
List<String> data = GraphQlTestUtils.getData(result, "greetings");
List<String> data = GraphQlResponse.from(result).toList("greetings", String.class);;
assertThat(data).containsExactly("Hi 007", "Bonjour 007", "Hola 007");
}
@@ -92,14 +93,14 @@ public class ContextDataFetcherDecoratorTests {
ExecutionInput input = ExecutionInput.newExecutionInput().query("subscription { greetings }").build();
ReactorContextManager.setReactorContext(Context.of("name", "007"), input);
Publisher<String> publisher = graphQl.executeAsync(input).get().getData();
ExecutionResult executionResult = graphQl.executeAsync(input).get();
List<String> actual = Flux.from(publisher).cast(ExecutionResult.class)
.map((result) -> GraphQlTestUtils.<String>getData(result, "greetings"))
.collectList()
.block();
Flux<String> greetingsFlux = GraphQlResponse.forSubscription(executionResult)
.map(response -> response.toEntity("greetings", String.class));
assertThat(actual).containsExactly("Hi 007", "Bonjour 007", "Hola 007");
StepVerifier.create(greetingsFlux)
.expectNext("Hi 007", "Bonjour 007", "Hola 007")
.verifyComplete();
}
@Test
@@ -116,12 +117,11 @@ public class ContextDataFetcherDecoratorTests {
ContextView view = ReactorContextManager.extractThreadLocalValues(accessor, Context.empty());
ReactorContextManager.setReactorContext(view, input);
ExecutionResult result = Mono.delay(Duration.ofMillis(10))
.flatMap((aLong) -> Mono.fromFuture(graphQl.executeAsync(input)))
.block();
Mono<ExecutionResult> resultMono = Mono.delay(Duration.ofMillis(10))
.flatMap((aLong) -> Mono.fromFuture(graphQl.executeAsync(input)));
Map<String, Object> data = GraphQlTestUtils.getData(result);
assertThat(data).hasSize(1).containsEntry("greeting", "Hello 007");
String greeting = GraphQlResponse.from(resultMono).toEntity("greeting", String.class);
assertThat(greeting).isEqualTo("Hello 007");
}
finally {
nameThreadLocal.remove();

View File

@@ -18,8 +18,6 @@ package org.springframework.graphql.execution;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import graphql.ExecutionInput;
@@ -33,6 +31,7 @@ import reactor.core.publisher.Mono;
import reactor.util.context.Context;
import reactor.util.context.ContextView;
import org.springframework.graphql.GraphQlResponse;
import org.springframework.graphql.GraphQlTestUtils;
import org.springframework.graphql.TestThreadLocalAccessor;
@@ -55,13 +54,13 @@ public class ExceptionResolversExceptionHandlerTests {
ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build();
ExecutionResult result = graphQl.executeAsync(input).get();
Map<String, Object> data = result.getData();
assertThat(data).hasSize(1).containsEntry("greeting", null);
GraphQlResponse response = GraphQlResponse.from(result);
assertThat(response.errorCount()).isEqualTo(1);
assertThat(response.error(0).message()).isEqualTo("Resolved error: Invalid greeting");
assertThat(response.error(0).errorType()).isEqualTo("BAD_REQUEST");
List<GraphQLError> errors = result.getErrors();
assertThat(errors).hasSize(1);
assertThat(errors.get(0).getMessage()).isEqualTo("Resolved error: Invalid greeting");
assertThat(errors.get(0).getErrorType().toString()).isEqualTo("BAD_REQUEST");
String greeting = response.rawValue("greeting");
assertThat(greeting).isNull();
}
@Test
@@ -77,8 +76,9 @@ public class ExceptionResolversExceptionHandlerTests {
ExecutionResult result = graphQl.executeAsync(input).get();
List<GraphQLError> errors = result.getErrors();
assertThat(errors.get(0).getMessage()).isEqualTo("Resolved error: Invalid greeting, name=007");
GraphQlResponse response = GraphQlResponse.from(result);
assertThat(response.errorCount()).isEqualTo(1);
assertThat(response.error(0).message()).isEqualTo("Resolved error: Invalid greeting, name=007");
}
@Test
@@ -97,12 +97,12 @@ public class ExceptionResolversExceptionHandlerTests {
ContextView view = ReactorContextManager.extractThreadLocalValues(accessor, Context.empty());
ReactorContextManager.setReactorContext(view, input);
ExecutionResult result = Mono.delay(Duration.ofMillis(10))
.flatMap((aLong) -> Mono.fromFuture(graphQl.executeAsync(input)))
.block();
Mono<ExecutionResult> result = Mono.delay(Duration.ofMillis(10))
.flatMap((aLong) -> Mono.fromFuture(graphQl.executeAsync(input)));
List<GraphQLError> errors = result.getErrors();
assertThat(errors.get(0).getMessage()).isEqualTo("Resolved error: Invalid greeting, name=007");
GraphQlResponse response = GraphQlResponse.from(result);
assertThat(response.errorCount()).isEqualTo(1);
assertThat(response.error(0).message()).isEqualTo("Resolved error: Invalid greeting, name=007");
}
finally {
nameThreadLocal.remove();
@@ -116,14 +116,13 @@ public class ExceptionResolversExceptionHandlerTests {
ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build();
ExecutionResult result = graphQl.executeAsync(input).get();
Map<String, Object> data = result.getData();
assertThat(data).hasSize(1).containsEntry("greeting", null);
GraphQlResponse response = GraphQlResponse.from(result);
assertThat(response.errorCount()).isEqualTo(1);
assertThat(response.error(0).message()).isEqualTo("Invalid greeting");
assertThat(response.error(0).errorType()).isEqualTo("INTERNAL_ERROR");
List<GraphQLError> errors = result.getErrors();
assertThat(errors).hasSize(1);
GraphQLError error = errors.get(0);
assertThat(error.getMessage()).isEqualTo("Invalid greeting");
assertThat(error.getErrorType().toString()).isEqualTo("INTERNAL_ERROR");
String greeting = response.rawValue("greeting");
assertThat(greeting).isNull();
}
@Test
@@ -133,9 +132,8 @@ public class ExceptionResolversExceptionHandlerTests {
ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build();
ExecutionResult result = graphQl.executeAsync(input).get();
Map<String, Object> data = result.getData();
assertThat(data).hasSize(1).containsEntry("greeting", null);
assertThat(result.getErrors()).hasSize(0);
String greeting = GraphQlResponse.from(result).rawValue("greeting");
assertThat(greeting).isNull();
}
private static GraphQL initGraphQl(DataFetcherExceptionResolver exceptionResolver) {

View File

@@ -20,16 +20,15 @@ import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import static org.assertj.core.api.Assertions.assertThat;
public class ConsumeOneAndNeverCompleteInterceptor implements WebInterceptor {
@Override
public Mono<WebOutput> intercept(WebInput webInput, WebInterceptorChain chain) {
return chain.next(webInput).map((output) -> output.transform((builder) -> {
Publisher<?> publisher = output.getData();
assertThat(publisher).isNotNull();
builder.data(Flux.from(publisher).take(1).concatWith(Flux.never()));
return chain.next(webInput).map((output) -> output.transform(builder -> {
if (output.getData() instanceof Publisher) {
Flux<?> flux = Flux.from((Publisher<?>) output.getData()).take(1).concatWith(Flux.never());
builder.data(flux);
}
}));
}

View File

@@ -19,8 +19,6 @@ package org.springframework.graphql.web;
import java.net.URI;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import graphql.GraphQLError;
@@ -29,6 +27,7 @@ import graphql.schema.DataFetchingEnvironment;
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.TestThreadLocalAccessor;
@@ -47,8 +46,7 @@ import static org.assertj.core.api.Assertions.assertThat;
public class WebGraphQlHandlerTests {
private static final WebInput webInput = new WebInput(
URI.create("http://abc.org"), new HttpHeaders(), Collections.singletonMap("query", "{ greeting }"),
null, "1");
URI.create("https://abc.org"), new HttpHeaders(), Collections.singletonMap("query", "{ greeting }"), null, "1");
@Test
void reactorContextPropagation() {
@@ -62,11 +60,11 @@ public class WebGraphQlHandlerTests {
GraphQlService service = new ExecutionGraphQlService(graphQlSource);
WebGraphQlHandler handler = WebGraphQlHandler.builder(service).build();
WebOutput webOutput = handler.handleRequest(webInput)
.contextWrite((context) -> context.put("name", "007")).block();
Mono<WebOutput> outputMono = handler.handleRequest(webInput)
.contextWrite((context) -> context.put("name", "007"));
Map<String, Object> data = webOutput.getData();
assertThat(data).hasSize(1).containsEntry("greeting", "Hello 007");
String greeting = GraphQlResponse.from(outputMono).toEntity("greeting", String.class);
assertThat(greeting).isEqualTo("Hello 007");
}
@Test
@@ -88,15 +86,15 @@ public class WebGraphQlHandlerTests {
GraphQlService service = new ExecutionGraphQlService(graphQlSource);
WebGraphQlHandler handler = WebGraphQlHandler.builder(service).build();
WebOutput webOutput = handler.handleRequest(webInput)
.contextWrite((context) -> context.put("name", "007")).block();
Mono<WebOutput> outputMono = handler.handleRequest(webInput)
.contextWrite((context) -> context.put("name", "007"));
Map<String, Object> data = webOutput.getData();
assertThat(data).hasSize(1).containsEntry("greeting", null);
GraphQlResponse response = GraphQlResponse.from(outputMono);
assertThat(response.errorCount()).isEqualTo(1);
assertThat(response.error(0).message()).isEqualTo("Resolved error: Invalid greeting, name=007");
List<GraphQLError> errors = webOutput.getErrors();
assertThat(errors).hasSize(1);
assertThat(errors.get(0).getMessage()).isEqualTo("Resolved error: Invalid greeting, name=007");
String greeting = response.rawValue("greeting");
assertThat(greeting).isNull();
}
@Test
@@ -117,9 +115,10 @@ public class WebGraphQlHandlerTests {
.threadLocalAccessor(threadLocalAccessor)
.build();
Map<String, Object> data = handler.handleRequest(webInput).block().getData();
Mono<WebOutput> outputMono = handler.handleRequest(webInput);
assertThat(data).hasSize(1).containsEntry("greeting", "Hello 007");
String greeting = GraphQlResponse.from(outputMono).toEntity("greeting", String.class);
assertThat(greeting).isEqualTo("Hello 007");
}
finally {
nameThreadLocal.remove();
@@ -152,10 +151,11 @@ public class WebGraphQlHandlerTests {
.threadLocalAccessor(threadLocalAccessor)
.build();
WebOutput webOutput = handler.handleRequest(webInput).block();
Mono<WebOutput> outputMono = handler.handleRequest(webInput);
List<GraphQLError> errors = webOutput.getErrors();
assertThat(errors.get(0).getMessage()).isEqualTo("Resolved error: Invalid greeting, name=007");
GraphQlResponse response = GraphQlResponse.from(outputMono);
assertThat(response.errorCount()).isEqualTo(1);
assertThat(response.error(0).message()).isEqualTo("Resolved error: Invalid greeting, name=007");
}
finally {
nameThreadLocal.remove();