Remove experimental Spring Boot Starter

This commit removes the leftover sources after the experimental Spring
Boot starter has been moved to Spring Boot.

See gh-207
This commit is contained in:
Brian Clozel
2022-01-13 16:43:37 +01:00
parent 5b0496cbce
commit be74ba2b59
63 changed files with 0 additions and 4837 deletions

View File

@@ -1,86 +0,0 @@
import org.springframework.boot.gradle.plugin.SpringBootPlugin
plugins {
id 'org.springframework.boot' version "${bootVersion}" apply false
id 'java-library'
id 'org.springframework.graphql.compiler'
}
group = 'org.springframework.experimental'
description = "GraphQL Spring Boot Starter"
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
dependencyManagement {
imports {
mavenBom SpringBootPlugin.BOM_COORDINATES
}
generatedPomCustomization {
enabled = false
}
}
dependencies {
api project(':spring-graphql')
api "com.graphql-java:graphql-java:${graphQlJavaVersion}"
api 'io.projectreactor:reactor-core'
api 'org.springframework:spring-context'
api 'org.springframework.boot:spring-boot-starter'
compileOnly 'org.springframework:spring-webflux'
compileOnly 'org.springframework:spring-webmvc'
compileOnly 'org.springframework:spring-websocket'
compileOnly 'javax.servlet:javax.servlet-api'
compileOnly 'javax.validation:validation-api'
compileOnly 'javax.websocket:javax.websocket-api'
compileOnly 'io.micrometer:micrometer-core'
compileOnly 'org.springframework.boot:spring-boot-actuator-autoconfigure'
compileOnly 'org.springframework.security:spring-security-config'
compileOnly 'org.springframework.security:spring-security-web'
compileOnly 'com.querydsl:querydsl-core'
compileOnly 'org.springframework.data:spring-data-commons'
compileOnly project(':spring-graphql-test')
compileOnly 'org.junit.jupiter:junit-jupiter-api'
compileOnly 'org.springframework.boot:spring-boot-test'
compileOnly 'org.springframework.boot:spring-boot-test-autoconfigure'
compileOnly 'com.google.code.findbugs:jsr305:3.0.2'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
annotationProcessor 'org.springframework.boot:spring-boot-autoconfigure-processor'
testImplementation project(':spring-graphql-test')
testImplementation 'com.fasterxml.jackson.core:jackson-databind'
testImplementation 'org.springframework:spring-webflux'
testImplementation 'org.springframework:spring-webmvc'
testImplementation 'org.springframework:spring-websocket'
testImplementation 'io.projectreactor:reactor-test'
testImplementation 'io.projectreactor.netty:reactor-netty'
testImplementation 'javax.servlet:javax.servlet-api'
testImplementation 'javax.validation:validation-api'
testImplementation 'org.hibernate.validator:hibernate-validator'
testImplementation 'org.apache.tomcat.embed:tomcat-embed-core'
testImplementation 'org.apache.tomcat.embed:tomcat-embed-el'
testImplementation 'org.apache.tomcat.embed:tomcat-embed-websocket'
testImplementation 'org.springframework.boot:spring-boot-actuator-autoconfigure'
testImplementation 'io.micrometer:micrometer-core'
testImplementation 'org.springframework.security:spring-security-config'
testImplementation 'org.springframework.security:spring-security-web'
testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
test {
useJUnitPlatform()
testLogging {
events "passed", "skipped", "failed"
}
}
apply from: "${rootDir}/gradle/publishing.gradle"

View File

@@ -1,98 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import graphql.GraphQL;
import graphql.execution.instrumentation.Instrumentation;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.graphql.execution.MissingSchemaException;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
/**
* {@link EnableAutoConfiguration Auto-configuration} for creating a
* {@link GraphQlSource}.
*
* @author Brian Clozel
* @since 1.0.0
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({GraphQL.class, GraphQlSource.class})
@EnableConfigurationProperties(GraphQlProperties.class)
public class GraphQlAutoConfiguration {
private static final Log logger = LogFactory.getLog(GraphQlAutoConfiguration.class);
@Bean
@ConditionalOnMissingBean
public GraphQlSource graphQlSource(ResourcePatternResolver resourcePatternResolver, GraphQlProperties properties,
ObjectProvider<DataFetcherExceptionResolver> exceptionResolversProvider,
ObjectProvider<Instrumentation> instrumentationsProvider,
ObjectProvider<GraphQlSourceBuilderCustomizer> sourceCustomizers,
ObjectProvider<RuntimeWiringConfigurer> wiringConfigurers) {
List<Resource> schemaResources = resolveSchemaResources(resourcePatternResolver, properties.getSchema().getLocations(),
properties.getSchema().getFileExtensions());
GraphQlSource.Builder builder = GraphQlSource.builder()
.schemaResources(schemaResources.toArray(new Resource[0]))
.exceptionResolvers(exceptionResolversProvider.orderedStream().collect(Collectors.toList()))
.instrumentation(instrumentationsProvider.orderedStream().collect(Collectors.toList()));
wiringConfigurers.orderedStream().forEach(builder::configureRuntimeWiring);
sourceCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
try {
return builder.build();
}
catch (MissingSchemaException exc) {
throw new InvalidSchemaLocationsException(properties.getSchema().getLocations(), resourcePatternResolver, exc);
}
}
private List<Resource> resolveSchemaResources(ResourcePatternResolver resolver, String[] schemaLocations, String[] fileExtensions) {
List<Resource> schemaResources = new ArrayList<>();
for (String location : schemaLocations) {
for (String extension : fileExtensions) {
String resourcePattern = location + "*" + extension;
try {
schemaResources.addAll(Arrays.asList(resolver.getResources(resourcePattern)));
}
catch (IOException ex) {
logger.debug("Could not resolve schema location: '" + resourcePattern + "'", ex);
}
}
}
return schemaResources;
}
}

View File

@@ -1,160 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.convert.DurationUnit;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
import org.springframework.web.cors.CorsConfiguration;
/**
* Configuration properties for GraphQL endpoint's CORS support.
*
* @author Andy Wilkinson
* @author Brian Clozel
* @since 1.0.0
*/
@ConfigurationProperties(prefix = "spring.graphql.cors")
public class GraphQlCorsProperties {
/**
* Comma-separated list of origins to allow with '*' allowing all origins.
* When allow-credentials is enabled, '*' cannot be used, and setting
* origin patterns should be considered instead.
* When neither allowed origins nor allowed origin patterns are set,
* cross-origin requests are effectively disabled.
*/
private List<String> allowedOrigins = new ArrayList<>();
/**
* Comma-separated list of origin patterns to allow. Unlike allowed origins
* which only support '*', origin patterns are more flexible, e.g.
* 'https://*.example.com', and can be used with allow-credentials.
* When neither allowed origins nor allowed origin patterns are set,
* cross-origin requests are effectively disabled.
*/
private List<String> allowedOriginPatterns = new ArrayList<>();
/**
* Comma-separated list of HTTP methods to allow. '*' allows all methods.
* When not set, defaults to GET.
*/
private List<String> allowedMethods = new ArrayList<>();
/**
* Comma-separated list of HTTP headers to allow in a request. '*' allows all headers.
*/
private List<String> allowedHeaders = new ArrayList<>();
/**
* Comma-separated list of headers to include in a response.
*/
private List<String> exposedHeaders = new ArrayList<>();
/**
* Whether credentials are supported. When not set, credentials are not supported.
*/
@Nullable
private Boolean allowCredentials;
/**
* How long the response from a pre-flight request can be cached by clients. If a
* duration suffix is not specified, seconds will be used.
*/
@DurationUnit(ChronoUnit.SECONDS)
private Duration maxAge = Duration.ofSeconds(1800);
public List<String> getAllowedOrigins() {
return this.allowedOrigins;
}
public void setAllowedOrigins(List<String> allowedOrigins) {
this.allowedOrigins = allowedOrigins;
}
public List<String> getAllowedOriginPatterns() {
return this.allowedOriginPatterns;
}
public void setAllowedOriginPatterns(List<String> allowedOriginPatterns) {
this.allowedOriginPatterns = allowedOriginPatterns;
}
public List<String> getAllowedMethods() {
return this.allowedMethods;
}
public void setAllowedMethods(List<String> allowedMethods) {
this.allowedMethods = allowedMethods;
}
public List<String> getAllowedHeaders() {
return this.allowedHeaders;
}
public void setAllowedHeaders(List<String> allowedHeaders) {
this.allowedHeaders = allowedHeaders;
}
public List<String> getExposedHeaders() {
return this.exposedHeaders;
}
public void setExposedHeaders(List<String> exposedHeaders) {
this.exposedHeaders = exposedHeaders;
}
@Nullable
public Boolean getAllowCredentials() {
return this.allowCredentials;
}
public void setAllowCredentials(Boolean allowCredentials) {
this.allowCredentials = allowCredentials;
}
public Duration getMaxAge() {
return this.maxAge;
}
public void setMaxAge(Duration maxAge) {
this.maxAge = maxAge;
}
@Nullable
public CorsConfiguration toCorsConfiguration() {
if (CollectionUtils.isEmpty(this.allowedOrigins) && CollectionUtils.isEmpty(this.allowedOriginPatterns)) {
return null;
}
PropertyMapper map = PropertyMapper.get();
CorsConfiguration config = new CorsConfiguration();
map.from(this::getAllowedOrigins).to(config::setAllowedOrigins);
map.from(this::getAllowedOriginPatterns).to(config::setAllowedOriginPatterns);
map.from(this::getAllowedHeaders).whenNot(CollectionUtils::isEmpty).to(config::setAllowedHeaders);
map.from(this::getAllowedMethods).whenNot(CollectionUtils::isEmpty).to(config::setAllowedMethods);
map.from(this::getExposedHeaders).whenNot(CollectionUtils::isEmpty).to(config::setExposedHeaders);
map.from(this::getMaxAge).whenNonNull().as(Duration::getSeconds).to(config::setMaxAge);
map.from(this::getAllowCredentials).whenNonNull().to(config::setAllowCredentials);
return config;
}
}

View File

@@ -1,184 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot;
import java.time.Duration;
import java.util.Arrays;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* {@link ConfigurationProperties properties} for Spring GraphQL.
*
* @author Brian Clozel
* @since 1.0.0
*/
@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();
private final GraphiQL graphiql = new GraphiQL();
private final Websocket websocket = new Websocket();
public String getPath() {
return this.path;
}
public void setPath(String path) {
this.path = path;
}
public Schema getSchema() {
return this.schema;
}
public GraphiQL getGraphiql() {
return this.graphiql;
}
public Websocket getWebsocket() {
return this.websocket;
}
public static class Schema {
/**
* Locations of GraphQL schema files.
*/
private String[] locations = new String[] { "classpath:graphql/**/"};
/**
* File extensions for GraphQL schema files.
*/
private String[] fileExtensions = new String[] { ".graphqls", ".gqls"};
private final Printer printer = new Printer();
public String[] getLocations() {
return this.locations;
}
public void setLocations(String[] locations) {
this.locations = appendSlashIfNecessary(locations);
}
public String[] getFileExtensions() {
return this.fileExtensions;
}
public void setFileExtensions(String[] fileExtensions) {
this.fileExtensions = fileExtensions;
}
private String[] appendSlashIfNecessary(String[] locations) {
return Arrays.stream(locations)
.map(location -> location.endsWith("/") ? location : location + "/")
.toArray(String[]::new);
}
public Printer getPrinter() {
return this.printer;
}
public static class Printer {
/**
* Whether the endpoint that prints the schema is enabled.
*/
private boolean enabled = false;
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}
}
public static class GraphiQL {
/**
* Path to the GraphiQL UI endpoint.
*/
private String path = "/graphiql";
/**
* Whether the default GraphiQL UI is enabled.
*/
private boolean enabled = true;
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;
}
}
public static class Websocket {
/**
* Path of the GraphQL WebSocket subscription endpoint.
*/
private String path;
/**
* Time within which the initial {@code CONNECTION_INIT} type message must be
* received.
*/
private Duration connectionInitTimeout = Duration.ofSeconds(60);
public String getPath() {
return this.path;
}
public void setPath(String path) {
this.path = path;
}
public Duration getConnectionInitTimeout() {
return this.connectionInitTimeout;
}
public void setConnectionInitTimeout(Duration connectionInitTimeout) {
this.connectionInitTimeout = connectionInitTimeout;
}
}
}

