Reinstate Spring for GraphQL auto-configuration

This commit adds the Spring for GraphQL auto-configuration back
into Spring Boot 3.0, now that a 1.1.0 release is scheduled with the
required baseline. This release also needs GraphQL Java 19.0 as a
baseline.

Closes gh-31809
pull/31948/head
Brian Clozel 2 years ago
parent ab469e8b6a
commit 38f1bc9793

@ -174,6 +174,7 @@ public class DocumentConfigurationProperties extends DefaultTask {
} }
private void webPrefixes(Config prefix) { private void webPrefixes(Config prefix) {
prefix.accept("spring.graphql");
prefix.accept("spring.hateoas"); prefix.accept("spring.hateoas");
prefix.accept("spring.http"); prefix.accept("spring.http");
prefix.accept("spring.servlet"); prefix.accept("spring.servlet");

@ -135,6 +135,7 @@ dependencies {
optional("org.springframework.data:spring-data-elasticsearch") { optional("org.springframework.data:spring-data-elasticsearch") {
exclude group: "commons-logging", module: "commons-logging" exclude group: "commons-logging", module: "commons-logging"
} }
optional("org.springframework.graphql:spring-graphql")
optional("org.springframework.integration:spring-integration-core") optional("org.springframework.integration:spring-integration-core")
optional("org.springframework.kafka:spring-kafka") optional("org.springframework.kafka:spring-kafka")
optional("org.springframework.security:spring-security-config") optional("org.springframework.security:spring-security-config")

@ -62,6 +62,8 @@ public class MetricsProperties {
private final Data data = new Data(); private final Data data = new Data();
private final Graphql graphql = new Graphql();
private final System system = new System(); private final System system = new System();
private final Distribution distribution = new Distribution(); private final Distribution distribution = new Distribution();
@ -90,6 +92,10 @@ public class MetricsProperties {
return this.data; return this.data;
} }
public Graphql getGraphql() {
return this.graphql;
}
public System getSystem() { public System getSystem() {
return this.system; return this.system;
} }
@ -268,6 +274,20 @@ public class MetricsProperties {
} }
public static class Graphql {
/**
* Auto-timed queries settings.
*/
@NestedConfigurationProperty
private final AutoTimeProperties autotime = new AutoTimeProperties();
public AutoTimeProperties getAutotime() {
return this.autotime;
}
}
public static class System { public static class System {
private final Diskspace diskspace = new Diskspace(); private final Diskspace diskspace = new Diskspace();

@ -0,0 +1,68 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.metrics.graphql;
import java.util.stream.Collectors;
import graphql.GraphQL;
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.MetricsProperties;
import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration;
import org.springframework.boot.actuate.metrics.graphql.DefaultGraphQlTagsProvider;
import org.springframework.boot.actuate.metrics.graphql.GraphQlMetricsInstrumentation;
import org.springframework.boot.actuate.metrics.graphql.GraphQlTagsContributor;
import org.springframework.boot.actuate.metrics.graphql.GraphQlTagsProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
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.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.graphql.execution.GraphQlSource;
/**
* {@link EnableAutoConfiguration Auto-configuration} for instrumentation of Spring
* GraphQL endpoints.
*
* @author Brian Clozel
* @since 2.7.0
*/
@AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class,
SimpleMetricsExportAutoConfiguration.class })
@ConditionalOnBean(MeterRegistry.class)
@ConditionalOnClass({ GraphQL.class, GraphQlSource.class })
@EnableConfigurationProperties(MetricsProperties.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, MetricsProperties properties) {
return new GraphQlMetricsInstrumentation(meterRegistry, tagsProvider, properties.getGraphql().getAutotime());
}
}

@ -0,0 +1,20 @@
/*
* Copyright 2012-2022 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 for Spring GraphQL metrics.
*/
package org.springframework.boot.actuate.autoconfigure.metrics.graphql;

@ -67,6 +67,7 @@ org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetri
org.springframework.boot.actuate.autoconfigure.metrics.export.stackdriver.StackdriverMetricsExportAutoConfiguration org.springframework.boot.actuate.autoconfigure.metrics.export.stackdriver.StackdriverMetricsExportAutoConfiguration
org.springframework.boot.actuate.autoconfigure.metrics.export.statsd.StatsdMetricsExportAutoConfiguration org.springframework.boot.actuate.autoconfigure.metrics.export.statsd.StatsdMetricsExportAutoConfiguration
org.springframework.boot.actuate.autoconfigure.metrics.export.wavefront.WavefrontMetricsExportAutoConfiguration org.springframework.boot.actuate.autoconfigure.metrics.export.wavefront.WavefrontMetricsExportAutoConfiguration
org.springframework.boot.actuate.autoconfigure.metrics.graphql.GraphQlMetricsAutoConfiguration
org.springframework.boot.actuate.autoconfigure.metrics.integration.IntegrationMetricsAutoConfiguration org.springframework.boot.actuate.autoconfigure.metrics.integration.IntegrationMetricsAutoConfiguration
org.springframework.boot.actuate.autoconfigure.metrics.jdbc.DataSourcePoolMetricsAutoConfiguration org.springframework.boot.actuate.autoconfigure.metrics.jdbc.DataSourcePoolMetricsAutoConfiguration
org.springframework.boot.actuate.autoconfigure.metrics.mongo.MongoMetricsAutoConfiguration org.springframework.boot.actuate.autoconfigure.metrics.mongo.MongoMetricsAutoConfiguration

@ -0,0 +1,98 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.metrics.graphql;
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.junit.jupiter.api.Test;
import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun;
import org.springframework.boot.actuate.metrics.graphql.DefaultGraphQlTagsProvider;
import org.springframework.boot.actuate.metrics.graphql.GraphQlMetricsInstrumentation;
import org.springframework.boot.actuate.metrics.graphql.GraphQlTagsProvider;
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 static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link GraphQlMetricsAutoConfiguration}.
*
* @author Brian Clozel
*/
class GraphQlMetricsAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().with(MetricsRun.simple())
.withConfiguration(AutoConfigurations.of(GraphQlMetricsAutoConfiguration.class));
@Test
void backsOffWhenMeterRegistryIsMissing() {
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(GraphQlMetricsAutoConfiguration.class))
.run((context) -> assertThat(context).doesNotHaveBean(DefaultGraphQlTagsProvider.class)
.doesNotHaveBean(GraphQlMetricsInstrumentation.class));
}
@Test
void definesTagsProviderAndInstrumentationWhenMeterRegistryIsPresent() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(DefaultGraphQlTagsProvider.class)
.hasSingleBean(GraphQlMetricsInstrumentation.class));
}
@Test
void tagsProviderBacksOffIfAlreadyPresent() {
this.contextRunner.withUserConfiguration(TagsProviderConfiguration.class).run((context) -> assertThat(context)
.doesNotHaveBean(DefaultGraphQlTagsProvider.class).hasSingleBean(TestGraphQlTagsProvider.class));
}
@Configuration(proxyBeanMethods = false)
static class TagsProviderConfiguration {
@Bean
TestGraphQlTagsProvider tagsProvider() {
return new TestGraphQlTagsProvider();
}
}
static class TestGraphQlTagsProvider implements GraphQlTagsProvider {
@Override
public Iterable<Tag> getExecutionTags(InstrumentationExecutionParameters parameters, ExecutionResult result,
Throwable exception) {
return null;
}
@Override
public Iterable<Tag> getErrorTags(InstrumentationExecutionParameters parameters, GraphQLError error) {
return null;
}
@Override
public Iterable<Tag> getDataFetchingTags(DataFetcher<?> dataFetcher,
InstrumentationFieldFetchParameters parameters, Throwable exception) {
return null;
}
}
}

@ -61,6 +61,7 @@ dependencies {
optional("org.springframework:spring-webflux") optional("org.springframework:spring-webflux")
optional("org.springframework:spring-web") optional("org.springframework:spring-web")
optional("org.springframework:spring-webmvc") optional("org.springframework:spring-webmvc")
optional("org.springframework.graphql:spring-graphql")
optional("org.springframework.amqp:spring-rabbit") optional("org.springframework.amqp:spring-rabbit")
optional("org.springframework.data:spring-data-cassandra") { optional("org.springframework.data:spring-data-cassandra") {
exclude group: "org.slf4j", module: "jcl-over-slf4j" exclude group: "org.slf4j", module: "jcl-over-slf4j"

@ -0,0 +1,77 @@
/*
* 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.boot.actuate.metrics.graphql;
import java.util.Collections;
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 2.7.0
*/
public class DefaultGraphQlTagsProvider implements GraphQlTagsProvider {
private final List<GraphQlTagsContributor> contributors;
public DefaultGraphQlTagsProvider(List<GraphQlTagsContributor> contributors) {
this.contributors = contributors;
}
public DefaultGraphQlTagsProvider() {
this(Collections.emptyList());
}
@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;
}
}

@ -0,0 +1,166 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.metrics.graphql;
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.InstrumentationCreateStateParameters;
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;
/**
* Micrometer-based {@link SimpleInstrumentation}.
*
* @author Brian Clozel
* @since 2.7.0
*/
public class GraphQlMetricsInstrumentation extends SimpleInstrumentation {
private final MeterRegistry registry;
private final GraphQlTagsProvider tagsProvider;
private final AutoTimer autoTimer;
private final DistributionSummary dataFetchingSummary;
public 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(InstrumentationCreateStateParameters parameters) {
return new RequestMetricsInstrumentationState(this.autoTimer, this.registry);
}
@Override
public InstrumentationContext<ExecutionResult> beginExecution(InstrumentationExecutionParameters parameters,
InstrumentationState state) {
if (this.autoTimer.isEnabled() && state instanceof RequestMetricsInstrumentationState instrumentationState) {
instrumentationState.startTimer();
return new SimpleInstrumentationContext<ExecutionResult>() {
@Override
public void onCompleted(ExecutionResult result, Throwable exc) {
Iterable<Tag> tags = GraphQlMetricsInstrumentation.this.tagsProvider.getExecutionTags(parameters,
result, exc);
instrumentationState.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(instrumentationState.getDataFetchingCount());
}
};
}
return super.beginExecution(parameters);
}
@Override
public DataFetcher<?> instrumentDataFetcher(DataFetcher<?> dataFetcher,
InstrumentationFieldFetchParameters parameters, InstrumentationState state) {
if (this.autoTimer.isEnabled() && !parameters.isTrivialDataFetcher()
&& state instanceof RequestMetricsInstrumentationState instrumentationState) {
return (environment) -> {
Timer.Sample sample = Timer.start(this.registry);
try {
Object value = dataFetcher.get(environment);
if (value instanceof CompletionStage<?> completion) {
return completion.whenComplete((result, error) -> recordDataFetcherMetric(sample,
instrumentationState, dataFetcher, parameters, error));
}
else {
recordDataFetcherMetric(sample, instrumentationState, dataFetcher, parameters, null);
return value;
}
}
catch (Throwable throwable) {
recordDataFetcherMetric(sample, instrumentationState, dataFetcher, parameters, throwable);
throw throwable;
}
};
}
return super.instrumentDataFetcher(dataFetcher, parameters, state);
}
private void recordDataFetcherMetric(Timer.Sample sample, RequestMetricsInstrumentationState instrumentationState,
DataFetcher<?> dataFetcher, InstrumentationFieldFetchParameters parameters, Throwable throwable) {
Timer.Builder timer = this.autoTimer.builder("graphql.datafetcher");
timer.tags(this.tagsProvider.getDataFetchingTags(dataFetcher, parameters, throwable));
sample.stop(timer.register(this.registry));
instrumentationState.incrementDataFetchingCount();
}
static class RequestMetricsInstrumentationState implements InstrumentationState {
private final MeterRegistry registry;
private final Timer.Builder timer;
private Timer.Sample sample;
private final AtomicLong dataFetchingCount = new AtomicLong();
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();
}
}
}

@ -0,0 +1,101 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.metrics.graphql;
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.util.CollectionUtils;
/**
* Factory methods for Tags associated with a GraphQL request.
*
* @author Brian Clozel
* @since 2.7.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_ERROR_TYPE = Tag.of("error.type", "UNKNOWN");
private GraphQlTags() {
}
public static Tag executionOutcome(ExecutionResult result, 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("error.type", ((ErrorType) errorType).name());
}
return UNKNOWN_ERROR_TYPE;
}
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 {
Integer.parseUnsignedInt(segment.toString());
builder.append("[*]");
}
catch (NumberFormatException exc) {
builder.append('.');
builder.append(segment);
}
}
}
return Tag.of("error.path", builder.toString());
}
public static Tag dataFetchingOutcome(Throwable exception) {
return (exception != null) ? OUTCOME_ERROR : OUTCOME_SUCCESS;
}
public static Tag dataFetchingPath(InstrumentationFieldFetchParameters parameters) {
ExecutionStepInfo executionStepInfo = parameters.getExecutionStepInfo();
StringBuilder dataFetchingPath = new StringBuilder();
if (executionStepInfo.hasParent() && executionStepInfo.getParent().getType() instanceof GraphQLObjectType) {
dataFetchingPath.append(((GraphQLObjectType) executionStepInfo.getParent().getType()).getName());
dataFetchingPath.append('.');
}
dataFetchingPath.append(executionStepInfo.getPath().getSegmentName());
return Tag.of("path", dataFetchingPath.toString());
}
}

@ -0,0 +1,43 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.metrics.graphql;
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;
/**
* 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 2.7.0
*/
public interface GraphQlTagsContributor {
Iterable<Tag> getExecutionTags(InstrumentationExecutionParameters parameters, ExecutionResult result,
Throwable exception);
Iterable<Tag> getErrorTags(InstrumentationExecutionParameters parameters, GraphQLError error);
Iterable<Tag> getDataFetchingTags(DataFetcher<?> dataFetcher, InstrumentationFieldFetchParameters parameters,
Throwable exception);
}

@ -0,0 +1,42 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.metrics.graphql;
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;
/**
* Provides {@link Tag Tags} for Spring GraphQL-based request handling.
*
* @author Brian Clozel
* @since 2.7.0
*/
public interface GraphQlTagsProvider {
Iterable<Tag> getExecutionTags(InstrumentationExecutionParameters parameters, ExecutionResult result,
Throwable exception);
Iterable<Tag> getErrorTags(InstrumentationExecutionParameters parameters, GraphQLError error);
Iterable<Tag> getDataFetchingTags(DataFetcher<?> dataFetcher, InstrumentationFieldFetchParameters parameters,
Throwable exception);
}

@ -0,0 +1,20 @@
/*
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Provides instrumentation support for Spring GraphQL.
*/
package org.springframework.boot.actuate.metrics.graphql;

@ -0,0 +1,185 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.metrics.graphql;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import graphql.ExecutionInput;
import graphql.ExecutionResult;
import graphql.ExecutionResultImpl;
import graphql.execution.instrumentation.InstrumentationContext;
import graphql.execution.instrumentation.InstrumentationState;
import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters;
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters;
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import graphql.schema.DataFetchingEnvironmentImpl;
import graphql.schema.GraphQLSchema;
import graphql.schema.idl.SchemaGenerator;
import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.MockClock;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.simple.SimpleConfig;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.metrics.AutoTimer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link GraphQlMetricsInstrumentation}.
*
* @author Brian Clozel
*/
@SuppressWarnings("unchecked")
class GraphQlMetricsInstrumentationTests {
private final ExecutionInput input = ExecutionInput.newExecutionInput("{greeting}").build();
private final GraphQLSchema schema = SchemaGenerator.createdMockedSchema("type Query { greeting: String }");
private MeterRegistry registry;
private GraphQlMetricsInstrumentation instrumentation;
private InstrumentationState state;
private InstrumentationExecutionParameters parameters;
@BeforeEach
void setup() {
this.registry = new SimpleMeterRegistry(SimpleConfig.DEFAULT, new MockClock());
this.instrumentation = new GraphQlMetricsInstrumentation(this.registry, mock(GraphQlTagsProvider.class),
AutoTimer.ENABLED);
this.state = this.instrumentation
.createState(new InstrumentationCreateStateParameters(this.schema, this.input));
this.parameters = new InstrumentationExecutionParameters(this.input, this.schema, this.state);
}
@Test
void shouldRecordTimerWhenResult() {
InstrumentationContext<ExecutionResult> execution = this.instrumentation.beginExecution(this.parameters,
this.state);
ExecutionResult result = new ExecutionResultImpl("Hello", null);
execution.onCompleted(result, null);
Timer timer = this.registry.find("graphql.request").timer();
assertThat(timer).isNotNull();
assertThat(timer.count()).isEqualTo(1);
}
@Test
void shouldRecordDataFetchingCount() throws Exception {
InstrumentationContext<ExecutionResult> execution = this.instrumentation.beginExecution(this.parameters,
this.state);
ExecutionResult result = new ExecutionResultImpl("Hello", null);
DataFetcher<String> dataFetcher = mock(DataFetcher.class);
given(dataFetcher.get(any())).willReturn("Hello");
InstrumentationFieldFetchParameters fieldFetchParameters = mockFieldFetchParameters(false);
DataFetcher<?> instrumented = this.instrumentation.instrumentDataFetcher(dataFetcher, fieldFetchParameters,
this.state);
DataFetchingEnvironment environment = DataFetchingEnvironmentImpl.newDataFetchingEnvironment().build();
instrumented.get(environment);
execution.onCompleted(result, null);
DistributionSummary summary = this.registry.find("graphql.request.datafetch.count").summary();
assertThat(summary).isNotNull();
assertThat(summary.count()).isEqualTo(1);
}
@Test
void shouldRecordDataFetchingMetricWhenSuccess() throws Exception {
DataFetcher<String> dataFetcher = mock(DataFetcher.class);
given(dataFetcher.get(any())).willReturn("Hello");
InstrumentationFieldFetchParameters fieldFetchParameters = mockFieldFetchParameters(false);
DataFetcher<?> instrumented = this.instrumentation.instrumentDataFetcher(dataFetcher, fieldFetchParameters,
this.state);
DataFetchingEnvironment environment = DataFetchingEnvironmentImpl.newDataFetchingEnvironment().build();
instrumented.get(environment);
Timer timer = this.registry.find("graphql.datafetcher").timer();
assertThat(timer).isNotNull();
assertThat(timer.count()).isEqualTo(1);
}
@Test
void shouldRecordDataFetchingMetricWhenSuccessCompletionStage() throws Exception {
DataFetcher<CompletionStage<String>> dataFetcher = mock(DataFetcher.class);
given(dataFetcher.get(any())).willReturn(CompletableFuture.completedFuture("Hello"));
InstrumentationFieldFetchParameters fieldFetchParameters = mockFieldFetchParameters(false);
DataFetcher<?> instrumented = this.instrumentation.instrumentDataFetcher(dataFetcher, fieldFetchParameters,
this.state);
DataFetchingEnvironment environment = DataFetchingEnvironmentImpl.newDataFetchingEnvironment().build();
instrumented.get(environment);
Timer timer = this.registry.find("graphql.datafetcher").timer();
assertThat(timer).isNotNull();
assertThat(timer.count()).isEqualTo(1);
}
@Test
void shouldRecordDataFetchingMetricWhenError() throws Exception {
DataFetcher<CompletionStage<String>> dataFetcher = mock(DataFetcher.class);
given(dataFetcher.get(any())).willThrow(new IllegalStateException("test"));
InstrumentationFieldFetchParameters fieldFetchParameters = mockFieldFetchParameters(false);
DataFetcher<?> instrumented = this.instrumentation.instrumentDataFetcher(dataFetcher, fieldFetchParameters,
this.state);
DataFetchingEnvironment environment = DataFetchingEnvironmentImpl.newDataFetchingEnvironment().build();
assertThatThrownBy(() -> instrumented.get(environment)).isInstanceOf(IllegalStateException.class);
Timer timer = this.registry.find("graphql.datafetcher").timer();
assertThat(timer).isNotNull();
assertThat(timer.count()).isEqualTo(1);
}
@Test
void shouldNotRecordDataFetchingMetricWhenTrivial() throws Exception {
DataFetcher<String> dataFetcher = mock(DataFetcher.class);
given(dataFetcher.get(any())).willReturn("Hello");
InstrumentationFieldFetchParameters fieldFetchParameters = mockFieldFetchParameters(true);
DataFetcher<?> instrumented = this.instrumentation.instrumentDataFetcher(dataFetcher, fieldFetchParameters,
this.state);
DataFetchingEnvironment environment = DataFetchingEnvironmentImpl.newDataFetchingEnvironment().build();
instrumented.get(environment);
Timer timer = this.registry.find("graphql.datafetcher").timer();
assertThat(timer).isNull();
}
private InstrumentationFieldFetchParameters mockFieldFetchParameters(boolean isTrivial) {
InstrumentationFieldFetchParameters fieldFetchParameters = mock(InstrumentationFieldFetchParameters.class);
given(fieldFetchParameters.isTrivialDataFetcher()).willReturn(isTrivial);
return fieldFetchParameters;
}
}

