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 16cb18749d2..c9d7e8bdfa9 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 @@ -34,8 +34,14 @@ public class GraphQlProperties { */ private String path = "/graphql"; + private final Graphiql graphiql = new Graphiql(); + private final Schema schema = new Schema(); + public Graphiql getGraphiql() { + return this.graphiql; + } + public String getPath() { return this.path; } @@ -107,4 +113,34 @@ public class GraphQlProperties { } + public static class Graphiql { + + /** + * Path to the GraphiQL UI endpoint. + */ + private String path = "/graphiql"; + + /** + * Whether the default GraphiQL UI is enabled. + */ + private boolean enabled = false; + + public String getPath() { + return this.path; + } + + public void setPath(String path) { + this.path = path; + } + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + } + } 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 index 0c068bbe08a..fd55b2d9c24 100644 --- 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 @@ -40,6 +40,7 @@ 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.graphql.web.webflux.GraphiQlHandler; import org.springframework.graphql.web.webflux.SchemaHandler; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -97,6 +98,11 @@ public class GraphQlWebFluxAutoConfiguration { .POST(graphQLPath, accept(MediaType.APPLICATION_JSON).and(contentType(MediaType.APPLICATION_JSON)), handler::handleRequest); + if (properties.getGraphiql().isEnabled()) { + GraphiQlHandler graphiQlHandler = new GraphiQlHandler(graphQLPath); + builder = builder.GET(properties.getGraphiql().getPath(), graphiQlHandler::handleRequest); + } + if (properties.getSchema().getPrinter().isEnabled()) { SchemaHandler schemaHandler = new SchemaHandler(graphQlSource); builder = builder.GET(graphQLPath + "/schema", schemaHandler::handleRequest); 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 index 6820c83b164..534b4956578 100644 --- 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 @@ -41,6 +41,7 @@ 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.graphql.web.webmvc.GraphiQlHandler; import org.springframework.graphql.web.webmvc.SchemaHandler; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -98,6 +99,11 @@ public class GraphQlWebMvcAutoConfiguration { .POST(graphQLPath, RequestPredicates.contentType(MediaType.APPLICATION_JSON) .and(RequestPredicates.accept(MediaType.APPLICATION_JSON)), handler::handleRequest); + if (properties.getGraphiql().isEnabled()) { + GraphiQlHandler graphiQLHandler = new GraphiQlHandler(graphQLPath); + builder = builder.GET(properties.getGraphiql().getPath(), graphiQLHandler::handleRequest); + } + if (properties.getSchema().getPrinter().isEnabled()) { SchemaHandler schemaHandler = new SchemaHandler(graphQlSource); builder = builder.GET(graphQLPath + "/schema", schemaHandler::handleRequest); 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 index c39d997fc5d..e7a05476bf9 100644 --- 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 @@ -47,20 +47,21 @@ import static org.hamcrest.Matchers.containsString; */ class GraphQlWebFluxAutoConfigurationTests { - private static final String BASE_URL = "https://spring.example.org/graphql"; + private static final String BASE_URL = "https://spring.example.org/"; 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", "spring.graphql.schema.printer.enabled=true"); + .withUserConfiguration(DataFetchersConfiguration.class, CustomWebInterceptor.class) + .withPropertyValues("spring.main.web-application-type=reactive", "spring.graphql.graphiql.enabled=true", + "spring.graphql.schema.printer.enabled=true"); @Test void simpleQueryShouldWork() { testWithWebClient((client) -> { String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }"; - client.post().uri("").bodyValue("{ \"query\": \"" + query + "\"}").exchange().expectStatus().isOk() + client.post().uri("/graphql").bodyValue("{ \"query\": \"" + query + "\"}").exchange().expectStatus().isOk() .expectBody().jsonPath("data.bookById.name").isEqualTo("GraphQL for beginners"); }); } @@ -69,19 +70,21 @@ class GraphQlWebFluxAutoConfigurationTests { void httpGetQueryShouldBeSupported() { testWithWebClient((client) -> { String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }"; - client.get().uri("?query={query}", "{ \"query\": \"" + query + "\"}").exchange().expectStatus() + client.get().uri("/graphql?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()); + testWithWebClient( + (client) -> client.post().uri("/graphql").bodyValue("{}").exchange().expectStatus().isBadRequest()); } @Test void shouldRejectQueryWithInvalidJson() { - testWithWebClient((client) -> client.post().uri("").bodyValue(":)").exchange().expectStatus().isBadRequest()); + testWithWebClient( + (client) -> client.post().uri("/graphql").bodyValue(":)").exchange().expectStatus().isBadRequest()); } @Test @@ -89,18 +92,28 @@ class GraphQlWebFluxAutoConfigurationTests { testWithWebClient((client) -> { String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }"; - client.post().uri("").bodyValue("{ \"query\": \"" + query + "\"}").exchange().expectStatus().isOk() + client.post().uri("/graphql").bodyValue("{ \"query\": \"" + query + "\"}").exchange().expectStatus().isOk() .expectHeader().valueEquals("X-Custom-Header", "42"); }); } @Test void shouldExposeSchemaEndpoint() { - testWithWebClient((client) -> client.get().uri("/schema").accept(MediaType.ALL).exchange() + testWithWebClient((client) -> client.get().uri("/graphql/schema").accept(MediaType.ALL).exchange() .expectStatus().isOk().expectHeader().contentType(MediaType.TEXT_PLAIN).expectBody(String.class) .value(containsString("type Book"))); } + @Test + void shouldExposeGraphiqlEndpoint() { + testWithWebClient((client) -> { + client.get().uri("/graphiql").exchange().expectStatus().is3xxRedirection().expectHeader() + .location("https://spring.example.org/graphiql?path=/graphql"); + client.get().uri("/graphiql?path=/graphql").accept(MediaType.ALL).exchange().expectStatus().isOk() + .expectHeader().contentType(MediaType.TEXT_HTML); + }); + } + private void testWithWebClient(Consumer consumer) { this.contextRunner.run((context) -> { WebTestClient client = WebTestClient.bindToApplicationContext(context).configureClient() 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 index 37c1f16f29b..1b9212d16b0 100644 --- 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 @@ -43,6 +43,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder 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.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** @@ -57,8 +58,9 @@ class GraphQlWebMvcAutoConfigurationTests { 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", "spring.graphql.schema.printer.enabled=true"); + .withUserConfiguration(DataFetchersConfiguration.class, CustomWebInterceptor.class) + .withPropertyValues("spring.main.web-application-type=servlet", "spring.graphql.graphiql.enabled=true", + "spring.graphql.schema.printer.enabled=true"); @Test void simpleQueryShouldWork() { @@ -107,6 +109,16 @@ class GraphQlWebMvcAutoConfigurationTests { .andExpect(content().string(Matchers.containsString("type Book")))); } + @Test + void shouldExposeGraphiqlEndpoint() { + testWith((mockMvc) -> { + mockMvc.perform(get("/graphiql")).andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("http://localhost/graphiql?path=/graphql")); + mockMvc.perform(get("/graphiql?path=/graphql")).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.TEXT_HTML)); + }); + } + private void testWith(MockMvcConsumer mockMvcConsumer) { this.contextRunner.run((context) -> { MediaType mediaType = MediaType.APPLICATION_JSON;