View File

@@ -1,71 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot;
import graphql.GraphQL;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.graphql.GraphQlService;
import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer;
import org.springframework.graphql.execution.BatchLoaderRegistry;
import org.springframework.graphql.execution.DefaultBatchLoaderRegistry;
import org.springframework.graphql.execution.ExecutionGraphQlService;
import org.springframework.graphql.execution.GraphQlSource;
/**
* {@link EnableAutoConfiguration Auto-configuration} for creating a
* {@link GraphQlService}.
*
* @author Brian Clozel
* @since 1.0.0
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({GraphQL.class, GraphQlService.class})
@AutoConfigureAfter(GraphQlAutoConfiguration.class)
public class GraphQlServiceAutoConfiguration {
private final BatchLoaderRegistry batchLoaderRegistry = new DefaultBatchLoaderRegistry();
@Bean
@ConditionalOnMissingBean
public BatchLoaderRegistry batchLoaderRegistry() {
return this.batchLoaderRegistry;
}
@Bean
@ConditionalOnMissingBean
public GraphQlService graphQlService(GraphQlSource graphQlSource) {
ExecutionGraphQlService service = new ExecutionGraphQlService(graphQlSource);
service.addDataLoaderRegistrar(this.batchLoaderRegistry);
return service;
}
@Bean
@ConditionalOnMissingBean
public AnnotatedControllerConfigurer annotatedControllerConfigurer() {
AnnotatedControllerConfigurer annotatedControllerConfigurer = new AnnotatedControllerConfigurer();
annotatedControllerConfigurer.setConversionService(new DefaultFormattingConversionService());
return annotatedControllerConfigurer;
}
}

View File

@@ -1,38 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot;
import org.springframework.graphql.execution.GraphQlSource;
/**
* Callback interface that can be implemented by beans wishing to customize
* properties of {@link org.springframework.graphql.execution.GraphQlSource.Builder}
* whilst retaining default auto-configuration.
*
* @author Rossen Stoyanchev
* @since 1.0.0
*/
@FunctionalInterface
public interface GraphQlSourceBuilderCustomizer {
/**
* Customize the {@link GraphQlSource.Builder} instance.
* @param builder builder the builder to customize
*/
void customize(GraphQlSource.Builder builder);
}

View File

@@ -1,172 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot;
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.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
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.graphql.web.webflux.GraphQlWebSocketHandler;
import org.springframework.graphql.web.webflux.GraphiQlHandler;
import org.springframework.graphql.web.webflux.SchemaHandler;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.config.CorsRegistry;
import org.springframework.web.reactive.config.WebFluxConfigurer;
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 org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
import org.springframework.web.reactive.socket.server.support.WebSocketUpgradeHandlerPredicate;
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 1.0.0
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@ConditionalOnClass({GraphQL.class, GraphQlHttpHandler.class})
@AutoConfigureAfter(GraphQlServiceAutoConfiguration.class)
@EnableConfigurationProperties(GraphQlCorsProperties.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<WebInterceptor> interceptorsProvider) {
return WebGraphQlHandler.builder(service)
.interceptors(interceptorsProvider.orderedStream().collect(Collectors.toList())).build();
}
@Bean
public RouterFunction<ServerResponse> 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);
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);
}
return builder.build();
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "spring.graphql.websocket", name = "path")
public static class WebSocketConfiguration {
@Bean
@ConditionalOnMissingBean
public GraphQlWebSocketHandler graphQlWebSocketHandler(WebGraphQlHandler webGraphQlHandler,
GraphQlProperties properties, ServerCodecConfigurer configurer) {
return new GraphQlWebSocketHandler(webGraphQlHandler, configurer,
properties.getWebsocket().getConnectionInitTimeout());
}
@Bean
public HandlerMapping graphQlWebSocketEndpoint(GraphQlWebSocketHandler graphQlWebSocketHandler,
GraphQlProperties properties) {
String path = properties.getWebsocket().getPath();
if (logger.isInfoEnabled()) {
logger.info("GraphQL endpoint WebSocket " + path);
}
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setHandlerPredicate(new WebSocketUpgradeHandlerPredicate());
mapping.setUrlMap(Collections.singletonMap(path, graphQlWebSocketHandler));
mapping.setOrder(-2); // Ahead of HTTP endpoint ("routerFunctionMapping" bean)
return mapping;
}
}
@Configuration(proxyBeanMethods = false)
public static class GraphQlEndpointCorsConfiguration implements WebFluxConfigurer {
final GraphQlProperties graphQlProperties;
final GraphQlCorsProperties corsProperties;
public GraphQlEndpointCorsConfiguration(GraphQlProperties graphQlProps, GraphQlCorsProperties corsProps) {
this.graphQlProperties = graphQlProps;
this.corsProperties = corsProps;
}
@Override
public void addCorsMappings(CorsRegistry registry) {
CorsConfiguration configuration = this.corsProperties.toCorsConfiguration();
if (configuration != null) {
registry.addMapping(this.graphQlProperties.getPath()).combine(configuration);
}
}
}
}

View File

@@ -1,193 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;
import javax.websocket.server.ServerContainer;
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.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
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.graphql.web.webmvc.GraphQlWebSocketHandler;
import org.springframework.graphql.web.webmvc.GraphiQlHandler;
import org.springframework.graphql.web.webmvc.SchemaHandler;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.RouterFunctions;
import org.springframework.web.servlet.function.ServerResponse;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.support.DefaultHandshakeHandler;
import org.springframework.web.socket.server.support.WebSocketHandlerMapping;
import org.springframework.web.socket.server.support.WebSocketHttpRequestHandler;
import static org.springframework.web.servlet.function.RequestPredicates.accept;
import static org.springframework.web.servlet.function.RequestPredicates.contentType;
/**
* {@link EnableAutoConfiguration Auto-configuration} for enabling Spring GraphQL over
* Spring MVC.
*
* @author Brian Clozel
* @since 1.0.0
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass({GraphQL.class, GraphQlHttpHandler.class})
@AutoConfigureAfter(GraphQlServiceAutoConfiguration.class)
@EnableConfigurationProperties(GraphQlCorsProperties.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<WebInterceptor> interceptorsProvider,
ObjectProvider<ThreadLocalAccessor> accessorsProvider) {
return WebGraphQlHandler.builder(service)
.interceptors(interceptorsProvider.orderedStream().collect(Collectors.toList()))
.threadLocalAccessors(accessorsProvider.orderedStream().collect(Collectors.toList())).build();
}
@Bean
public RouterFunction<ServerResponse> 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,
contentType(MediaType.APPLICATION_JSON).and(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);
}
return builder.build();
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ServerContainer.class, WebSocketHandler.class})
@ConditionalOnProperty(prefix = "spring.graphql.websocket", name = "path")
public static class WebSocketConfiguration {
@Bean
@ConditionalOnMissingBean
public GraphQlWebSocketHandler graphQlWebSocketHandler(WebGraphQlHandler webGraphQlHandler,
GraphQlProperties properties, HttpMessageConverters converters) {
return new GraphQlWebSocketHandler(webGraphQlHandler, getJsonConverter(converters),
properties.getWebsocket().getConnectionInitTimeout());
}
@SuppressWarnings("unchecked")
private static GenericHttpMessageConverter<Object> getJsonConverter(HttpMessageConverters converters) {
return converters.getConverters().stream()
.filter((candidate) -> candidate.canRead(Map.class, MediaType.APPLICATION_JSON))
.findFirst()
.map(converter -> (GenericHttpMessageConverter<Object>) converter)
.orElseThrow(() -> new IllegalStateException("No JSON converter"));
}
@Bean
public HandlerMapping graphQlWebSocketMapping(GraphQlWebSocketHandler handler, GraphQlProperties properties) {
String path = properties.getWebsocket().getPath();
if (logger.isInfoEnabled()) {
logger.info("GraphQL endpoint WebSocket " + path);
}
WebSocketHandlerMapping mapping = new WebSocketHandlerMapping();
mapping.setWebSocketUpgradeMatch(true);
mapping.setUrlMap(Collections.singletonMap(path,
new WebSocketHttpRequestHandler(handler, new DefaultHandshakeHandler())));
mapping.setOrder(2); // Ahead of HTTP endpoint ("routerFunctionMapping" bean)
return mapping;
}
}
@Configuration(proxyBeanMethods = false)
public static class GraphQlEndpointCorsConfiguration implements WebMvcConfigurer {
final GraphQlProperties graphQlProperties;
final GraphQlCorsProperties corsProperties;
public GraphQlEndpointCorsConfiguration(GraphQlProperties graphQlProps, GraphQlCorsProperties corsProps) {
this.graphQlProperties = graphQlProps;
this.corsProperties = corsProps;
}
@Override
public void addCorsMappings(CorsRegistry registry) {
CorsConfiguration configuration = this.corsProperties.toCorsConfiguration();
if (configuration != null) {
registry.addMapping(this.graphQlProperties.getPath()).combine(configuration);
}
}
}
}

View File

@@ -1,92 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.springframework.core.NestedRuntimeException;
import org.springframework.core.io.support.ResourcePatternResolver;
/**
* {@link InvalidSchemaLocationsException} thrown when no schema file could be found in the provided locations.
*
* @author Brian Clozel
* @since 1.0.0
*/
@SuppressWarnings("serial")
public class InvalidSchemaLocationsException extends NestedRuntimeException {
private final List<SchemaLocation> schemaLocations;
public InvalidSchemaLocationsException(String[] locations, ResourcePatternResolver resolver) {
this(locations, resolver, null);
}
public InvalidSchemaLocationsException(String[] locations, ResourcePatternResolver resolver, Throwable cause) {
super("No schema file could be found in the provided locations.", cause);
List<SchemaLocation> providedLocations = new ArrayList<>();
for (String location : locations) {
try {
String uri = resolver.getResource(location).getURI().toASCIIString();
providedLocations.add(new SchemaLocation(location, uri));
}
catch (IOException ex) {
providedLocations.add(new SchemaLocation(location, ""));
}
}
this.schemaLocations = Collections.unmodifiableList(providedLocations);
}
/**
* Return the list of provided locations where to look for schemas.
*/
public List<SchemaLocation> getSchemaLocations() {
return this.schemaLocations;
}
/**
* The location where to look for schemas.
*/
public static class SchemaLocation {
private final String location;
private final String uri;
SchemaLocation(String location, String uri) {
this.location = location;
this.uri = uri;
}
/**
* Return the location String to be resolved by a {@link ResourcePatternResolver}.
*/
public String getLocation() {
return this.location;
}
/**
* Return the resolved URI String for this location, an empty String if resolution failed.
*/
public String getUri() {
return this.uri;
}
}
}

View File

@@ -1,39 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot;
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;
/**
* An implementation of {@link AbstractFailureAnalyzer} to analyze failures caused by
* {@link InvalidSchemaLocationsException}.
*
* @author Brian Clozel
*/
class InvalidSchemaLocationsExceptionFailureAnalyzer extends AbstractFailureAnalyzer<InvalidSchemaLocationsException> {
@Override
protected FailureAnalysis analyze(Throwable rootFailure, InvalidSchemaLocationsException cause) {
String message = "Could not find any GraphQL schema file under configured locations.";
StringBuilder action = new StringBuilder("Check that the following locations contain schema files: " + System.lineSeparator());
for (InvalidSchemaLocationsException.SchemaLocation schemaLocation : cause.getSchemaLocations()) {
action.append(String.format("- '%s' (%s)" + System.lineSeparator(), schemaLocation.getUri(), schemaLocation.getLocation()));
}
return new FailureAnalysis(message, action.toString(), cause);
}
}

View File