@ -0,0 +1,95 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.metrics.graphql;
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).isEqualTo(Tag.of("outcome", "SUCCESS"));
}
@Test
void executionOutcomeShouldErrorWhenExceptionThrown() {
ExecutionResult result = ExecutionResultImpl.newExecutionResult().build();
Tag tag = GraphQlTags.executionOutcome(result, new IllegalArgumentException("test error"));
assertThat(tag).isEqualTo(Tag.of("outcome", "ERROR"));
}
@Test
void executionOutcomeShouldErrorWhenResponseErrors() {
GraphQLError error = GraphqlErrorBuilder.newError().message("Invalid query").build();
Tag tag = GraphQlTags.executionOutcome(ExecutionResultImpl.newExecutionResult().addError(error).build(), null);
assertThat(tag).isEqualTo(Tag.of("outcome", "ERROR"));
}
@Test
void errorTypeShouldBeDefinedIfPresent() {
GraphQLError error = GraphqlErrorBuilder.newError().errorType(ErrorType.DataFetchingException)
.message("test error").build();
Tag errorTypeTag = GraphQlTags.errorType(error);
assertThat(errorTypeTag).isEqualTo(Tag.of("error.type", "DataFetchingException"));
}
@Test
void errorPathShouldUseJsonPathFormat() {
GraphQLError error = GraphqlErrorBuilder.newError().path(Arrays.asList("project", "name")).message("test error")
.build();
Tag errorPathTag = GraphQlTags.errorPath(error);
assertThat(errorPathTag).isEqualTo(Tag.of("error.path", "$.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).isEqualTo(Tag.of("error.path", "$.issues[*].title"));
}
@Test
void dataFetchingOutcomeShouldBeSuccessfulIfNoException() {
Tag fetchingOutcomeTag = GraphQlTags.dataFetchingOutcome(null);
assertThat(fetchingOutcomeTag).isEqualTo(Tag.of("outcome", "SUCCESS"));
}
@Test
void dataFetchingOutcomeShouldBeErrorIfException() {
Tag fetchingOutcomeTag = GraphQlTags.dataFetchingOutcome(new IllegalStateException("error state"));
assertThat(fetchingOutcomeTag).isEqualTo(Tag.of("outcome", "ERROR"));
}
}

@ -47,6 +47,7 @@ dependencies {
optional("jakarta.persistence:jakarta.persistence-api") optional("jakarta.persistence:jakarta.persistence-api")
optional("jakarta.transaction:jakarta.transaction-api") optional("jakarta.transaction:jakarta.transaction-api")
optional("jakarta.validation:jakarta.validation-api") optional("jakarta.validation:jakarta.validation-api")
optional("jakarta.websocket:jakarta.websocket-api")
optional("jakarta.ws.rs:jakarta.ws.rs-api") optional("jakarta.ws.rs:jakarta.ws.rs-api")
optional("javax.cache:cache-api") optional("javax.cache:cache-api")
optional("javax.money:money-api") optional("javax.money:money-api")
@ -155,6 +156,7 @@ dependencies {
optional("org.springframework.data:spring-data-neo4j") optional("org.springframework.data:spring-data-neo4j")
optional("org.springframework.data:spring-data-r2dbc") optional("org.springframework.data:spring-data-r2dbc")
optional("org.springframework.data:spring-data-redis") optional("org.springframework.data:spring-data-redis")
optional("org.springframework.graphql:spring-graphql")
optional("org.springframework.hateoas:spring-hateoas") optional("org.springframework.hateoas:spring-hateoas")
optional("org.springframework.security:spring-security-acl") optional("org.springframework.security:spring-security-acl")
optional("org.springframework.security:spring-security-config") optional("org.springframework.security:spring-security-config")
@ -196,6 +198,7 @@ dependencies {
testImplementation("com.github.h-thurow:simple-jndi") testImplementation("com.github.h-thurow:simple-jndi")
testImplementation("com.ibm.db2:jcc") testImplementation("com.ibm.db2:jcc")
testImplementation("com.jayway.jsonpath:json-path") testImplementation("com.jayway.jsonpath:json-path")
testImplementation("com.querydsl:querydsl-core")
testImplementation("com.squareup.okhttp3:mockwebserver") testImplementation("com.squareup.okhttp3:mockwebserver")
testImplementation("com.sun.xml.messaging.saaj:saaj-impl") testImplementation("com.sun.xml.messaging.saaj:saaj-impl")
testImplementation("io.projectreactor:reactor-test") testImplementation("io.projectreactor:reactor-test")
@ -215,6 +218,7 @@ dependencies {
testImplementation("org.mockito:mockito-junit-jupiter") testImplementation("org.mockito:mockito-junit-jupiter")
testImplementation("org.skyscreamer:jsonassert") testImplementation("org.skyscreamer:jsonassert")
testImplementation("org.springframework:spring-test") testImplementation("org.springframework:spring-test")
testImplementation("org.springframework.graphql:spring-graphql-test")
testImplementation("org.springframework.kafka:spring-kafka-test") testImplementation("org.springframework.kafka:spring-kafka-test")
testImplementation("org.springframework.security:spring-security-test") testImplementation("org.springframework.security:spring-security-test")
testImplementation("org.testcontainers:cassandra") testImplementation("org.testcontainers:cassandra")

@ -0,0 +1,40 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Conditional;
/**
* {@link Conditional @Conditional} that only matches when a GraphQL schema is defined for
* the application, via schema files or infrastructure beans.
*
* @author Brian Clozel
* @since 2.7.0
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(DefaultGraphQlSchemaCondition.class)
public @interface ConditionalOnGraphQlSchema {
}

@ -0,0 +1,108 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.ConfigurationCondition;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternUtils;
import org.springframework.core.type.AnnotatedTypeMetadata;
/**
* {@link Condition} that checks whether a GraphQL schema has been defined in the
* application. This is looking for:
* <ul>
* <li>schema files in the {@link GraphQlProperties configured locations}</li>
* <li>or infrastructure beans such as {@link GraphQlSourceBuilderCustomizer}</li>
* </ul>
*
* @author Brian Clozel
* @see ConditionalOnGraphQlSchema
*/
class DefaultGraphQlSchemaCondition extends SpringBootCondition implements ConfigurationCondition {
@Override
public ConfigurationCondition.ConfigurationPhase getConfigurationPhase() {
return ConfigurationCondition.ConfigurationPhase.REGISTER_BEAN;
}
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
boolean match = false;
List<ConditionMessage> messages = new ArrayList<>(2);
ConditionMessage.Builder message = ConditionMessage.forCondition(ConditionalOnGraphQlSchema.class);
Binder binder = Binder.get(context.getEnvironment());
GraphQlProperties.Schema schema = binder.bind("spring.graphql.schema", GraphQlProperties.Schema.class)
.orElse(new GraphQlProperties.Schema());
ResourcePatternResolver resourcePatternResolver = ResourcePatternUtils
.getResourcePatternResolver(context.getResourceLoader());
List<Resource> schemaResources = resolveSchemaResources(resourcePatternResolver, schema.getLocations(),
schema.getFileExtensions());
if (!schemaResources.isEmpty()) {
match = true;
messages.add(message.found("schema", "schemas").items(ConditionMessage.Style.QUOTE, schemaResources));
}
else {
messages.add(message.didNotFind("schema files in locations").items(ConditionMessage.Style.QUOTE,
Arrays.asList(schema.getLocations())));
}
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
String[] customizerBeans = beanFactory.getBeanNamesForType(GraphQlSourceBuilderCustomizer.class, false, false);
if (customizerBeans.length != 0) {
match = true;
messages.add(message.found("customizer", "customizers").items(Arrays.asList(customizerBeans)));
}
else {
messages.add((message.didNotFind("GraphQlSourceBuilderCustomizer").atAll()));
}
return new ConditionOutcome(match, ConditionMessage.of(messages));
}
private List<Resource> resolveSchemaResources(ResourcePatternResolver resolver, String[] locations,
String[] extensions) {
List<Resource> resources = new ArrayList<>();
for (String location : locations) {
for (String extension : extensions) {
resources.addAll(resolveSchemaResources(resolver, location + "*" + extension));
}
}
return resources;
}
private List<Resource> resolveSchemaResources(ResourcePatternResolver resolver, String pattern) {
try {
return Arrays.asList(resolver.getResources(pattern));
}
catch (IOException ex) {
return Collections.emptyList();
}
}
}

@ -0,0 +1,151 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import graphql.GraphQL;
import graphql.execution.instrumentation.Instrumentation;
import graphql.schema.idl.RuntimeWiring.Builder;
import graphql.schema.visibility.NoIntrospectionGraphqlFieldVisibility;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
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.boot.convert.ApplicationConversionService;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.log.LogMessage;
import org.springframework.graphql.ExecutionGraphQlService;
import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer;
import org.springframework.graphql.execution.BatchLoaderRegistry;
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
import org.springframework.graphql.execution.DefaultBatchLoaderRegistry;
import org.springframework.graphql.execution.DefaultExecutionGraphQlService;
import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
import org.springframework.graphql.execution.SubscriptionExceptionResolver;
/**
* {@link EnableAutoConfiguration Auto-configuration} for creating a Spring GraphQL base
* infrastructure.
*
* @author Brian Clozel
* @since 2.7.0
*/
@AutoConfiguration
@ConditionalOnClass({ GraphQL.class, GraphQlSource.class })
@ConditionalOnGraphQlSchema
@EnableConfigurationProperties(GraphQlProperties.class)
public class GraphQlAutoConfiguration {
private static final Log logger = LogFactory.getLog(GraphQlAutoConfiguration.class);
private final ListableBeanFactory beanFactory;
public GraphQlAutoConfiguration(ListableBeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
@Bean
@ConditionalOnMissingBean
public GraphQlSource graphQlSource(ResourcePatternResolver resourcePatternResolver, GraphQlProperties properties,
ObjectProvider<DataFetcherExceptionResolver> exceptionResolvers,
ObjectProvider<SubscriptionExceptionResolver> subscriptionExceptionResolvers,
ObjectProvider<Instrumentation> instrumentations, ObjectProvider<RuntimeWiringConfigurer> wiringConfigurers,
ObjectProvider<GraphQlSourceBuilderCustomizer> sourceCustomizers) {
String[] schemaLocations = properties.getSchema().getLocations();
Resource[] schemaResources = resolveSchemaResources(resourcePatternResolver, schemaLocations,
properties.getSchema().getFileExtensions());
GraphQlSource.SchemaResourceBuilder builder = GraphQlSource.schemaResourceBuilder()
.schemaResources(schemaResources).exceptionResolvers(toList(exceptionResolvers))
.subscriptionExceptionResolvers(toList(subscriptionExceptionResolvers))
.instrumentation(toList(instrumentations));
if (!properties.getSchema().getIntrospection().isEnabled()) {
builder.configureRuntimeWiring(this::enableIntrospection);
}
wiringConfigurers.orderedStream().forEach(builder::configureRuntimeWiring);
sourceCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
return builder.build();
}
private Builder enableIntrospection(Builder wiring) {
return wiring.fieldVisibility(NoIntrospectionGraphqlFieldVisibility.NO_INTROSPECTION_FIELD_VISIBILITY);
}
private Resource[] resolveSchemaResources(ResourcePatternResolver resolver, String[] locations,
String[] extensions) {
List<Resource> resources = new ArrayList<>();
for (String location : locations) {
for (String extension : extensions) {
resources.addAll(resolveSchemaResources(resolver, location + "*" + extension));
}
}
return resources.toArray(new Resource[0]);
}
private List<Resource> resolveSchemaResources(ResourcePatternResolver resolver, String pattern) {
try {
return Arrays.asList(resolver.getResources(pattern));
}
catch (IOException ex) {
logger.debug(LogMessage.format("Could not resolve schema location: '%s'", pattern), ex);
return Collections.emptyList();
}
}
@Bean
@ConditionalOnMissingBean
public BatchLoaderRegistry batchLoaderRegistry() {
return new DefaultBatchLoaderRegistry();
}
@Bean
@ConditionalOnMissingBean
public ExecutionGraphQlService executionGraphQlService(GraphQlSource graphQlSource,
BatchLoaderRegistry batchLoaderRegistry) {
DefaultExecutionGraphQlService service = new DefaultExecutionGraphQlService(graphQlSource);
service.addDataLoaderRegistrar(batchLoaderRegistry);
return service;
}
@Bean
@ConditionalOnMissingBean
public AnnotatedControllerConfigurer annotatedControllerConfigurer() {
AnnotatedControllerConfigurer controllerConfigurer = new AnnotatedControllerConfigurer();
controllerConfigurer
.addFormatterRegistrar((registry) -> ApplicationConversionService.addBeans(registry, this.beanFactory));
return controllerConfigurer;
}
private <T> List<T> toList(ObjectProvider<T> provider) {
return provider.orderedStream().collect(Collectors.toList());
}
}

@ -0,0 +1,156 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql;
import java.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.util.CollectionUtils;
import org.springframework.web.cors.CorsConfiguration;
/**
* Configuration properties for GraphQL endpoint's CORS support.
*
* @author Andy Wilkinson
* @author Brian Clozel
* @since 2.7.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.
*/
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;
}
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;
}
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;
}
}

@ -0,0 +1,230 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql;
import java.time.Duration;
import java.util.Arrays;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* {@link ConfigurationProperties properties} for Spring GraphQL.
*
* @author Brian Clozel
* @since 2.7.0
*/
@ConfigurationProperties(prefix = "spring.graphql")
public class GraphQlProperties {
/**
* Path at which to expose a GraphQL request HTTP endpoint.
*/
private String path = "/graphql";
private final Graphiql graphiql = new Graphiql();
private final Schema schema = new Schema();
private final Websocket websocket = new Websocket();
private final Rsocket rsocket = new Rsocket();
public Graphiql getGraphiql() {
return this.graphiql;
}
public String getPath() {
return this.path;
}
public void setPath(String path) {
this.path = path;
}
public Schema getSchema() {
return this.schema;
}
public Websocket getWebsocket() {
return this.websocket;
}
public Rsocket getRsocket() {
return this.rsocket;
}
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 Introspection introspection = new Introspection();
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 Introspection getIntrospection() {
return this.introspection;
}
public Printer getPrinter() {
return this.printer;
}
public static class Introspection {
/**
* Whether field introspection should be enabled at the schema level.
*/
private boolean enabled = true;
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}
public static class Printer {
/**
* Whether the endpoint that prints the schema is enabled. Schema is available
* under spring.graphql.path + "/schema".
*/
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 = false;
public String getPath() {
return this.path;
}
public void setPath(String path) {
this.path = path;
}
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}
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;
}
}
public static class Rsocket {
/**
* Mapping of the RSocket message handler.
*/
private String mapping;
public String getMapping() {
return this.mapping;
}
public void setMapping(String mapping) {
this.mapping = mapping;
}
}
}

