From b38d04556e9969a36517e32953f2778d48f42e7a Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 21 Dec 2021 08:32:56 +0100 Subject: [PATCH] Auto-configure HTTP transport for MVC and WebFlux This commit adds two new auto-configuration classes for Spring GraphQL support. Once the base GraphQL infrastructure is in place, we can now expose the `GraphQlService` over an HTTP transport. Spring GraphQL supports both MVC and WebFlux, so this commit ships with one auto-configuration for each. Developers can configure the HTTP path where the GraphQL resource is exposed using the `spring.graphql.path` configuration property (this defaults to `"/graphql"`). See gh-29140 --- .../graphql/GraphQlProperties.java | 13 ++ .../GraphQlWebFluxAutoConfiguration.java | 102 +++++++++++++ .../graphql/reactive/package-info.java | 20 +++ .../GraphQlWebMvcAutoConfiguration.java | 103 +++++++++++++ .../graphql/servlet/package-info.java | 20 +++ .../main/resources/META-INF/spring.factories | 2 + .../boot/autoconfigure/graphql/Book.java | 76 ++++++++++ .../graphql/GraphQlTestDataFetchers.java | 59 ++++++++ .../GraphQlWebFluxAutoConfigurationTests.java | 128 ++++++++++++++++ .../GraphQlWebMvcAutoConfigurationTests.java | 139 ++++++++++++++++++ 10 files changed, 662 insertions(+) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/reactive/GraphQlWebFluxAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/reactive/package-info.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/servlet/GraphQlWebMvcAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/servlet/package-info.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/Book.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/GraphQlTestDataFetchers.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/reactive/GraphQlWebFluxAutoConfigurationTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/servlet/GraphQlWebMvcAutoConfigurationTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlProperties.java index cb9699ef0b..2fc1a76429 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlProperties.java @@ -29,8 +29,21 @@ import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "spring.graphql") public class GraphQlProperties { + /** + * Path at which to expose a GraphQL request HTTP endpoint. + */ + private String path = "/graphql"; + private final Schema schema = new Schema(); + public String getPath() { + return this.path; + } + + public void setPath(String path) { + this.path = path; + } + public Schema getSchema() { return this.schema; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/reactive/GraphQlWebFluxAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/reactive/GraphQlWebFluxAutoConfiguration.java new file mode 100644 index 0000000000..53a4ccd96e --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/reactive/GraphQlWebFluxAutoConfiguration.java @@ -0,0 +1,102 @@ +/* + * Copyright 2012-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.boot.autoconfigure.graphql.reactive; + +import java.util.Collections; +import java.util.stream.Collectors; + +import graphql.GraphQL; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +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.autoconfigure.graphql.GraphQlAutoConfiguration; +import org.springframework.boot.autoconfigure.graphql.GraphQlProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ResourceLoader; +import org.springframework.graphql.GraphQlService; +import org.springframework.graphql.execution.GraphQlSource; +import org.springframework.graphql.web.WebGraphQlHandler; +import org.springframework.graphql.web.WebInterceptor; +import org.springframework.graphql.web.webflux.GraphQlHttpHandler; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerResponse; + +import static org.springframework.web.reactive.function.server.RequestPredicates.accept; +import static org.springframework.web.reactive.function.server.RequestPredicates.contentType; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for enabling Spring GraphQL over + * WebFlux. + * + * @author Brian Clozel + * @since 2.7.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) +@ConditionalOnClass({ GraphQL.class, GraphQlHttpHandler.class }) +@ConditionalOnBean(GraphQlService.class) +@AutoConfigureAfter(GraphQlAutoConfiguration.class) +public class GraphQlWebFluxAutoConfiguration { + + private static final Log logger = LogFactory.getLog(GraphQlWebFluxAutoConfiguration.class); + + @Bean + @ConditionalOnMissingBean + public GraphQlHttpHandler graphQlHttpHandler(WebGraphQlHandler webGraphQlHandler) { + return new GraphQlHttpHandler(webGraphQlHandler); + } + + @Bean + @ConditionalOnMissingBean + public WebGraphQlHandler webGraphQlHandler(GraphQlService service, + ObjectProvider interceptorsProvider) { + return WebGraphQlHandler.builder(service) + .interceptors(interceptorsProvider.orderedStream().collect(Collectors.toList())).build(); + } + + @Bean + public RouterFunction graphQlEndpoint(GraphQlHttpHandler handler, GraphQlSource graphQlSource, + GraphQlProperties properties, ResourceLoader resourceLoader) { + + String graphQLPath = properties.getPath(); + if (logger.isInfoEnabled()) { + logger.info("GraphQL endpoint HTTP POST " + graphQLPath); + } + + RouterFunctions.Builder builder = RouterFunctions.route() + .GET(graphQLPath, + (request) -> ServerResponse.status(HttpStatus.METHOD_NOT_ALLOWED) + .headers((headers) -> headers.setAllow(Collections.singleton(HttpMethod.POST))).build()) + .POST(graphQLPath, accept(MediaType.APPLICATION_JSON).and(contentType(MediaType.APPLICATION_JSON)), + handler::handleRequest); + + return builder.build(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/reactive/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/reactive/package-info.java new file mode 100644 index 0000000000..c0b8ec565e --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/reactive/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2020-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. + */ + +/** + * Auto-configuration classes for WebFlux support in Spring GraphQL. + */ +package org.springframework.boot.autoconfigure.graphql.reactive; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/servlet/GraphQlWebMvcAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/servlet/GraphQlWebMvcAutoConfiguration.java new file mode 100644 index 0000000000..281eacac44 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/servlet/GraphQlWebMvcAutoConfiguration.java @@ -0,0 +1,103 @@ +/* + * Copyright 2012-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.boot.autoconfigure.graphql.servlet; + +import java.util.Collections; +import java.util.stream.Collectors; + +import graphql.GraphQL; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +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.autoconfigure.graphql.GraphQlAutoConfiguration; +import org.springframework.boot.autoconfigure.graphql.GraphQlProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ResourceLoader; +import org.springframework.graphql.GraphQlService; +import org.springframework.graphql.execution.GraphQlSource; +import org.springframework.graphql.execution.ThreadLocalAccessor; +import org.springframework.graphql.web.WebGraphQlHandler; +import org.springframework.graphql.web.WebInterceptor; +import org.springframework.graphql.web.webmvc.GraphQlHttpHandler; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +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.RouterFunctions; +import org.springframework.web.servlet.function.ServerResponse; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for enabling Spring GraphQL over + * Spring MVC. + * + * @author Brian Clozel + * @since 2.7.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) +@ConditionalOnClass({ GraphQL.class, GraphQlHttpHandler.class }) +@ConditionalOnBean(GraphQlService.class) +@AutoConfigureAfter(GraphQlAutoConfiguration.class) +public class GraphQlWebMvcAutoConfiguration { + + private static final Log logger = LogFactory.getLog(GraphQlWebMvcAutoConfiguration.class); + + @Bean + @ConditionalOnMissingBean + public GraphQlHttpHandler graphQlHttpHandler(WebGraphQlHandler webGraphQlHandler) { + return new GraphQlHttpHandler(webGraphQlHandler); + } + + @Bean + @ConditionalOnMissingBean + public WebGraphQlHandler webGraphQlHandler(GraphQlService service, + ObjectProvider interceptorsProvider, + ObjectProvider accessorsProvider) { + return WebGraphQlHandler.builder(service) + .interceptors(interceptorsProvider.orderedStream().collect(Collectors.toList())) + .threadLocalAccessors(accessorsProvider.orderedStream().collect(Collectors.toList())).build(); + } + + @Bean + public RouterFunction graphQlRouterFunction(GraphQlHttpHandler handler, GraphQlSource graphQlSource, + GraphQlProperties properties, ResourceLoader resourceLoader) { + + String graphQLPath = properties.getPath(); + if (logger.isInfoEnabled()) { + logger.info("GraphQL endpoint HTTP POST " + graphQLPath); + } + + RouterFunctions.Builder builder = RouterFunctions.route() + .GET(graphQLPath, + (request) -> ServerResponse.status(HttpStatus.METHOD_NOT_ALLOWED) + .headers((headers) -> headers.setAllow(Collections.singleton(HttpMethod.POST))).build()) + .POST(graphQLPath, RequestPredicates.contentType(MediaType.APPLICATION_JSON) + .and(RequestPredicates.accept(MediaType.APPLICATION_JSON)), handler::handleRequest); + + return builder.build(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/servlet/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/servlet/package-info.java new file mode 100644 index 0000000000..04cf403792 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/servlet/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2020-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. + */ + +/** + * Auto-configuration classes for MVC support in Spring GraphQL. + */ +package org.springframework.boot.autoconfigure.graphql.servlet; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index 71dda4d197..bad6ab475c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -69,6 +69,8 @@ org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAuto org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration,\ org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,\ org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration,\ +org.springframework.boot.autoconfigure.graphql.reactive.GraphQlWebFluxAutoConfiguration,\ +org.springframework.boot.autoconfigure.graphql.servlet.GraphQlWebMvcAutoConfiguration,\ org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration,\ org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration,\ org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration,\ diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/Book.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/Book.java new file mode 100644 index 0000000000..ecd0468216 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/Book.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012-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.boot.autoconfigure.graphql; + +/** + * Sample class for + * + * @author Brian Clozel + */ +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 this.id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public int getPageCount() { + return this.pageCount; + } + + public void setPageCount(int pageCount) { + this.pageCount = pageCount; + } + + public String getAuthor() { + return this.author; + } + + public void setAuthor(String author) { + this.author = author; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/GraphQlTestDataFetchers.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/GraphQlTestDataFetchers.java new file mode 100644 index 0000000000..c7dbc44238 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/GraphQlTestDataFetchers.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-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.boot.autoconfigure.graphql; + +import java.util.Arrays; +import java.util.List; + +import graphql.schema.DataFetcher; +import reactor.core.publisher.Flux; + +import org.springframework.lang.Nullable; + +/** + * Test utility class holding {@link DataFetcher} implementations. + * + * @author Brian Clozel + */ +public final class GraphQlTestDataFetchers { + + 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")); + + private GraphQlTestDataFetchers() { + + } + + public static DataFetcher getBookByIdDataFetcher() { + return (environment) -> getBookById(environment.getArgument("id")); + } + + public static DataFetcher getBooksOnSaleDataFetcher() { + return (environment) -> getBooksOnSale(environment.getArgument("minPages")); + } + + @Nullable + public static Book getBookById(String id) { + return books.stream().filter((book) -> book.getId().equals(id)).findFirst().orElse(null); + } + + public static Flux getBooksOnSale(int minPages) { + return Flux.fromIterable(books).filter((book) -> book.getPageCount() >= minPages); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/reactive/GraphQlWebFluxAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/reactive/GraphQlWebFluxAutoConfigurationTests.java new file mode 100644 index 0000000000..2f4ed179ab --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/reactive/GraphQlWebFluxAutoConfigurationTests.java @@ -0,0 +1,128 @@ +/* + * Copyright 2012-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.boot.autoconfigure.graphql.reactive; + +import java.util.Collections; +import java.util.function.Consumer; + +import graphql.schema.idl.TypeRuntimeWiring; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration; +import org.springframework.boot.autoconfigure.graphql.GraphQlTestDataFetchers; +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.test.context.runner.ReactiveWebApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.graphql.execution.RuntimeWiringConfigurer; +import org.springframework.graphql.web.WebInterceptor; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient; + +/** + * Tests for {@link GraphQlWebFluxAutoConfiguration} + * + * @author Brian Clozel + */ +class GraphQlWebFluxAutoConfigurationTests { + + private static final String BASE_URL = "https://spring.example.org/graphql"; + + private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(HttpHandlerAutoConfiguration.class, WebFluxAutoConfiguration.class, + CodecsAutoConfiguration.class, JacksonAutoConfiguration.class, GraphQlAutoConfiguration.class, + GraphQlWebFluxAutoConfiguration.class)) + .withUserConfiguration(DataFetchersConfiguration.class, CustomWebInterceptor.class) + .withPropertyValues("spring.main.web-application-type=reactive"); + + @Test + void simpleQueryShouldWork() { + testWithWebClient((client) -> { + String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }"; + client.post().uri("").bodyValue("{ \"query\": \"" + query + "\"}").exchange().expectStatus().isOk() + .expectBody().jsonPath("data.bookById.name").isEqualTo("GraphQL for beginners"); + }); + } + + @Test + void httpGetQueryShouldBeSupported() { + testWithWebClient((client) -> { + String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }"; + client.get().uri("?query={query}", "{ \"query\": \"" + query + "\"}").exchange().expectStatus() + .isEqualTo(HttpStatus.METHOD_NOT_ALLOWED).expectHeader().valueEquals("Allow", "POST"); + }); + } + + @Test + void shouldRejectMissingQuery() { + testWithWebClient((client) -> client.post().uri("").bodyValue("{}").exchange().expectStatus().isBadRequest()); + } + + @Test + void shouldRejectQueryWithInvalidJson() { + testWithWebClient((client) -> client.post().uri("").bodyValue(":)").exchange().expectStatus().isBadRequest()); + } + + @Test + void shouldConfigureWebInterceptors() { + testWithWebClient((client) -> { + String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }"; + + client.post().uri("").bodyValue("{ \"query\": \"" + query + "\"}").exchange().expectStatus().isOk() + .expectHeader().valueEquals("X-Custom-Header", "42"); + }); + } + + private void testWithWebClient(Consumer consumer) { + this.contextRunner.run((context) -> { + WebTestClient client = WebTestClient.bindToApplicationContext(context).configureClient() + .defaultHeaders((headers) -> { + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + }).baseUrl(BASE_URL).build(); + consumer.accept(client); + }); + } + + @Configuration(proxyBeanMethods = false) + static class DataFetchersConfiguration { + + @Bean + RuntimeWiringConfigurer bookDataFetcher() { + return (builder) -> builder.type(TypeRuntimeWiring.newTypeWiring("Query").dataFetcher("bookById", + GraphQlTestDataFetchers.getBookByIdDataFetcher())); + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomWebInterceptor { + + @Bean + WebInterceptor customWebInterceptor() { + return (webInput, interceptorChain) -> interceptorChain.next(webInput) + .map((output) -> output.transform((builder) -> builder.responseHeader("X-Custom-Header", "42"))); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/servlet/GraphQlWebMvcAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/servlet/GraphQlWebMvcAutoConfigurationTests.java new file mode 100644 index 0000000000..f9e19a9df8 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/servlet/GraphQlWebMvcAutoConfigurationTests.java @@ -0,0 +1,139 @@ +/* + * Copyright 2012-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.boot.autoconfigure.graphql.servlet; + +import graphql.schema.idl.TypeRuntimeWiring; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration; +import org.springframework.boot.autoconfigure.graphql.GraphQlTestDataFetchers; +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.test.context.runner.WebApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.graphql.execution.RuntimeWiringConfigurer; +import org.springframework.graphql.web.WebInterceptor; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Tests for {@link GraphQlWebMvcAutoConfiguration}. + * + * @author Brian Clozel + */ +class GraphQlWebMvcAutoConfigurationTests { + + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of(DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, JacksonAutoConfiguration.class, + GraphQlAutoConfiguration.class, GraphQlWebMvcAutoConfiguration.class)) + .withUserConfiguration(DataFetchersConfiguration.class, CustomWebInterceptor.class) + .withPropertyValues("spring.main.web-application-type=servlet"); + + @Test + void simpleQueryShouldWork() { + testWith((mockMvc) -> { + String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }"; + MvcResult result = mockMvc.perform(post("/graphql").content("{\"query\": \"" + query + "\"}")).andReturn(); + mockMvc.perform(asyncDispatch(result)).andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("data.bookById.name").value("GraphQL for beginners")); + }); + } + + @Test + void httpGetQueryShouldBeSupported() { + testWith((mockMvc) -> { + String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }"; + mockMvc.perform(get("/graphql?query={query}", "{\"query\": \"" + query + "\"}")) + .andExpect(status().isMethodNotAllowed()).andExpect(header().string("Allow", "POST")); + }); + } + + @Test + void shouldRejectMissingQuery() { + testWith((mockMvc) -> mockMvc.perform(post("/graphql").content("{}")).andExpect(status().isBadRequest())); + } + + @Test + void shouldRejectQueryWithInvalidJson() { + testWith((mockMvc) -> mockMvc.perform(post("/graphql").content(":)")).andExpect(status().isBadRequest())); + } + + @Test + void shouldConfigureWebInterceptors() { + testWith((mockMvc) -> { + String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }"; + MvcResult result = mockMvc.perform(post("/graphql").content("{\"query\": \"" + query + "\"}")).andReturn(); + mockMvc.perform(asyncDispatch(result)).andExpect(status().isOk()) + .andExpect(header().string("X-Custom-Header", "42")); + }); + } + + private void testWith(MockMvcConsumer mockMvcConsumer) { + this.contextRunner.run((context) -> { + MediaType mediaType = MediaType.APPLICATION_JSON; + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(context) + .defaultRequest(post("/graphql").contentType(mediaType).accept(mediaType)).build(); + mockMvcConsumer.accept(mockMvc); + }); + } + + private interface MockMvcConsumer { + + void accept(MockMvc mockMvc) throws Exception; + + } + + @Configuration(proxyBeanMethods = false) + static class DataFetchersConfiguration { + + @Bean + RuntimeWiringConfigurer bookDataFetcher() { + return (builder) -> builder.type(TypeRuntimeWiring.newTypeWiring("Query").dataFetcher("bookById", + GraphQlTestDataFetchers.getBookByIdDataFetcher())); + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomWebInterceptor { + + @Bean + WebInterceptor customWebInterceptor() { + return (webInput, interceptorChain) -> interceptorChain.next(webInput) + .map((output) -> output.transform((builder) -> builder.responseHeader("X-Custom-Header", "42"))); + } + + } + +}