@@ -1,72 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot.actuate.metrics;
import java.util.List;
import graphql.ExecutionResult;
import graphql.GraphQLError;
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters;
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
import graphql.schema.DataFetcher;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Tags;
/**
* Default implementation for {@link GraphQlTagsProvider}.
*
* @author Brian Clozel
* @since 1.0.0
*/
public class DefaultGraphQlTagsProvider implements GraphQlTagsProvider {
private final List<GraphQlTagsContributor> contributors;
public DefaultGraphQlTagsProvider(List<GraphQlTagsContributor> contributors) {
this.contributors = contributors;
}
@Override
public Iterable<Tag> getExecutionTags(InstrumentationExecutionParameters parameters, ExecutionResult result,
Throwable exception) {
Tags tags = Tags.of(GraphQlTags.executionOutcome(result, exception));
for (GraphQlTagsContributor contributor : this.contributors) {
tags = tags.and(contributor.getExecutionTags(parameters, result, exception));
}
return tags;
}
@Override
public Iterable<Tag> getErrorTags(InstrumentationExecutionParameters parameters, GraphQLError error) {
Tags tags = Tags.of(GraphQlTags.errorType(error), GraphQlTags.errorPath(error));
for (GraphQlTagsContributor contributor : this.contributors) {
tags = tags.and(contributor.getErrorTags(parameters, error));
}
return tags;
}
@Override
public Iterable<Tag> getDataFetchingTags(DataFetcher<?> dataFetcher, InstrumentationFieldFetchParameters parameters,
Throwable exception) {
Tags tags = Tags.of(GraphQlTags.dataFetchingOutcome(exception), GraphQlTags.dataFetchingPath(parameters));
for (GraphQlTagsContributor contributor : this.contributors) {
tags = tags.and(contributor.getDataFetchingTags(dataFetcher, parameters, exception));
}
return tags;
}
}

View File

@@ -1,61 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot.actuate.metrics;
import java.util.stream.Collectors;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration;
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.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* {@link EnableAutoConfiguration Auto-configuration} for instrumentation of Spring
* GraphQL endpoints.
*
* @author Brian Clozel
* @since 1.0.0
*/
@Configuration(proxyBeanMethods = false)
@AutoConfigureAfter({ MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class,
SimpleMetricsExportAutoConfiguration.class })
@ConditionalOnBean(MeterRegistry.class)
@EnableConfigurationProperties(GraphQlMetricsProperties.class)
public class GraphQlMetricsAutoConfiguration {
@Bean
@ConditionalOnMissingBean(GraphQlTagsProvider.class)
public DefaultGraphQlTagsProvider graphQlTagsProvider(ObjectProvider<GraphQlTagsContributor> contributors) {
return new DefaultGraphQlTagsProvider(contributors.orderedStream().collect(Collectors.toList()));
}
@Bean
public GraphQlMetricsInstrumentation graphQlMetricsInstrumentation(MeterRegistry meterRegistry,
GraphQlTagsProvider tagsProvider, GraphQlMetricsProperties properties) {
return new GraphQlMetricsInstrumentation(meterRegistry, tagsProvider, properties.getAutotime());
}
}

View File

@@ -1,161 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot.actuate.metrics;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.atomic.AtomicLong;
import graphql.ExecutionResult;
import graphql.execution.instrumentation.InstrumentationContext;
import graphql.execution.instrumentation.InstrumentationState;
import graphql.execution.instrumentation.SimpleInstrumentation;
import graphql.execution.instrumentation.SimpleInstrumentationContext;
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters;
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
import graphql.schema.DataFetcher;
import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Timer;
import org.springframework.boot.actuate.metrics.AutoTimer;
import org.springframework.lang.Nullable;
class GraphQlMetricsInstrumentation extends SimpleInstrumentation {
private final MeterRegistry registry;
private final GraphQlTagsProvider tagsProvider;
private final AutoTimer autoTimer;
private final DistributionSummary dataFetchingSummary;
GraphQlMetricsInstrumentation(MeterRegistry registry, GraphQlTagsProvider tagsProvider, AutoTimer autoTimer) {
this.registry = registry;
this.tagsProvider = tagsProvider;
this.autoTimer = autoTimer;
this.dataFetchingSummary = DistributionSummary.builder("graphql.request.datafetch.count")
.baseUnit("calls")
.description("Count of DataFetcher calls per request.")
.register(this.registry);
}
@Override
public InstrumentationState createState() {
return new RequestMetricsInstrumentationState(this.autoTimer, this.registry);
}
@Override
public InstrumentationContext<ExecutionResult> beginExecution(InstrumentationExecutionParameters parameters) {
if (this.autoTimer.isEnabled()) {
RequestMetricsInstrumentationState state = parameters.getInstrumentationState();
state.startTimer();
return new SimpleInstrumentationContext<ExecutionResult>() {
@Override
public void onCompleted(ExecutionResult result, Throwable exc) {
Iterable<Tag> tags = GraphQlMetricsInstrumentation.this.tagsProvider.getExecutionTags(parameters,
result, exc);
state.tags(tags).stopTimer();
if (!result.getErrors().isEmpty()) {
result.getErrors()
.forEach((error) -> GraphQlMetricsInstrumentation.this.registry.counter("graphql.error",
GraphQlMetricsInstrumentation.this.tagsProvider.getErrorTags(parameters, error))
.increment());
}
GraphQlMetricsInstrumentation.this.dataFetchingSummary.record(state.getDataFetchingCount());
}
};
}
return super.beginExecution(parameters);
}
@Override
public DataFetcher<?> instrumentDataFetcher(DataFetcher<?> dataFetcher,
InstrumentationFieldFetchParameters parameters) {
if (this.autoTimer.isEnabled() && !parameters.isTrivialDataFetcher()) {
return (environment) -> {
Timer.Sample sample = Timer.start(this.registry);
try {
Object value = dataFetcher.get(environment);
if (value instanceof CompletionStage<?>) {
CompletionStage<?> completion = (CompletionStage<?>) value;
return completion.whenComplete(
(result, error) -> recordDataFetcherMetric(sample, dataFetcher, parameters, error));
}
else {
recordDataFetcherMetric(sample, dataFetcher, parameters, null);
return value;
}
}
catch (Throwable throwable) {
recordDataFetcherMetric(sample, dataFetcher, parameters, throwable);
throw throwable;
}
};
}
return super.instrumentDataFetcher(dataFetcher, parameters);
}
private void recordDataFetcherMetric(Timer.Sample sample, DataFetcher<?> dataFetcher,
InstrumentationFieldFetchParameters parameters, @Nullable Throwable throwable) {
Timer.Builder timer = this.autoTimer.builder("graphql.datafetcher");
timer.tags(this.tagsProvider.getDataFetchingTags(dataFetcher, parameters, throwable));
sample.stop(timer.register(this.registry));
RequestMetricsInstrumentationState state = parameters.getInstrumentationState();
state.incrementDataFetchingCount();
}
static class RequestMetricsInstrumentationState implements InstrumentationState {
private final MeterRegistry registry;
private final Timer.Builder timer;
private Timer.Sample sample;
private AtomicLong dataFetchingCount = new AtomicLong(0L);
RequestMetricsInstrumentationState(AutoTimer autoTimer, MeterRegistry registry) {
this.timer = autoTimer.builder("graphql.request");
this.registry = registry;
}
RequestMetricsInstrumentationState tags(Iterable<Tag> tags) {
this.timer.tags(tags);
return this;
}
void startTimer() {
this.sample = Timer.start(this.registry);
}
void stopTimer() {
this.sample.stop(this.timer.register(this.registry));
}
void incrementDataFetchingCount() {
this.dataFetchingCount.incrementAndGet();
}
long getDataFetchingCount() {
return this.dataFetchingCount.get();
}
}
}

View File

@@ -1,45 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot.actuate.metrics;
import org.springframework.boot.actuate.autoconfigure.metrics.AutoTimeProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
/**
* {@link ConfigurationProperties properties} for Spring GraphQL.
* <p>
* This class could be later merged with
* {@link org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties}.
*
* @author Brian Clozel
* @since 1.0.0
*/
@ConfigurationProperties("management.metrics.graphql")
public class GraphQlMetricsProperties {
/**
* Auto-timed queries settings.
*/
@NestedConfigurationProperty
private final AutoTimeProperties autotime = new AutoTimeProperties();
public AutoTimeProperties getAutotime() {
return this.autotime;
}
}

View File

@@ -1,102 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot.actuate.metrics;
import java.util.List;
import graphql.ErrorClassification;
import graphql.ErrorType;
import graphql.ExecutionResult;
import graphql.GraphQLError;
import graphql.execution.ExecutionStepInfo;
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
import graphql.schema.GraphQLObjectType;
import io.micrometer.core.instrument.Tag;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
/**
* Factory methods for Tags associated with a GraphQL request.
*
* @author Brian Clozel
* @since 1.0.0
*/
public final class GraphQlTags {
private static final Tag OUTCOME_SUCCESS = Tag.of("outcome", "SUCCESS");
private static final Tag OUTCOME_ERROR = Tag.of("outcome", "ERROR");
private static final Tag UNKNOWN_ERRORTYPE = Tag.of("errorType", "UNKNOWN");
private GraphQlTags() {
}
public static Tag executionOutcome(ExecutionResult result, @Nullable Throwable exception) {
if (exception == null && result.getErrors().isEmpty()) {
return OUTCOME_SUCCESS;
}
else {
return OUTCOME_ERROR;
}
}
public static Tag errorType(GraphQLError error) {
ErrorClassification errorType = error.getErrorType();
if (errorType instanceof ErrorType) {
return Tag.of("errorType", ((ErrorType) errorType).name());
}
return UNKNOWN_ERRORTYPE;
}
public static Tag errorPath(GraphQLError error) {
StringBuilder builder = new StringBuilder();
List<Object> pathSegments = error.getPath();
if (!CollectionUtils.isEmpty(pathSegments)) {
builder.append('$');
for (Object segment : pathSegments) {
try {
int index = Integer.parseUnsignedInt(segment.toString());
builder.append("[*]");
}
catch (NumberFormatException exc) {
builder.append('.');
builder.append(segment);
}
}
}
return Tag.of("errorPath", builder.toString());
}
public static Tag dataFetchingOutcome(@Nullable Throwable exception) {
return (exception != null) ? OUTCOME_ERROR : OUTCOME_SUCCESS;
}
public static Tag dataFetchingPath(InstrumentationFieldFetchParameters parameters) {
ExecutionStepInfo executionStepInfo = parameters.getExecutionStepInfo();
StringBuilder dataFetchingType = new StringBuilder();
if (executionStepInfo.hasParent() && executionStepInfo.getParent().getType() instanceof GraphQLObjectType) {
dataFetchingType.append(((GraphQLObjectType) executionStepInfo.getParent().getType()).getName());
dataFetchingType.append('.');
}
dataFetchingType.append(executionStepInfo.getPath().getSegmentName());
return Tag.of("path", dataFetchingType.toString());
}
}

View File

@@ -1,45 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot.actuate.metrics;
import graphql.ExecutionResult;
import graphql.GraphQLError;
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters;
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
import graphql.schema.DataFetcher;
import io.micrometer.core.instrument.Tag;
import org.springframework.lang.Nullable;
/**
* A contributor of {@link Tag Tags} for Spring GraphQL-based request handling. Typically
* used by a {@link GraphQlTagsProvider} to provide tags in addition to its defaults.
*
* @author Brian Clozel
* @since 1.0.0
*/
public interface GraphQlTagsContributor {
Iterable<Tag> getExecutionTags(InstrumentationExecutionParameters parameters, ExecutionResult result,
@Nullable Throwable exception);
Iterable<Tag> getErrorTags(InstrumentationExecutionParameters parameters, GraphQLError error);
Iterable<Tag> getDataFetchingTags(DataFetcher<?> dataFetcher, InstrumentationFieldFetchParameters parameters,
@Nullable Throwable exception);
}

View File

@@ -1,44 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot.actuate.metrics;
import graphql.ExecutionResult;
import graphql.GraphQLError;
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters;
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
import graphql.schema.DataFetcher;
import io.micrometer.core.instrument.Tag;
import org.springframework.lang.Nullable;
/**
* Provides {@link Tag Tags} for Spring GraphQL-based request handling.
*
* @author Brian Clozel
* @since 1.0.0
*/
public interface GraphQlTagsProvider {
Iterable<Tag> getExecutionTags(InstrumentationExecutionParameters parameters, ExecutionResult result,
@Nullable Throwable exception);
Iterable<Tag> getErrorTags(InstrumentationExecutionParameters parameters, GraphQLError error);
Iterable<Tag> getDataFetchingTags(DataFetcher<?> dataFetcher, InstrumentationFieldFetchParameters parameters,
@Nullable Throwable exception);
}