@ -0,0 +1,40 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql;
import org.springframework.graphql.execution.GraphQlSource;
/**
* Callback interface that can be implemented by beans wishing to customize properties of
* {@link org.springframework.graphql.execution.GraphQlSource.SchemaResourceBuilder
* Builder} whilst retaining default auto-configuration.
*
* @author Rossen Stoyanchev
* @since 2.7.0
*/
@FunctionalInterface
public interface GraphQlSourceBuilderCustomizer {
/**
* Customize the
* {@link org.springframework.graphql.execution.GraphQlSource.SchemaResourceBuilder
* Builder} instance.
* @param builder builder the builder to customize
*/
void customize(GraphQlSource.SchemaResourceBuilder builder);
}

@ -0,0 +1,58 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql.data;
import java.util.List;
import graphql.GraphQL;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
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.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.autoconfigure.graphql.GraphQlSourceBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.data.repository.query.QueryByExampleExecutor;
import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor;
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 2.7.0
* @see QueryByExampleDataFetcher#autoRegistrationConfigurer(List, List)
*/
@AutoConfiguration(after = GraphQlAutoConfiguration.class)
@ConditionalOnClass({ GraphQL.class, QueryByExampleDataFetcher.class, QueryByExampleExecutor.class })
@ConditionalOnBean(GraphQlSource.class)
public class GraphQlQueryByExampleAutoConfiguration {
@Bean
public GraphQlSourceBuilderCustomizer queryByExampleRegistrar(ObjectProvider<QueryByExampleExecutor<?>> executors,
ObjectProvider<ReactiveQueryByExampleExecutor<?>> reactiveExecutors) {
return new GraphQlQuerydslSourceBuilderCustomizer<>(QueryByExampleDataFetcher::autoRegistrationConfigurer,
executors, reactiveExecutors);
}
}

@ -0,0 +1,59 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql.data;
import java.util.List;
import graphql.GraphQL;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
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.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.autoconfigure.graphql.GraphQlSourceBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor;
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
* @author Brian Clozel
* @since 2.7.0
* @see QuerydslDataFetcher#autoRegistrationConfigurer(List, List)
*/
@AutoConfiguration(after = GraphQlAutoConfiguration.class)
@ConditionalOnClass({ GraphQL.class, QuerydslDataFetcher.class, QuerydslPredicateExecutor.class })
@ConditionalOnBean(GraphQlSource.class)
public class GraphQlQuerydslAutoConfiguration {
@Bean
public GraphQlSourceBuilderCustomizer querydslRegistrar(ObjectProvider<QuerydslPredicateExecutor<?>> executors,
ObjectProvider<ReactiveQuerydslPredicateExecutor<?>> reactiveExecutors) {
return new GraphQlQuerydslSourceBuilderCustomizer<>(QuerydslDataFetcher::autoRegistrationConfigurer, executors,
reactiveExecutors);
}
}

@ -0,0 +1,72 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql.data;
import java.util.Collections;
import java.util.List;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.graphql.GraphQlSourceBuilderCustomizer;
import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
/**
* {@link GraphQlSourceBuilderCustomizer} to apply auto-configured QueryDSL
* {@link RuntimeWiringConfigurer RuntimeWiringConfigurers}.
*
* @param <E> the executor type
* @param <R> the reactive executor type
* @author Phillip Webb
* @author Rossen Stoyanchev
* @author Brian Clozel
*/
class GraphQlQuerydslSourceBuilderCustomizer<E, R> implements GraphQlSourceBuilderCustomizer {
private final BiFunction<List<E>, List<R>, RuntimeWiringConfigurer> wiringConfigurerFactory;
private final List<E> executors;
private final List<R> reactiveExecutors;
GraphQlQuerydslSourceBuilderCustomizer(
BiFunction<List<E>, List<R>, RuntimeWiringConfigurer> wiringConfigurerFactory, ObjectProvider<E> executors,
ObjectProvider<R> reactiveExecutors) {
this(wiringConfigurerFactory, toList(executors), toList(reactiveExecutors));
}
GraphQlQuerydslSourceBuilderCustomizer(
BiFunction<List<E>, List<R>, RuntimeWiringConfigurer> wiringConfigurerFactory, List<E> executors,
List<R> reactiveExecutors) {
this.wiringConfigurerFactory = wiringConfigurerFactory;
this.executors = executors;
this.reactiveExecutors = reactiveExecutors;
}
@Override
public void customize(GraphQlSource.SchemaResourceBuilder builder) {
if (!this.executors.isEmpty() || !this.reactiveExecutors.isEmpty()) {
builder.configureRuntimeWiring(this.wiringConfigurerFactory.apply(this.executors, this.reactiveExecutors));
}
}
private static <T> List<T> toList(ObjectProvider<T> provider) {
return (provider != null) ? provider.orderedStream().collect(Collectors.toList()) : Collections.emptyList();
}
}

@ -0,0 +1,58 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql.data;
import java.util.List;
import graphql.GraphQL;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
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.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.autoconfigure.graphql.GraphQlSourceBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.data.repository.query.QueryByExampleExecutor;
import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor;
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 2.7.0
* @see QueryByExampleDataFetcher#autoRegistrationConfigurer(List, List)
*/
@AutoConfiguration(after = GraphQlAutoConfiguration.class)
@ConditionalOnClass({ GraphQL.class, QueryByExampleDataFetcher.class, ReactiveQueryByExampleExecutor.class })
@ConditionalOnBean(GraphQlSource.class)
public class GraphQlReactiveQueryByExampleAutoConfiguration {
@Bean
public GraphQlSourceBuilderCustomizer reactiveQueryByExampleRegistrar(
ObjectProvider<ReactiveQueryByExampleExecutor<?>> reactiveExecutors) {
return new GraphQlQuerydslSourceBuilderCustomizer<>(QueryByExampleDataFetcher::autoRegistrationConfigurer,
(ObjectProvider<QueryByExampleExecutor<?>>) null, reactiveExecutors);
}
}

@ -0,0 +1,59 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql.data;
import java.util.List;
import graphql.GraphQL;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
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.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.autoconfigure.graphql.GraphQlSourceBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor;
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
* @author Brian Clozel
* @since 2.7.0
* @see QuerydslDataFetcher#autoRegistrationConfigurer(List, List)
*/
@AutoConfiguration(after = GraphQlAutoConfiguration.class)
@ConditionalOnClass({ GraphQL.class, QuerydslDataFetcher.class, ReactiveQuerydslPredicateExecutor.class })
@ConditionalOnBean(GraphQlSource.class)
public class GraphQlReactiveQuerydslAutoConfiguration {
@Bean
public GraphQlSourceBuilderCustomizer reactiveQuerydslRegistrar(
ObjectProvider<ReactiveQuerydslPredicateExecutor<?>> reactiveExecutors) {
return new GraphQlQuerydslSourceBuilderCustomizer<>(QuerydslDataFetcher::autoRegistrationConfigurer,
(ObjectProvider<QuerydslPredicateExecutor<?>>) null, reactiveExecutors);
}
}

@ -0,0 +1,20 @@
/*
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Auto-configuration classes for data integrations with GraphQL.
*/
package org.springframework.boot.autoconfigure.graphql.data;

@ -0,0 +1,20 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Auto-configuration for Spring GraphQL.
*/
package org.springframework.boot.autoconfigure.graphql;

@ -0,0 +1,180 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql.reactive;
import java.util.Collections;
import java.util.stream.Collectors;
import graphql.GraphQL;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
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.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.autoconfigure.graphql.GraphQlCorsProperties;
import org.springframework.boot.autoconfigure.graphql.GraphQlProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.core.log.LogMessage;
import org.springframework.graphql.ExecutionGraphQlService;
import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.graphql.server.WebGraphQlHandler;
import org.springframework.graphql.server.WebGraphQlInterceptor;
import org.springframework.graphql.server.webflux.GraphQlHttpHandler;
import org.springframework.graphql.server.webflux.GraphQlWebSocketHandler;
import org.springframework.graphql.server.webflux.GraphiQlHandler;
import org.springframework.graphql.server.webflux.SchemaHandler;
import org.springframework.http.HttpHeaders;
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.RequestPredicate;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerRequest;
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 2.7.0
*/
@AutoConfiguration(after = GraphQlAutoConfiguration.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@ConditionalOnClass({ GraphQL.class, GraphQlHttpHandler.class })
@ConditionalOnBean(ExecutionGraphQlService.class)
@EnableConfigurationProperties(GraphQlCorsProperties.class)
public class GraphQlWebFluxAutoConfiguration {
private static final RequestPredicate SUPPORTS_MEDIATYPES = accept(MediaType.APPLICATION_GRAPHQL,
MediaType.APPLICATION_JSON).and(contentType(MediaType.APPLICATION_GRAPHQL, MediaType.APPLICATION_JSON));
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(ExecutionGraphQlService service,
ObjectProvider<WebGraphQlInterceptor> interceptorsProvider) {
return WebGraphQlHandler.builder(service)
.interceptors(interceptorsProvider.orderedStream().collect(Collectors.toList())).build();
}
@Bean
@Order(0)
public RouterFunction<ServerResponse> graphQlRouterFunction(GraphQlHttpHandler httpHandler,
GraphQlSource graphQlSource, GraphQlProperties properties) {
String path = properties.getPath();
logger.info(LogMessage.format("GraphQL endpoint HTTP POST %s", path));
RouterFunctions.Builder builder = RouterFunctions.route();
builder = builder.GET(path, this::onlyAllowPost);
builder = builder.POST(path, SUPPORTS_MEDIATYPES, httpHandler::handleRequest);
if (properties.getGraphiql().isEnabled()) {
GraphiQlHandler graphQlHandler = new GraphiQlHandler(path, properties.getWebsocket().getPath());
builder = builder.GET(properties.getGraphiql().getPath(), graphQlHandler::handleRequest);
}
if (properties.getSchema().getPrinter().isEnabled()) {
SchemaHandler schemaHandler = new SchemaHandler(graphQlSource);
builder = builder.GET(path + "/schema", schemaHandler::handleRequest);
}
return builder.build();
}
private Mono<ServerResponse> onlyAllowPost(ServerRequest request) {
return ServerResponse.status(HttpStatus.METHOD_NOT_ALLOWED).headers(this::onlyAllowPost).build();
}
private void onlyAllowPost(HttpHeaders headers) {
headers.setAllow(Collections.singleton(HttpMethod.POST));
}
@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);
}
}
}
@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();
logger.info(LogMessage.format("GraphQL endpoint WebSocket %s", 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;
}
}
}

@ -0,0 +1,20 @@
/*
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Auto-configuration classes for WebFlux support in Spring GraphQL.
*/
package org.springframework.boot.autoconfigure.graphql.reactive;

@ -0,0 +1,73 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql.rsocket;
import java.util.List;
import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.ObjectMapper;
import graphql.GraphQL;
import io.rsocket.core.RSocketServer;
import reactor.netty.http.server.HttpServer;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
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.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.graphql.ExecutionGraphQlService;
import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer;
import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.graphql.server.GraphQlRSocketHandler;
import org.springframework.graphql.server.RSocketGraphQlInterceptor;
import org.springframework.http.codec.json.Jackson2JsonEncoder;
import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler;
/**
* {@link EnableAutoConfiguration Auto-configuration} for enabling Spring GraphQL over
* RSocket.
*
* @author Brian Clozel
* @since 2.7.0
*/
@AutoConfiguration(after = { GraphQlAutoConfiguration.class, RSocketMessagingAutoConfiguration.class })
@ConditionalOnClass({ GraphQL.class, GraphQlSource.class, RSocketServer.class, HttpServer.class })
@ConditionalOnBean({ RSocketMessageHandler.class, AnnotatedControllerConfigurer.class })
@ConditionalOnProperty(prefix = "spring.graphql.rsocket", name = "mapping")
public class GraphQlRSocketAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public GraphQlRSocketHandler graphQlRSocketHandler(ExecutionGraphQlService graphQlService,
ObjectProvider<RSocketGraphQlInterceptor> interceptorsProvider, ObjectMapper objectMapper) {
List<RSocketGraphQlInterceptor> interceptors = interceptorsProvider.orderedStream()
.collect(Collectors.toList());
return new GraphQlRSocketHandler(graphQlService, interceptors, new Jackson2JsonEncoder(objectMapper));
}
@Bean
@ConditionalOnMissingBean
public GraphQlRSocketController graphQlRSocketController(GraphQlRSocketHandler handler) {
return new GraphQlRSocketController(handler);
}
}

@ -0,0 +1,47 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql.rsocket;
import java.util.Map;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.graphql.server.GraphQlRSocketHandler;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.stereotype.Controller;
@Controller
class GraphQlRSocketController {
private final GraphQlRSocketHandler handler;
GraphQlRSocketController(GraphQlRSocketHandler handler) {
this.handler = handler;
}
@MessageMapping("${spring.graphql.rsocket.mapping}")
Mono<Map<String, Object>> handle(Map<String, Object> payload) {
return this.handler.handle(payload);
}
@MessageMapping("${spring.graphql.rsocket.mapping}")
Flux<Map<String, Object>> handleSubscription(Map<String, Object> payload) {
return this.handler.handleSubscription(payload);
}
}

@ -0,0 +1,57 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql.rsocket;
import graphql.GraphQL;
import io.rsocket.RSocket;
import io.rsocket.transport.netty.client.TcpClientTransport;
import org.springframework.boot.autoconfigure.AutoConfiguration;
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.rsocket.RSocketRequesterAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
import org.springframework.graphql.client.RSocketGraphQlClient;
import org.springframework.messaging.rsocket.RSocketRequester;
import org.springframework.util.MimeTypeUtils;
/**
* {@link EnableAutoConfiguration Auto-configuration} for {@link RSocketGraphQlClient}.
* This auto-configuration creates
* {@link org.springframework.graphql.client.RSocketGraphQlClient.Builder
* RSocketGraphQlClient.Builder} prototype beans, as the builders are stateful and should
* not be reused to build client instances with different configurations.
*
* @author Brian Clozel
* @since 2.7.0
*/
@AutoConfiguration(after = RSocketRequesterAutoConfiguration.class)
@ConditionalOnClass({ GraphQL.class, RSocketGraphQlClient.class, RSocketRequester.class, RSocket.class,
TcpClientTransport.class })
public class RSocketGraphQlClientAutoConfiguration {
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
public RSocketGraphQlClient.Builder<?> rsocketGraphQlClientBuilder(
RSocketRequester.Builder rsocketRequesterBuilder) {
return RSocketGraphQlClient.builder(rsocketRequesterBuilder.dataMimeType(MimeTypeUtils.APPLICATION_JSON));
}
}

@ -0,0 +1,20 @@
/*
* Copyright 2020-2022 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 RSocket integration with GraphQL.
*/
package org.springframework.boot.autoconfigure.graphql.rsocket;

@ -0,0 +1,52 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql.security;
import graphql.GraphQL;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.graphql.reactive.GraphQlWebFluxAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.graphql.execution.ReactiveSecurityDataFetcherExceptionResolver;
import org.springframework.graphql.server.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 2.7.0
*/
@AutoConfiguration(after = GraphQlWebFluxAutoConfiguration.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@ConditionalOnClass({ GraphQL.class, GraphQlHttpHandler.class, EnableWebFluxSecurity.class })
@ConditionalOnBean(GraphQlHttpHandler.class)
public class GraphQlWebFluxSecurityAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public ReactiveSecurityDataFetcherExceptionResolver reactiveSecurityDataFetcherExceptionResolver() {
return new ReactiveSecurityDataFetcherExceptionResolver();
}
}

