From 2693742b63bbe0d71c406a85b3f038a0c387a684 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Wed, 16 Sep 2020 16:34:00 +0200 Subject: [PATCH] Add GraphQL web support This commit removes the controller components and instead moves the web support to specific auto-configurations. This commit also adds tests for both web stacks. --- .../GraphQLWebFluxAutoConfiguration.java | 36 +++++++ .../graphql/reactive}/package-info.java | 2 +- .../servlet/GraphQLWebAutoConfiguration.java | 41 +++++++ .../graphql/servlet}/package-info.java | 2 +- .../components/GraphQLController.java | 86 --------------- .../GraphQLReactiveRequestBody.java | 33 ------ .../servlet/components/GraphQLController.java | 100 ------------------ .../components/GraphQLServletRequestBody.java | 33 ------ .../springframework/boot/graphql/Book.java | 54 ++++++++++ .../boot/graphql/GraphQLDataFetchers.java | 27 +++++ .../reactive/GraphQLWebFluxEndpointTests.java | 70 ++++++++++++ .../servlet/GraphQLWebEndpointTests.java | 65 ++++++++++++ .../src/test/resources/books/schema.graphqls | 10 ++ 13 files changed, 305 insertions(+), 254 deletions(-) create mode 100644 spring-graphql-web/src/main/java/org/springframework/boot/graphql/reactive/GraphQLWebFluxAutoConfiguration.java rename spring-graphql-web/src/main/java/org/springframework/{graphql/servlet/components => boot/graphql/reactive}/package-info.java (68%) create mode 100644 spring-graphql-web/src/main/java/org/springframework/boot/graphql/servlet/GraphQLWebAutoConfiguration.java rename spring-graphql-web/src/main/java/org/springframework/{graphql/reactive/components => boot/graphql/servlet}/package-info.java (67%) delete mode 100644 spring-graphql-web/src/main/java/org/springframework/graphql/reactive/components/GraphQLController.java delete mode 100644 spring-graphql-web/src/main/java/org/springframework/graphql/reactive/components/GraphQLReactiveRequestBody.java delete mode 100644 spring-graphql-web/src/main/java/org/springframework/graphql/servlet/components/GraphQLController.java delete mode 100644 spring-graphql-web/src/main/java/org/springframework/graphql/servlet/components/GraphQLServletRequestBody.java create mode 100644 spring-graphql-web/src/test/java/org/springframework/boot/graphql/Book.java create mode 100644 spring-graphql-web/src/test/java/org/springframework/boot/graphql/GraphQLDataFetchers.java create mode 100644 spring-graphql-web/src/test/java/org/springframework/boot/graphql/reactive/GraphQLWebFluxEndpointTests.java create mode 100644 spring-graphql-web/src/test/java/org/springframework/boot/graphql/servlet/GraphQLWebEndpointTests.java create mode 100644 spring-graphql-web/src/test/resources/books/schema.graphqls diff --git a/spring-graphql-web/src/main/java/org/springframework/boot/graphql/reactive/GraphQLWebFluxAutoConfiguration.java b/spring-graphql-web/src/main/java/org/springframework/boot/graphql/reactive/GraphQLWebFluxAutoConfiguration.java new file mode 100644 index 00000000..209a5f89 --- /dev/null +++ b/spring-graphql-web/src/main/java/org/springframework/boot/graphql/reactive/GraphQLWebFluxAutoConfiguration.java @@ -0,0 +1,36 @@ +package org.springframework.boot.graphql.reactive; + +import graphql.GraphQL; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.graphql.GraphQLAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.graphql.reactive.GraphQLHandler; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerResponse; + +@Configuration +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) +@ConditionalOnClass(GraphQL.class) +@ConditionalOnBean(GraphQL.Builder.class) +@AutoConfigureAfter(GraphQLAutoConfiguration.class) +public class GraphQLWebFluxAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public GraphQLHandler graphQLHandler(GraphQL.Builder graphQLBuilder) { + return new GraphQLHandler(graphQLBuilder); + } + + @Bean + public RouterFunction graphQLQueryEndpoint(GraphQLHandler handler) { + return RouterFunctions.route().POST("/graphql", handler::handle).build(); + } + +} diff --git a/spring-graphql-web/src/main/java/org/springframework/graphql/servlet/components/package-info.java b/spring-graphql-web/src/main/java/org/springframework/boot/graphql/reactive/package-info.java similarity index 68% rename from spring-graphql-web/src/main/java/org/springframework/graphql/servlet/components/package-info.java rename to spring-graphql-web/src/main/java/org/springframework/boot/graphql/reactive/package-info.java index 588911a7..b112b1d9 100644 --- a/spring-graphql-web/src/main/java/org/springframework/graphql/servlet/components/package-info.java +++ b/spring-graphql-web/src/main/java/org/springframework/boot/graphql/reactive/package-info.java @@ -1,6 +1,6 @@ @NonNullApi @NonNullFields -package org.springframework.graphql.servlet.components; +package org.springframework.boot.graphql.reactive; import org.springframework.lang.NonNullApi; import org.springframework.lang.NonNullFields; diff --git a/spring-graphql-web/src/main/java/org/springframework/boot/graphql/servlet/GraphQLWebAutoConfiguration.java b/spring-graphql-web/src/main/java/org/springframework/boot/graphql/servlet/GraphQLWebAutoConfiguration.java new file mode 100644 index 00000000..eba15e89 --- /dev/null +++ b/spring-graphql-web/src/main/java/org/springframework/boot/graphql/servlet/GraphQLWebAutoConfiguration.java @@ -0,0 +1,41 @@ +package org.springframework.boot.graphql.servlet; + +import graphql.GraphQL; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.graphql.GraphQLAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.graphql.servlet.GraphQLHandler; +import org.springframework.http.MediaType; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.RouterFunctions; +import org.springframework.web.servlet.function.ServerResponse; + +import static org.springframework.web.servlet.function.RequestPredicates.accept; + +@Configuration +@ConditionalOnWebApplication +@ConditionalOnClass(GraphQL.class) +@ConditionalOnBean(GraphQL.Builder.class) +@AutoConfigureAfter(GraphQLAutoConfiguration.class) +public class GraphQLWebAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public GraphQLHandler graphQLHandler(GraphQL.Builder graphQLBuilder) { + return new GraphQLHandler(graphQLBuilder); + } + + @Bean + public RouterFunction graphQLQueryEndpoint(GraphQLHandler handler) { + return RouterFunctions.route() + .POST("/graphql", accept(MediaType.APPLICATION_JSON), handler::handle) + .build(); + } + +} diff --git a/spring-graphql-web/src/main/java/org/springframework/graphql/reactive/components/package-info.java b/spring-graphql-web/src/main/java/org/springframework/boot/graphql/servlet/package-info.java similarity index 67% rename from spring-graphql-web/src/main/java/org/springframework/graphql/reactive/components/package-info.java rename to spring-graphql-web/src/main/java/org/springframework/boot/graphql/servlet/package-info.java index f60fdeab..7909e686 100644 --- a/spring-graphql-web/src/main/java/org/springframework/graphql/reactive/components/package-info.java +++ b/spring-graphql-web/src/main/java/org/springframework/boot/graphql/servlet/package-info.java @@ -1,6 +1,6 @@ @NonNullApi @NonNullFields -package org.springframework.graphql.reactive.components; +package org.springframework.boot.graphql.servlet; import org.springframework.lang.NonNullApi; import org.springframework.lang.NonNullFields; diff --git a/spring-graphql-web/src/main/java/org/springframework/graphql/reactive/components/GraphQLController.java b/spring-graphql-web/src/main/java/org/springframework/graphql/reactive/components/GraphQLController.java deleted file mode 100644 index d24ea4b3..00000000 --- a/spring-graphql-web/src/main/java/org/springframework/graphql/reactive/components/GraphQLController.java +++ /dev/null @@ -1,86 +0,0 @@ -package org.springframework.graphql.reactive.components; - - -import graphql.GraphQL; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.graphql.DefaultGraphQLInterceptor; -import org.springframework.graphql.GraphQLHandler; -import org.springframework.graphql.GraphQLHttpRequest; -import org.springframework.graphql.GraphQLInterceptor; -import org.springframework.web.reactive.function.server.RouterFunction; -import org.springframework.web.reactive.function.server.ServerRequest; -import org.springframework.web.reactive.function.server.ServerResponse; -import reactor.core.publisher.Mono; - -import javax.annotation.PostConstruct; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; - -import static org.springframework.web.reactive.function.server.RouterFunctions.route; - -@Configuration -public class GraphQLController { - - @Autowired - GraphQL graphQL; - - GraphQLHandler graphQLHandler; - - @Autowired(required = false) - GraphQLInterceptor graphQLInterceptor; - - @PostConstruct - public void init() { - GraphQLInterceptor interceptor = graphQLInterceptor == null ? new DefaultGraphQLInterceptor() : graphQLInterceptor; - this.graphQLHandler = new GraphQLHandler(graphQL, interceptor); - } - - @Bean - public RouterFunction routerFunction() { - RouterFunction route = route() - .POST("/graphql", this::graphqlPOST) - .build(); - return route; - - } - - private Mono graphqlPOST(ServerRequest serverRequest) { - Mono bodyMono = serverRequest.bodyToMono(GraphQLReactiveRequestBody.class); - return bodyMono.flatMap(body -> { - String query = body.getQuery(); - if (query == null) { - query = ""; - } - Map variables = body.getVariables(); - if (variables == null) { - variables = Collections.emptyMap(); - } - GraphQLHttpRequest graphQLHttpRequest = new GraphQLHttpRequest( - query, - body.getOperationName(), - variables, - serverRequest.headers().asHttpHeaders(), - serverRequest.queryParams()); - return graphQLHandler.graphqlPOST(graphQLHttpRequest); - }).flatMap(graphQLHttpResponse -> { - //TODO: this should be handled better: - // we don't want to serialize `null` values for `errors` and `extensions` - // this is why we convert it to a Map here - Map responseBodyRaw = new LinkedHashMap<>(); - responseBodyRaw.put("data", graphQLHttpResponse.getData()); - if (graphQLHttpResponse.getErrors() != null) { - responseBodyRaw.put("errors", graphQLHttpResponse.getErrors()); - } - if (graphQLHttpResponse.getExtensions() != null) { - responseBodyRaw.put("extensions", graphQLHttpResponse.getExtensions()); - } - return ServerResponse.ok().headers(httpHeaders -> { - httpHeaders.addAll(graphQLHttpResponse.getHttpHeaders()); - }).bodyValue(responseBodyRaw); - }); - } - -} diff --git a/spring-graphql-web/src/main/java/org/springframework/graphql/reactive/components/GraphQLReactiveRequestBody.java b/spring-graphql-web/src/main/java/org/springframework/graphql/reactive/components/GraphQLReactiveRequestBody.java deleted file mode 100644 index f95661e2..00000000 --- a/spring-graphql-web/src/main/java/org/springframework/graphql/reactive/components/GraphQLReactiveRequestBody.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.springframework.graphql.reactive.components; - -import java.util.Map; - -public class GraphQLReactiveRequestBody { - private String query; - private String operationName; - private Map variables; - - public String getQuery() { - return query; - } - - public void setQuery(String query) { - this.query = query; - } - - public String getOperationName() { - return operationName; - } - - public void setOperationName(String operationName) { - this.operationName = operationName; - } - - public Map getVariables() { - return variables; - } - - public void setVariables(Map variables) { - this.variables = variables; - } -} diff --git a/spring-graphql-web/src/main/java/org/springframework/graphql/servlet/components/GraphQLController.java b/spring-graphql-web/src/main/java/org/springframework/graphql/servlet/components/GraphQLController.java deleted file mode 100644 index 343f5d57..00000000 --- a/spring-graphql-web/src/main/java/org/springframework/graphql/servlet/components/GraphQLController.java +++ /dev/null @@ -1,100 +0,0 @@ -package org.springframework.graphql.servlet.components; - - -import graphql.GraphQL; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.graphql.DefaultGraphQLInterceptor; -import org.springframework.graphql.GraphQLHandler; -import org.springframework.graphql.GraphQLHttpRequest; -import org.springframework.graphql.GraphQLInterceptor; -import org.springframework.http.MediaType; -import org.springframework.web.servlet.function.RequestPredicates; -import org.springframework.web.servlet.function.RouterFunction; -import org.springframework.web.servlet.function.ServerRequest; -import org.springframework.web.servlet.function.ServerResponse; -import reactor.core.publisher.Mono; - -import javax.annotation.PostConstruct; -import javax.servlet.ServletException; -import java.io.IOException; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; - -import static org.springframework.web.servlet.function.RouterFunctions.route; - -@Configuration -public class GraphQLController { - - @Autowired - GraphQL graphQL; - - GraphQLHandler graphQLHandler; - - @Autowired(required = false) - GraphQLInterceptor graphQLInterceptor; - - - @PostConstruct - public void init() { - GraphQLInterceptor interceptor = graphQLInterceptor == null ? new DefaultGraphQLInterceptor() : graphQLInterceptor; - this.graphQLHandler = new GraphQLHandler(graphQL, interceptor); - } - - - @Bean - public RouterFunction routerFunction() { - RouterFunction route = route() - .POST("/graphql", RequestPredicates.contentType(MediaType.APPLICATION_JSON) - .or(RequestPredicates.contentType(MediaType.APPLICATION_JSON_UTF8)), this::graphqlPOST) - .build(); - return route; - - } - - private ServerResponse graphqlPOST(ServerRequest serverRequest) { - GraphQLServletRequestBody body = null; - try { - body = serverRequest.body(GraphQLServletRequestBody.class); - } catch (ServletException | IOException e) { - e.printStackTrace(); - } - String query = body.getQuery(); - if (query == null) { - query = ""; - } - Map variables = body.getVariables(); - if (variables == null) { - variables = Collections.emptyMap(); - } - - GraphQLHttpRequest graphQLHttpRequest = new GraphQLHttpRequest( - query, - body.getOperationName(), - variables, - serverRequest.headers().asHttpHeaders(), - serverRequest.params()); - - Mono> responseRawMono = graphQLHandler.graphqlPOST(graphQLHttpRequest) - .map(graphQLHttpResponse -> { - //TODO: this should be handled better: - // we don't want to serialize `null` values for `errors` and `extensions` - // this is why we convert it to a Map here - Map responseBodyRaw = new LinkedHashMap<>(); - responseBodyRaw.put("data", graphQLHttpResponse.getData()); - if (graphQLHttpResponse.getErrors() != null) { - responseBodyRaw.put("errors", graphQLHttpResponse.getErrors()); - } - if (graphQLHttpResponse.getExtensions() != null) { - responseBodyRaw.put("extensions", graphQLHttpResponse.getExtensions()); - } - return responseBodyRaw; - }); - //TODO: how to add http headers for the response here? It is part of responseRawMono but - // ServerResponse don't accept a CompletableFuture or Mono as headers. - return ServerResponse.ok().body(responseRawMono); - } - -} diff --git a/spring-graphql-web/src/main/java/org/springframework/graphql/servlet/components/GraphQLServletRequestBody.java b/spring-graphql-web/src/main/java/org/springframework/graphql/servlet/components/GraphQLServletRequestBody.java deleted file mode 100644 index 6fd50364..00000000 --- a/spring-graphql-web/src/main/java/org/springframework/graphql/servlet/components/GraphQLServletRequestBody.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.springframework.graphql.servlet.components; - -import java.util.Map; - -public class GraphQLServletRequestBody { - private String query; - private String operationName; - private Map variables; - - public String getQuery() { - return query; - } - - public void setQuery(String query) { - this.query = query; - } - - public String getOperationName() { - return operationName; - } - - public void setOperationName(String operationName) { - this.operationName = operationName; - } - - public Map getVariables() { - return variables; - } - - public void setVariables(Map variables) { - this.variables = variables; - } -} diff --git a/spring-graphql-web/src/test/java/org/springframework/boot/graphql/Book.java b/spring-graphql-web/src/test/java/org/springframework/boot/graphql/Book.java new file mode 100644 index 00000000..6b877a4e --- /dev/null +++ b/spring-graphql-web/src/test/java/org/springframework/boot/graphql/Book.java @@ -0,0 +1,54 @@ +package org.springframework.boot.graphql; + +public class Book { + + String id; + + String name; + + int pageCount; + + String author; + + public Book() { + } + + public Book(String id, String name, int pageCount, String author) { + this.id = id; + this.name = name; + this.pageCount = pageCount; + this.author = author; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getPageCount() { + return pageCount; + } + + public void setPageCount(int pageCount) { + this.pageCount = pageCount; + } + + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } +} diff --git a/spring-graphql-web/src/test/java/org/springframework/boot/graphql/GraphQLDataFetchers.java b/spring-graphql-web/src/test/java/org/springframework/boot/graphql/GraphQLDataFetchers.java new file mode 100644 index 00000000..44d26e32 --- /dev/null +++ b/spring-graphql-web/src/test/java/org/springframework/boot/graphql/GraphQLDataFetchers.java @@ -0,0 +1,27 @@ +package org.springframework.boot.graphql; + +import java.util.Arrays; +import java.util.List; + +import graphql.schema.DataFetcher; + +public class GraphQLDataFetchers { + + private static List books = Arrays.asList( + new Book("book-1", "GraphQL for beginners", 100, "John GraphQL"), + new Book("book-2", "Harry Potter and the Philosopher's Stone", 223, "Joanne Rowling"), + new Book("book-3", "Moby Dick", 635, "Moby Dick"), + new Book("book-3", "Moby Dick", 635, "Moby Dick")); + + + public static DataFetcher getBookByIdDataFetcher() { + return dataFetchingEnvironment -> { + String bookId = dataFetchingEnvironment.getArgument("id"); + return books + .stream() + .filter(book -> book.getId().equals(bookId)) + .findFirst() + .orElse(null); + }; + } +} diff --git a/spring-graphql-web/src/test/java/org/springframework/boot/graphql/reactive/GraphQLWebFluxEndpointTests.java b/spring-graphql-web/src/test/java/org/springframework/boot/graphql/reactive/GraphQLWebFluxEndpointTests.java new file mode 100644 index 00000000..d04be639 --- /dev/null +++ b/spring-graphql-web/src/test/java/org/springframework/boot/graphql/reactive/GraphQLWebFluxEndpointTests.java @@ -0,0 +1,70 @@ +package org.springframework.boot.graphql.reactive; + + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration; +import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration; +import org.springframework.boot.graphql.GraphQLAutoConfiguration; +import org.springframework.boot.graphql.GraphQLDataFetchers; +import org.springframework.boot.graphql.RuntimeWiringCustomizer; +import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring; + +class GraphQLWebFluxEndpointTests { + + @Test + void endpointHandlesGraphQLQueries() throws Exception { + new ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class, CodecsAutoConfiguration.class, + WebFluxAutoConfiguration.class, HttpHandlerAutoConfiguration.class, + GraphQLAutoConfiguration.class, GraphQLWebFluxAutoConfiguration.class)) + .withUserConfiguration(DataFetchersConfiguration.class) + .withPropertyValues("spring.main.web-application-type=reactive", "spring.graphql.schema:classpath:books/schema.graphqls").run((context) -> { + WebTestClient client = createWebTestClient(context); + + String query = "{" + + " bookById(id: \\\"book-1\\\"){ " + + " id" + + " name" + + " pageCount" + + " author" + + " }" + + "}"; + + String body = "{" + + " \"query\": \"" + query + "\"" + + "}"; + + client.post().uri("/graphql").contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON) + .bodyValue(body).exchange().expectStatus().isOk() + .expectBody() + .jsonPath("data.bookById.name").isEqualTo("GraphQL for beginners"); + }); + } + + private WebTestClient createWebTestClient(ApplicationContext context) { + return WebTestClient.bindToApplicationContext(context).configureClient().baseUrl("https://spring.example.org") + .build(); + } + + @Configuration(proxyBeanMethods = false) + static class DataFetchersConfiguration { + + @Bean + public RuntimeWiringCustomizer bookDataFetcher() { + return (runtimeWiring) -> runtimeWiring.type(newTypeWiring("Query") + .dataFetcher("bookById", GraphQLDataFetchers.getBookByIdDataFetcher())); + } + + } +} diff --git a/spring-graphql-web/src/test/java/org/springframework/boot/graphql/servlet/GraphQLWebEndpointTests.java b/spring-graphql-web/src/test/java/org/springframework/boot/graphql/servlet/GraphQLWebEndpointTests.java new file mode 100644 index 00000000..ae539869 --- /dev/null +++ b/spring-graphql-web/src/test/java/org/springframework/boot/graphql/servlet/GraphQLWebEndpointTests.java @@ -0,0 +1,65 @@ +package org.springframework.boot.graphql.servlet; + + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; +import org.springframework.boot.graphql.GraphQLAutoConfiguration; +import org.springframework.boot.graphql.GraphQLDataFetchers; +import org.springframework.boot.graphql.RuntimeWiringCustomizer; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; + +class GraphQLWebEndpointTests { + + @Test + void endpointHandlesGraphQLQueries() throws Exception { + new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, + WebMvcAutoConfiguration.class, DispatcherServletAutoConfiguration.class, + GraphQLAutoConfiguration.class, GraphQLWebAutoConfiguration.class)) + .withUserConfiguration(DataFetchersConfiguration.class) + .withPropertyValues("spring.main.web-application-type=servlet", "spring.graphql.schema:classpath:books/schema.graphqls").run((context) -> { + + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(context).build(); + String query = "{" + + " bookById(id: \\\"book-1\\\"){ " + + " id" + + " name" + + " pageCount" + + " author" + + " }" + + "}"; + + String body = "{" + + " \"query\": \"" + query + "\"" + + "}"; + + mockMvc.perform(post("/graphql").content(body).contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("data.bookById.name").value("GraphQL for beginners")); + }); + } + + @Configuration(proxyBeanMethods = false) + static class DataFetchersConfiguration { + + @Bean + public RuntimeWiringCustomizer bookDataFetcher() { + return (runtimeWiring) -> runtimeWiring.type(newTypeWiring("Query") + .dataFetcher("bookById", GraphQLDataFetchers.getBookByIdDataFetcher())); + } + + } +} diff --git a/spring-graphql-web/src/test/resources/books/schema.graphqls b/spring-graphql-web/src/test/resources/books/schema.graphqls new file mode 100644 index 00000000..975b7ca6 --- /dev/null +++ b/spring-graphql-web/src/test/resources/books/schema.graphqls @@ -0,0 +1,10 @@ +type Query { + bookById(id: ID): Book +} + +type Book { + id: ID + name: String + pageCount: Int + author: String +} \ No newline at end of file