View File

@@ -1,25 +0,0 @@
/*
* 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.
*/
/**
* Provides instrumentation support.
*/
@NonNullApi
@NonNullFields
package org.springframework.graphql.boot.actuate.metrics;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

View File

@@ -1,70 +0,0 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.boot.data;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import graphql.GraphQL;
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.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor;
import org.springframework.graphql.boot.GraphQlAutoConfiguration;
import org.springframework.graphql.boot.GraphQlSourceBuilderCustomizer;
import org.springframework.graphql.data.query.QueryByExampleDataFetcher;
import org.springframework.graphql.execution.GraphQlSource;
/**
* {@link EnableAutoConfiguration Auto-configuration} that creates a
* {@link GraphQlSourceBuilderCustomizer}s to detect Spring Data repositories
* with Query By Example support and register them as {@code DataFetcher}s for any
* queries with a matching return type.
*
* @author Rossen Stoyanchev
* @since 1.0.0
* @see QueryByExampleDataFetcher#autoRegistrationTypeVisitor(List, List)
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@ConditionalOnClass({GraphQL.class, ReactiveQueryByExampleExecutor.class })
@ConditionalOnBean(GraphQlSource.class)
@AutoConfigureAfter(GraphQlAutoConfiguration.class)
public class GraphQlWebFluxQueryByExampleAutoConfiguration {
@Bean
public GraphQlSourceBuilderCustomizer reactiveQueryByExampleRegistrar(
ObjectProvider<ReactiveQueryByExampleExecutor<?>> executorsProvider) {
return builder -> {
List<ReactiveQueryByExampleExecutor<?>> executors =
executorsProvider.stream().collect(Collectors.toList());
if (!executors.isEmpty()) {
builder.typeVisitors(Collections.singletonList(
QueryByExampleDataFetcher.autoRegistrationTypeVisitor(Collections.emptyList(), executors)));
}
};
}
}

View File

@@ -1,72 +0,0 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.boot.data;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import graphql.GraphQL;
import graphql.schema.GraphQLTypeVisitor;
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.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor;
import org.springframework.graphql.boot.GraphQlAutoConfiguration;
import org.springframework.graphql.boot.GraphQlSourceBuilderCustomizer;
import org.springframework.graphql.data.query.QuerydslDataFetcher;
import org.springframework.graphql.execution.GraphQlSource;
/**
* {@link EnableAutoConfiguration Auto-configuration} that creates a
* {@link GraphQlSourceBuilderCustomizer}s to detect Spring Data repositories
* with Querydsl support and register them as {@code DataFetcher}s for any
* queries with a matching return type.
*
* @author Rossen Stoyanchev
* @since 1.0.0
* @see QuerydslDataFetcher#autoRegistrationTypeVisitor(List, List)
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@ConditionalOnClass({GraphQL.class, ReactiveQuerydslPredicateExecutor.class })
@ConditionalOnBean(GraphQlSource.class)
@AutoConfigureAfter(GraphQlAutoConfiguration.class)
public class GraphQlWebFluxQuerydslAutoConfiguration {
@Bean
public GraphQlSourceBuilderCustomizer reactiveQuerydslRegistrar(
ObjectProvider<ReactiveQuerydslPredicateExecutor<?>> executorsProvider) {
return builder -> {
List<ReactiveQuerydslPredicateExecutor<?>> executors =
executorsProvider.stream().collect(Collectors.toList());
if (!executors.isEmpty()) {
builder.typeVisitors(Collections.singletonList(
QuerydslDataFetcher.autoRegistrationTypeVisitor(Collections.emptyList(), executors)));
}
};
}
}

View File

@@ -1,75 +0,0 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.boot.data;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import graphql.GraphQL;
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.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.repository.query.QueryByExampleExecutor;
import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor;
import org.springframework.graphql.boot.GraphQlAutoConfiguration;
import org.springframework.graphql.boot.GraphQlSourceBuilderCustomizer;
import org.springframework.graphql.data.query.QueryByExampleDataFetcher;
import org.springframework.graphql.execution.GraphQlSource;
/**
* {@link EnableAutoConfiguration Auto-configuration} that creates a
* {@link GraphQlSourceBuilderCustomizer}s to detect Spring Data repositories
* with Query By Example support and register them as {@code DataFetcher}s for
* any queries with a matching return type.
*
* @author Rossen Stoyanchev
* @since 1.0.0
* @see QueryByExampleDataFetcher#autoRegistrationTypeVisitor(List, List)
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass({GraphQL.class, QueryByExampleExecutor.class })
@ConditionalOnBean(GraphQlSource.class)
@AutoConfigureAfter(GraphQlAutoConfiguration.class)
public class GraphQlWebMvcQueryByExampleAutoConfiguration {
@Bean
public GraphQlSourceBuilderCustomizer queryByExampleRegistrar(
ObjectProvider<QueryByExampleExecutor<?>> executorsProvider,
ObjectProvider<ReactiveQueryByExampleExecutor<?>> reactiveExecutorsProvider) {
return builder -> {
List<QueryByExampleExecutor<?>> executors =
executorsProvider.stream().collect(Collectors.toList());
List<ReactiveQueryByExampleExecutor<?>> reactiveExecutors =
reactiveExecutorsProvider.stream().collect(Collectors.toList());
if (!executors.isEmpty()) {
builder.typeVisitors(Collections.singletonList(
QueryByExampleDataFetcher.autoRegistrationTypeVisitor(executors, reactiveExecutors)));
}
};
}
}

View File

@@ -1,76 +0,0 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.boot.data;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import graphql.GraphQL;
import graphql.schema.GraphQLTypeVisitor;
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.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor;
import org.springframework.graphql.boot.GraphQlAutoConfiguration;
import org.springframework.graphql.boot.GraphQlSourceBuilderCustomizer;
import org.springframework.graphql.data.query.QuerydslDataFetcher;
import org.springframework.graphql.execution.GraphQlSource;
/**
* {@link EnableAutoConfiguration Auto-configuration} that creates a
* {@link GraphQlSourceBuilderCustomizer}s to detect Spring Data repositories
* with Querydsl support and register them as {@code DataFetcher}s for any
* queries with a matching return type.
*
* @author Rossen Stoyanchev
* @since 1.0.0
* @see QuerydslDataFetcher#autoRegistrationTypeVisitor(List, List)
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass({GraphQL.class, QuerydslPredicateExecutor.class })
@ConditionalOnBean(GraphQlSource.class)
@AutoConfigureAfter(GraphQlAutoConfiguration.class)
public class GraphQlWebMvcQuerydslAutoConfiguration {
@Bean
public GraphQlSourceBuilderCustomizer querydslRegistrar(
ObjectProvider<QuerydslPredicateExecutor<?>> executorsProvider,
ObjectProvider<ReactiveQuerydslPredicateExecutor<?>> reactiveExecutorsProvider) {
return builder -> {
List<QuerydslPredicateExecutor<?>> executors =
executorsProvider.stream().collect(Collectors.toList());
List<ReactiveQuerydslPredicateExecutor<?>> reactiveExecutors =
reactiveExecutorsProvider.stream().collect(Collectors.toList());
if (!executors.isEmpty()) {
builder.typeVisitors(Collections.singletonList(
QuerydslDataFetcher.autoRegistrationTypeVisitor(executors, reactiveExecutors)));
}
};
}
}

View File

@@ -1,25 +0,0 @@
/*
* 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 data integrations.
*/
@NonNullApi
@NonNullFields
package org.springframework.graphql.boot.data;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

View File

@@ -1,27 +0,0 @@
/*
* 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 to configure
* {@link org.springframework.graphql.execution.GraphQlSource},
* {@link org.springframework.graphql.GraphQlService}, and HTTP and WebSocket endpoints.
*/
@NonNullApi
@NonNullFields
package org.springframework.graphql.boot;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

View File

@@ -1,53 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot.security;
import graphql.GraphQL;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.boot.GraphQlWebFluxAutoConfiguration;
import org.springframework.graphql.security.ReactiveSecurityDataFetcherExceptionResolver;
import org.springframework.graphql.web.webflux.GraphQlHttpHandler;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
/**
* {@link EnableAutoConfiguration Auto-configuration} for enabling Security support
* for Spring GraphQL with WebFlux.
*
* @author Brian Clozel
* @since 1.0.0
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@ConditionalOnClass({GraphQL.class, GraphQlHttpHandler.class, EnableWebFluxSecurity.class})
@ConditionalOnBean(GraphQlHttpHandler.class)
@AutoConfigureAfter(GraphQlWebFluxAutoConfiguration.class)
public class GraphQlWebFluxSecurityAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public ReactiveSecurityDataFetcherExceptionResolver reactiveSecurityDataFetcherExceptionResolver() {
return new ReactiveSecurityDataFetcherExceptionResolver();
}
}

View File

@@ -1,61 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot.security;
import graphql.GraphQL;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.boot.GraphQlWebMvcAutoConfiguration;
import org.springframework.graphql.security.SecurityContextThreadLocalAccessor;
import org.springframework.graphql.security.SecurityDataFetcherExceptionResolver;
import org.springframework.graphql.web.webmvc.GraphQlHttpHandler;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
/**
* {@link EnableAutoConfiguration Auto-configuration} for enabling Security support
* for Spring GraphQL with MVC.
*
* @author Brian Clozel
* @since 1.0.0
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass({GraphQL.class, GraphQlHttpHandler.class, EnableWebSecurity.class})
@ConditionalOnBean(GraphQlHttpHandler.class)
@AutoConfigureAfter(GraphQlWebMvcAutoConfiguration.class)
public class GraphQlWebMvcSecurityAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public SecurityDataFetcherExceptionResolver securityDataFetcherExceptionResolver() {
return new SecurityDataFetcherExceptionResolver();
}
@Bean
@ConditionalOnMissingBean
public SecurityContextThreadLocalAccessor securityContextThreadLocalAccessor() {
return new SecurityContextThreadLocalAccessor();
}
}

View File

@@ -1,27 +0,0 @@
/*
* 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 to configure
* {@link org.springframework.graphql.security.SecurityDataFetcherExceptionResolver},
* {@link org.springframework.graphql.security.SecurityContextThreadLocalAccessor} for Security support.
*/
@NonNullApi
@NonNullFields
package org.springframework.graphql.boot.security;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

View File