@ -0,0 +1,59 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql.security;
import graphql.GraphQL;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.graphql.servlet.GraphQlWebMvcAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.graphql.execution.SecurityContextThreadLocalAccessor;
import org.springframework.graphql.execution.SecurityDataFetcherExceptionResolver;
import org.springframework.graphql.server.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 2.7.0
*/
@AutoConfiguration(after = GraphQlWebMvcAutoConfiguration.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass({ GraphQL.class, GraphQlHttpHandler.class, EnableWebSecurity.class })
@ConditionalOnBean(GraphQlHttpHandler.class)
public class GraphQlWebMvcSecurityAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public SecurityDataFetcherExceptionResolver securityDataFetcherExceptionResolver() {
return new SecurityDataFetcherExceptionResolver();
}
@Bean
@ConditionalOnMissingBean
public SecurityContextThreadLocalAccessor securityContextThreadLocalAccessor() {
return new SecurityContextThreadLocalAccessor();
}
}

@ -0,0 +1,20 @@
/*
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Auto-configuration classes for Security support in Spring GraphQL.
*/
package org.springframework.boot.autoconfigure.graphql.security;

@ -0,0 +1,201 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql.servlet;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;
import graphql.GraphQL;
import jakarta.websocket.server.ServerContainer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
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.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.autoconfigure.graphql.GraphQlCorsProperties;
import org.springframework.boot.autoconfigure.graphql.GraphQlProperties;
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.annotation.Order;
import org.springframework.core.log.LogMessage;
import org.springframework.graphql.ExecutionGraphQlService;
import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.graphql.execution.ThreadLocalAccessor;
import org.springframework.graphql.server.WebGraphQlHandler;
import org.springframework.graphql.server.WebGraphQlInterceptor;
import org.springframework.graphql.server.webmvc.GraphQlHttpHandler;
import org.springframework.graphql.server.webmvc.GraphQlWebSocketHandler;
import org.springframework.graphql.server.webmvc.GraphiQlHandler;
import org.springframework.graphql.server.webmvc.SchemaHandler;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
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.RequestPredicates;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.RouterFunctions;
import org.springframework.web.servlet.function.ServerRequest;
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;
/**
* {@link EnableAutoConfiguration Auto-configuration} for enabling Spring GraphQL over
* Spring MVC.
*
* @author Brian Clozel
* @since 2.7.0
*/
@AutoConfiguration(after = GraphQlAutoConfiguration.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass({ GraphQL.class, GraphQlHttpHandler.class })
@ConditionalOnBean(ExecutionGraphQlService.class)
@EnableConfigurationProperties(GraphQlCorsProperties.class)
public class GraphQlWebMvcAutoConfiguration {
private static final Log logger = LogFactory.getLog(GraphQlWebMvcAutoConfiguration.class);
private static MediaType[] SUPPORTED_MEDIA_TYPES = new MediaType[] { MediaType.APPLICATION_GRAPHQL,
MediaType.APPLICATION_JSON };
@Bean
@ConditionalOnMissingBean
public GraphQlHttpHandler graphQlHttpHandler(WebGraphQlHandler webGraphQlHandler) {
return new GraphQlHttpHandler(webGraphQlHandler);
}
@Bean
@ConditionalOnMissingBean
public WebGraphQlHandler webGraphQlHandler(ExecutionGraphQlService service,
ObjectProvider<WebGraphQlInterceptor> interceptorsProvider,
ObjectProvider<ThreadLocalAccessor> accessorsProvider) {
return WebGraphQlHandler.builder(service)
.interceptors(interceptorsProvider.orderedStream().collect(Collectors.toList()))
.threadLocalAccessors(accessorsProvider.orderedStream().collect(Collectors.toList())).build();
}
@Bean
@Order(0)
public RouterFunction<ServerResponse> graphQlRouterFunction(GraphQlHttpHandler httpHandler,
GraphQlSource graphQlSource, GraphQlProperties properties) {
String path = properties.getPath();
logger.info(LogMessage.format("GraphQL endpoint HTTP POST %s", path));
RouterFunctions.Builder builder = RouterFunctions.route();
builder = builder.GET(path, this::onlyAllowPost);
builder = builder.POST(path, RequestPredicates.contentType(SUPPORTED_MEDIA_TYPES)
.and(RequestPredicates.accept(SUPPORTED_MEDIA_TYPES)), httpHandler::handleRequest);
if (properties.getGraphiql().isEnabled()) {
GraphiQlHandler graphiQLHandler = new GraphiQlHandler(path, properties.getWebsocket().getPath());
builder = builder.GET(properties.getGraphiql().getPath(), graphiQLHandler::handleRequest);
}
if (properties.getSchema().getPrinter().isEnabled()) {
SchemaHandler schemaHandler = new SchemaHandler(graphQlSource);
builder = builder.GET(path + "/schema", schemaHandler::handleRequest);
}
return builder.build();
}
private ServerResponse onlyAllowPost(ServerRequest request) {
return ServerResponse.status(HttpStatus.METHOD_NOT_ALLOWED).headers(this::onlyAllowPost).build();
}
private void onlyAllowPost(HttpHeaders headers) {
headers.setAllow(Collections.singleton(HttpMethod.POST));
}
@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);
}
}
}
@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());
}
private GenericHttpMessageConverter<Object> getJsonConverter(HttpMessageConverters converters) {
return converters.getConverters().stream().filter(this::canReadJsonMap).findFirst()
.map(this::asGenericHttpMessageConverter)
.orElseThrow(() -> new IllegalStateException("No JSON converter"));
}
private boolean canReadJsonMap(HttpMessageConverter<?> candidate) {
return candidate.canRead(Map.class, MediaType.APPLICATION_JSON);
}
@SuppressWarnings("unchecked")
private GenericHttpMessageConverter<Object> asGenericHttpMessageConverter(HttpMessageConverter<?> converter) {
return (GenericHttpMessageConverter<Object>) converter;
}
@Bean
public HandlerMapping graphQlWebSocketMapping(GraphQlWebSocketHandler handler, GraphQlProperties properties) {
String path = properties.getWebsocket().getPath();
logger.info(LogMessage.format("GraphQL endpoint WebSocket %s", path));
WebSocketHandlerMapping mapping = new WebSocketHandlerMapping();
mapping.setWebSocketUpgradeMatch(true);
mapping.setUrlMap(Collections.singletonMap(path,
handler.asWebSocketHttpRequestHandler(new DefaultHandshakeHandler())));
mapping.setOrder(2); // Ahead of HTTP endpoint ("routerFunctionMapping" bean)
return mapping;
}
}
}

@ -0,0 +1,20 @@
/*
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Auto-configuration classes for MVC support in Spring GraphQL.
*/
package org.springframework.boot.autoconfigure.graphql.servlet;

@ -1162,6 +1162,14 @@
"level": "error" "level": "error"
} }
}, },
{
"name": "spring.graphql.schema.file-extensions",
"defaultValue": ".graphqls,.gqls"
},
{
"name": "spring.graphql.schema.locations",
"defaultValue": "classpath:graphql/**/"
},
{ {
"name": "spring.groovy.template.prefix", "name": "spring.groovy.template.prefix",
"defaultValue": "" "defaultValue": ""
@ -2422,6 +2430,56 @@
} }
] ]
}, },
{
"name": "spring.datasource.xa.data-source-class-name",
"providers": [
{
"name": "class-reference",
"parameters": {
"target": "javax.sql.XADataSource"
}
}
]
},
{
"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"
}
]
},
{ {
"name": "spring.jmx.server", "name": "spring.jmx.server",
"providers": [ "providers": [

@ -44,6 +44,17 @@ org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAuto
org.springframework.boot.autoconfigure.elasticsearch.ReactiveElasticsearchClientAutoConfiguration org.springframework.boot.autoconfigure.elasticsearch.ReactiveElasticsearchClientAutoConfiguration
org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration
org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration
org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration
org.springframework.boot.autoconfigure.graphql.data.GraphQlReactiveQueryByExampleAutoConfiguration
org.springframework.boot.autoconfigure.graphql.data.GraphQlReactiveQuerydslAutoConfiguration
org.springframework.boot.autoconfigure.graphql.data.GraphQlQueryByExampleAutoConfiguration
org.springframework.boot.autoconfigure.graphql.data.GraphQlQuerydslAutoConfiguration
org.springframework.boot.autoconfigure.graphql.reactive.GraphQlWebFluxAutoConfiguration
org.springframework.boot.autoconfigure.graphql.rsocket.GraphQlRSocketAutoConfiguration
org.springframework.boot.autoconfigure.graphql.rsocket.RSocketGraphQlClientAutoConfiguration
org.springframework.boot.autoconfigure.graphql.security.GraphQlWebFluxSecurityAutoConfiguration
org.springframework.boot.autoconfigure.graphql.security.GraphQlWebMvcSecurityAutoConfiguration
org.springframework.boot.autoconfigure.graphql.servlet.GraphQlWebMvcAutoConfiguration
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration
org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration
org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration

@ -0,0 +1,79 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql;
import org.springframework.data.annotation.Id;
/**
* Sample class for
*
* @author Brian Clozel
*/
public class Book {
@Id
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;
}
}

@ -0,0 +1,105 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql;
import java.util.Collection;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ConditionalOnGraphQlSchema}.
*
* @author Brian Clozel
*/
class DefaultGraphQlSchemaConditionTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner();
@Test
void matchesWhenSchemaFilesAreDetected() {
this.contextRunner.withUserConfiguration(TestingConfiguration.class).run((context) -> {
didMatch(context);
assertThat(conditionReportMessage(context)).contains("@ConditionalOnGraphQlSchema found schemas")
.contains("@ConditionalOnGraphQlSchema did not find GraphQlSourceBuilderCustomizer");
});
}
@Test
void matchesWhenCustomizerIsDetected() {
this.contextRunner.withUserConfiguration(CustomCustomizerConfiguration.class, TestingConfiguration.class)
.withPropertyValues("spring.graphql.schema.locations=classpath:graphql/missing").run((context) -> {
didMatch(context);
assertThat(conditionReportMessage(context)).contains(
"@ConditionalOnGraphQlSchema did not find schema files in locations 'classpath:graphql/missing/'")
.contains("@ConditionalOnGraphQlSchema found customizer myBuilderCuystomizer");
});
}
@Test
void doesNotMatchWhenBothAreMissing() {
this.contextRunner.withUserConfiguration(TestingConfiguration.class)
.withPropertyValues("spring.graphql.schema.locations=classpath:graphql/missing").run((context) -> {
assertThat(context).doesNotHaveBean("success");
assertThat(conditionReportMessage(context)).contains(
"@ConditionalOnGraphQlSchema did not find schema files in locations 'classpath:graphql/missing/'")
.contains("@ConditionalOnGraphQlSchema did not find GraphQlSourceBuilderCustomizer");
});
}
private void didMatch(AssertableApplicationContext context) {
assertThat(context).hasBean("success");
assertThat(context.getBean("success")).isEqualTo("success");
}
private String conditionReportMessage(AssertableApplicationContext context) {
Collection<ConditionEvaluationReport.ConditionAndOutcomes> conditionAndOutcomes = ConditionEvaluationReport
.get(context.getSourceApplicationContext().getBeanFactory()).getConditionAndOutcomesBySource().values();
return conditionAndOutcomes.iterator().next().iterator().next().getOutcome().getMessage();
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnGraphQlSchema
static class TestingConfiguration {
@Bean
String success() {
return "success";
}
}
@Configuration(proxyBeanMethods = false)
static class CustomCustomizerConfiguration {
@Bean
GraphQlSourceBuilderCustomizer myBuilderCuystomizer() {
return (builder) -> {
};
}
}
}

@ -0,0 +1,270 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql;
import java.nio.charset.StandardCharsets;
import graphql.GraphQL;
import graphql.execution.instrumentation.ChainedInstrumentation;
import graphql.execution.instrumentation.Instrumentation;
import graphql.schema.GraphQLSchema;
import graphql.schema.idl.RuntimeWiring;
import graphql.schema.visibility.DefaultGraphqlFieldVisibility;
import graphql.schema.visibility.NoIntrospectionGraphqlFieldVisibility;
import org.assertj.core.api.InstanceOfAssertFactories;
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.ByteArrayResource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.graphql.ExecutionGraphQlService;
import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer;
import org.springframework.graphql.execution.BatchLoaderRegistry;
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
import org.springframework.graphql.execution.DataLoaderRegistrar;
import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
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 shouldContributeDefaultBeans() {
this.contextRunner.run((context) -> {
assertThat(context).hasSingleBean(GraphQlSource.class);
assertThat(context).hasSingleBean(BatchLoaderRegistry.class);
assertThat(context).hasSingleBean(ExecutionGraphQlService.class);
assertThat(context).hasSingleBean(AnnotatedControllerConfigurer.class);
});
}
@Test
void schemaShouldScanNestedFolders() {
this.contextRunner.run((context) -> {
assertThat(context).hasSingleBean(GraphQlSource.class);
GraphQlSource graphQlSource = context.getBean(GraphQlSource.class);
GraphQLSchema schema = graphQlSource.schema();
assertThat(schema.getObjectType("Book")).isNotNull();
});
}
@Test
void shouldBackoffWhenSchemaFileIsMissing() {
this.contextRunner.withPropertyValues("spring.graphql.schema.locations:classpath:missing/")
.run((context) -> assertThat(context).hasNotFailed().doesNotHaveBean(GraphQlSource.class));
}
@Test
void shouldUseProgrammaticallyDefinedBuilder() {
this.contextRunner.withUserConfiguration(CustomGraphQlBuilderConfiguration.class).run((context) -> {
assertThat(context).hasBean("customGraphQlSourceBuilder");
assertThat(context).hasSingleBean(GraphQlSource.Builder.class);
});
}
@Test
void shouldScanLocationsWithCustomExtension() {
this.contextRunner.withPropertyValues("spring.graphql.schema.file-extensions:.graphqls,.custom")
.run((context) -> {
assertThat(context).hasSingleBean(GraphQlSource.class);
GraphQlSource graphQlSource = context.getBean(GraphQlSource.class);
GraphQLSchema schema = graphQlSource.schema();
assertThat(schema.getObjectType("Book")).isNotNull();
assertThat(schema.getObjectType("Person")).isNotNull();
});
}
@Test
void shouldBackOffWithCustomGraphQlSource() {
this.contextRunner.withUserConfiguration(CustomGraphQlSourceConfiguration.class).run((context) -> {
assertThat(context).getBeanNames(GraphQlSource.class).containsOnly("customGraphQlSource");
assertThat(context).hasSingleBean(GraphQlProperties.class);
});
}
@Test
void shouldConfigureDataFetcherExceptionResolvers() {
this.contextRunner.withUserConfiguration(DataFetcherExceptionResolverConfiguration.class).run((context) -> {
GraphQlSource graphQlSource = context.getBean(GraphQlSource.class);
GraphQL graphQL = graphQlSource.graphQl();
assertThat(graphQL.getQueryStrategy()).extracting("dataFetcherExceptionHandler")
.satisfies((exceptionHandler) -> assertThat(exceptionHandler.getClass().getName())
.endsWith("ExceptionResolversExceptionHandler"));
});
}
@Test
void shouldConfigureInstrumentation() {
this.contextRunner.withUserConfiguration(InstrumentationConfiguration.class).run((context) -> {
GraphQlSource graphQlSource = context.getBean(GraphQlSource.class);
Instrumentation customInstrumentation = context.getBean("customInstrumentation", Instrumentation.class);
GraphQL graphQL = graphQlSource.graphQl();
assertThat(graphQL).extracting("instrumentation").isInstanceOf(ChainedInstrumentation.class)
.extracting("instrumentations", InstanceOfAssertFactories.iterable(Instrumentation.class))
.contains(customInstrumentation);
});
}
@Test
void shouldApplyRuntimeWiringConfigurers() {
this.contextRunner.withUserConfiguration(RuntimeWiringConfigurerConfiguration.class).run((context) -> {
RuntimeWiringConfigurerConfiguration.CustomRuntimeWiringConfigurer configurer = context
.getBean(RuntimeWiringConfigurerConfiguration.CustomRuntimeWiringConfigurer.class);
assertThat(configurer.applied).isTrue();
});
}
@Test
void shouldApplyGraphQlSourceBuilderCustomizer() {
this.contextRunner.withUserConfiguration(GraphQlSourceBuilderCustomizerConfiguration.class).run((context) -> {
GraphQlSourceBuilderCustomizerConfiguration.CustomGraphQlSourceBuilderCustomizer customizer = context
.getBean(GraphQlSourceBuilderCustomizerConfiguration.CustomGraphQlSourceBuilderCustomizer.class);
assertThat(customizer.applied).isTrue();
});
}
@Test
void fieldIntrospectionShouldBeEnabledByDefault() {
this.contextRunner.run((context) -> {
GraphQlSource graphQlSource = context.getBean(GraphQlSource.class);
GraphQLSchema schema = graphQlSource.schema();
assertThat(schema.getCodeRegistry().getFieldVisibility()).isInstanceOf(DefaultGraphqlFieldVisibility.class);
});
}
@Test
void shouldDisableFieldIntrospection() {
this.contextRunner.withPropertyValues("spring.graphql.schema.introspection.enabled:false").run((context) -> {
GraphQlSource graphQlSource = context.getBean(GraphQlSource.class);
GraphQLSchema schema = graphQlSource.schema();
assertThat(schema.getCodeRegistry().getFieldVisibility())
.isInstanceOf(NoIntrospectionGraphqlFieldVisibility.class);
});
}
@Test
void shouldConfigureCustomBatchLoaderRegistry() {
this.contextRunner
.withBean("customBatchLoaderRegistry", BatchLoaderRegistry.class, () -> mock(BatchLoaderRegistry.class))
.run((context) -> {
assertThat(context).hasSingleBean(BatchLoaderRegistry.class);
assertThat(context.getBean("customBatchLoaderRegistry"))
.isSameAs(context.getBean(BatchLoaderRegistry.class));
assertThat(context.getBean(ExecutionGraphQlService.class))
.extracting("dataLoaderRegistrars",
InstanceOfAssertFactories.list(DataLoaderRegistrar.class))
.containsOnly(context.getBean(BatchLoaderRegistry.class));
});
}
@Configuration(proxyBeanMethods = false)
static class CustomGraphQlBuilderConfiguration {
@Bean
GraphQlSource.SchemaResourceBuilder customGraphQlSourceBuilder() {
return GraphQlSource.schemaResourceBuilder().schemaResources(
new ClassPathResource("graphql/schema.graphqls"),
new ClassPathResource("graphql/types/book.graphqls"));
}
}
@Configuration(proxyBeanMethods = false)
static class CustomGraphQlSourceConfiguration {
@Bean
GraphQlSource customGraphQlSource() {
ByteArrayResource schemaResource = new ByteArrayResource(
"type Query { greeting: String }".getBytes(StandardCharsets.UTF_8));
return GraphQlSource.schemaResourceBuilder().schemaResources(schemaResource).build();
}
}
@Configuration(proxyBeanMethods = false)
static class DataFetcherExceptionResolverConfiguration {
@Bean
DataFetcherExceptionResolver customDataFetcherExceptionResolver() {
return mock(DataFetcherExceptionResolver.class);
}
}
@Configuration(proxyBeanMethods = false)
static class InstrumentationConfiguration {
@Bean
Instrumentation customInstrumentation() {
return mock(Instrumentation.class);
}
}
@Configuration(proxyBeanMethods = false)
static class RuntimeWiringConfigurerConfiguration {
@Bean
CustomRuntimeWiringConfigurer customRuntimeWiringConfigurer() {
return new CustomRuntimeWiringConfigurer();
}
public static class CustomRuntimeWiringConfigurer implements RuntimeWiringConfigurer {
public boolean applied = false;
@Override
public void configure(RuntimeWiring.Builder builder) {
this.applied = true;
}
}
}
static class GraphQlSourceBuilderCustomizerConfiguration {
@Bean
CustomGraphQlSourceBuilderCustomizer customGraphQlSourceBuilderCustomizer() {
return new CustomGraphQlSourceBuilderCustomizer();
}
public static class CustomGraphQlSourceBuilderCustomizer implements GraphQlSourceBuilderCustomizer {
public boolean applied = false;
@Override
public void customize(GraphQlSource.SchemaResourceBuilder builder) {
this.applied = true;
}
}
}
}

@ -0,0 +1,59 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql;
import java.util.Arrays;
import java.util.List;
import graphql.schema.DataFetcher;
import reactor.core.publisher.Flux;
import org.springframework.lang.Nullable;
/**
* Test utility class holding {@link DataFetcher} implementations.
*
* @author Brian Clozel
*/
public final class GraphQlTestDataFetchers {
private static List<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 GraphQlTestDataFetchers() {
}
public static DataFetcher<Book> getBookByIdDataFetcher() {
return (environment) -> getBookById(environment.getArgument("id"));
}
public static DataFetcher<Flux<Book>> 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);
}
}