@@ -1,48 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot.test;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
/**
* {@link ImportAutoConfiguration Auto-configuration imports} for typical Spring GraphQL
* tests. Most tests should consider using {@link GraphQlTest @GraphQlTest} rather than
* using this annotation directly.
*
* @author Brian Clozel
* @since 1.0.0
* @see GraphQlTest
* @see org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration
* @see org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration
* @see org.springframework.graphql.boot.GraphQlAutoConfiguration
* @see org.springframework.graphql.boot.GraphQlServiceAutoConfiguration
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@ImportAutoConfiguration
public @interface AutoConfigureGraphQl {
}

View File

@@ -1,152 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot.test;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.autoconfigure.OverrideAutoConfiguration;
import org.springframework.boot.test.autoconfigure.core.AutoConfigureCache;
import org.springframework.boot.test.autoconfigure.filter.TypeExcludeFilters;
import org.springframework.boot.test.autoconfigure.json.AutoConfigureJson;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.core.annotation.AliasFor;
import org.springframework.core.env.Environment;
import org.springframework.graphql.boot.test.tester.AutoConfigureGraphQlTester;
import org.springframework.graphql.boot.test.tester.AutoConfigureWebGraphQlTester;
import org.springframework.test.context.BootstrapWith;
import org.springframework.test.context.junit.jupiter.SpringExtension;
/**
* Annotation to perform GraphQL tests focusing on GraphQL request execution
* without a Web layer, and loading only a subset of the application
* configuration.
* <p>
* The annotation disables full auto-configuration and instead loads only
* components relevant to GraphQL tests, including the following:
* <ul>
* <li>{@code @Controller}
* <li>{@code RuntimeWiringConfigurer}
* <li>{@code @JsonComponent}
* <li>{@code Converter}
* <li>{@code GenericConverter}
* </ul>
* <p>
* The annotation does not automatically load {@code @Component}, {@code @Service},
* {@code @Repository}, and other beans.
* <p>
* By default, tests annotated with {@code @GraphQlTest} have a
* {@link org.springframework.graphql.test.tester.GraphQlTester} configured.
* For more fine-grained control of the GraphQlTester, use
* {@link AutoConfigureGraphQlTester @AutoConfigureGraphQlTester}.
* <p>
* Typically {@code @GraphQlTest} is used in combination with
* {@link org.springframework.boot.test.mock.mockito.MockBean @MockBean} or
* {@link org.springframework.context.annotation.Import @Import} to load any
* collaborators and other components required for the tests.
* <p>
* To load your full application configuration instead and test via
* {@code WebGraphQlTester}, consider using
* {@link org.springframework.boot.test.context.SpringBootTest @SpringBootTest}
* combined with {@link AutoConfigureWebGraphQlTester @AutoConfigureWebGraphQlTester}.
*
* @author Brian Clozel
* @since 1.0.0
* @see AutoConfigureGraphQlTester
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(GraphQlTestContextBootstrapper.class)
@ExtendWith(SpringExtension.class)
@OverrideAutoConfiguration(enabled = false)
@TypeExcludeFilters(GraphQlTypeExcludeFilter.class)
@AutoConfigureCache
@AutoConfigureJson
@AutoConfigureGraphQl
@AutoConfigureGraphQlTester
@ImportAutoConfiguration
public @interface GraphQlTest {
/**
* Properties in form {@literal key=value} that should be added to the Spring
* {@link Environment} before the test runs.
* @return the properties to add
*/
String[] properties() default {};
/**
* Specifies the controllers to test. This is an alias of {@link #controllers()} which
* can be used for brevity if no other attributes are defined. See
* {@link #controllers()} for details.
* @see #controllers()
* @return the controllers to test
*/
@AliasFor("controllers")
Class<?>[] value() default {};
/**
* Specifies the controllers to test. May be left blank if all {@code @Controller}
* beans should be added to the application context.
* @see #value()
* @return the controllers to test
*/
@AliasFor("value")
Class<?>[] controllers() default {};
/**
* Determines if default filtering should be used with
* {@link SpringBootApplication @SpringBootApplication}. By default, only
* {@code @Controller} (when no explicit {@link #controllers() controllers} are
* defined), {@code RuntimeWiringConfigurer}, {@code @JsonComponent},
* {@code Converter}, and {@code GenericConverter} beans are included.
* @see #includeFilters()
* @see #excludeFilters()
* @return if default filters should be used
*/
boolean useDefaultFilters() default true;
/**
* A set of include filters which can be used to add otherwise filtered beans to the
* application context.
* @return include filters to apply
*/
ComponentScan.Filter[] includeFilters() default {};
/**
* A set of exclude filters which can be used to filter beans that would otherwise be
* added to the application context.
* @return exclude filters to apply
*/
ComponentScan.Filter[] excludeFilters() default {};
/**
* Auto-configuration exclusions that should be applied for this test.
* @return auto-configuration exclusions to apply
*/
@AliasFor(annotation = ImportAutoConfiguration.class, attribute = "exclude")
Class<?>[] excludeAutoConfiguration() default {};
}

View File

@@ -1,35 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot.test;
import org.springframework.boot.test.context.SpringBootTestContextBootstrapper;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.test.context.TestContextBootstrapper;
/**
* {@link TestContextBootstrapper} for {@link GraphQlTest @GraphQlTest}
*
* @author Brian Clozel
*/
class GraphQlTestContextBootstrapper extends SpringBootTestContextBootstrapper {
@Override
protected String[] getProperties(Class<?> testClass) {
return MergedAnnotations.from(testClass, MergedAnnotations.SearchStrategy.INHERITED_ANNOTATIONS).get(GraphQlTest.class)
.getValue("properties", String[].class).orElse(null);
}
}

View File

@@ -1,92 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot.test;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import org.springframework.boot.context.TypeExcludeFilter;
import org.springframework.boot.jackson.JsonComponent;
import org.springframework.boot.test.autoconfigure.filter.StandardAnnotationCustomizableTypeExcludeFilter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
import org.springframework.stereotype.Controller;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
/**
* {@link TypeExcludeFilter} for {@link GraphQlTest @GraphQlTest}.
*
* @author Brian Clozel
* @since 1.0.0
*/
public class GraphQlTypeExcludeFilter extends StandardAnnotationCustomizableTypeExcludeFilter<GraphQlTest> {
private static final Class<?>[] NO_CONTROLLERS = {};
private static final String[] OPTIONAL_INCLUDES = { "com.fasterxml.jackson.databind.Module"};
private static final Set<Class<?>> DEFAULT_INCLUDES;
static {
Set<Class<?>> includes = new LinkedHashSet<>();
includes.add(JsonComponent.class);
includes.add(RuntimeWiringConfigurer.class);
includes.add(Converter.class);
includes.add(GenericConverter.class);
for (String optionalInclude : OPTIONAL_INCLUDES) {
try {
includes.add(ClassUtils.forName(optionalInclude, null));
}
catch (Exception ex) {
// Ignore
}
}
DEFAULT_INCLUDES = Collections.unmodifiableSet(includes);
}
private static final Set<Class<?>> DEFAULT_INCLUDES_AND_CONTROLLER;
static {
Set<Class<?>> includes = new LinkedHashSet<>(DEFAULT_INCLUDES);
includes.add(Controller.class);
DEFAULT_INCLUDES_AND_CONTROLLER = Collections.unmodifiableSet(includes);
}
private final Class<?>[] controllers;
GraphQlTypeExcludeFilter(Class<?> testClass) {
super(testClass);
this.controllers = getAnnotation().getValue("controllers", Class[].class).orElse(NO_CONTROLLERS);
}
@Override
protected Set<Class<?>> getDefaultIncludes() {
if (ObjectUtils.isEmpty(this.controllers)) {
return DEFAULT_INCLUDES_AND_CONTROLLER;
}
return DEFAULT_INCLUDES;
}
@Override
protected Set<Class<?>> getComponentIncludes() {
return new LinkedHashSet<>(Arrays.asList(this.controllers));
}
}

View File

@@ -1,43 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot.test.tester;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.graphql.test.tester.GraphQlTester;
/**
* Annotation that can be applied to a test class to enable a {@link GraphQlTester}.
*
* @author Brian Clozel
* @since 1.0.0
* @see GraphQlTesterAutoConfiguration
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@ImportAutoConfiguration
public @interface AutoConfigureGraphQlTester {
}

View File

@@ -1,50 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot.test.tester;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.graphql.test.tester.WebGraphQlTester;
/**
* Annotation that can be applied to a test class to enable a {@link WebGraphQlTester}.
*
* <p>This annotation should be used with {@link org.springframework.boot.test.context.SpringBootTest @SpringBootTest}
* tests with Spring MVC or Spring WebFlux mock infrastructures.
*
* @author Brian Clozel
* @since 1.0.0
* @see WebGraphQlTesterAutoConfiguration
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigureMockMvc
@AutoConfigureWebTestClient
@ImportAutoConfiguration
public @interface AutoConfigureWebGraphQlTester {
}

View File

@@ -1,48 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot.test.tester;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.GraphQlService;
import org.springframework.graphql.boot.GraphQlServiceAutoConfiguration;
import org.springframework.graphql.test.tester.GraphQlTester;
/**
* Auto-configuration for {@link GraphQlTester}.
*
* @author Brian Clozel
* @since 1.0.0
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({GraphQlTester.class})
@AutoConfigureAfter(value = GraphQlServiceAutoConfiguration.class)
public class GraphQlTesterAutoConfiguration {
@Bean
@ConditionalOnBean(GraphQlService.class)
@ConditionalOnMissingBean
public GraphQlTester graphQlTester(GraphQlService graphQlService) {
return GraphQlTester.create(graphQlService);
}
}

View File

@@ -1,177 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot.test.tester;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.AbstractConfigurableWebServerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.graphql.test.tester.GraphQlTester;
import org.springframework.graphql.test.tester.WebGraphQlTester;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.TestContextAnnotationUtils;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.util.StringUtils;
/**
* {@link ContextCustomizer} for {@link GraphQlTester}.
*
* @author Brian Clozel
*/
class GraphQlTesterContextCustomizer implements ContextCustomizer {
@Override
public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
SpringBootTest springBootTest = TestContextAnnotationUtils.findMergedAnnotation(mergedConfig.getTestClass(),
SpringBootTest.class);
if (springBootTest.webEnvironment().isEmbedded()) {
registerGraphQlTester(context);
}
}
private void registerGraphQlTester(ConfigurableApplicationContext context) {
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
if (beanFactory instanceof BeanDefinitionRegistry) {
registerGraphQlTester((BeanDefinitionRegistry) beanFactory);
}
}
private void registerGraphQlTester(BeanDefinitionRegistry registry) {
RootBeanDefinition definition = new RootBeanDefinition(
GraphQlTesterContextCustomizer.GraphQlTesterRegistrar.class);
definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
registry.registerBeanDefinition(GraphQlTesterContextCustomizer.GraphQlTesterRegistrar.class.getName(),
definition);
}
@Override
public boolean equals(Object obj) {
return (obj != null) && (obj.getClass() == getClass());
}
@Override
public int hashCode() {
return getClass().hashCode();
}
private static class GraphQlTesterRegistrar
implements BeanDefinitionRegistryPostProcessor, Ordered, BeanFactoryAware {
private BeanFactory beanFactory;
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors((ListableBeanFactory) this.beanFactory,
GraphQlTester.class, false, false).length == 0) {
registry.registerBeanDefinition(WebGraphQlTester.class.getName(),
new RootBeanDefinition(GraphQlTesterContextCustomizer.GraphQlTesterFactory.class));
}
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE - 1;
}
}
public static class GraphQlTesterFactory implements FactoryBean<GraphQlTester>, ApplicationContextAware {
private ApplicationContext applicationContext;
private GraphQlTester object;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public boolean isSingleton() {
return true;
}
@Override
public Class<?> getObjectType() {
return GraphQlTester.class;
}
@Override
public GraphQlTester getObject() throws Exception {
if (this.object == null) {
this.object = createGraphQlTester();
}
return this.object;
}
private WebGraphQlTester createGraphQlTester() {
WebTestClient webTestClient = this.applicationContext.getBean(WebTestClient.class);
boolean sslEnabled = isSslEnabled(this.applicationContext);
String port = this.applicationContext.getEnvironment().getProperty("local.server.port", "8080");
WebTestClient mutatedWebClient = webTestClient.mutate().baseUrl(getBaseUrl(sslEnabled, port)).build();
return WebGraphQlTester.create(mutatedWebClient);
}
private String getBaseUrl(boolean sslEnabled, String port) {
String basePath = deduceBasePath();
return (sslEnabled ? "https" : "http") + "://localhost:" + port + basePath;
}
private String deduceBasePath() {
String configuredPath = this.applicationContext.getEnvironment().getProperty("spring.graphql.path");
return StringUtils.hasText(configuredPath) ? configuredPath : "/graphql";
}
private boolean isSslEnabled(ApplicationContext context) {
try {
AbstractConfigurableWebServerFactory webServerFactory = context
.getBean(AbstractConfigurableWebServerFactory.class);
return webServerFactory.getSsl() != null && webServerFactory.getSsl().isEnabled();
}
catch (NoSuchBeanDefinitionException ex) {
return false;
}
}
}
}

View File

@@ -1,53 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot.test.tester;
import java.util.List;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.graphql.test.tester.GraphQlTester;
import org.springframework.test.context.ContextConfigurationAttributes;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.ContextCustomizerFactory;
import org.springframework.test.context.TestContextAnnotationUtils;
import org.springframework.util.ClassUtils;
/**
* {@link ContextCustomizer} for {@link GraphQlTester}.
*
* @author Brian Clozel
*/
class GraphQlTesterContextCustomizerFactory implements ContextCustomizerFactory {
private static final String GRAPHQLTESTER_CLASS = "org.springframework.graphql.test.tester.GraphQlTester";
private static final String WEBTESTCLIENT_CLASS = "org.springframework.test.web.reactive.server.WebTestClient";
@Override
public ContextCustomizer createContextCustomizer(Class<?> testClass,
List<ContextConfigurationAttributes> configAttributes) {
SpringBootTest springBootTest = TestContextAnnotationUtils.findMergedAnnotation(testClass,
SpringBootTest.class);
return (springBootTest != null && isGraphQlTesterPresent()) ? new GraphQlTesterContextCustomizer() : null;
}
private boolean isGraphQlTesterPresent() {
return ClassUtils.isPresent(WEBTESTCLIENT_CLASS, getClass().getClassLoader())
&& ClassUtils.isPresent(GRAPHQLTESTER_CLASS, getClass().getClassLoader());
}
}

View File

@@ -1,50 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot.test.tester;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.boot.GraphQlProperties;
import org.springframework.graphql.test.tester.WebGraphQlTester;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.client.WebClient;
/**
* Auto-configuration for {@link WebGraphQlTester}.
*
* @author Brian Clozel
* @since 1.0.0
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({WebClient.class, WebTestClient.class, WebGraphQlTester.class})
@AutoConfigureAfter(name = {"org.springframework.boot.test.autoconfigure.web.reactive.WebTestClientAutoConfiguration",
"org.springframework.boot.test.autoconfigure.web.servlet.MockMvcAutoConfiguration"})
public class WebGraphQlTesterAutoConfiguration {
@Bean
@ConditionalOnBean(WebTestClient.class)
@ConditionalOnMissingBean
public WebGraphQlTester webTestClientGraphQlTester(WebTestClient webTestClient, GraphQlProperties properties) {
WebTestClient mutatedWebTestClient = webTestClient.mutate().baseUrl(properties.getPath()).build();
return WebGraphQlTester.create(mutatedWebTestClient);
}
}

View File

@@ -1,67 +0,0 @@
{
"properties": [
{
"name": "spring.graphql.schema.locations",
"defaultValue": "classpath:graphql/**/"
},
{
"name": "spring.graphql.schema.file-extensions",
"defaultValue": ".graphqls,.gqls"
},
{
"name": "management.metrics.graphql.autotime.enabled",
"description": "Whether to automatically time graphql requests.",
"defaultValue": true
},
{
"name": "management.metrics.graphql.autotime.percentiles",
"description": "Computed non-aggregable percentiles to publish."
},
{
"name": "management.metrics.graphql.autotime.percentiles-histogram",
"description": "Whether percentile histograms should be published.",
"defaultValue": false
}
],
"hints": [
{
"name": "spring.graphql.cors.allowed-headers",
"values": [
{
"value": "*"
}
],
"providers": [
{
"name": "any"
}
]
},
{
"name": "spring.graphql.cors.allowed-methods",
"values": [
{
"value": "*"
}
],
"providers": [
{
"name": "any"
}
]
},
{
"name": "spring.graphql.cors.allowed-origins",
"values": [
{
"value": "*"
}
],
"providers": [
{
"name": "any"
}
]
}
]
}

View File

@@ -1 +0,0 @@
restart.include.graphqljava=graphql-java[\\w-]+\.jar

View File

@@ -1,33 +0,0 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.graphql.boot.GraphQlAutoConfiguration,\
org.springframework.graphql.boot.GraphQlServiceAutoConfiguration,\
org.springframework.graphql.boot.GraphQlWebFluxAutoConfiguration,\
org.springframework.graphql.boot.GraphQlWebMvcAutoConfiguration,\
org.springframework.graphql.boot.actuate.metrics.GraphQlMetricsAutoConfiguration,\
org.springframework.graphql.boot.security.GraphQlWebFluxSecurityAutoConfiguration,\
org.springframework.graphql.boot.security.GraphQlWebMvcSecurityAutoConfiguration,\
org.springframework.graphql.boot.data.GraphQlWebMvcQuerydslAutoConfiguration,\
org.springframework.graphql.boot.data.GraphQlWebFluxQuerydslAutoConfiguration
# Failure Analyzers
org.springframework.boot.diagnostics.FailureAnalyzer=\
org.springframework.graphql.boot.InvalidSchemaLocationsExceptionFailureAnalyzer
# Spring Test @AutoConfigureGraphQlTester
org.springframework.graphql.boot.test.tester.AutoConfigureGraphQlTester=\
org.springframework.graphql.boot.test.tester.GraphQlTesterAutoConfiguration
# Spring Test @AutoConfigureWebGraphQlTester
org.springframework.graphql.boot.test.tester.AutoConfigureWebGraphQlTester=\
org.springframework.graphql.boot.test.tester.WebGraphQlTesterAutoConfiguration
# Spring Test ContextCustomizerFactories
org.springframework.test.context.ContextCustomizerFactory=\
org.springframework.graphql.boot.test.tester.GraphQlTesterContextCustomizerFactory
# AutoConfigureGraphQl auto-configuration imports
org.springframework.graphql.boot.test.AutoConfigureGraphQl=\
org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration,\
org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration,\
org.springframework.graphql.boot.GraphQlAutoConfiguration,\
org.springframework.graphql.boot.GraphQlServiceAutoConfiguration

View File

@@ -1,71 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot;
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;
}
}

View File

@@ -1,113 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot;
import graphql.schema.GraphQLSchema;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.graphql.execution.MissingSchemaException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link GraphQlAutoConfiguration}
*/
class GraphQlAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(GraphQlAutoConfiguration.class));
@Test
void shouldFailWhenSchemaFileIsMissing() {
this.contextRunner.withPropertyValues("spring.graphql.schema.locations:classpath:missing/")
.run((context) -> {
assertThat(context).hasFailed();
assertThat(context).getFailure().getRootCause().isInstanceOf(MissingSchemaException.class);
});
}
@Test
void shouldCreateBuilderWithSdl() {
this.contextRunner.withPropertyValues("spring.graphql.schema.locations:classpath:books/")
.run((context) -> assertThat(context).hasSingleBean(GraphQlSource.class));
}
@Test
void shouldScanNestedLocations() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(GraphQlSource.class));
}
@Test
void shouldUseProgrammaticallyDefinedBuilder() {
this.contextRunner.withPropertyValues("spring.graphql.schema.locations:classpath:books/")
.withUserConfiguration(CustomGraphQlBuilderConfiguration.class).run((context) -> {
assertThat(context).hasBean("customGraphQlSourceBuilder");
assertThat(context).hasSingleBean(GraphQlSource.Builder.class);
});
}
@Test
void shouldScanLocationsForSchemaFiles() {
this.contextRunner.withPropertyValues("spring.graphql.schema.locations:classpath:schema/",
"spring.graphql.schema.file-extensions:.gql,.gqls,.graphql,.graphqls")
.run((context) -> {
assertThat(context).hasSingleBean(GraphQlSource.class);
GraphQlSource graphQlSource = context.getBean(GraphQlSource.class);
GraphQLSchema schema = graphQlSource.schema();
assertThat(schema.getObjectType("Movie")).isNotNull();
assertThat(schema.getObjectType("Book")).isNotNull();
assertThat(schema.getObjectType("Store")).isNotNull();
});
}
@Test
void shouldBackOffWithCustomGraphQlSource() {
this.contextRunner.withUserConfiguration(CustomGraphQlSourceConfiguration.class)
.run((context) -> {
assertThat(context).getBeanNames(GraphQlSource.class).containsOnly("customGraphQlSource");
assertThat(context).hasSingleBean(GraphQlProperties.class);
});
}
@Configuration(proxyBeanMethods = false)
static class CustomGraphQlBuilderConfiguration {
@Bean
GraphQlSource.Builder customGraphQlSourceBuilder() {
return GraphQlSource.builder().schemaResources(new ClassPathResource("books/schema.graphqls"));
}
}
@Configuration(proxyBeanMethods = false)
static class CustomGraphQlSourceConfiguration {
@Bean
GraphQlSource customGraphQlSource() {
return mock(GraphQlSource.class);
}
}
}

View File

@@ -1,54 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot;
import java.util.Arrays;
import java.util.List;
import graphql.schema.DataFetcher;
import reactor.core.publisher.Flux;
import org.springframework.lang.Nullable;
public final class GraphQlDataFetchers {
private static List<Book> 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 GraphQlDataFetchers() {
}
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<Book> getBooksOnSale(int minPages) {
return Flux.fromIterable(books).filter((book) -> book.getPageCount() >= minPages);
}
}

View File

@@ -1,82 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.GraphQlService;
import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer;
import org.springframework.graphql.execution.BatchLoaderRegistry;
import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link GraphQlServiceAutoConfiguration}.
*
* @author Brian Clozel
*/
class GraphQlServiceAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(GraphQlServiceAutoConfiguration.class))
.withUserConfiguration(GraphQlSourceConfiguration.class);
@Test
void shouldContributeStandardBeans() {
this.contextRunner.run((context) -> {
assertThat(context).hasSingleBean(BatchLoaderRegistry.class);
assertThat(context).hasSingleBean(GraphQlService.class);
assertThat(context).hasSingleBean(AnnotatedControllerConfigurer.class);
});
}
@Test
void shouldConfigureValidation() {
this.contextRunner.withUserConfiguration(ValidationConfiguration.class)
.run((context) -> {
assertThat(context).hasSingleBean(AnnotatedControllerConfigurer.class);
assertThat(context.getBean(AnnotatedControllerConfigurer.class))
.extracting("validator").isNotNull();
});
}
@Configuration(proxyBeanMethods = false)
static class GraphQlSourceConfiguration {
@Bean
GraphQlSource graphQlSource() {
return mock(GraphQlSource.class);
}
}
@Configuration(proxyBeanMethods = false)
static class ValidationConfiguration {
@Bean
public LocalValidatorFactoryBean defaultValidator() {
return new LocalValidatorFactoryBean();
}
}
}

View File

@@ -1,208 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot;
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.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.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import static org.hamcrest.Matchers.containsString;
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,
GraphQlServiceAutoConfiguration.class, GraphQlWebFluxAutoConfiguration.class))
.withUserConfiguration(DataFetchersConfiguration.class, CustomWebInterceptor.class)
.withPropertyValues(
"spring.main.web-application-type=reactive",
"spring.graphql.schema.printer.enabled=true",
"spring.graphql.schema.locations=classpath:books/",
"spring.graphql.cors.allowed-origins=https://example.com",
"spring.graphql.cors.allowed-methods=POST",
"spring.graphql.cors.allow-credentials=true");
@Test
void query() {
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 queryHttpGet() {
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 queryMissing() {
testWithWebClient((client) ->
client.post().uri("").bodyValue("{}")
.exchange()
.expectStatus()
.isBadRequest());
}
@Test
void queryIsInvalidJson() {
testWithWebClient((client) ->
client.post().uri("").bodyValue(":)")
.exchange()
.expectStatus()
.isBadRequest());
}
@Test
void interceptedQuery() {
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");
});
}
@Test
void schemaEndpoint() {
testWithWebClient((client) ->
client.get().uri("/schema").accept(MediaType.ALL)
.exchange()
.expectStatus()
.isOk()
.expectHeader()
.contentType(MediaType.TEXT_PLAIN)
.expectBody(String.class)
.value(containsString("type Book")));
}
@Test
void corsConfiguration() {
testWithWebClient((client) -> {
String query = "{" +
" bookById(id: \\\"book-1\\\"){ " +
" id" +
" name" +
" pageCount" +
" author" +
" }" +
"}";
client.post().uri("").bodyValue("{ \"query\": \"" + query + "\"}")
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST")
.header(HttpHeaders.ORIGIN, "https://example.com")
.exchange().expectStatus().isOk()
.expectHeader().valueEquals(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "https://example.com")
.expectHeader().valueEquals(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); ;
});
}
private void testWithWebClient(Consumer<WebTestClient> 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", GraphQlDataFetchers.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")));
}
}
}

View File