@ -0,0 +1,56 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql;
import com.querydsl.core.types.Path;
import com.querydsl.core.types.PathMetadata;
import com.querydsl.core.types.PathMetadataFactory;
import com.querydsl.core.types.dsl.EntityPathBase;
import com.querydsl.core.types.dsl.NumberPath;
import com.querydsl.core.types.dsl.StringPath;
/**
* QBook is a Querydsl query type for Book. This class is usually generated by the
* Querydsl annotation processor.
*/
public class QBook extends EntityPathBase<Book> {
private static final long serialVersionUID = -1932588188L;
public static final QBook book = new QBook("book");
public final StringPath author = createString("author");
public final StringPath id = createString("id");
public final StringPath name = createString("name");
public final NumberPath<Integer> pageCount = createNumber("pageCount", Integer.class);
public QBook(String variable) {
super(Book.class, PathMetadataFactory.forVariable(variable));
}
public QBook(Path<? extends Book> path) {
super(path.getType(), path.getMetadata());
}
public QBook(PathMetadata metadata) {
super(Book.class, metadata);
}
}

@ -0,0 +1,81 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql.data;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.graphql.Book;
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.QueryByExampleExecutor;
import org.springframework.graphql.ExecutionGraphQlService;
import org.springframework.graphql.data.GraphQlRepository;
import org.springframework.graphql.test.tester.ExecutionGraphQlServiceTester;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link GraphQlQueryByExampleAutoConfiguration}
*
* @author Brian Clozel
*/
class GraphQlQueryByExampleAutoConfigurationTests {
private static final Book book = new Book("42", "Test title", 42, "Test Author");
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withConfiguration(
AutoConfigurations.of(GraphQlAutoConfiguration.class, GraphQlQueryByExampleAutoConfiguration.class))
.withUserConfiguration(MockRepositoryConfig.class)
.withPropertyValues("spring.main.web-application-type=reactive");
@Test
void shouldRegisterDataFetcherForQueryByExampleRepositories() {
this.contextRunner.run((context) -> {
ExecutionGraphQlService graphQlService = context.getBean(ExecutionGraphQlService.class);
ExecutionGraphQlServiceTester graphQlTester = ExecutionGraphQlServiceTester.create(graphQlService);
graphQlTester.document("{ bookById(id: 1) {name}}").execute().path("bookById.name").entity(String.class)
.isEqualTo("Test title");
});
}
@Configuration(proxyBeanMethods = false)
static class MockRepositoryConfig {
@Bean
MockRepository mockRepository() {
MockRepository mockRepository = mock(MockRepository.class);
given(mockRepository.findBy(any(), any())).willReturn(Optional.of(book));
return mockRepository;
}
}
@GraphQlRepository
interface MockRepository extends CrudRepository<Book, Long>, QueryByExampleExecutor<Book> {
}
}

@ -0,0 +1,82 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql.data;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.graphql.Book;
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.repository.CrudRepository;
import org.springframework.graphql.ExecutionGraphQlService;
import org.springframework.graphql.data.GraphQlRepository;
import org.springframework.graphql.test.tester.ExecutionGraphQlServiceTester;
import org.springframework.graphql.test.tester.GraphQlTester;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link GraphQlQuerydslAutoConfiguration}.
*
* @author Brian Clozel
*/
class GraphQlQuerydslAutoConfigurationTests {
private static final Book book = new Book("42", "Test title", 42, "Test Author");
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withConfiguration(
AutoConfigurations.of(GraphQlAutoConfiguration.class, GraphQlQuerydslAutoConfiguration.class))
.withUserConfiguration(MockRepositoryConfig.class)
.withPropertyValues("spring.main.web-application-type=reactive");
@Test
void shouldRegisterDataFetcherForQueryDslRepositories() {
this.contextRunner.run((context) -> {
ExecutionGraphQlService graphQlService = context.getBean(ExecutionGraphQlService.class);
GraphQlTester graphQlTester = ExecutionGraphQlServiceTester.create(graphQlService);
graphQlTester.document("{ bookById(id: 1) {name}}").execute().path("bookById.name").entity(String.class)
.isEqualTo("Test title");
});
}
@Configuration(proxyBeanMethods = false)
static class MockRepositoryConfig {
@Bean
MockRepository mockRepository() {
MockRepository mockRepository = mock(MockRepository.class);
given(mockRepository.findBy(any(), any())).willReturn(Optional.of(book));
return mockRepository;
}
}
@GraphQlRepository
interface MockRepository extends CrudRepository<Book, Long>, QuerydslPredicateExecutor<Book> {
}
}

@ -0,0 +1,81 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql.data;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.graphql.Book;
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.graphql.ExecutionGraphQlService;
import org.springframework.graphql.data.GraphQlRepository;
import org.springframework.graphql.test.tester.ExecutionGraphQlServiceTester;
import org.springframework.graphql.test.tester.GraphQlTester;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link GraphQlReactiveQueryByExampleAutoConfiguration}
*
* @author Brian Clozel
*/
class GraphQlReactiveQueryByExampleAutoConfigurationTests {
private static final Mono<Book> bookPublisher = Mono.just(new Book("42", "Test title", 42, "Test Author"));
private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(GraphQlAutoConfiguration.class,
GraphQlReactiveQueryByExampleAutoConfiguration.class))
.withUserConfiguration(MockRepositoryConfig.class)
.withPropertyValues("spring.main.web-application-type=reactive");
@Test
void shouldRegisterDataFetcherForQueryByExampleRepositories() {
this.contextRunner.run((context) -> {
ExecutionGraphQlService graphQlService = context.getBean(ExecutionGraphQlService.class);
GraphQlTester graphQlTester = ExecutionGraphQlServiceTester.create(graphQlService);
graphQlTester.document("{ bookById(id: 1) {name}}").execute().path("bookById.name").entity(String.class)
.isEqualTo("Test title");
});
}
@Configuration(proxyBeanMethods = false)
static class MockRepositoryConfig {
@Bean
MockRepository mockRepository() {
MockRepository mockRepository = mock(MockRepository.class);
given(mockRepository.findBy(any(), any())).willReturn(bookPublisher);
return mockRepository;
}
}
@GraphQlRepository
interface MockRepository extends ReactiveCrudRepository<Book, Long>, ReactiveQueryByExampleExecutor<Book> {
}
}

@ -0,0 +1,81 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql.data;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.graphql.Book;
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.graphql.ExecutionGraphQlService;
import org.springframework.graphql.data.GraphQlRepository;
import org.springframework.graphql.test.tester.ExecutionGraphQlServiceTester;
import org.springframework.graphql.test.tester.GraphQlTester;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link GraphQlReactiveQuerydslAutoConfiguration}
*
* @author Brian Clozel
*/
class GraphQlReactiveQuerydslAutoConfigurationTests {
private static final Mono<Book> bookPublisher = Mono.just(new Book("42", "Test title", 42, "Test Author"));
private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(GraphQlAutoConfiguration.class,
GraphQlReactiveQuerydslAutoConfiguration.class))
.withUserConfiguration(MockRepositoryConfig.class)
.withPropertyValues("spring.main.web-application-type=reactive");
@Test
void shouldRegisterDataFetcherForQueryDslRepositories() {
this.contextRunner.run((context) -> {
ExecutionGraphQlService graphQlService = context.getBean(ExecutionGraphQlService.class);
GraphQlTester graphQlTester = ExecutionGraphQlServiceTester.create(graphQlService);
graphQlTester.document("{ bookById(id: 1) {name}}").execute().path("bookById.name").entity(String.class)
.isEqualTo("Test title");
});
}
@Configuration(proxyBeanMethods = false)
static class MockRepositoryConfig {
@Bean
MockRepository mockRepository() {
MockRepository mockRepository = mock(MockRepository.class);
given(mockRepository.findBy(any(), any())).willReturn(bookPublisher);
return mockRepository;
}
}
@GraphQlRepository
interface MockRepository extends ReactiveCrudRepository<Book, Long>, ReactiveQuerydslPredicateExecutor<Book> {
}
}

@ -0,0 +1,214 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql.reactive;
import java.util.Collections;
import java.util.Map;
import java.util.function.Consumer;
import graphql.schema.idl.TypeRuntimeWiring;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.autoconfigure.graphql.GraphQlTestDataFetchers;
import org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
import org.springframework.graphql.server.WebGraphQlHandler;
import org.springframework.graphql.server.WebGraphQlInterceptor;
import org.springframework.graphql.server.webflux.GraphQlHttpHandler;
import org.springframework.graphql.server.webflux.GraphQlWebSocketHandler;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.server.RouterFunction;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
/**
* Tests for {@link GraphQlWebFluxAutoConfiguration}
*
* @author Brian Clozel
*/
class GraphQlWebFluxAutoConfigurationTests {
private static final String BASE_URL = "https://spring.example.org/";
private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(HttpHandlerAutoConfiguration.class, WebFluxAutoConfiguration.class,
CodecsAutoConfiguration.class, JacksonAutoConfiguration.class, GraphQlAutoConfiguration.class,
GraphQlWebFluxAutoConfiguration.class))
.withUserConfiguration(DataFetchersConfiguration.class, CustomWebInterceptor.class)
.withPropertyValues("spring.main.web-application-type=reactive", "spring.graphql.graphiql.enabled=true",
"spring.graphql.schema.printer.enabled=true",
"spring.graphql.cors.allowed-origins=https://example.com",
"spring.graphql.cors.allowed-methods=POST", "spring.graphql.cors.allow-credentials=true");
@Test
void shouldContributeDefaultBeans() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(GraphQlHttpHandler.class)
.hasSingleBean(WebGraphQlHandler.class).doesNotHaveBean(GraphQlWebSocketHandler.class));
}
@Test
void simpleQueryShouldWork() {
testWithWebClient((client) -> {
String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }";
client.post().uri("/graphql").bodyValue("{ \"query\": \"" + query + "\"}").exchange().expectStatus().isOk()
.expectHeader().contentType("application/json").expectBody().jsonPath("data.bookById.name")
.isEqualTo("GraphQL for beginners");
});
}
@Test
void httpGetQueryShouldBeSupported() {
testWithWebClient((client) -> {
String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }";
client.get().uri("/graphql?query={query}", "{ \"query\": \"" + query + "\"}").exchange().expectStatus()
.isEqualTo(HttpStatus.METHOD_NOT_ALLOWED).expectHeader().valueEquals("Allow", "POST");
});
}
@Test
void shouldRejectMissingQuery() {
testWithWebClient(
(client) -> client.post().uri("/graphql").bodyValue("{}").exchange().expectStatus().isBadRequest());
}
@Test
void shouldRejectQueryWithInvalidJson() {
testWithWebClient(
(client) -> client.post().uri("/graphql").bodyValue(":)").exchange().expectStatus().isBadRequest());
}
@Test
void shouldConfigureWebInterceptors() {
testWithWebClient((client) -> {
String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }";
client.post().uri("/graphql").bodyValue("{ \"query\": \"" + query + "\"}").exchange().expectStatus().isOk()
.expectHeader().valueEquals("X-Custom-Header", "42");
});
}
@Test
void shouldExposeSchemaEndpoint() {
testWithWebClient((client) -> client.get().uri("/graphql/schema").accept(MediaType.ALL).exchange()
.expectStatus().isOk().expectHeader().contentType(MediaType.TEXT_PLAIN).expectBody(String.class)
.value(containsString("type Book")));
}
@Test
void shouldExposeGraphiqlEndpoint() {
testWithWebClient((client) -> {
client.get().uri("/graphiql").exchange().expectStatus().is3xxRedirection().expectHeader()
.location("https://spring.example.org/graphiql?path=/graphql");
client.get().uri("/graphiql?path=/graphql").accept(MediaType.ALL).exchange().expectStatus().isOk()
.expectHeader().contentType(MediaType.TEXT_HTML);
});
}
@Test
void shouldSupportCors() {
testWithWebClient((client) -> {
String query = "{" + " bookById(id: \\\"book-1\\\"){ " + " id" + " name" + " pageCount"
+ " author" + " }" + "}";
client.post().uri("/graphql").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");
});
}
@Test
void shouldConfigureWebSocketBeans() {
this.contextRunner.withPropertyValues("spring.graphql.websocket.path=/ws")
.run((context) -> assertThat(context).hasSingleBean(GraphQlWebSocketHandler.class));
}
@Test
void routerFunctionShouldHaveOrderZero() throws Exception {
this.contextRunner.withUserConfiguration(CustomRouterFunctions.class).run((context) -> {
Map<String, ?> beans = context.getBeansOfType(RouterFunction.class);
Object[] ordered = context.getBeanProvider(RouterFunction.class).orderedStream().toArray();
assertThat(beans.get("before")).isSameAs(ordered[0]);
assertThat(beans.get("graphQlRouterFunction")).isSameAs(ordered[1]);
assertThat(beans.get("after")).isSameAs(ordered[2]);
});
}
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",
GraphQlTestDataFetchers.getBookByIdDataFetcher()));
}
}
@Configuration(proxyBeanMethods = false)
static class CustomWebInterceptor {
@Bean
WebGraphQlInterceptor customWebGraphQlInterceptor() {
return (webInput, interceptorChain) -> interceptorChain.next(webInput)
.doOnNext((output) -> output.getResponseHeaders().add("X-Custom-Header", "42"));
}
}
@Configuration
static class CustomRouterFunctions {
@Bean
@Order(-1)
RouterFunction<?> before() {
return (r) -> null;
}
@Bean
@Order(1)
RouterFunction<?> after() {
return (r) -> null;
}
}
}