@@ -1,196 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot;
import graphql.schema.idl.TypeRuntimeWiring;
import org.hamcrest.Matchers;
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.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.HttpHeaders;
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;
class GraphQlWebMvcAutoConfigurationTests {
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(DispatcherServletAutoConfiguration.class,
WebMvcAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class,
JacksonAutoConfiguration.class, GraphQlAutoConfiguration.class,
GraphQlServiceAutoConfiguration.class, GraphQlWebMvcAutoConfiguration.class))
.withUserConfiguration(DataFetchersConfiguration.class, CustomWebInterceptor.class)
.withPropertyValues(
"spring.main.web-application-type=servlet",
"spring.graphql.schema.printer.enabled=true",
"spring.graphql.schema.locations=classpath:books/",
"spring.graphql.cors.allowed-origins=https://example.com",
"spring.graphql.cors.allowed-methods=POST",
"spring.graphql.cors.allow-credentials=true");
@Test
void query() {
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 queryHttpGet() {
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 missingQuery() {
testWith((mockMvc) -> mockMvc.perform(post("/graphql").content("{}")).andExpect(status().isBadRequest()));
}
@Test
void invalidJson() {
testWith((mockMvc) -> mockMvc.perform(post("/graphql").content(":)")).andExpect(status().isBadRequest()));
}
@Test
void interceptedQuery() {
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"));
});
}
@Test
void schemaEndpoint() {
testWith((mockMvc) -> mockMvc.perform(get("/graphql/schema"))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.TEXT_PLAIN))
.andExpect(content().string(Matchers.containsString("type Book"))));
}
@Test
void corsConfiguration() {
testWith((mockMvc) -> {
String query = "{" +
" bookById(id: \\\"book-1\\\"){ " +
" id" +
" name" +
" pageCount" +
" author" +
" }" +
"}";
MvcResult result = mockMvc.perform(post("/graphql")
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST")
.header(HttpHeaders.ORIGIN, "https://example.com")
.content("{\"query\": \"" + query + "\"}")).andReturn();
mockMvc.perform(asyncDispatch(result))
.andExpect(status().isOk())
.andExpect(header().stringValues(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "https://example.com"))
.andExpect(header().stringValues(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"));
});
}
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", GraphQlDataFetchers.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")));
}
}
}

View File

@@ -1,101 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot.actuate.metrics;
import java.util.Arrays;
import graphql.ErrorType;
import graphql.ExecutionResult;
import graphql.ExecutionResultImpl;
import graphql.GraphQLError;
import graphql.GraphqlErrorBuilder;
import io.micrometer.core.instrument.Tag;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link GraphQlTags}
*
* @author Brian Clozel
*/
class GraphQlTagsTests {
@Test
void executionOutcomeShouldSucceed() {
ExecutionResult result = ExecutionResultImpl.newExecutionResult().build();
Tag outcomeTag = GraphQlTags.executionOutcome(result, null);
assertThat(outcomeTag.getValue()).isEqualTo("SUCCESS");
}
@Test
void executionOutcomeShouldErrorWhenExceptionThrown() {
ExecutionResult result = ExecutionResultImpl.newExecutionResult().build();
Tag tag = GraphQlTags.executionOutcome(result, new IllegalArgumentException("test error"));
assertThat(tag.getValue()).isEqualTo("ERROR");
}
@Test
void executionOutcomeShouldErrorWhenResponseErrors() {
GraphQLError error = GraphqlErrorBuilder.newError().message("Invalid query").build();
Tag tag = GraphQlTags.executionOutcome(ExecutionResultImpl.newExecutionResult().addError(error).build(), null);
assertThat(tag.getValue()).isEqualTo("ERROR");
}
@Test
void errorTypeShouldBeDefinedIfPresent() {
GraphQLError error = GraphqlErrorBuilder.newError()
.errorType(ErrorType.DataFetchingException)
.message("test error")
.build();
Tag errorTypeTag = GraphQlTags.errorType(error);
assertThat(errorTypeTag.getValue()).isEqualTo("DataFetchingException");
}
@Test
void errorPathShouldUseJsonPathFormat() {
GraphQLError error = GraphqlErrorBuilder.newError()
.path(Arrays.asList("project", "name"))
.message("test error")
.build();
Tag errorPathTag = GraphQlTags.errorPath(error);
assertThat(errorPathTag.getValue()).isEqualTo("$.project.name");
}
@Test
void errorPathShouldUseJsonPathFormatForIndices() {
GraphQLError error = GraphqlErrorBuilder.newError()
.path(Arrays.asList("issues", "42", "title"))
.message("test error")
.build();
Tag errorPathTag = GraphQlTags.errorPath(error);
assertThat(errorPathTag.getValue()).isEqualTo("$.issues[*].title");
}
@Test
void dataFetchingOutcomeShouldBeSuccessfulIfNoException() {
Tag fetchingOutcomeTag = GraphQlTags.dataFetchingOutcome(null);
assertThat(fetchingOutcomeTag.getValue()).isEqualTo("SUCCESS");
}
@Test
void dataFetchingOutcomeShouldBeErrorIfException() {
Tag fetchingOutcomeTag = GraphQlTags.dataFetchingOutcome(new IllegalStateException("error state"));
assertThat(fetchingOutcomeTag.getValue()).isEqualTo("ERROR");
}
}

View File

@@ -1,195 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot.security;
import java.util.Collections;
import java.util.function.Consumer;
import graphql.schema.idl.TypeRuntimeWiring;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
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.security.reactive.ReactiveSecurityAutoConfiguration;
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.boot.Book;
import org.springframework.graphql.boot.GraphQlAutoConfiguration;
import org.springframework.graphql.boot.GraphQlDataFetchers;
import org.springframework.graphql.boot.GraphQlServiceAutoConfiguration;
import org.springframework.graphql.boot.GraphQlWebFluxAutoConfiguration;
import org.springframework.graphql.execution.ErrorType;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
import org.springframework.graphql.security.ReactiveSecurityDataFetcherExceptionResolver;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.test.web.reactive.server.WebTestClient;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.security.config.Customizer.withDefaults;
/**
* Tests for {@link GraphQlWebFluxSecurityAutoConfiguration}.
* @author Brian Clozel
*/
class GraphQlWebFluxSecurityAutoConfigurationTests {
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,
GraphQlServiceAutoConfiguration.class, GraphQlWebFluxAutoConfiguration.class,
GraphQlWebFluxSecurityAutoConfiguration.class, ReactiveSecurityAutoConfiguration.class))
.withUserConfiguration(DataFetchersConfiguration.class, SecurityConfig.class)
.withPropertyValues(
"spring.main.web-application-type=reactive",
"spring.graphql.schema.locations=classpath:books/");
@Test
void contributesExceptionResolver() {
this.contextRunner.run((context) -> {
assertThat(context).hasSingleBean(ReactiveSecurityDataFetcherExceptionResolver.class);
});
}
@Test
void anonymousUserShouldBeUnauthorized() {
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")
.doesNotExist()
.jsonPath("errors[0].extensions.classification").isEqualTo(ErrorType.UNAUTHORIZED.toString());
});
}
@Test
void authenticatedUserShouldGetData() {
testWithWebClient((client) -> {
String query = "{" +
" bookById(id: \\\"book-1\\\"){ " +
" id" +
" name" +
" pageCount" +
" author" +
" }" +
"}";
client.post().uri("").headers(headers -> headers.setBasicAuth("rob", "rob"))
.bodyValue("{ \"query\": \"" + query + "\"}")
.exchange()
.expectStatus()
.isOk()
.expectBody()
.jsonPath("data.bookById.name")
.isEqualTo("GraphQL for beginners")
.jsonPath("errors[0].extensions.classification").doesNotExist();
});
}
private void testWithWebClient(Consumer<WebTestClient> 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(BookService bookService) {
return (builder) -> builder.type(
TypeRuntimeWiring.newTypeWiring("Query")
.dataFetcher("bookById", env -> bookService.getBookdById(env.getArgument("id"))));
}
@Bean
BookService bookService() {
return new BookService();
}
}
static class BookService {
@PreAuthorize("hasRole('USER')")
@Nullable
public Mono<Book> getBookdById(String id) {
return Mono.justOrEmpty(GraphQlDataFetchers.getBookById(id));
}
}
@Configuration(proxyBeanMethods = false)
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
static class SecurityConfig {
@Bean
SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) throws Exception {
return http
.csrf(spec -> spec.disable())
// Demonstrate that method security works
// Best practice to use both for defense in depth
.authorizeExchange(requests -> requests.anyExchange().permitAll())
.httpBasic(withDefaults())
.build();
}
@Bean
@SuppressWarnings("deprecation")
public MapReactiveUserDetailsService userDetailsService() {
User.UserBuilder userBuilder = User.withDefaultPasswordEncoder();
UserDetails rob = userBuilder.username("rob").password("rob").roles("USER").build();
UserDetails admin = userBuilder.username("admin").password("admin").roles("USER", "ADMIN").build();
return new MapReactiveUserDetailsService(rob, admin);
}
}
}

View File

@@ -1,201 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot.security;
import graphql.schema.idl.TypeRuntimeWiring;
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.security.servlet.SecurityAutoConfiguration;
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.boot.Book;
import org.springframework.graphql.boot.GraphQlAutoConfiguration;
import org.springframework.graphql.boot.GraphQlDataFetchers;
import org.springframework.graphql.boot.GraphQlServiceAutoConfiguration;
import org.springframework.graphql.boot.GraphQlWebMvcAutoConfiguration;
import org.springframework.graphql.execution.ErrorType;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
import org.springframework.graphql.security.SecurityContextThreadLocalAccessor;
import org.springframework.graphql.security.SecurityDataFetcherExceptionResolver;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.DefaultSecurityFilterChain;
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.assertj.core.api.Assertions.assertThat;
import static org.springframework.security.config.Customizer.withDefaults;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
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.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Tests for {@link GraphQlWebMvcSecurityAutoConfiguration}.
* @author Brian Clozel
*/
class GraphQlWebMvcSecurityAutoConfigurationTests {
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(DispatcherServletAutoConfiguration.class,
WebMvcAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class,
JacksonAutoConfiguration.class, GraphQlAutoConfiguration.class,
GraphQlServiceAutoConfiguration.class, GraphQlWebMvcAutoConfiguration.class,
GraphQlWebMvcSecurityAutoConfiguration.class, SecurityAutoConfiguration.class))
.withUserConfiguration(DataFetchersConfiguration.class, SecurityConfig.class)
.withPropertyValues(
"spring.main.web-application-type=servlet",
"spring.graphql.schema.locations=classpath:books/");
@Test
void contributesSecurityComponents() {
this.contextRunner.run((context) -> {
assertThat(context).hasSingleBean(SecurityDataFetcherExceptionResolver.class);
assertThat(context).hasSingleBean(SecurityContextThreadLocalAccessor.class);
});
}
@Test
void anonymousUserShouldBeUnauthorized() {
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").doesNotExist())
.andExpect(jsonPath("errors[0].extensions.classification").value(ErrorType.UNAUTHORIZED.toString()));
});
}
@Test
void authenticatedUserShouldGetData() {
testWith((mockMvc) -> {
String query = "{" +
" bookById(id: \\\"book-1\\\"){ " +
" id" +
" name" +
" pageCount" +
" author" +
" }" +
"}";
MvcResult result = mockMvc.perform(post("/graphql").content("{\"query\": \"" + query + "\"}").with(user("rob"))).andReturn();
mockMvc.perform(asyncDispatch(result))
.andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("data.bookById.name").value("GraphQL for beginners"))
.andExpect(jsonPath("errors").doesNotExist());
});
}
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))
.apply(springSecurity())
.build();
mockMvcConsumer.accept(mockMvc);
});
}
private interface MockMvcConsumer {
void accept(MockMvc mockMvc) throws Exception;
}
@Configuration(proxyBeanMethods = false)
static class DataFetchersConfiguration {
@Bean
RuntimeWiringConfigurer bookDataFetcher(BookService bookService) {
return (builder) -> builder.type(
TypeRuntimeWiring.newTypeWiring("Query")
.dataFetcher("bookById", env -> bookService.getBookdById(env.getArgument("id"))));
}
@Bean
BookService bookService() {
return new BookService();
}
}
static class BookService {
@PreAuthorize("hasRole('USER')")
@Nullable
public Book getBookdById(String id) {
return GraphQlDataFetchers.getBookById(id);
}
}
@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
static class SecurityConfig {
@Bean
DefaultSecurityFilterChain springWebFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(c -> c.disable())
// Demonstrate that method security works
// Best practice to use both for defense in depth
.authorizeRequests(requests -> requests
.anyRequest().permitAll()
)
.httpBasic(withDefaults())
.build();
}
@Bean
public static InMemoryUserDetailsManager userDetailsService() {
User.UserBuilder userBuilder = User.withDefaultPasswordEncoder();
UserDetails rob = userBuilder.username("rob").password("rob").roles("USER").build();
UserDetails admin = userBuilder.username("admin").password("admin").roles("USER", "ADMIN").build();
return new InMemoryUserDetailsManager(rob, admin);
}
}
}