@ -0,0 +1,150 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql.rsocket;
import java.net.URI;
import java.time.Duration;
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.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.autoconfigure.graphql.GraphQlTestDataFetchers;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration;
import org.springframework.boot.autoconfigure.rsocket.RSocketServerAutoConfiguration;
import org.springframework.boot.autoconfigure.rsocket.RSocketStrategiesAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration;
import org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer;
import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory;
import org.springframework.boot.web.embedded.netty.NettyRouteProvider;
import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.client.RSocketGraphQlClient;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
import org.springframework.graphql.server.GraphQlRSocketHandler;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link GraphQlRSocketAutoConfiguration}
*
* @author Brian Clozel
*/
class GraphQlRSocketAutoConfigurationTests {
private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
.withConfiguration(
AutoConfigurations.of(JacksonAutoConfiguration.class, RSocketStrategiesAutoConfiguration.class,
RSocketMessagingAutoConfiguration.class, RSocketServerAutoConfiguration.class,
GraphQlAutoConfiguration.class, GraphQlRSocketAutoConfiguration.class))
.withUserConfiguration(DataFetchersConfiguration.class)
.withPropertyValues("spring.main.web-application-type=reactive", "spring.graphql.rsocket.mapping=graphql");
@Test
void shouldContributeDefaultBeans() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(GraphQlRSocketHandler.class)
.hasSingleBean(GraphQlRSocketController.class));
}
@Test
void simpleQueryShouldWorkWithTcpServer() {
testWithRSocketTcp(this::assertThatSimpleQueryWorks);
}
@Test
void simpleQueryShouldWorkWithWebSocketServer() {
testWithRSocketWebSocket(this::assertThatSimpleQueryWorks);
}
private void assertThatSimpleQueryWorks(RSocketGraphQlClient client) {
String document = "{ bookById(id: \"book-1\"){ id name pageCount author } }";
String bookName = client.document(document).retrieve("bookById.name").toEntity(String.class)
.block(Duration.ofSeconds(5));
assertThat(bookName).isEqualTo("GraphQL for beginners");
}
private void testWithRSocketTcp(Consumer<RSocketGraphQlClient> consumer) {
ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
.withConfiguration(
AutoConfigurations.of(JacksonAutoConfiguration.class, RSocketStrategiesAutoConfiguration.class,
RSocketMessagingAutoConfiguration.class, RSocketServerAutoConfiguration.class,
GraphQlAutoConfiguration.class, GraphQlRSocketAutoConfiguration.class))
.withUserConfiguration(DataFetchersConfiguration.class).withPropertyValues(
"spring.main.web-application-type=reactive", "spring.graphql.rsocket.mapping=graphql");
contextRunner.withInitializer(new RSocketPortInfoApplicationContextInitializer())
.withPropertyValues("spring.rsocket.server.port=0").run((context) -> {
String serverPort = context.getEnvironment().getProperty("local.rsocket.server.port");
RSocketGraphQlClient client = RSocketGraphQlClient.builder()
.tcp("localhost", Integer.parseInt(serverPort)).route("graphql").build();
consumer.accept(client);
});
}
private void testWithRSocketWebSocket(Consumer<RSocketGraphQlClient> consumer) {
ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner(
AnnotationConfigReactiveWebServerApplicationContext::new).withConfiguration(
AutoConfigurations.of(HttpHandlerAutoConfiguration.class, WebFluxAutoConfiguration.class,
ErrorWebFluxAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class,
JacksonAutoConfiguration.class, RSocketStrategiesAutoConfiguration.class,
RSocketMessagingAutoConfiguration.class, RSocketServerAutoConfiguration.class,
GraphQlAutoConfiguration.class, GraphQlRSocketAutoConfiguration.class))
.withInitializer(new ServerPortInfoApplicationContextInitializer())
.withUserConfiguration(DataFetchersConfiguration.class, NettyServerConfiguration.class)
.withPropertyValues("spring.main.web-application-type=reactive", "server.port=0",
"spring.graphql.rsocket.mapping=graphql", "spring.rsocket.server.transport=websocket",
"spring.rsocket.server.mapping-path=/rsocket");
contextRunner.run((context) -> {
String serverPort = context.getEnvironment().getProperty("local.server.port");
RSocketGraphQlClient client = RSocketGraphQlClient.builder()
.webSocket(URI.create("ws://localhost:" + serverPort + "/rsocket")).route("graphql").build();
consumer.accept(client);
});
}
@Configuration(proxyBeanMethods = false)
static class NettyServerConfiguration {
@Bean
NettyReactiveWebServerFactory serverFactory(NettyRouteProvider routeProvider) {
NettyReactiveWebServerFactory serverFactory = new NettyReactiveWebServerFactory(0);
serverFactory.addRouteProviders(routeProvider);
return serverFactory;
}
}
@Configuration(proxyBeanMethods = false)
static class DataFetchersConfiguration {
@Bean
RuntimeWiringConfigurer bookDataFetcher() {
return (builder) -> builder.type(TypeRuntimeWiring.newTypeWiring("Query").dataFetcher("bookById",
GraphQlTestDataFetchers.getBookByIdDataFetcher()));
}
}
}

@ -0,0 +1,76 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql.rsocket;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.rsocket.RSocketRequesterAutoConfiguration;
import org.springframework.boot.autoconfigure.rsocket.RSocketStrategiesAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.client.RSocketGraphQlClient;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link RSocketGraphQlClientAutoConfiguration}.
*
* @author Brian Clozel
*/
class RSocketGraphQlClientAutoConfigurationTests {
private static final RSocketGraphQlClient.Builder<?> builderInstance = RSocketGraphQlClient.builder();
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(RSocketStrategiesAutoConfiguration.class,
RSocketRequesterAutoConfiguration.class, RSocketGraphQlClientAutoConfiguration.class));
@Test
void shouldCreateBuilder() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(RSocketGraphQlClient.Builder.class));
}
@Test
void shouldGetPrototypeScopedBean() {
this.contextRunner.run((context) -> {
RSocketGraphQlClient.Builder<?> first = context.getBean(RSocketGraphQlClient.Builder.class);
RSocketGraphQlClient.Builder<?> second = context.getBean(RSocketGraphQlClient.Builder.class);
assertThat(first).isNotEqualTo(second);
});
}
@Test
void shouldNotCreateBuilderIfAlreadyPresent() {
this.contextRunner.withUserConfiguration(CustomRSocketGraphQlClientBuilder.class).run((context) -> {
RSocketGraphQlClient.Builder<?> builder = context.getBean(RSocketGraphQlClient.Builder.class);
assertThat(builder).isEqualTo(builderInstance);
});
}
@Configuration(proxyBeanMethods = false)
static class CustomRSocketGraphQlClientBuilder {
@Bean
RSocketGraphQlClient.Builder<?> myRSocketGraphQlClientBuilder() {
return builderInstance;
}
}
}

@ -0,0 +1,163 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql.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.graphql.Book;
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.autoconfigure.graphql.GraphQlTestDataFetchers;
import org.springframework.boot.autoconfigure.graphql.reactive.GraphQlWebFluxAutoConfiguration;
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.execution.ErrorType;
import org.springframework.graphql.execution.ReactiveSecurityDataFetcherExceptionResolver;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
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,
GraphQlWebFluxAutoConfiguration.class, GraphQlWebFluxSecurityAutoConfiguration.class,
ReactiveSecurityAutoConfiguration.class))
.withUserConfiguration(DataFetchersConfiguration.class, SecurityConfig.class)
.withPropertyValues("spring.main.web-application-type=reactive");
@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
Mono<Book> getBookdById(String id) {
return Mono.justOrEmpty(GraphQlTestDataFetchers.getBookById(id));
}
}
@Configuration(proxyBeanMethods = false)
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
static class SecurityConfig {
@Bean
SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) {
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")
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);
}
}
}

@ -0,0 +1,180 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql.security;
import graphql.schema.idl.TypeRuntimeWiring;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.graphql.Book;
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.autoconfigure.graphql.GraphQlTestDataFetchers;
import org.springframework.boot.autoconfigure.graphql.servlet.GraphQlWebMvcAutoConfiguration;
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.execution.ErrorType;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
import org.springframework.graphql.execution.SecurityContextThreadLocalAccessor;
import org.springframework.graphql.execution.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, GraphQlWebMvcAutoConfiguration.class,
GraphQlWebMvcSecurityAutoConfiguration.class, SecurityAutoConfiguration.class))
.withUserConfiguration(DataFetchersConfiguration.class, SecurityConfig.class)
.withPropertyValues("spring.main.web-application-type=servlet");
@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
Book getBookdById(String id) {
return GraphQlTestDataFetchers.getBookById(id);
}
}
@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@SuppressWarnings("deprecation")
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
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);
}
}
}

@ -0,0 +1,225 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.graphql.servlet;
import java.util.Map;
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.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.autoconfigure.graphql.GraphQlTestDataFetchers;
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
import org.springframework.graphql.server.WebGraphQlHandler;
import org.springframework.graphql.server.WebGraphQlInterceptor;
import org.springframework.graphql.server.webmvc.GraphQlHttpHandler;
import org.springframework.graphql.server.webmvc.GraphQlWebSocketHandler;
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 org.springframework.web.servlet.function.RouterFunction;
import static org.assertj.core.api.Assertions.assertThat;
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.redirectedUrl;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Tests for {@link GraphQlWebMvcAutoConfiguration}.
*
* @author Brian Clozel
*/
class GraphQlWebMvcAutoConfigurationTests {
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withConfiguration(
AutoConfigurations.of(DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class, JacksonAutoConfiguration.class,
GraphQlAutoConfiguration.class, GraphQlWebMvcAutoConfiguration.class))
.withUserConfiguration(DataFetchersConfiguration.class, CustomWebInterceptor.class)
.withPropertyValues("spring.main.web-application-type=servlet", "spring.graphql.graphiql.enabled=true",
"spring.graphql.schema.printer.enabled=true",
"spring.graphql.cors.allowed-origins=https://example.com",
"spring.graphql.cors.allowed-methods=POST", "spring.graphql.cors.allow-credentials=true");
@Test
void shouldContributeDefaultBeans() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(GraphQlHttpHandler.class)
.hasSingleBean(WebGraphQlHandler.class).doesNotHaveBean(GraphQlWebSocketHandler.class));
}
@Test
void simpleQueryShouldWork() {
testWith((mockMvc) -> {
String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }";
MvcResult result = mockMvc.perform(post("/graphql").content("{\"query\": \"" + query + "\"}")).andReturn();
mockMvc.perform(asyncDispatch(result)).andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_GRAPHQL))
.andExpect(jsonPath("data.bookById.name").value("GraphQL for beginners"));
});
}
@Test
void httpGetQueryShouldBeSupported() {
testWith((mockMvc) -> {
String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }";
mockMvc.perform(get("/graphql?query={query}", "{\"query\": \"" + query + "\"}"))
.andExpect(status().isMethodNotAllowed()).andExpect(header().string("Allow", "POST"));
});
}
@Test
void shouldRejectMissingQuery() {
testWith((mockMvc) -> mockMvc.perform(post("/graphql").content("{}")).andExpect(status().isBadRequest()));
}
@Test
void shouldRejectQueryWithInvalidJson() {
testWith((mockMvc) -> mockMvc.perform(post("/graphql").content(":)")).andExpect(status().isBadRequest()));
}
@Test
void shouldConfigureWebInterceptors() {
testWith((mockMvc) -> {
String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }";
MvcResult result = mockMvc.perform(post("/graphql").content("{\"query\": \"" + query + "\"}")).andReturn();
mockMvc.perform(asyncDispatch(result)).andExpect(status().isOk())
.andExpect(header().string("X-Custom-Header", "42"));
});
}
@Test
void shouldExposeSchemaEndpoint() {
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 shouldExposeGraphiqlEndpoint() {
testWith((mockMvc) -> {
mockMvc.perform(get("/graphiql")).andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("http://localhost/graphiql?path=/graphql"));
mockMvc.perform(get("/graphiql?path=/graphql")).andExpect(status().isOk())
.andExpect(content().contentType(MediaType.TEXT_HTML));
});
}
@Test
void shouldSupportCors() {
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"));
});
}
@Test
void shouldConfigureWebSocketBeans() {
this.contextRunner.withPropertyValues("spring.graphql.websocket.path=/ws")
.run((context) -> assertThat(context).hasSingleBean(GraphQlWebSocketHandler.class));
}
@Test
void routerFunctionShouldHaveOrderZero() throws Exception {
this.contextRunner.withUserConfiguration(CustomRouterFunctions.class).run((context) -> {
Map<String, ?> beans = context.getBeansOfType(RouterFunction.class);
Object[] ordered = context.getBeanProvider(RouterFunction.class).orderedStream().toArray();
assertThat(beans.get("before")).isSameAs(ordered[0]);
assertThat(beans.get("graphQlRouterFunction")).isSameAs(ordered[1]);
assertThat(beans.get("after")).isSameAs(ordered[2]);
});
}
private void testWith(MockMvcConsumer mockMvcConsumer) {
this.contextRunner.run((context) -> {
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(context).defaultRequest(
post("/graphql").contentType(MediaType.APPLICATION_GRAPHQL).accept(MediaType.APPLICATION_GRAPHQL))
.build();
mockMvcConsumer.accept(mockMvc);
});
}
private interface MockMvcConsumer {
void accept(MockMvc mockMvc) throws Exception;
}
@Configuration(proxyBeanMethods = false)
static class DataFetchersConfiguration {
@Bean
RuntimeWiringConfigurer bookDataFetcher() {
return (builder) -> builder.type(TypeRuntimeWiring.newTypeWiring("Query").dataFetcher("bookById",
GraphQlTestDataFetchers.getBookByIdDataFetcher()));
}
}
@Configuration(proxyBeanMethods = false)
static class CustomWebInterceptor {
@Bean
WebGraphQlInterceptor customWebGraphQlInterceptor() {
return (webInput, interceptorChain) -> interceptorChain.next(webInput)
.doOnNext((output) -> output.getResponseHeaders().add("X-Custom-Header", "42"));
}
}
@Configuration
static class CustomRouterFunctions {
@Bean
@Order(-1)
RouterFunction<?> before() {
return (r) -> null;
}
@Bean
@Order(1)
RouterFunction<?> after() {
return (r) -> null;
}
}
}

@ -0,0 +1,4 @@
type Query {
greeting(name: String! = "Spring"): String!
bookById(id: ID): Book
}

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