View File

@@ -1,38 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot.test;
import reactor.core.publisher.Mono;
import org.springframework.graphql.boot.Book;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
/**
* Example {@code @Controller} to be tested with {@link GraphQlTest @GraphQlTest}.
*
* @author Brian Clozel
*/
@Controller
public class BookController {
@QueryMapping
public Book bookById(@Argument String id) {
return new Book("42", "Sample Book", 100, "Jane Spring");
}
}

View File

@@ -1,30 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot.test;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Example {@link SpringBootApplication @SpringBootApplication} used with
* {@link GraphQlTest @GraphQlTest} tests.
*
* @author Brian Clozel
*/
@SpringBootApplication
public class ExampleGraphQlApplication {
}

View File

@@ -1,50 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot.test;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.graphql.test.tester.GraphQlTester;
/**
* Integration test for {@link GraphQlTest @GraphQlTest} annotated tests.
*
* @author Brian Clozel
*/
@GraphQlTest(controllers = BookController.class,
properties = {"spring.graphql.schema.locations:classpath:books/"})
public class GraphQlTestIntegrationTest {
@Autowired
private GraphQlTester graphQlTester;
@Test
void getBookdByIdShouldReturnTestBook() {
String query = "{" +
" bookById(id: \"book-1\"){ " +
" id" +
" name" +
" pageCount" +
" author" +
" }" +
"}";
graphQlTester.query(query).execute()
.path("data.bookById.id").entity(String.class).isEqualTo("42");
}
}

View File

@@ -1,183 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot.test;
import java.io.IOException;
import com.fasterxml.jackson.databind.module.SimpleModule;
import graphql.schema.idl.RuntimeWiring;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.classreading.SimpleMetadataReaderFactory;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
import org.springframework.graphql.web.WebInput;
import org.springframework.graphql.web.WebInterceptor;
import org.springframework.graphql.web.WebInterceptorChain;
import org.springframework.graphql.web.WebOutput;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link GraphQlTypeExcludeFilter}
*
* @author Brian Clozel
*/
class GraphQlTypeExcludeFilterTests {
private MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory();
@Test
void matchWhenHasNoControllers() throws Exception {
GraphQlTypeExcludeFilter filter = new GraphQlTypeExcludeFilter(WithNoControllers.class);
assertThat(excludes(filter, Controller1.class)).isFalse();
assertThat(excludes(filter, Controller2.class)).isFalse();
assertThat(excludes(filter, ExampleRuntimeWiringConfigurer.class)).isFalse();
assertThat(excludes(filter, ExampleService.class)).isTrue();
assertThat(excludes(filter, ExampleRepository.class)).isTrue();
assertThat(excludes(filter, ExampleWebInterceptor.class)).isTrue();
assertThat(excludes(filter, ExampleModule.class)).isFalse();
}
@Test
void matchWhenHasController() throws Exception {
GraphQlTypeExcludeFilter filter = new GraphQlTypeExcludeFilter(WithController.class);
assertThat(excludes(filter, Controller1.class)).isFalse();
assertThat(excludes(filter, Controller2.class)).isTrue();
assertThat(excludes(filter, ExampleRuntimeWiringConfigurer.class)).isFalse();
assertThat(excludes(filter, ExampleService.class)).isTrue();
assertThat(excludes(filter, ExampleRepository.class)).isTrue();
assertThat(excludes(filter, ExampleWebInterceptor.class)).isTrue();
assertThat(excludes(filter, ExampleModule.class)).isFalse();
}
@Test
void matchNotUsingDefaultFilters() throws Exception {
GraphQlTypeExcludeFilter filter = new GraphQlTypeExcludeFilter(NotUsingDefaultFilters.class);
assertThat(excludes(filter, Controller1.class)).isTrue();
assertThat(excludes(filter, Controller2.class)).isTrue();
assertThat(excludes(filter, ExampleRuntimeWiringConfigurer.class)).isTrue();
assertThat(excludes(filter, ExampleService.class)).isTrue();
assertThat(excludes(filter, ExampleRepository.class)).isTrue();
assertThat(excludes(filter, ExampleWebInterceptor.class)).isTrue();
assertThat(excludes(filter, ExampleModule.class)).isTrue();
}
@Test
void matchWithIncludeFilter() throws Exception {
GraphQlTypeExcludeFilter filter = new GraphQlTypeExcludeFilter(WithIncludeFilter.class);
assertThat(excludes(filter, Controller1.class)).isFalse();
assertThat(excludes(filter, Controller2.class)).isFalse();
assertThat(excludes(filter, ExampleRuntimeWiringConfigurer.class)).isFalse();
assertThat(excludes(filter, ExampleService.class)).isTrue();
assertThat(excludes(filter, ExampleRepository.class)).isFalse();
assertThat(excludes(filter, ExampleWebInterceptor.class)).isTrue();
assertThat(excludes(filter, ExampleModule.class)).isFalse();
}
@Test
void matchWithExcludeFilter() throws Exception {
GraphQlTypeExcludeFilter filter = new GraphQlTypeExcludeFilter(WithExcludeFilter.class);
assertThat(excludes(filter, Controller1.class)).isTrue();
assertThat(excludes(filter, Controller2.class)).isFalse();
assertThat(excludes(filter, ExampleRuntimeWiringConfigurer.class)).isFalse();
assertThat(excludes(filter, ExampleService.class)).isTrue();
assertThat(excludes(filter, ExampleRepository.class)).isTrue();
assertThat(excludes(filter, ExampleWebInterceptor.class)).isTrue();
assertThat(excludes(filter, ExampleModule.class)).isFalse();
}
private boolean excludes(GraphQlTypeExcludeFilter filter, Class<?> type) throws IOException {
MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(type.getName());
return filter.match(metadataReader, this.metadataReaderFactory);
}
@GraphQlTest
static class WithNoControllers {
}
@GraphQlTest(Controller1.class)
static class WithController {
}
@GraphQlTest(useDefaultFilters = false)
static class NotUsingDefaultFilters {
}
@GraphQlTest(includeFilters = @ComponentScan.Filter(Repository.class))
static class WithIncludeFilter {
}
@GraphQlTest(excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = Controller1.class))
static class WithExcludeFilter {
}
@Controller
static class Controller1 {
}
@Controller
static class Controller2 {
}
@Service
static class ExampleService {
}
@Repository
static class ExampleRepository {
}
static class ExampleRuntimeWiringConfigurer implements RuntimeWiringConfigurer {
@Override
public void configure(RuntimeWiring.Builder builder) {
}
}
static class ExampleWebInterceptor implements WebInterceptor {
@Override
public Mono<WebOutput> intercept(WebInput webInput, WebInterceptorChain chain) {
return null;
}
}
@SuppressWarnings("serial")
static class ExampleModule extends SimpleModule {
}
}

View File

@@ -1,67 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot.test.tester;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.GraphQlService;
import org.springframework.graphql.test.tester.GraphQlTester;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link GraphQlTesterAutoConfiguration}
*
* @author Brian Clozel
*/
class GraphQlTesterAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(GraphQlTesterAutoConfiguration.class));
@Test
void shouldNotBeConfiguredWithoutGraphQlService() {
this.contextRunner.run((context) -> {
assertThat(context).hasNotFailed();
assertThat(context).doesNotHaveBean(GraphQlTester.class);
});
}
@Test
void shouldBeConfiguredWhenGraphQlServicePresent() {
this.contextRunner.withUserConfiguration(GraphQlServiceConfiguration.class).run((context) -> {
assertThat(context).hasNotFailed();
assertThat(context).hasSingleBean(GraphQlTester.class);
});
}
@Configuration(proxyBeanMethods = false)
static class GraphQlServiceConfiguration {
@Bean
GraphQlService graphQlService() {
return mock(GraphQlService.class);
}
}
}

View File

@@ -1,90 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot.test.tester;
import java.util.Collections;
import java.util.Map;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.embedded.tomcat.TomcatReactiveWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.graphql.boot.GraphQlProperties;
import org.springframework.graphql.test.tester.GraphQlTester;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ContextPathCompositeHandler;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.test.annotation.DirtiesContext;
/**
* Integration test for {@link GraphQlTesterContextCustomizer}
*
* @author Brian Clozel
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = "spring.main.web-application-type=reactive")
@DirtiesContext
class GraphQlTesterContextCustomizerIntegrationTests {
@Autowired
GraphQlTester graphQlTester;
@Test
void test() {
this.graphQlTester.query("{}").executeAndVerify();
}
@Configuration(proxyBeanMethods = false)
static class TestConfig {
@Bean
TomcatReactiveWebServerFactory webServerFactory() {
return new TomcatReactiveWebServerFactory(0);
}
@Bean
HttpHandler httpHandler() {
TestHandler httpHandler = new TestHandler();
GraphQlProperties properties = new GraphQlProperties();
Map<String, HttpHandler> handlersMap = Collections.singletonMap(properties.getPath(), httpHandler);
return new ContextPathCompositeHandler(handlersMap);
}
}
static class TestHandler implements HttpHandler {
private static final DefaultDataBufferFactory factory = new DefaultDataBufferFactory();
@Override
public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
response.setStatusCode(HttpStatus.OK);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
return response.writeWith(Mono.just(factory.wrap("{\"data\":{}}".getBytes())));
}
}
}

View File

@@ -1,77 +0,0 @@
/*
* 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.
*/
package org.springframework.graphql.boot.test.tester;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.boot.GraphQlProperties;
import org.springframework.graphql.test.tester.WebGraphQlTester;
import org.springframework.test.web.reactive.server.WebTestClient;
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.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link WebGraphQlTesterAutoConfiguration}
*
* @author Brian Clozel
*/
class WebGraphQlTesterAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(WebGraphQlTesterAutoConfiguration.class));
@Test
void shouldNotBeConfiguredWithoutWebTestClient() {
this.contextRunner.run((context) -> {
assertThat(context).hasNotFailed();
assertThat(context).doesNotHaveBean(WebGraphQlTester.class);
});
}
@Test
void shouldBeConfiguredWhenWebTestClientPresent() {
this.contextRunner.withUserConfiguration(WebTestClientConfiguration.class).run((context) -> {
assertThat(context).hasNotFailed();
assertThat(context).hasSingleBean(WebGraphQlTester.class);
});
}
@Configuration(proxyBeanMethods = false)
static class WebTestClientConfiguration {
@Bean
WebTestClient webTestClient() {
RouterFunction<ServerResponse> routes =
RouterFunctions.route().POST("/graphql", (request) -> ServerResponse.ok().build()).build();
return WebTestClient.bindToRouterFunction(routes).build();
}
@Bean
GraphQlProperties graphQlProperties() {
return new GraphQlProperties();
}
}
}

View File

@@ -1,14 +0,0 @@
type Query {
bookById(id: ID): Book
}
type Book {
id: ID
name: String
pageCount: Int
author: String
}
type Subscription {
bookSearch(minPages:Int) : Book!
}

View File

@@ -1,3 +0,0 @@
type Query {
bookById(id: ID): Book
}

View File

@@ -1,5 +0,0 @@
type Book {
id: ID
name: String
pageCount: Int
}

View File

@@ -1,11 +0,0 @@
type Book {
id: ID
name: String
pageCount: Int
author: Author
}
type Author {
id: ID
name: String
}

View File

@@ -1,10 +0,0 @@
type Movie {
id: ID
title: String
director: Director
}
type Director {
id: ID
name: String
}

View File

@@ -1,5 +0,0 @@
type Query {
bookById(id: ID): Book
movieById(id: ID): Movie
storeById(id: ID): Store
}

View File

@@ -1,10 +0,0 @@
type Store {
id: ID
location: Location
}
type Location {
address: String
city: String
country: String
}