@ -295,6 +295,13 @@ bom {
] ]
} }
} }
library("GraphQL Java", "19.0") {
group("com.graphql-java") {
modules = [
"graphql-java"
]
}
}
library("Groovy", "4.0.3") { library("Groovy", "4.0.3") {
group("org.apache.groovy") { group("org.apache.groovy") {
imports = [ imports = [
@ -1281,6 +1288,7 @@ bom {
"spring-boot-starter-data-neo4j", "spring-boot-starter-data-neo4j",
"spring-boot-starter-data-rest", "spring-boot-starter-data-rest",
"spring-boot-starter-freemarker", "spring-boot-starter-freemarker",
"spring-boot-starter-graphql",
"spring-boot-starter-groovy-templates", "spring-boot-starter-groovy-templates",
"spring-boot-starter-hateoas", "spring-boot-starter-hateoas",
"spring-boot-starter-integration", "spring-boot-starter-integration",
@ -1415,6 +1423,14 @@ bom {
] ]
} }
} }
library("Spring GraphQL", "1.1.0-SNAPSHOT") {
group("org.springframework.graphql") {
modules = [
"spring-graphql",
"spring-graphql-test"
]
}
}
library("Spring HATEOAS", "2.0.0-SNAPSHOT") { library("Spring HATEOAS", "2.0.0-SNAPSHOT") {
group("org.springframework.hateoas") { group("org.springframework.hateoas") {
modules = [ modules = [

@ -4,6 +4,7 @@ server.error.include-stacktrace=always
server.servlet.jsp.init-parameters.development=true server.servlet.jsp.init-parameters.development=true
server.servlet.session.persistent=true server.servlet.session.persistent=true
spring.freemarker.cache=false spring.freemarker.cache=false
spring.graphql.graphiql.enabled=true
spring.groovy.template.cache=false spring.groovy.template.cache=false
spring.h2.console.enabled=true spring.h2.console.enabled=true
spring.mustache.servlet.cache=false spring.mustache.servlet.cache=false

@ -137,6 +137,8 @@ dependencies {
implementation("org.springframework.data:spring-data-neo4j") implementation("org.springframework.data:spring-data-neo4j")
implementation("org.springframework.data:spring-data-redis") implementation("org.springframework.data:spring-data-redis")
implementation("org.springframework.data:spring-data-r2dbc") implementation("org.springframework.data:spring-data-r2dbc")
implementation("org.springframework.graphql:spring-graphql")
implementation("org.springframework.graphql:spring-graphql-test")
implementation("org.springframework.kafka:spring-kafka") implementation("org.springframework.kafka:spring-kafka")
implementation("org.springframework.kafka:spring-kafka-test") implementation("org.springframework.kafka:spring-kafka-test")
implementation("org.springframework.restdocs:spring-restdocs-mockmvc") { implementation("org.springframework.restdocs:spring-restdocs-mockmvc") {
@ -270,8 +272,9 @@ tasks.withType(org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask) {
"spring-data-r2dbc-version": versionConstraints["org.springframework.data:spring-data-r2dbc"], "spring-data-r2dbc-version": versionConstraints["org.springframework.data:spring-data-r2dbc"],
"spring-data-rest-version": versionConstraints["org.springframework.data:spring-data-rest-core"], "spring-data-rest-version": versionConstraints["org.springframework.data:spring-data-rest-core"],
"spring-framework-version": versionConstraints["org.springframework:spring-core"], "spring-framework-version": versionConstraints["org.springframework:spring-core"],
"spring-kafka-version": versionConstraints["org.springframework.kafka:spring-kafka"], "spring-graphql-version": versionConstraints["org.springframework.graphql:spring-graphql"],
"spring-integration-version": versionConstraints["org.springframework.integration:spring-integration-core"], "spring-integration-version": versionConstraints["org.springframework.integration:spring-integration-core"],
"spring-kafka-version": versionConstraints["org.springframework.kafka:spring-kafka"],
"spring-security-version": securityVersion, "spring-security-version": securityVersion,
"spring-webservices-version": versionConstraints["org.springframework.ws:spring-ws-core"] "spring-webservices-version": versionConstraints["org.springframework.ws:spring-ws-core"]
} }

@ -888,6 +888,55 @@ NOTE: Only caches that are configured on startup are bound to the registry.
For caches not defined in the caches configuration, such as caches created on the fly or programmatically after the startup phase, an explicit registration is required. For caches not defined in the caches configuration, such as caches created on the fly or programmatically after the startup phase, an explicit registration is required.
A `CacheMetricsRegistrar` bean is made available to make that process easier. A `CacheMetricsRegistrar` bean is made available to make that process easier.
[[actuator.metrics.supported.spring-graphql]]
==== Spring GraphQL Metrics
Auto-configuration enables the instrumentation of GraphQL queries, for any supported transport.
Spring Boot records a `graphql.request` timer with:
[cols="1,2,2"]
|===
|Tag | Description| Sample values
|outcome
|Request outcome
|"SUCCESS", "ERROR"
|===
A single GraphQL query can involve many `DataFetcher` calls, so there is a dedicated `graphql.datafetcher` timer:
[cols="1,2,2"]
|===
|Tag | Description| Sample values
|path
|data fetcher path
|"Query.project"
|outcome
|data fetching outcome
|"SUCCESS", "ERROR"
|===
The `graphql.request.datafetch.count` https://micrometer.io/docs/concepts#_distribution_summaries[distribution summary] counts the number of non-trivia
This metric is useful for detecting "N+1" data fetching issues and considering batch loading; it provides the `"TOTAL"` number of data fetcher calls ma
More options are available for <<application-properties#application-properties.actuator.management.metrics.distribution.maximum-expected-value, configu
A single response can contain many GraphQL errors, counted by the `graphql.error` counter:
[cols="1,2,2"]
|===
|Tag | Description| Sample values
|errorType
|error type
|"DataFetchingException"
|errorPath
|error JSON Path
|"$.project"
|===
[[actuator.metrics.supported.jdbc]] [[actuator.metrics.supported.jdbc]]

@ -83,6 +83,9 @@
:spring-framework: https://spring.io/projects/spring-framework :spring-framework: https://spring.io/projects/spring-framework
:spring-framework-api: https://docs.spring.io/spring-framework/docs/{spring-framework-version}/javadoc-api/org/springframework :spring-framework-api: https://docs.spring.io/spring-framework/docs/{spring-framework-version}/javadoc-api/org/springframework
:spring-framework-docs: https://docs.spring.io/spring-framework/docs/{spring-framework-version}/reference/html :spring-framework-docs: https://docs.spring.io/spring-framework/docs/{spring-framework-version}/reference/html
:spring-graphql: https://spring.io/projects/spring-graphql
:spring-graphql-api: https://docs.spring.io/spring-graphql/docs/{spring-graphql-version}/api/
:spring-graphql-docs: https://docs.spring.io/spring-graphql/docs/{spring-graphql-version}/reference/html/
:spring-integration: https://spring.io/projects/spring-integration :spring-integration: https://spring.io/projects/spring-integration
:spring-integration-docs: https://docs.spring.io/spring-integration/docs/{spring-integration-version}/reference/html/ :spring-integration-docs: https://docs.spring.io/spring-integration/docs/{spring-integration-version}/reference/html/
:spring-kafka-docs: https://docs.spring.io/spring-kafka/docs/{spring-kafka-version}/reference/html/ :spring-kafka-docs: https://docs.spring.io/spring-kafka/docs/{spring-kafka-version}/reference/html/

@ -405,6 +405,42 @@ TIP: Sometimes writing Spring WebFlux tests is not enough; Spring Boot can help
[[features.testing.spring-boot-applications.spring-graphql-tests]]
==== Auto-configured Spring GraphQL Tests
Spring GraphQL offers a dedicated testing support module; you'll need to add it to your project:
.Maven
[source,xml,indent=0,subs="verbatim"]
----
<dependencies>
<dependency>
<groupId>org.springframework.graphql</groupId>
<artifactId>spring-graphql-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Unless already present in the compile scope -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
----
.Gradle
[source,gradle,indent=0,subs="verbatim"]
----
dependencies {
testImplementation("org.springframework.graphql:spring-graphql-test")
// Unless already present in the implementation configuration
testImplementation("org.springframework.boot:spring-boot-starter-webflux")
}
----
This testing module ships the {spring-graphql-docs}/#testing-graphqltester[GraphQlTester].
spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc
[[features.testing.spring-boot-applications.autoconfigured-spring-data-cassandra]] [[features.testing.spring-boot-applications.autoconfigured-spring-data-cassandra]]
==== Auto-configured Data Cassandra Tests ==== Auto-configured Data Cassandra Tests
You can use `@DataCassandraTest` to test Cassandra applications. You can use `@DataCassandraTest` to test Cassandra applications.

@ -19,6 +19,8 @@ include::web/spring-security.adoc[]
include::web/spring-session.adoc[] include::web/spring-session.adoc[]
include::web/spring-graphql.adoc[]
include::web/spring-hateoas.adoc[] include::web/spring-hateoas.adoc[]
include::web/whats-next.adoc[] include::web/whats-next.adoc[]

@ -0,0 +1,152 @@
[[web.graphql]]
== Spring for GraphQL
If you want to build GraphQL applications, you can take advantage of Spring Boot's auto-configuration for {spring-graphql}[Spring for GraphQL].
The Spring for GraphQL project is based on https://github.com/graphql-java/graphql-java[GraphQL Java].
You'll need the `spring-boot-starter-graphql` starter at a minimum.
Because GraphQL is transport-agnostic, you'll also need to have one or more additional starters in your application to expose your GraphQL API over the web:
[cols="1,1,1"]
|===
| Starter | Transport | Implementation
| `spring-boot-starter-web`
| HTTP
| Spring MVC
| `spring-boot-starter-websocket`
| WebSocket
| WebSocket for Servlet apps
| `spring-boot-starter-webflux`
| HTTP, WebSocket
| Spring WebFlux
| `spring-boot-starter-rsocket`
| TCP, WebSocket
| Spring WebFlux on Reactor Netty
|===
[[web.graphql.schema]]
=== GraphQL Schema
A Spring GraphQL application requires a defined schema at startup.
By default, you can write ".graphqls" or ".gqls" schema files under `src/main/resources/graphql/**` and Spring Boot will pick them up automatically.
You can customize the locations with configprop:spring.graphql.schema.locations[] and the file extensions with configprop:spring.graphql.schema.file-extensions[].
In the following sections, we'll consider this sample GraphQL schema, defining two types and two queries:
[source,json,indent=0,subs="verbatim,quotes"]
----
include::{docs-resources}/graphql/schema.graphqls[]
----
NOTE: By default, https://spec.graphql.org/draft/#sec-Introspection[field introspection] will be allowed on the schema as it is required for tools such as GraphiQL.
If you wish to not expose information about the schema, you can disable introspection by setting configprop:spring.graphql.schema.introspection.enabled[] to `false`.
[[web.graphql.runtimewiring]]
=== GraphQL RuntimeWiring
The GraphQL Java `RuntimeWiring.Builder` can be used to register custom scalar types, directives, type resolvers, `DataFetcher`s, and more.
You can declare `RuntimeWiringConfigurer` beans in your Spring config to get access to the `RuntimeWiring.Builder`.
Spring Boot detects such beans and adds them to the {spring-graphql-docs}#execution-graphqlsource[GraphQlSource builder].
Typically, however, applications will not implement `DataFetcher` directly and will instead create {spring-graphql-docs}#controllers[annotated controllers].
Spring Boot will automatically detect `@Controller` classes with annotated handler methods and register those as `DataFetcher`s.
Here's a sample implementation for our greeting query with a `@Controller` class:
include::code:GreetingController[]
[[web.graphql.data-query]]
=== Querydsl and QueryByExample Repositories support
Spring Data offers support for both Querydsl and QueryByExample repositories.
Spring GraphQL can {spring-graphql-docs}#data[configure Querydsl and QueryByExample repositories as `DataFetcher`].
Spring Data repositories annotated with `@GraphQlRepository` and extending one of:
* `QuerydslPredicateExecutor`
* `ReactiveQuerydslPredicateExecutor`
* `QueryByExampleExecutor`
* `ReactiveQueryByExampleExecutor`
are detected by Spring Boot and considered as candidates for `DataFetcher` for matching top-level queries.
[[web.graphql.transports]]
=== Transports
[[web.graphql.transports.http-websocket]]
==== HTTP and WebSocket
The GraphQL HTTP endpoint is at HTTP POST "/graphql" by default.
The path can be customized with configprop:spring.graphql.path[].
TIP: The HTTP endpoint for both Spring MVC and Spring WebFlux is provided by a `RouterFunction` bean with an `@Order` of `0`.
If you define your own `RouterFunction` beans, you may want to add appropriate `@Order` annotations to ensure that they are sorted correctly.
The GraphQL WebSocket endpoint is off by default. To enable it:
* For a Servlet application, add the WebSocket starter `spring-boot-starter-websocket`
* For a WebFlux application, no additional dependency is required
* For both, the configprop:spring.graphql.websocket.path[] application property must be set
Spring GraphQL provides a {spring-graphql-docs}#web-interception[Web Interception] model.
This is quite useful for retrieving information from an HTTP request header and set it in the GraphQL context or fetching information from the same context and writing it to a response header.
With Spring Boot, you can declare a `WebInterceptor` bean to have it registered with the web transport.
{spring-framework-docs}/web.html#mvc-cors[Spring MVC] and {spring-framework-docs}/web-reactive.html#webflux-cors[Spring WebFlux] support CORS (Cross-Origin Resource Sharing) requests.
CORS is a critical part of the web config for GraphQL applications that are accessed from browsers using different domains.
Spring Boot supports many configuration properties under the `spring.graphql.cors.*` namespace; here's a short configuration sample:
[source,yaml,indent=0,subs="verbatim",configblocks]
----
spring:
graphql:
cors:
allowed-origins: "https://example.org"
allowed-methods: GET,POST
max-age: 1800s
----
[[web.graphql.transports.rsocket]]
==== RSocket
RSocket is also supported as a transport, on top of WebSocket or TCP.
Once the <<messaging#messaging.rsocket.server-auto-configuration,RSocket server is configured>>, we can configure our GraphQL handler on a particular route using configprop:spring.graphql.rsocket.mapping[].
For example, configuring that mapping as `"graphql"` means we can use that as a route when sending requests with the `RSocketGraphQlClient`.
Spring Boot auto-configures a `RSocketGraphQlClient.Builder<?>` bean that you can inject in your components:
include::code:RSocketGraphQlClientExample[tag=builder]
And then send a request:
include::code:RSocketGraphQlClientExample[tag=request]
[[web.graphql.exception-handling]]
=== Exceptions Handling
Spring GraphQL enables applications to register one or more Spring `DataFetcherExceptionResolver` components that are invoked sequentially.
The Exception must be resolved to a list of `graphql.GraphQLError` objects, see {spring-graphql-docs}#execution-exceptions[Spring GraphQL exception handling documentation].
Spring Boot will automatically detect `DataFetcherExceptionResolver` beans and register them with the `GraphQlSource.Builder`.
[[web.graphql.graphiql]]
=== GraphiQL and Schema printer
Spring GraphQL offers infrastructure for helping developers when consuming or developing a GraphQL API.
Spring GraphQL ships with a default https://github.com/graphql/graphiql[GraphiQL] page that is exposed at `"/graphiql"` by default.
This page is disabled by default and can be turned on with the configprop:spring.graphql.graphiql.enabled[] property.
Many applications exposing such a page will prefer a custom build.
A default implementation is very useful during development, this is why it is exposed automatically with <<using#using.devtools,`spring-boot-devtools`>> during development.
You can also choose to expose the GraphQL schema in text format at `/graphql/schema` when the configprop:spring.graphql.schema.printer.enabled[] property is enabled.

@ -0,0 +1,40 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.docs.features.testing.springbootapplications.springgraphqltests;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.graphql.tester.AutoConfigureHttpGraphQlTester;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.graphql.test.tester.HttpGraphQlTester;
@AutoConfigureHttpGraphQlTester
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
class GraphQlIntegrationTests {
@Test
void shouldGreetWithSpecificName(@Autowired HttpGraphQlTester graphQlTester) {
HttpGraphQlTester authenticatedTester = graphQlTester.mutate()
.webTestClient(
(client) -> client.defaultHeaders((headers) -> headers.setBasicAuth("admin", "ilovespring")))
.build();
authenticatedTester.document("{ greeting(name: \"Alice\") } ").execute().path("greeting").entity(String.class)
.isEqualTo("Hello, Alice!");
}
}

@ -0,0 +1,44 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.docs.features.testing.springbootapplications.springgraphqltests;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.docs.web.graphql.runtimewiring.GreetingController;
import org.springframework.boot.test.autoconfigure.graphql.GraphQlTest;
import org.springframework.graphql.test.tester.GraphQlTester;
@GraphQlTest(GreetingController.class)
class GreetingControllerTests {
@Autowired
private GraphQlTester graphQlTester;
@Test
void shouldGreetWithSpecificName() {
this.graphQlTester.document("{ greeting(name: \"Alice\") } ").execute().path("greeting").entity(String.class)
.isEqualTo("Hello, Alice!");
}
@Test
void shouldGreetWithDefaultName() {
this.graphQlTester.document("{ greeting } ").execute().path("greeting").entity(String.class)
.isEqualTo("Hello, Spring!");
}
}

@ -0,0 +1,31 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.docs.web.graphql.runtimewiring;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
@Controller
public class GreetingController {
@QueryMapping
public String greeting(@Argument String name) {
return "Hello, " + name + "!";
}
}

@ -0,0 +1,49 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.docs.web.graphql.transports.rsocket;
import java.time.Duration;
import reactor.core.publisher.Mono;
import org.springframework.graphql.client.RSocketGraphQlClient;
import org.springframework.stereotype.Component;
// tag::builder[]
@Component
public class RSocketGraphQlClientExample {
private final RSocketGraphQlClient graphQlClient;
public RSocketGraphQlClientExample(RSocketGraphQlClient.Builder<?> builder) {
this.graphQlClient = builder.tcp("example.spring.io", 8181).route("graphql").build();
}
// end::builder[]
public void rsocketOverTcp() {
// tag::request[]
Mono<Book> book = this.graphQlClient.document("{ bookById(id: \"book-1\"){ id name pageCount author } }")
.retrieve("bookById").toEntity(Book.class);
// end::request[]
book.block(Duration.ofSeconds(5));
}
static class Book {
}
}

@ -0,0 +1,42 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.docs.features.testing.springbootapplications.springgraphqltests
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.graphql.tester.AutoConfigureHttpGraphQlTester
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.graphql.test.tester.HttpGraphQlTester
import org.springframework.http.HttpHeaders
import org.springframework.test.web.reactive.server.WebTestClient
@AutoConfigureHttpGraphQlTester
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
class GraphQlIntegrationTests {
@Test
fun shouldGreetWithSpecificName(@Autowired graphQlTester: HttpGraphQlTester) {
val authenticatedTester = graphQlTester.mutate()
.webTestClient { client: WebTestClient.Builder ->
client.defaultHeaders { headers: HttpHeaders ->
headers.setBasicAuth("admin", "ilovespring")
}
}.build()
authenticatedTester.document("{ greeting(name: \"Alice\") } ").execute()
.path("greeting").entity(String::class.java).isEqualTo("Hello, Alice!")
}
}

@ -0,0 +1,43 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.docs.features.testing.springbootapplications.springgraphqltests
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.docs.web.graphql.runtimewiring.GreetingController
import org.springframework.boot.test.autoconfigure.graphql.GraphQlTest
import org.springframework.graphql.test.tester.GraphQlTester
@GraphQlTest(GreetingController::class)
internal class GreetingControllerTests {
@Autowired
lateinit var graphQlTester: GraphQlTester
@Test
fun shouldGreetWithSpecificName() {
graphQlTester.document("{ greeting(name: \"Alice\") } ").execute().path("greeting").entity(String::class.java)
.isEqualTo("Hello, Alice!")
}
@Test
fun shouldGreetWithDefaultName() {
graphQlTester.document("{ greeting } ").execute().path("greeting").entity(String::class.java)
.isEqualTo("Hello, Spring!")
}
}

@ -0,0 +1,31 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.docs.web.graphql.runtimewiring;
import org.springframework.graphql.data.method.annotation.Argument
import org.springframework.graphql.data.method.annotation.QueryMapping
import org.springframework.stereotype.Controller
@Controller
class GreetingController {
@QueryMapping
fun greeting(@Argument name: String): String {
return "Hello, $name!"
}
}

@ -0,0 +1,52 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.docs.web.graphql.transports.rsocket
import org.springframework.graphql.client.RSocketGraphQlClient
import org.springframework.stereotype.Component
import java.time.Duration
// tag::builder[]
@Component
class RSocketGraphQlClientExample(private val builder: RSocketGraphQlClient.Builder<*>) {
// end::builder[]
val graphQlClient = builder.tcp("example.spring.io", 8181)
.route("graphql")
.build()
fun rsocketOverTcp() {
// tag::request[]
val book = graphQlClient.document(
"""
{
bookById(id: "book-1"){
id
name
pageCount
author
}
}
"""
)
.retrieve("bookById").toEntity(Book::class.java)
// end::request[]
book.block(Duration.ofSeconds(5))
}
internal class Book
}

@ -0,0 +1,11 @@
plugins {
id "org.springframework.boot.starter"
}
description = "Starter for building GraphQL applications with Spring GraphQL"
dependencies {
api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter"))
api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-json"))
api("org.springframework.graphql:spring-graphql")
}

@ -46,6 +46,7 @@ dependencies {
optional("org.springframework.data:spring-data-neo4j") optional("org.springframework.data:spring-data-neo4j")
optional("org.springframework.data:spring-data-r2dbc") optional("org.springframework.data:spring-data-r2dbc")
optional("org.springframework.data:spring-data-redis") optional("org.springframework.data:spring-data-redis")
optional("org.springframework.graphql:spring-graphql-test")
optional("org.springframework.restdocs:spring-restdocs-mockmvc") { optional("org.springframework.restdocs:spring-restdocs-mockmvc") {
exclude group: "javax.servlet", module: "javax.servlet-api" exclude group: "javax.servlet", module: "javax.servlet-api"
} }

@ -0,0 +1,47 @@
/*
* 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.boot.test.autoconfigure.graphql;
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 2.7.0
* @see GraphQlTest
* @see org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration
* @see org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration
* @see org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@ImportAutoConfiguration
public @interface AutoConfigureGraphQl {
}

@ -0,0 +1,157 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.test.autoconfigure.graphql;
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.graphql.tester.AutoConfigureGraphQlTester;
import org.springframework.boot.test.autoconfigure.graphql.tester.AutoConfigureHttpGraphQlTester;
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.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}
* <li>{@code DataFetcherExceptionResolver}
* <li>{@code Instrumentation}
* <li>{@code GraphQlSourceBuilderCustomizer}
* </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 HttpGraphQlTester}, consider using
* {@link org.springframework.boot.test.context.SpringBootTest @SpringBootTest} combined
* with {@link AutoConfigureHttpGraphQlTester @AutoConfigureHttpGraphQlTester}.
*
* @author Brian Clozel
* @since 2.7.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}, {@code GenericConverter}, {@code DataFetcherExceptionResolver},
* {@code Instrumentation} and {@code GraphQlSourceBuilderCustomizer} 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 {};
}

@ -0,0 +1,36 @@
/*
* 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.boot.test.autoconfigure.graphql;
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);
}
}

@ -0,0 +1,100 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.test.autoconfigure.graphql;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import graphql.execution.instrumentation.Instrumentation;
import org.springframework.boot.autoconfigure.graphql.GraphQlSourceBuilderCustomizer;
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.DataFetcherExceptionResolver;
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 2.7.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);
includes.add(DataFetcherExceptionResolver.class);
includes.add(Instrumentation.class);
includes.add(GraphQlSourceBuilderCustomizer.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));
}
}

@ -0,0 +1,20 @@
/*
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Auto-configuration for GraphQL testing.
*/
package org.springframework.boot.test.autoconfigure.graphql;

@ -0,0 +1,43 @@
/*
* 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.boot.test.autoconfigure.graphql.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 2.7.0
* @see GraphQlTesterAutoConfiguration
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@ImportAutoConfiguration
public @interface AutoConfigureGraphQlTester {
}

@ -0,0 +1,52 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.test.autoconfigure.graphql.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.HttpGraphQlTester;
/**
* Annotation that can be applied to a test class to enable a {@link HttpGraphQlTester}.
*
* <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 2.7.0
* @see HttpGraphQlTesterAutoConfiguration
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigureMockMvc
@AutoConfigureWebTestClient
@ImportAutoConfiguration
public @interface AutoConfigureHttpGraphQlTester {
}

@ -0,0 +1,61 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.test.autoconfigure.graphql.tester;
import com.fasterxml.jackson.databind.ObjectMapper;
import graphql.GraphQL;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
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.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.graphql.ExecutionGraphQlService;
import org.springframework.graphql.test.tester.ExecutionGraphQlServiceTester;
import org.springframework.graphql.test.tester.GraphQlTester;
import org.springframework.http.MediaType;
import org.springframework.http.codec.json.Jackson2JsonDecoder;
import org.springframework.http.codec.json.Jackson2JsonEncoder;
/**
* Auto-configuration for {@link GraphQlTester}.
*
* @author Brian Clozel
* @since 2.7.0
*/
@AutoConfiguration(after = { JacksonAutoConfiguration.class, GraphQlAutoConfiguration.class })
@ConditionalOnClass({ GraphQL.class, GraphQlTester.class })
public class GraphQlTesterAutoConfiguration {
@Bean
@ConditionalOnBean(ExecutionGraphQlService.class)
@ConditionalOnMissingBean
public ExecutionGraphQlServiceTester graphQlTester(ExecutionGraphQlService graphQlService,
ObjectProvider<ObjectMapper> objectMapperProvider) {
ExecutionGraphQlServiceTester.Builder<?> builder = ExecutionGraphQlServiceTester.builder(graphQlService);
objectMapperProvider.ifAvailable((objectMapper) -> {
MediaType[] mediaTypes = new MediaType[] { MediaType.APPLICATION_JSON, MediaType.APPLICATION_GRAPHQL };
builder.encoder(new Jackson2JsonEncoder(objectMapper, mediaTypes));
builder.decoder(new Jackson2JsonDecoder(objectMapper, mediaTypes));
});
return builder.build();
}
}

@ -0,0 +1,50 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.test.autoconfigure.graphql.tester;
import org.springframework.boot.autoconfigure.AutoConfiguration;
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.graphql.GraphQlProperties;
import org.springframework.boot.test.autoconfigure.web.reactive.WebTestClientAutoConfiguration;
import org.springframework.boot.test.autoconfigure.web.servlet.MockMvcAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.graphql.test.tester.HttpGraphQlTester;
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 HttpGraphQlTester}.
*
* @author Brian Clozel
* @since 2.7.0
*/
@AutoConfiguration(after = { WebTestClientAutoConfiguration.class, MockMvcAutoConfiguration.class })
@ConditionalOnClass({ WebClient.class, WebTestClient.class, WebGraphQlTester.class })
public class HttpGraphQlTesterAutoConfiguration {
@Bean
@ConditionalOnBean(WebTestClient.class)
@ConditionalOnMissingBean
public HttpGraphQlTester webTestClientGraphQlTester(WebTestClient webTestClient, GraphQlProperties properties) {
WebTestClient mutatedWebTestClient = webTestClient.mutate().baseUrl(properties.getPath()).build();
return HttpGraphQlTester.create(mutatedWebTestClient);
}
}

@ -0,0 +1,20 @@
/*
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Auto-configuration for GraphQL tester.
*/
package org.springframework.boot.test.autoconfigure.graphql.tester;

@ -0,0 +1,4 @@
# AutoConfigureGraphQl auto-configuration imports
org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration
org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration
org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration

@ -0,0 +1,2 @@
# AutoConfigureGraphQlTester auto-configuration imports
org.springframework.boot.test.autoconfigure.graphql.tester.GraphQlTesterAutoConfiguration

@ -0,0 +1,2 @@
# AutoConfigureHttpGraphQlTester auto-configuration imports
org.springframework.boot.test.autoconfigure.graphql.tester.HttpGraphQlTesterAutoConfiguration

@ -0,0 +1,71 @@
/*
* 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.boot.test.autoconfigure.graphql;
public class Book {
String id;
String name;
int pageCount;
String author;
public Book() {
}
public Book(String id, String name, int pageCount, String author) {
this.id = id;
this.name = name;
this.pageCount = pageCount;
this.author = author;
}
public String getId() {
return 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;
}
}

@ -0,0 +1,36 @@
/*
* 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.boot.test.autoconfigure.graphql;
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");
}
}

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

@ -0,0 +1,41 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.test.autoconfigure.graphql;
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(BookController.class)
public class GraphQlTestIntegrationTest {
@Autowired
private GraphQlTester graphQlTester;
@Test
void getBookdByIdShouldReturnTestBook() {
String query = "{ bookById(id: \"book-1\"){ id name pageCount author } }";
this.graphQlTester.document(query).execute().path("data.bookById.id").entity(String.class).isEqualTo("42");
}
}

@ -0,0 +1,227 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.test.autoconfigure.graphql;
import java.io.IOException;
import java.util.List;
import com.fasterxml.jackson.databind.module.SimpleModule;
import graphql.GraphQLError;
import graphql.execution.instrumentation.Instrumentation;
import graphql.schema.DataFetchingEnvironment;
import graphql.schema.idl.RuntimeWiring;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.boot.autoconfigure.graphql.GraphQlSourceBuilderCustomizer;
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.DataFetcherExceptionResolver;
import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
import org.springframework.graphql.server.WebGraphQlInterceptor;
import org.springframework.graphql.server.WebGraphQlRequest;
import org.springframework.graphql.server.WebGraphQlResponse;
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();
assertThat(excludes(filter, ExampleDataFetcherExceptionResolver.class)).isFalse();
assertThat(excludes(filter, ExampleInstrumentation.class)).isFalse();
assertThat(excludes(filter, ExampleGraphQlSourceBuilderCustomizer.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();
assertThat(excludes(filter, ExampleDataFetcherExceptionResolver.class)).isFalse();
assertThat(excludes(filter, ExampleInstrumentation.class)).isFalse();
assertThat(excludes(filter, ExampleGraphQlSourceBuilderCustomizer.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();
assertThat(excludes(filter, ExampleDataFetcherExceptionResolver.class)).isTrue();
assertThat(excludes(filter, ExampleInstrumentation.class)).isTrue();
assertThat(excludes(filter, ExampleGraphQlSourceBuilderCustomizer.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();
assertThat(excludes(filter, ExampleDataFetcherExceptionResolver.class)).isFalse();
assertThat(excludes(filter, ExampleInstrumentation.class)).isFalse();
assertThat(excludes(filter, ExampleGraphQlSourceBuilderCustomizer.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();
assertThat(excludes(filter, ExampleDataFetcherExceptionResolver.class)).isFalse();
assertThat(excludes(filter, ExampleInstrumentation.class)).isFalse();
assertThat(excludes(filter, ExampleGraphQlSourceBuilderCustomizer.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 WebGraphQlInterceptor {
@Override
public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) {
return null;
}
}
@SuppressWarnings("serial")
static class ExampleModule extends SimpleModule {
}
static class ExampleDataFetcherExceptionResolver implements DataFetcherExceptionResolver {
@Override
public Mono<List<GraphQLError>> resolveException(Throwable exception, DataFetchingEnvironment environment) {
return null;
}
}
static class ExampleInstrumentation implements Instrumentation {
}
static class ExampleGraphQlSourceBuilderCustomizer implements GraphQlSourceBuilderCustomizer {
@Override
public void customize(GraphQlSource.SchemaResourceBuilder builder) {
}
}
}

@ -0,0 +1,63 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.test.autoconfigure.graphql.tester;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.ExecutionGraphQlService;
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(JacksonAutoConfiguration.class, GraphQlTesterAutoConfiguration.class));
@Test
void shouldNotContributeTesterIfGraphQlServiceNotPresent() {
this.contextRunner.run((context) -> assertThat(context).hasNotFailed().doesNotHaveBean(GraphQlTester.class));
}
@Test
void shouldContributeTester() {
this.contextRunner.withUserConfiguration(CustomGraphQlServiceConfiguration.class)
.run((context) -> assertThat(context).hasNotFailed().hasSingleBean(GraphQlTester.class));
}
@Configuration(proxyBeanMethods = false)
static class CustomGraphQlServiceConfiguration {
@Bean
ExecutionGraphQlService graphQlService() {
return mock(ExecutionGraphQlService.class);
}
}
}

@ -36,6 +36,7 @@ dependencies {
optional("org.springframework:spring-test") optional("org.springframework:spring-test")
optional("org.springframework:spring-web") optional("org.springframework:spring-web")
optional("org.springframework:spring-webflux") optional("org.springframework:spring-webflux")
optional("org.springframework.graphql:spring-graphql-test")
optional("net.sourceforge.htmlunit:htmlunit") { optional("net.sourceforge.htmlunit:htmlunit") {
exclude(group: "commons-logging", module: "commons-logging") exclude(group: "commons-logging", module: "commons-logging")
} }

@ -0,0 +1,217 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.test.graphql.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.WebApplicationType;
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.HttpGraphQlTester;
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.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.context.WebApplicationContext;
/**
* {@link ContextCustomizer} for {@link HttpGraphQlTester}.
*
* @author Brian Clozel
*/
class HttpGraphQlTesterContextCustomizer implements ContextCustomizer {
@Override
public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
SpringBootTest springBootTest = TestContextAnnotationUtils.findMergedAnnotation(mergedConfig.getTestClass(),
SpringBootTest.class);
if (springBootTest.webEnvironment().isEmbedded()) {
registerHttpGraphQlTester(context);
}
}
private void registerHttpGraphQlTester(ConfigurableApplicationContext context) {
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
if (beanFactory instanceof BeanDefinitionRegistry) {
registerHttpGraphQlTester((BeanDefinitionRegistry) beanFactory);
}
}
private void registerHttpGraphQlTester(BeanDefinitionRegistry registry) {
RootBeanDefinition definition = new RootBeanDefinition(HttpGraphQlTesterRegistrar.class);
definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
registry.registerBeanDefinition(HttpGraphQlTesterRegistrar.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 HttpGraphQlTesterRegistrar
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,
HttpGraphQlTester.class, false, false).length == 0) {
registry.registerBeanDefinition(HttpGraphQlTester.class.getName(),
new RootBeanDefinition(HttpGraphQlTesterFactory.class));
}
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE - 1;
}
}
public static class HttpGraphQlTesterFactory implements FactoryBean<HttpGraphQlTester>, ApplicationContextAware {
private static final String SERVLET_APPLICATION_CONTEXT_CLASS = "org.springframework.web.context.WebApplicationContext";
private static final String REACTIVE_APPLICATION_CONTEXT_CLASS = "org.springframework.boot.web.reactive.context.ReactiveWebApplicationContext";
private ApplicationContext applicationContext;
private HttpGraphQlTester object;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public boolean isSingleton() {
return true;
}
@Override
public Class<?> getObjectType() {
return HttpGraphQlTester.class;
}
@Override
public HttpGraphQlTester getObject() throws Exception {
if (this.object == null) {
this.object = createGraphQlTester();
}
return this.object;
}
private HttpGraphQlTester 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 HttpGraphQlTester.create(mutatedWebClient);
}
private String getBaseUrl(boolean sslEnabled, String port) {
String basePath = deduceBasePath();
return (sslEnabled ? "https" : "http") + "://localhost:" + port + basePath;
}
private String deduceBasePath() {
return deduceServerBasePath() + findConfiguredGraphQlPath();
}
private String findConfiguredGraphQlPath() {
String configuredPath = this.applicationContext.getEnvironment().getProperty("spring.graphql.path");
return StringUtils.hasText(configuredPath) ? configuredPath : "/graphql";
}
private String deduceServerBasePath() {
String serverBasePath = "";
WebApplicationType webApplicationType = deduceFromApplicationContext(this.applicationContext.getClass());
if (webApplicationType == WebApplicationType.REACTIVE) {
serverBasePath = this.applicationContext.getEnvironment().getProperty("spring.webflux.base-path");
}
else if (webApplicationType == WebApplicationType.SERVLET) {
serverBasePath = ((WebApplicationContext) this.applicationContext).getServletContext().getContextPath();
}
return (serverBasePath != null) ? serverBasePath : "";
}
static WebApplicationType deduceFromApplicationContext(Class<?> applicationContextClass) {
if (isAssignable(SERVLET_APPLICATION_CONTEXT_CLASS, applicationContextClass)) {
return WebApplicationType.SERVLET;
}
if (isAssignable(REACTIVE_APPLICATION_CONTEXT_CLASS, applicationContextClass)) {
return WebApplicationType.REACTIVE;
}
return WebApplicationType.NONE;
}
private static boolean isAssignable(String target, Class<?> type) {
try {
return ClassUtils.resolveClassName(target, null).isAssignableFrom(type);
}
catch (Throwable ex) {
return false;
}
}
private boolean isSslEnabled(ApplicationContext context) {
try {
AbstractConfigurableWebServerFactory webServerFactory = context
.getBean(AbstractConfigurableWebServerFactory.class);
return webServerFactory.getSsl() != null && webServerFactory.getSsl().isEnabled();
}
catch (NoSuchBeanDefinitionException ex) {
return false;
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save