Merge branch '2.7.x'
commit
f0677a119c
@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* 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.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.AutoConfigureAfter;
|
||||||
|
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.graphql.execution.GraphQlSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link EnableAutoConfiguration Auto-configuration} for instrumentation of Spring
|
||||||
|
* GraphQL endpoints.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
* @since 2.7.0
|
||||||
|
*/
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@AutoConfigureAfter({ 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,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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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,160 @@
|
|||||||
|
/*
|
||||||
|
* 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.concurrent.CompletionStage;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
|
import graphql.ExecutionResult;
|
||||||
|
import graphql.execution.instrumentation.InstrumentationContext;
|
||||||
|
import graphql.execution.instrumentation.InstrumentationState;
|
||||||
|
import graphql.execution.instrumentation.SimpleInstrumentation;
|
||||||
|
import graphql.execution.instrumentation.SimpleInstrumentationContext;
|
||||||
|
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters;
|
||||||
|
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
|
||||||
|
import graphql.schema.DataFetcher;
|
||||||
|
import io.micrometer.core.instrument.DistributionSummary;
|
||||||
|
import io.micrometer.core.instrument.MeterRegistry;
|
||||||
|
import io.micrometer.core.instrument.Tag;
|
||||||
|
import io.micrometer.core.instrument.Timer;
|
||||||
|
|
||||||
|
import org.springframework.boot.actuate.metrics.AutoTimer;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
|
||||||
|
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() {
|
||||||
|
return new RequestMetricsInstrumentationState(this.autoTimer, this.registry);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InstrumentationContext<ExecutionResult> beginExecution(InstrumentationExecutionParameters parameters) {
|
||||||
|
if (this.autoTimer.isEnabled()) {
|
||||||
|
RequestMetricsInstrumentationState state = parameters.getInstrumentationState();
|
||||||
|
state.startTimer();
|
||||||
|
return new SimpleInstrumentationContext<ExecutionResult>() {
|
||||||
|
@Override
|
||||||
|
public void onCompleted(ExecutionResult result, Throwable exc) {
|
||||||
|
Iterable<Tag> tags = GraphQlMetricsInstrumentation.this.tagsProvider.getExecutionTags(parameters,
|
||||||
|
result, exc);
|
||||||
|
state.tags(tags).stopTimer();
|
||||||
|
if (!result.getErrors().isEmpty()) {
|
||||||
|
result.getErrors()
|
||||||
|
.forEach((error) -> GraphQlMetricsInstrumentation.this.registry.counter("graphql.error",
|
||||||
|
GraphQlMetricsInstrumentation.this.tagsProvider.getErrorTags(parameters, error))
|
||||||
|
.increment());
|
||||||
|
}
|
||||||
|
GraphQlMetricsInstrumentation.this.dataFetchingSummary.record(state.getDataFetchingCount());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return super.beginExecution(parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DataFetcher<?> instrumentDataFetcher(DataFetcher<?> dataFetcher,
|
||||||
|
InstrumentationFieldFetchParameters parameters) {
|
||||||
|
if (this.autoTimer.isEnabled() && !parameters.isTrivialDataFetcher()) {
|
||||||
|
return (environment) -> {
|
||||||
|
Timer.Sample sample = Timer.start(this.registry);
|
||||||
|
try {
|
||||||
|
Object value = dataFetcher.get(environment);
|
||||||
|
if (value instanceof CompletionStage<?>) {
|
||||||
|
CompletionStage<?> completion = (CompletionStage<?>) value;
|
||||||
|
return completion.whenComplete(
|
||||||
|
(result, error) -> recordDataFetcherMetric(sample, dataFetcher, parameters, error));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
recordDataFetcherMetric(sample, dataFetcher, parameters, null);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Throwable throwable) {
|
||||||
|
recordDataFetcherMetric(sample, dataFetcher, parameters, throwable);
|
||||||
|
throw throwable;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return super.instrumentDataFetcher(dataFetcher, parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void recordDataFetcherMetric(Timer.Sample sample, DataFetcher<?> dataFetcher,
|
||||||
|
InstrumentationFieldFetchParameters parameters, @Nullable Throwable throwable) {
|
||||||
|
Timer.Builder timer = this.autoTimer.builder("graphql.datafetcher");
|
||||||
|
timer.tags(this.tagsProvider.getDataFetchingTags(dataFetcher, parameters, throwable));
|
||||||
|
sample.stop(timer.register(this.registry));
|
||||||
|
RequestMetricsInstrumentationState state = parameters.getInstrumentationState();
|
||||||
|
state.incrementDataFetchingCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
static class RequestMetricsInstrumentationState implements InstrumentationState {
|
||||||
|
|
||||||
|
private final MeterRegistry registry;
|
||||||
|
|
||||||
|
private final Timer.Builder timer;
|
||||||
|
|
||||||
|
private Timer.Sample sample;
|
||||||
|
|
||||||
|
private AtomicLong dataFetchingCount = new AtomicLong(0L);
|
||||||
|
|
||||||
|
RequestMetricsInstrumentationState(AutoTimer autoTimer, MeterRegistry registry) {
|
||||||
|
this.timer = autoTimer.builder("graphql.request");
|
||||||
|
this.registry = registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestMetricsInstrumentationState tags(Iterable<Tag> tags) {
|
||||||
|
this.timer.tags(tags);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
void startTimer() {
|
||||||
|
this.sample = Timer.start(this.registry);
|
||||||
|
}
|
||||||
|
|
||||||
|
void stopTimer() {
|
||||||
|
this.sample.stop(this.timer.register(this.registry));
|
||||||
|
}
|
||||||
|
|
||||||
|
void incrementDataFetchingCount() {
|
||||||
|
this.dataFetchingCount.incrementAndGet();
|
||||||
|
}
|
||||||
|
|
||||||
|
long getDataFetchingCount() {
|
||||||
|
return this.dataFetchingCount.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,102 @@
|
|||||||
|
/*
|
||||||
|
* 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.List;
|
||||||
|
|
||||||
|
import graphql.ErrorClassification;
|
||||||
|
import graphql.ErrorType;
|
||||||
|
import graphql.ExecutionResult;
|
||||||
|
import graphql.GraphQLError;
|
||||||
|
import graphql.execution.ExecutionStepInfo;
|
||||||
|
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
|
||||||
|
import graphql.schema.GraphQLObjectType;
|
||||||
|
import io.micrometer.core.instrument.Tag;
|
||||||
|
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory methods for Tags associated with a GraphQL request.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
public final class GraphQlTags {
|
||||||
|
|
||||||
|
private static final Tag OUTCOME_SUCCESS = Tag.of("outcome", "SUCCESS");
|
||||||
|
|
||||||
|
private static final Tag OUTCOME_ERROR = Tag.of("outcome", "ERROR");
|
||||||
|
|
||||||
|
private static final Tag UNKNOWN_ERRORTYPE = Tag.of("errorType", "UNKNOWN");
|
||||||
|
|
||||||
|
private GraphQlTags() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Tag executionOutcome(ExecutionResult result, @Nullable Throwable exception) {
|
||||||
|
if (exception == null && result.getErrors().isEmpty()) {
|
||||||
|
return OUTCOME_SUCCESS;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return OUTCOME_ERROR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Tag errorType(GraphQLError error) {
|
||||||
|
ErrorClassification errorType = error.getErrorType();
|
||||||
|
if (errorType instanceof ErrorType) {
|
||||||
|
return Tag.of("errorType", ((ErrorType) errorType).name());
|
||||||
|
}
|
||||||
|
return UNKNOWN_ERRORTYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Tag errorPath(GraphQLError error) {
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
List<Object> pathSegments = error.getPath();
|
||||||
|
if (!CollectionUtils.isEmpty(pathSegments)) {
|
||||||
|
builder.append('$');
|
||||||
|
for (Object segment : pathSegments) {
|
||||||
|
try {
|
||||||
|
int index = Integer.parseUnsignedInt(segment.toString());
|
||||||
|
builder.append("[*]");
|
||||||
|
}
|
||||||
|
catch (NumberFormatException exc) {
|
||||||
|
builder.append('.');
|
||||||
|
builder.append(segment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Tag.of("errorPath", builder.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Tag dataFetchingOutcome(@Nullable Throwable exception) {
|
||||||
|
return (exception != null) ? OUTCOME_ERROR : OUTCOME_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Tag dataFetchingPath(InstrumentationFieldFetchParameters parameters) {
|
||||||
|
ExecutionStepInfo executionStepInfo = parameters.getExecutionStepInfo();
|
||||||
|
StringBuilder dataFetchingType = new StringBuilder();
|
||||||
|
if (executionStepInfo.hasParent() && executionStepInfo.getParent().getType() instanceof GraphQLObjectType) {
|
||||||
|
dataFetchingType.append(((GraphQLObjectType) executionStepInfo.getParent().getType()).getName());
|
||||||
|
dataFetchingType.append('.');
|
||||||
|
}
|
||||||
|
dataFetchingType.append(executionStepInfo.getPath().getSegmentName());
|
||||||
|
return Tag.of("path", dataFetchingType.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* 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 graphql.ExecutionResult;
|
||||||
|
import graphql.GraphQLError;
|
||||||
|
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters;
|
||||||
|
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
|
||||||
|
import graphql.schema.DataFetcher;
|
||||||
|
import io.micrometer.core.instrument.Tag;
|
||||||
|
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A contributor of {@link Tag Tags} for Spring GraphQL-based request handling. Typically,
|
||||||
|
* used by a {@link GraphQlTagsProvider} to provide tags in addition to its defaults.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
* @since 2.7.0
|
||||||
|
*/
|
||||||
|
public interface GraphQlTagsContributor {
|
||||||
|
|
||||||
|
Iterable<Tag> getExecutionTags(InstrumentationExecutionParameters parameters, ExecutionResult result,
|
||||||
|
@Nullable Throwable exception);
|
||||||
|
|
||||||
|
Iterable<Tag> getErrorTags(InstrumentationExecutionParameters parameters, GraphQLError error);
|
||||||
|
|
||||||
|
Iterable<Tag> getDataFetchingTags(DataFetcher<?> dataFetcher, InstrumentationFieldFetchParameters parameters,
|
||||||
|
@Nullable Throwable exception);
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* 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 graphql.ExecutionResult;
|
||||||
|
import graphql.GraphQLError;
|
||||||
|
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters;
|
||||||
|
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
|
||||||
|
import graphql.schema.DataFetcher;
|
||||||
|
import io.micrometer.core.instrument.Tag;
|
||||||
|
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides {@link Tag Tags} for Spring GraphQL-based request handling.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
* @since 2.7.0
|
||||||
|
*/
|
||||||
|
public interface GraphQlTagsProvider {
|
||||||
|
|
||||||
|
Iterable<Tag> getExecutionTags(InstrumentationExecutionParameters parameters, ExecutionResult result,
|
||||||
|
@Nullable Throwable exception);
|
||||||
|
|
||||||
|
Iterable<Tag> getErrorTags(InstrumentationExecutionParameters parameters, GraphQLError error);
|
||||||
|
|
||||||
|
Iterable<Tag> getDataFetchingTags(DataFetcher<?> dataFetcher, InstrumentationFieldFetchParameters parameters,
|
||||||
|
@Nullable Throwable exception);
|
||||||
|
|
||||||
|
}
|
@ -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,176 @@
|
|||||||
|
/*
|
||||||
|
* 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.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.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
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
this.parameters = new InstrumentationExecutionParameters(this.input, this.schema, this.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRecordTimerWhenResult() {
|
||||||
|
InstrumentationContext<ExecutionResult> execution = this.instrumentation.beginExecution(this.parameters);
|
||||||
|
ExecutionResult result = new ExecutionResultImpl("Hello", null);
|
||||||
|
execution.onCompleted(result, null);
|
||||||
|
|
||||||
|
Timer timer = this.registry.find("graphql.request").timer();
|
||||||
|
assertThat(timer).isNotNull();
|
||||||
|
assertThat(timer.takeSnapshot().count()).isEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRecordDataFetchingCount() throws Exception {
|
||||||
|
InstrumentationContext<ExecutionResult> execution = this.instrumentation.beginExecution(this.parameters);
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
DataFetchingEnvironment environment = DataFetchingEnvironmentImpl.newDataFetchingEnvironment().build();
|
||||||
|
instrumented.get(environment);
|
||||||
|
|
||||||
|
Timer timer = this.registry.find("graphql.datafetcher").timer();
|
||||||
|
assertThat(timer).isNotNull();
|
||||||
|
assertThat(timer.takeSnapshot().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);
|
||||||
|
DataFetchingEnvironment environment = DataFetchingEnvironmentImpl.newDataFetchingEnvironment().build();
|
||||||
|
instrumented.get(environment);
|
||||||
|
|
||||||
|
Timer timer = this.registry.find("graphql.datafetcher").timer();
|
||||||
|
assertThat(timer).isNotNull();
|
||||||
|
assertThat(timer.takeSnapshot().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);
|
||||||
|
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.takeSnapshot().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);
|
||||||
|
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);
|
||||||
|
given(fieldFetchParameters.getInstrumentationState()).willReturn(this.state);
|
||||||
|
return fieldFetchParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
* 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.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.getValue()).isEqualTo("SUCCESS");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void executionOutcomeShouldErrorWhenExceptionThrown() {
|
||||||
|
ExecutionResult result = ExecutionResultImpl.newExecutionResult().build();
|
||||||
|
Tag tag = GraphQlTags.executionOutcome(result, new IllegalArgumentException("test error"));
|
||||||
|
assertThat(tag.getValue()).isEqualTo("ERROR");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void executionOutcomeShouldErrorWhenResponseErrors() {
|
||||||
|
GraphQLError error = GraphqlErrorBuilder.newError().message("Invalid query").build();
|
||||||
|
Tag tag = GraphQlTags.executionOutcome(ExecutionResultImpl.newExecutionResult().addError(error).build(), null);
|
||||||
|
assertThat(tag.getValue()).isEqualTo("ERROR");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void errorTypeShouldBeDefinedIfPresent() {
|
||||||
|
GraphQLError error = GraphqlErrorBuilder.newError().errorType(ErrorType.DataFetchingException)
|
||||||
|
.message("test error").build();
|
||||||
|
Tag errorTypeTag = GraphQlTags.errorType(error);
|
||||||
|
assertThat(errorTypeTag.getValue()).isEqualTo("DataFetchingException");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void errorPathShouldUseJsonPathFormat() {
|
||||||
|
GraphQLError error = GraphqlErrorBuilder.newError().path(Arrays.asList("project", "name")).message("test error")
|
||||||
|
.build();
|
||||||
|
Tag errorPathTag = GraphQlTags.errorPath(error);
|
||||||
|
assertThat(errorPathTag.getValue()).isEqualTo("$.project.name");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void errorPathShouldUseJsonPathFormatForIndices() {
|
||||||
|
GraphQLError error = GraphqlErrorBuilder.newError().path(Arrays.asList("issues", "42", "title"))
|
||||||
|
.message("test error").build();
|
||||||
|
Tag errorPathTag = GraphQlTags.errorPath(error);
|
||||||
|
assertThat(errorPathTag.getValue()).isEqualTo("$.issues[*].title");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void dataFetchingOutcomeShouldBeSuccessfulIfNoException() {
|
||||||
|
Tag fetchingOutcomeTag = GraphQlTags.dataFetchingOutcome(null);
|
||||||
|
assertThat(fetchingOutcomeTag.getValue()).isEqualTo("SUCCESS");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void dataFetchingOutcomeShouldBeErrorIfException() {
|
||||||
|
Tag fetchingOutcomeTag = GraphQlTags.dataFetchingOutcome(new IllegalStateException("error state"));
|
||||||
|
assertThat(fetchingOutcomeTag.getValue()).isEqualTo("ERROR");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,130 @@
|
|||||||
|
/*
|
||||||
|
* 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.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import graphql.GraphQL;
|
||||||
|
import graphql.execution.instrumentation.Instrumentation;
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
|
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.core.io.support.ResourcePatternResolver;
|
||||||
|
import org.springframework.format.support.DefaultFormattingConversionService;
|
||||||
|
import org.springframework.graphql.GraphQlService;
|
||||||
|
import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer;
|
||||||
|
import org.springframework.graphql.execution.BatchLoaderRegistry;
|
||||||
|
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
|
||||||
|
import org.springframework.graphql.execution.DefaultBatchLoaderRegistry;
|
||||||
|
import org.springframework.graphql.execution.ExecutionGraphQlService;
|
||||||
|
import org.springframework.graphql.execution.GraphQlSource;
|
||||||
|
import org.springframework.graphql.execution.MissingSchemaException;
|
||||||
|
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link EnableAutoConfiguration Auto-configuration} for creating a Spring GraphQL base
|
||||||
|
* infrastructure.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
* @since 2.7.0
|
||||||
|
*/
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@ConditionalOnClass({ GraphQL.class, GraphQlSource.class })
|
||||||
|
@EnableConfigurationProperties(GraphQlProperties.class)
|
||||||
|
public class GraphQlAutoConfiguration {
|
||||||
|
|
||||||
|
private static final Log logger = LogFactory.getLog(GraphQlAutoConfiguration.class);
|
||||||
|
|
||||||
|
private final BatchLoaderRegistry batchLoaderRegistry = new DefaultBatchLoaderRegistry();
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
public GraphQlSource graphQlSource(ResourcePatternResolver resourcePatternResolver, GraphQlProperties properties,
|
||||||
|
ObjectProvider<DataFetcherExceptionResolver> exceptionResolversProvider,
|
||||||
|
ObjectProvider<Instrumentation> instrumentationsProvider,
|
||||||
|
ObjectProvider<RuntimeWiringConfigurer> wiringConfigurers,
|
||||||
|
ObjectProvider<GraphQlSourceBuilderCustomizer> sourceCustomizers) {
|
||||||
|
|
||||||
|
List<Resource> schemaResources = resolveSchemaResources(resourcePatternResolver,
|
||||||
|
properties.getSchema().getLocations(), properties.getSchema().getFileExtensions());
|
||||||
|
GraphQlSource.Builder builder = GraphQlSource.builder()
|
||||||
|
.schemaResources(schemaResources.toArray(new Resource[0]))
|
||||||
|
.exceptionResolvers(exceptionResolversProvider.orderedStream().collect(Collectors.toList()))
|
||||||
|
.instrumentation(instrumentationsProvider.orderedStream().collect(Collectors.toList()));
|
||||||
|
wiringConfigurers.orderedStream().forEach(builder::configureRuntimeWiring);
|
||||||
|
sourceCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
|
||||||
|
try {
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
catch (MissingSchemaException exc) {
|
||||||
|
throw new InvalidSchemaLocationsException(properties.getSchema().getLocations(), resourcePatternResolver,
|
||||||
|
exc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
public BatchLoaderRegistry batchLoaderRegistry() {
|
||||||
|
return this.batchLoaderRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
public GraphQlService graphQlService(GraphQlSource graphQlSource) {
|
||||||
|
ExecutionGraphQlService service = new ExecutionGraphQlService(graphQlSource);
|
||||||
|
service.addDataLoaderRegistrar(this.batchLoaderRegistry);
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
public AnnotatedControllerConfigurer annotatedControllerConfigurer() {
|
||||||
|
AnnotatedControllerConfigurer annotatedControllerConfigurer = new AnnotatedControllerConfigurer();
|
||||||
|
annotatedControllerConfigurer.setConversionService(new DefaultFormattingConversionService());
|
||||||
|
return annotatedControllerConfigurer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Resource> resolveSchemaResources(ResourcePatternResolver resolver, String[] schemaLocations,
|
||||||
|
String[] fileExtensions) {
|
||||||
|
List<Resource> schemaResources = new ArrayList<>();
|
||||||
|
for (String location : schemaLocations) {
|
||||||
|
for (String extension : fileExtensions) {
|
||||||
|
String resourcePattern = location + "*" + extension;
|
||||||
|
try {
|
||||||
|
schemaResources.addAll(Arrays.asList(resolver.getResources(resourcePattern)));
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
logger.debug("Could not resolve schema location: '" + resourcePattern + "'", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return schemaResources;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,160 @@
|
|||||||
|
/*
|
||||||
|
* 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.time.Duration;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.boot.context.properties.PropertyMapper;
|
||||||
|
import org.springframework.boot.convert.DurationUnit;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration properties for GraphQL endpoint's CORS support.
|
||||||
|
*
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Brian Clozel
|
||||||
|
* @since 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.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
private Boolean allowCredentials;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How long the response from a pre-flight request can be cached by clients. If a
|
||||||
|
* duration suffix is not specified, seconds will be used.
|
||||||
|
*/
|
||||||
|
@DurationUnit(ChronoUnit.SECONDS)
|
||||||
|
private Duration maxAge = Duration.ofSeconds(1800);
|
||||||
|
|
||||||
|
public List<String> getAllowedOrigins() {
|
||||||
|
return this.allowedOrigins;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAllowedOrigins(List<String> allowedOrigins) {
|
||||||
|
this.allowedOrigins = allowedOrigins;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getAllowedOriginPatterns() {
|
||||||
|
return this.allowedOriginPatterns;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAllowedOriginPatterns(List<String> allowedOriginPatterns) {
|
||||||
|
this.allowedOriginPatterns = allowedOriginPatterns;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getAllowedMethods() {
|
||||||
|
return this.allowedMethods;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAllowedMethods(List<String> allowedMethods) {
|
||||||
|
this.allowedMethods = allowedMethods;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getAllowedHeaders() {
|
||||||
|
return this.allowedHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAllowedHeaders(List<String> allowedHeaders) {
|
||||||
|
this.allowedHeaders = allowedHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getExposedHeaders() {
|
||||||
|
return this.exposedHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExposedHeaders(List<String> exposedHeaders) {
|
||||||
|
this.exposedHeaders = exposedHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public Boolean getAllowCredentials() {
|
||||||
|
return this.allowCredentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAllowCredentials(Boolean allowCredentials) {
|
||||||
|
this.allowCredentials = allowCredentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Duration getMaxAge() {
|
||||||
|
return this.maxAge;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMaxAge(Duration maxAge) {
|
||||||
|
this.maxAge = maxAge;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public CorsConfiguration toCorsConfiguration() {
|
||||||
|
if (CollectionUtils.isEmpty(this.allowedOrigins) && CollectionUtils.isEmpty(this.allowedOriginPatterns)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
PropertyMapper map = PropertyMapper.get();
|
||||||
|
CorsConfiguration config = new CorsConfiguration();
|
||||||
|
map.from(this::getAllowedOrigins).to(config::setAllowedOrigins);
|
||||||
|
map.from(this::getAllowedOriginPatterns).to(config::setAllowedOriginPatterns);
|
||||||
|
map.from(this::getAllowedHeaders).whenNot(CollectionUtils::isEmpty).to(config::setAllowedHeaders);
|
||||||
|
map.from(this::getAllowedMethods).whenNot(CollectionUtils::isEmpty).to(config::setAllowedMethods);
|
||||||
|
map.from(this::getExposedHeaders).whenNot(CollectionUtils::isEmpty).to(config::setExposedHeaders);
|
||||||
|
map.from(this::getMaxAge).whenNonNull().as(Duration::getSeconds).to(config::setMaxAge);
|
||||||
|
map.from(this::getAllowCredentials).whenNonNull().to(config::setAllowCredentials);
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,184 @@
|
|||||||
|
/*
|
||||||
|
* 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.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();
|
||||||
|
|
||||||
|
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 static class Schema {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Locations of GraphQL schema files.
|
||||||
|
*/
|
||||||
|
private String[] locations = new String[] { "classpath:graphql/**/" };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File extensions for GraphQL schema files.
|
||||||
|
*/
|
||||||
|
private String[] fileExtensions = new String[] { ".graphqls", ".gqls" };
|
||||||
|
|
||||||
|
private final Printer printer = new Printer();
|
||||||
|
|
||||||
|
public String[] getLocations() {
|
||||||
|
return this.locations;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLocations(String[] locations) {
|
||||||
|
this.locations = appendSlashIfNecessary(locations);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String[] getFileExtensions() {
|
||||||
|
return this.fileExtensions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFileExtensions(String[] fileExtensions) {
|
||||||
|
this.fileExtensions = fileExtensions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String[] appendSlashIfNecessary(String[] locations) {
|
||||||
|
return Arrays.stream(locations).map((location) -> location.endsWith("/") ? location : location + "/")
|
||||||
|
.toArray(String[]::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Printer getPrinter() {
|
||||||
|
return this.printer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Printer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the endpoint that prints the schema is enabled. 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* 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.graphql.execution.GraphQlSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback interface that can be implemented by beans wishing to customize properties of
|
||||||
|
* {@link org.springframework.graphql.execution.GraphQlSource.Builder} whilst retaining
|
||||||
|
* default auto-configuration.
|
||||||
|
*
|
||||||
|
* @author Rossen Stoyanchev
|
||||||
|
* @since 2.7.0
|
||||||
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface GraphQlSourceBuilderCustomizer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Customize the {@link GraphQlSource.Builder} instance.
|
||||||
|
* @param builder builder the builder to customize
|
||||||
|
*/
|
||||||
|
void customize(GraphQlSource.Builder builder);
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,101 @@
|
|||||||
|
/*
|
||||||
|
* 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.autoconfigure.graphql;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.core.NestedRuntimeException;
|
||||||
|
import org.springframework.core.io.support.ResourcePatternResolver;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link InvalidSchemaLocationsException} thrown when no schema file could be found in
|
||||||
|
* the provided locations.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
* @since 2.7.0
|
||||||
|
*/
|
||||||
|
public class InvalidSchemaLocationsException extends NestedRuntimeException {
|
||||||
|
|
||||||
|
private final List<SchemaLocation> schemaLocations;
|
||||||
|
|
||||||
|
public InvalidSchemaLocationsException(String[] locations, ResourcePatternResolver resolver) {
|
||||||
|
this(locations, resolver, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public InvalidSchemaLocationsException(String[] locations, ResourcePatternResolver resolver, Throwable cause) {
|
||||||
|
super("No schema file could be found in the provided locations.", cause);
|
||||||
|
Assert.notEmpty(locations, "locations should not be empty");
|
||||||
|
Assert.notNull(resolver, "resolver should not be null");
|
||||||
|
List<SchemaLocation> providedLocations = new ArrayList<>();
|
||||||
|
for (String location : locations) {
|
||||||
|
try {
|
||||||
|
String uri = resolver.getResource(location).getURI().toASCIIString();
|
||||||
|
providedLocations.add(new SchemaLocation(location, uri));
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
providedLocations.add(new SchemaLocation(location, ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.schemaLocations = Collections.unmodifiableList(providedLocations);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the list of provided locations where to look for schemas.
|
||||||
|
* @return the list of locations
|
||||||
|
*/
|
||||||
|
public List<SchemaLocation> getSchemaLocations() {
|
||||||
|
return this.schemaLocations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The location where to look for schemas.
|
||||||
|
*/
|
||||||
|
public static class SchemaLocation {
|
||||||
|
|
||||||
|
private final String location;
|
||||||
|
|
||||||
|
private final String uri;
|
||||||
|
|
||||||
|
SchemaLocation(String location, String uri) {
|
||||||
|
this.location = location;
|
||||||
|
this.uri = uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the location String to be resolved by a {@link ResourcePatternResolver}.
|
||||||
|
* @return the location
|
||||||
|
*/
|
||||||
|
public String getLocation() {
|
||||||
|
return this.location;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the resolved URI String for this location, an empty String if resolution
|
||||||
|
* failed.
|
||||||
|
* @return the resolved location or an empty String
|
||||||
|
*/
|
||||||
|
public String getUri() {
|
||||||
|
return this.uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* 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.boot.diagnostics.AbstractFailureAnalyzer;
|
||||||
|
import org.springframework.boot.diagnostics.FailureAnalysis;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of {@link AbstractFailureAnalyzer} to analyze failures caused by
|
||||||
|
* {@link InvalidSchemaLocationsException}.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
*/
|
||||||
|
class InvalidSchemaLocationsExceptionFailureAnalyzer extends AbstractFailureAnalyzer<InvalidSchemaLocationsException> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected FailureAnalysis analyze(Throwable rootFailure, InvalidSchemaLocationsException cause) {
|
||||||
|
String message = "Could not find any GraphQL schema file under configured locations.";
|
||||||
|
StringBuilder action = new StringBuilder(
|
||||||
|
"Check that the following locations contain schema files: " + System.lineSeparator());
|
||||||
|
for (InvalidSchemaLocationsException.SchemaLocation schemaLocation : cause.getSchemaLocations()) {
|
||||||
|
action.append(String.format("- '%s' (%s)" + System.lineSeparator(), schemaLocation.getUri(),
|
||||||
|
schemaLocation.getLocation()));
|
||||||
|
}
|
||||||
|
return new FailureAnalysis(message, action.toString(), cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2021 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.autoconfigure.graphql.data;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import graphql.GraphQL;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||||
|
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
|
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.graphql.GraphQlSourceBuilderCustomizer;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.data.repository.query.QueryByExampleExecutor;
|
||||||
|
import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor;
|
||||||
|
import org.springframework.graphql.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#autoRegistrationTypeVisitor(List, List)
|
||||||
|
*/
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@ConditionalOnClass({ GraphQL.class, QueryByExampleDataFetcher.class, QueryByExampleExecutor.class })
|
||||||
|
@ConditionalOnBean(GraphQlSource.class)
|
||||||
|
@AutoConfigureAfter(GraphQlAutoConfiguration.class)
|
||||||
|
public class GraphQlQueryByExampleAutoConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public GraphQlSourceBuilderCustomizer queryByExampleRegistrar(
|
||||||
|
ObjectProvider<QueryByExampleExecutor<?>> executorsProvider,
|
||||||
|
ObjectProvider<ReactiveQueryByExampleExecutor<?>> reactiveExecutorsProvider) {
|
||||||
|
|
||||||
|
return (builder) -> {
|
||||||
|
List<QueryByExampleExecutor<?>> executors = executorsProvider.stream().collect(Collectors.toList());
|
||||||
|
List<ReactiveQueryByExampleExecutor<?>> reactiveExecutors = reactiveExecutorsProvider.stream()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (!executors.isEmpty()) {
|
||||||
|
builder.typeVisitors(Collections.singletonList(
|
||||||
|
QueryByExampleDataFetcher.autoRegistrationTypeVisitor(executors, reactiveExecutors)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2021 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.autoconfigure.graphql.data;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import graphql.GraphQL;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||||
|
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
|
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.graphql.GraphQlSourceBuilderCustomizer;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
|
||||||
|
import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor;
|
||||||
|
import org.springframework.graphql.data.query.QuerydslDataFetcher;
|
||||||
|
import org.springframework.graphql.execution.GraphQlSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link EnableAutoConfiguration Auto-configuration} that creates a
|
||||||
|
* {@link GraphQlSourceBuilderCustomizer}s to detect Spring Data repositories with
|
||||||
|
* Querydsl support and register them as {@code DataFetcher}s for any queries with a
|
||||||
|
* matching return type.
|
||||||
|
*
|
||||||
|
* @author Rossen Stoyanchev
|
||||||
|
* @since 2.7.0
|
||||||
|
* @see QuerydslDataFetcher#autoRegistrationTypeVisitor(List, List)
|
||||||
|
*/
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@ConditionalOnClass({ GraphQL.class, QuerydslDataFetcher.class, QuerydslPredicateExecutor.class })
|
||||||
|
@ConditionalOnBean(GraphQlSource.class)
|
||||||
|
@AutoConfigureAfter(GraphQlAutoConfiguration.class)
|
||||||
|
public class GraphQlQuerydslAutoConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public GraphQlSourceBuilderCustomizer querydslRegistrar(
|
||||||
|
ObjectProvider<QuerydslPredicateExecutor<?>> executorsProvider,
|
||||||
|
ObjectProvider<ReactiveQuerydslPredicateExecutor<?>> reactiveExecutorsProvider) {
|
||||||
|
|
||||||
|
return (builder) -> {
|
||||||
|
List<QuerydslPredicateExecutor<?>> executors = executorsProvider.stream().collect(Collectors.toList());
|
||||||
|
List<ReactiveQuerydslPredicateExecutor<?>> reactiveExecutors = reactiveExecutorsProvider.stream()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (!executors.isEmpty()) {
|
||||||
|
builder.typeVisitors(Collections
|
||||||
|
.singletonList(QuerydslDataFetcher.autoRegistrationTypeVisitor(executors, reactiveExecutors)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2021 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.autoconfigure.graphql.data;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import graphql.GraphQL;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||||
|
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
|
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.graphql.GraphQlSourceBuilderCustomizer;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
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#autoRegistrationTypeVisitor(List, List)
|
||||||
|
*/
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@ConditionalOnClass({ GraphQL.class, QueryByExampleDataFetcher.class, ReactiveQueryByExampleExecutor.class })
|
||||||
|
@ConditionalOnBean(GraphQlSource.class)
|
||||||
|
@AutoConfigureAfter(GraphQlAutoConfiguration.class)
|
||||||
|
public class GraphQlReactiveQueryByExampleAutoConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public GraphQlSourceBuilderCustomizer reactiveQueryByExampleRegistrar(
|
||||||
|
ObjectProvider<ReactiveQueryByExampleExecutor<?>> executorsProvider) {
|
||||||
|
|
||||||
|
return (builder) -> {
|
||||||
|
List<ReactiveQueryByExampleExecutor<?>> executors = executorsProvider.stream().collect(Collectors.toList());
|
||||||
|
if (!executors.isEmpty()) {
|
||||||
|
builder.typeVisitors(Collections.singletonList(
|
||||||
|
QueryByExampleDataFetcher.autoRegistrationTypeVisitor(Collections.emptyList(), executors)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2021 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.autoconfigure.graphql.data;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import graphql.GraphQL;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||||
|
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
|
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.graphql.GraphQlSourceBuilderCustomizer;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
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
|
||||||
|
* @since 2.7.0
|
||||||
|
* @see QuerydslDataFetcher#autoRegistrationTypeVisitor(List, List)
|
||||||
|
*/
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@ConditionalOnClass({ GraphQL.class, QuerydslDataFetcher.class, ReactiveQuerydslPredicateExecutor.class })
|
||||||
|
@ConditionalOnBean(GraphQlSource.class)
|
||||||
|
@AutoConfigureAfter(GraphQlAutoConfiguration.class)
|
||||||
|
public class GraphQlReactiveQuerydslAutoConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public GraphQlSourceBuilderCustomizer reactiveQuerydslRegistrar(
|
||||||
|
ObjectProvider<ReactiveQuerydslPredicateExecutor<?>> executorsProvider) {
|
||||||
|
|
||||||
|
return (builder) -> {
|
||||||
|
List<ReactiveQuerydslPredicateExecutor<?>> executors = executorsProvider.stream()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (!executors.isEmpty()) {
|
||||||
|
builder.typeVisitors(Collections.singletonList(
|
||||||
|
QuerydslDataFetcher.autoRegistrationTypeVisitor(Collections.emptyList(), executors)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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,176 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2021 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.autoconfigure.graphql.reactive;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import graphql.GraphQL;
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||||
|
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.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.io.ResourceLoader;
|
||||||
|
import org.springframework.graphql.GraphQlService;
|
||||||
|
import org.springframework.graphql.execution.GraphQlSource;
|
||||||
|
import org.springframework.graphql.web.WebGraphQlHandler;
|
||||||
|
import org.springframework.graphql.web.WebInterceptor;
|
||||||
|
import org.springframework.graphql.web.webflux.GraphQlHttpHandler;
|
||||||
|
import org.springframework.graphql.web.webflux.GraphQlWebSocketHandler;
|
||||||
|
import org.springframework.graphql.web.webflux.GraphiQlHandler;
|
||||||
|
import org.springframework.graphql.web.webflux.SchemaHandler;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.codec.ServerCodecConfigurer;
|
||||||
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
|
import org.springframework.web.reactive.HandlerMapping;
|
||||||
|
import org.springframework.web.reactive.config.CorsRegistry;
|
||||||
|
import org.springframework.web.reactive.config.WebFluxConfigurer;
|
||||||
|
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||||
|
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||||
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
|
import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
|
||||||
|
import org.springframework.web.reactive.socket.server.support.WebSocketUpgradeHandlerPredicate;
|
||||||
|
|
||||||
|
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
|
||||||
|
import static org.springframework.web.reactive.function.server.RequestPredicates.contentType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link EnableAutoConfiguration Auto-configuration} for enabling Spring GraphQL over
|
||||||
|
* WebFlux.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
* @since 2.7.0
|
||||||
|
*/
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
|
||||||
|
@ConditionalOnClass({ GraphQL.class, GraphQlHttpHandler.class })
|
||||||
|
@ConditionalOnBean(GraphQlService.class)
|
||||||
|
@AutoConfigureAfter(GraphQlAutoConfiguration.class)
|
||||||
|
@EnableConfigurationProperties(GraphQlCorsProperties.class)
|
||||||
|
public class GraphQlWebFluxAutoConfiguration {
|
||||||
|
|
||||||
|
private static final Log logger = LogFactory.getLog(GraphQlWebFluxAutoConfiguration.class);
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
public GraphQlHttpHandler graphQlHttpHandler(WebGraphQlHandler webGraphQlHandler) {
|
||||||
|
return new GraphQlHttpHandler(webGraphQlHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
public WebGraphQlHandler webGraphQlHandler(GraphQlService service,
|
||||||
|
ObjectProvider<WebInterceptor> interceptorsProvider) {
|
||||||
|
return WebGraphQlHandler.builder(service)
|
||||||
|
.interceptors(interceptorsProvider.orderedStream().collect(Collectors.toList())).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public RouterFunction<ServerResponse> graphQlEndpoint(GraphQlHttpHandler handler, GraphQlSource graphQlSource,
|
||||||
|
GraphQlProperties properties, ResourceLoader resourceLoader) {
|
||||||
|
|
||||||
|
String graphQLPath = properties.getPath();
|
||||||
|
if (logger.isInfoEnabled()) {
|
||||||
|
logger.info("GraphQL endpoint HTTP POST " + graphQLPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
RouterFunctions.Builder builder = RouterFunctions.route()
|
||||||
|
.GET(graphQLPath,
|
||||||
|
(request) -> ServerResponse.status(HttpStatus.METHOD_NOT_ALLOWED)
|
||||||
|
.headers((headers) -> headers.setAllow(Collections.singleton(HttpMethod.POST))).build())
|
||||||
|
.POST(graphQLPath, accept(MediaType.APPLICATION_JSON).and(contentType(MediaType.APPLICATION_JSON)),
|
||||||
|
handler::handleRequest);
|
||||||
|
|
||||||
|
if (properties.getGraphiql().isEnabled()) {
|
||||||
|
GraphiQlHandler graphiQlHandler = new GraphiQlHandler(graphQLPath);
|
||||||
|
builder = builder.GET(properties.getGraphiql().getPath(), graphiQlHandler::handleRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (properties.getSchema().getPrinter().isEnabled()) {
|
||||||
|
SchemaHandler schemaHandler = new SchemaHandler(graphQlSource);
|
||||||
|
builder = builder.GET(graphQLPath + "/schema", schemaHandler::handleRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
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();
|
||||||
|
if (logger.isInfoEnabled()) {
|
||||||
|
logger.info("GraphQL endpoint WebSocket " + path);
|
||||||
|
}
|
||||||
|
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
|
||||||
|
mapping.setHandlerPredicate(new WebSocketUpgradeHandlerPredicate());
|
||||||
|
mapping.setUrlMap(Collections.singletonMap(path, graphQlWebSocketHandler));
|
||||||
|
mapping.setOrder(-2); // Ahead of HTTP endpoint ("routerFunctionMapping" bean)
|
||||||
|
return mapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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,54 @@
|
|||||||
|
/*
|
||||||
|
* 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.autoconfigure.graphql.security;
|
||||||
|
|
||||||
|
import graphql.GraphQL;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||||
|
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.graphql.reactive.GraphQlWebFluxAutoConfiguration;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.graphql.security.ReactiveSecurityDataFetcherExceptionResolver;
|
||||||
|
import org.springframework.graphql.web.webflux.GraphQlHttpHandler;
|
||||||
|
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link EnableAutoConfiguration Auto-configuration} for enabling Security support for
|
||||||
|
* Spring GraphQL with WebFlux.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
* @since 2.7.0
|
||||||
|
*/
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
|
||||||
|
@ConditionalOnClass({ GraphQL.class, GraphQlHttpHandler.class, EnableWebFluxSecurity.class })
|
||||||
|
@ConditionalOnBean(GraphQlHttpHandler.class)
|
||||||
|
@AutoConfigureAfter(GraphQlWebFluxAutoConfiguration.class)
|
||||||
|
public class GraphQlWebFluxSecurityAutoConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
public ReactiveSecurityDataFetcherExceptionResolver reactiveSecurityDataFetcherExceptionResolver() {
|
||||||
|
return new ReactiveSecurityDataFetcherExceptionResolver();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* 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.autoconfigure.graphql.security;
|
||||||
|
|
||||||
|
import graphql.GraphQL;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||||
|
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.graphql.servlet.GraphQlWebMvcAutoConfiguration;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.graphql.security.SecurityContextThreadLocalAccessor;
|
||||||
|
import org.springframework.graphql.security.SecurityDataFetcherExceptionResolver;
|
||||||
|
import org.springframework.graphql.web.webmvc.GraphQlHttpHandler;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link EnableAutoConfiguration Auto-configuration} for enabling Security support for
|
||||||
|
* Spring GraphQL with MVC.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
* @since 2.7.0
|
||||||
|
*/
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
|
||||||
|
@ConditionalOnClass({ GraphQL.class, GraphQlHttpHandler.class, EnableWebSecurity.class })
|
||||||
|
@ConditionalOnBean(GraphQlHttpHandler.class)
|
||||||
|
@AutoConfigureAfter(GraphQlWebMvcAutoConfiguration.class)
|
||||||
|
public class GraphQlWebMvcSecurityAutoConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
public SecurityDataFetcherExceptionResolver securityDataFetcherExceptionResolver() {
|
||||||
|
return new SecurityDataFetcherExceptionResolver();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
public SecurityContextThreadLocalAccessor securityContextThreadLocalAccessor() {
|
||||||
|
return new SecurityContextThreadLocalAccessor();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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,193 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2021 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.autoconfigure.graphql.servlet;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import javax.websocket.server.ServerContainer;
|
||||||
|
|
||||||
|
import graphql.GraphQL;
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||||
|
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.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.io.ResourceLoader;
|
||||||
|
import org.springframework.graphql.GraphQlService;
|
||||||
|
import org.springframework.graphql.execution.GraphQlSource;
|
||||||
|
import org.springframework.graphql.execution.ThreadLocalAccessor;
|
||||||
|
import org.springframework.graphql.web.WebGraphQlHandler;
|
||||||
|
import org.springframework.graphql.web.WebInterceptor;
|
||||||
|
import org.springframework.graphql.web.webmvc.GraphQlHttpHandler;
|
||||||
|
import org.springframework.graphql.web.webmvc.GraphQlWebSocketHandler;
|
||||||
|
import org.springframework.graphql.web.webmvc.GraphiQlHandler;
|
||||||
|
import org.springframework.graphql.web.webmvc.SchemaHandler;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.converter.GenericHttpMessageConverter;
|
||||||
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
|
import org.springframework.web.servlet.HandlerMapping;
|
||||||
|
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
import org.springframework.web.servlet.function.RequestPredicates;
|
||||||
|
import org.springframework.web.servlet.function.RouterFunction;
|
||||||
|
import org.springframework.web.servlet.function.RouterFunctions;
|
||||||
|
import org.springframework.web.servlet.function.ServerResponse;
|
||||||
|
import org.springframework.web.socket.WebSocketHandler;
|
||||||
|
import org.springframework.web.socket.server.support.DefaultHandshakeHandler;
|
||||||
|
import org.springframework.web.socket.server.support.WebSocketHandlerMapping;
|
||||||
|
import org.springframework.web.socket.server.support.WebSocketHttpRequestHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link EnableAutoConfiguration Auto-configuration} for enabling Spring GraphQL over
|
||||||
|
* Spring MVC.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
* @since 2.7.0
|
||||||
|
*/
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
|
||||||
|
@ConditionalOnClass({ GraphQL.class, GraphQlHttpHandler.class })
|
||||||
|
@ConditionalOnBean(GraphQlService.class)
|
||||||
|
@AutoConfigureAfter(GraphQlAutoConfiguration.class)
|
||||||
|
@EnableConfigurationProperties(GraphQlCorsProperties.class)
|
||||||
|
public class GraphQlWebMvcAutoConfiguration {
|
||||||
|
|
||||||
|
private static final Log logger = LogFactory.getLog(GraphQlWebMvcAutoConfiguration.class);
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
public GraphQlHttpHandler graphQlHttpHandler(WebGraphQlHandler webGraphQlHandler) {
|
||||||
|
return new GraphQlHttpHandler(webGraphQlHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
public WebGraphQlHandler webGraphQlHandler(GraphQlService service,
|
||||||
|
ObjectProvider<WebInterceptor> interceptorsProvider,
|
||||||
|
ObjectProvider<ThreadLocalAccessor> accessorsProvider) {
|
||||||
|
return WebGraphQlHandler.builder(service)
|
||||||
|
.interceptors(interceptorsProvider.orderedStream().collect(Collectors.toList()))
|
||||||
|
.threadLocalAccessors(accessorsProvider.orderedStream().collect(Collectors.toList())).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public RouterFunction<ServerResponse> graphQlRouterFunction(GraphQlHttpHandler handler, GraphQlSource graphQlSource,
|
||||||
|
GraphQlProperties properties, ResourceLoader resourceLoader) {
|
||||||
|
|
||||||
|
String graphQLPath = properties.getPath();
|
||||||
|
if (logger.isInfoEnabled()) {
|
||||||
|
logger.info("GraphQL endpoint HTTP POST " + graphQLPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
RouterFunctions.Builder builder = RouterFunctions.route()
|
||||||
|
.GET(graphQLPath,
|
||||||
|
(request) -> ServerResponse.status(HttpStatus.METHOD_NOT_ALLOWED)
|
||||||
|
.headers((headers) -> headers.setAllow(Collections.singleton(HttpMethod.POST))).build())
|
||||||
|
.POST(graphQLPath, RequestPredicates.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.and(RequestPredicates.accept(MediaType.APPLICATION_JSON)), handler::handleRequest);
|
||||||
|
|
||||||
|
if (properties.getGraphiql().isEnabled()) {
|
||||||
|
GraphiQlHandler graphiQLHandler = new GraphiQlHandler(graphQLPath);
|
||||||
|
builder = builder.GET(properties.getGraphiql().getPath(), graphiQLHandler::handleRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (properties.getSchema().getPrinter().isEnabled()) {
|
||||||
|
SchemaHandler schemaHandler = new SchemaHandler(graphQlSource);
|
||||||
|
builder = builder.GET(graphQLPath + "/schema", schemaHandler::handleRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@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());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static GenericHttpMessageConverter<Object> getJsonConverter(HttpMessageConverters converters) {
|
||||||
|
return converters.getConverters().stream()
|
||||||
|
.filter((candidate) -> candidate.canRead(Map.class, MediaType.APPLICATION_JSON)).findFirst()
|
||||||
|
.map((converter) -> (GenericHttpMessageConverter<Object>) converter)
|
||||||
|
.orElseThrow(() -> new IllegalStateException("No JSON converter"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public HandlerMapping graphQlWebSocketMapping(GraphQlWebSocketHandler handler, GraphQlProperties properties) {
|
||||||
|
String path = properties.getWebsocket().getPath();
|
||||||
|
if (logger.isInfoEnabled()) {
|
||||||
|
logger.info("GraphQL endpoint WebSocket " + path);
|
||||||
|
}
|
||||||
|
WebSocketHandlerMapping mapping = new WebSocketHandlerMapping();
|
||||||
|
mapping.setWebSocketUpgradeMatch(true);
|
||||||
|
mapping.setUrlMap(Collections.singletonMap(path,
|
||||||
|
new WebSocketHttpRequestHandler(handler, new DefaultHandshakeHandler())));
|
||||||
|
mapping.setOrder(2); // Ahead of HTTP endpoint ("routerFunctionMapping" bean)
|
||||||
|
return mapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
@ -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,230 @@
|
|||||||
|
/*
|
||||||
|
* 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 graphql.GraphQL;
|
||||||
|
import graphql.execution.instrumentation.ChainedInstrumentation;
|
||||||
|
import graphql.execution.instrumentation.Instrumentation;
|
||||||
|
import graphql.schema.GraphQLSchema;
|
||||||
|
import graphql.schema.idl.RuntimeWiring;
|
||||||
|
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.ClassPathResource;
|
||||||
|
import org.springframework.graphql.GraphQlService;
|
||||||
|
import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer;
|
||||||
|
import org.springframework.graphql.execution.BatchLoaderRegistry;
|
||||||
|
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
|
||||||
|
import org.springframework.graphql.execution.GraphQlSource;
|
||||||
|
import org.springframework.graphql.execution.MissingSchemaException;
|
||||||
|
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(GraphQlService.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 shouldFailWhenSchemaFileIsMissing() {
|
||||||
|
this.contextRunner.withPropertyValues("spring.graphql.schema.locations:classpath:missing/").run((context) -> {
|
||||||
|
assertThat(context).hasFailed();
|
||||||
|
assertThat(context).getFailure().getRootCause().isInstanceOf(MissingSchemaException.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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
static class CustomGraphQlBuilderConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
GraphQlSource.Builder customGraphQlSourceBuilder() {
|
||||||
|
return GraphQlSource.builder().schemaResources(new ClassPathResource("graphql/schema.graphqls"),
|
||||||
|
new ClassPathResource("graphql/types/book.graphqls"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
static class CustomGraphQlSourceConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
GraphQlSource customGraphQlSource() {
|
||||||
|
return mock(GraphQlSource.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@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.Builder 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 getBookByIdDataFetcher() {
|
||||||
|
return (environment) -> getBookById(environment.getArgument("id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DataFetcher getBooksOnSaleDataFetcher() {
|
||||||
|
return (environment) -> getBooksOnSale(environment.getArgument("minPages"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static Book getBookById(String id) {
|
||||||
|
return books.stream().filter((book) -> book.getId().equals(id)).findFirst().orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Flux<Book> getBooksOnSale(int minPages) {
|
||||||
|
return Flux.fromIterable(books).filter((book) -> book.getPageCount() >= minPages);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* 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.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.boot.diagnostics.FailureAnalysis;
|
||||||
|
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||||
|
import org.springframework.core.io.support.ResourcePatternResolver;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link InvalidSchemaLocationsExceptionFailureAnalyzer}
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
*/
|
||||||
|
class InvalidSchemaLocationsExceptionFailureAnalyzerTests {
|
||||||
|
|
||||||
|
private final InvalidSchemaLocationsExceptionFailureAnalyzer analyzer = new InvalidSchemaLocationsExceptionFailureAnalyzer();
|
||||||
|
|
||||||
|
private final ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
|
||||||
|
|
||||||
|
private final String[] missingLocation = new String[] {
|
||||||
|
"classpath:org/springframework/boot/autoconfigure/graphql/missing/" };
|
||||||
|
|
||||||
|
private final String[] existingLocation = new String[] {
|
||||||
|
"classpath:org/springframework/boot/autoconfigure/graphql/" };
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReportCause() {
|
||||||
|
InvalidSchemaLocationsException exception = new InvalidSchemaLocationsException(this.existingLocation,
|
||||||
|
this.resolver);
|
||||||
|
FailureAnalysis analysis = this.analyzer.analyze(exception);
|
||||||
|
assertThat(analysis.getCause()).isInstanceOf(InvalidSchemaLocationsException.class);
|
||||||
|
assertThat(analysis.getAction()).contains("Check that the following locations contain schema files:");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldListUnresolvableLocation() {
|
||||||
|
InvalidSchemaLocationsException exception = new InvalidSchemaLocationsException(this.missingLocation,
|
||||||
|
this.resolver);
|
||||||
|
FailureAnalysis analysis = this.analyzer.analyze(exception);
|
||||||
|
assertThat(analysis.getAction()).contains(this.existingLocation[0]).doesNotContain("file:");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldListExistingLocation() {
|
||||||
|
InvalidSchemaLocationsException exception = new InvalidSchemaLocationsException(this.existingLocation,
|
||||||
|
this.resolver);
|
||||||
|
FailureAnalysis analysis = this.analyzer.analyze(exception);
|
||||||
|
assertThat(analysis.getAction()).contains(this.existingLocation[0]).contains("file:");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
* 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.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link InvalidSchemaLocationsException}.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
*/
|
||||||
|
class InvalidSchemaLocationsExceptionTests {
|
||||||
|
|
||||||
|
private final String schemaFolder = "graphql/";
|
||||||
|
|
||||||
|
private final String[] locations = new String[] { "classpath:" + this.schemaFolder };
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectEmptyLocations() {
|
||||||
|
assertThatIllegalArgumentException().isThrownBy(
|
||||||
|
() -> new InvalidSchemaLocationsException(new String[] {}, new PathMatchingResourcePatternResolver()))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class).withMessage("locations should not be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectNullResolver() {
|
||||||
|
assertThatIllegalArgumentException().isThrownBy(() -> new InvalidSchemaLocationsException(this.locations, null))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class).withMessage("resolver should not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldExposeConfiguredLocations() {
|
||||||
|
InvalidSchemaLocationsException exception = new InvalidSchemaLocationsException(this.locations,
|
||||||
|
new PathMatchingResourcePatternResolver());
|
||||||
|
assertThat(exception.getSchemaLocations()).hasSize(1);
|
||||||
|
InvalidSchemaLocationsException.SchemaLocation schemaLocation = exception.getSchemaLocations().get(0);
|
||||||
|
assertThat(schemaLocation.getLocation()).isEqualTo(this.locations[0]);
|
||||||
|
assertThat(schemaLocation.getUri()).endsWith(this.schemaFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotFailWithUnresolvableLocations() {
|
||||||
|
String unresolved = "classpath:unresolved/";
|
||||||
|
InvalidSchemaLocationsException exception = new InvalidSchemaLocationsException(new String[] { unresolved },
|
||||||
|
new PathMatchingResourcePatternResolver());
|
||||||
|
assertThat(exception.getSchemaLocations()).hasSize(1);
|
||||||
|
InvalidSchemaLocationsException.SchemaLocation schemaLocation = exception.getSchemaLocations().get(0);
|
||||||
|
assertThat(schemaLocation.getLocation()).isEqualTo(unresolved);
|
||||||
|
assertThat(schemaLocation.getUri()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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-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.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.GraphQlService;
|
||||||
|
import org.springframework.graphql.data.GraphQlRepository;
|
||||||
|
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 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) -> {
|
||||||
|
GraphQlService graphQlService = context.getBean(GraphQlService.class);
|
||||||
|
GraphQlTester graphQlTester = GraphQlTester.create(graphQlService);
|
||||||
|
graphQlTester.query("{ 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,81 @@
|
|||||||
|
/*
|
||||||
|
* 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.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.GraphQlService;
|
||||||
|
import org.springframework.graphql.data.GraphQlRepository;
|
||||||
|
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) -> {
|
||||||
|
GraphQlService graphQlService = context.getBean(GraphQlService.class);
|
||||||
|
GraphQlTester graphQlTester = GraphQlTester.create(graphQlService);
|
||||||
|
graphQlTester.query("{ 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,80 @@
|
|||||||
|
/*
|
||||||
|
* 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.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.GraphQlService;
|
||||||
|
import org.springframework.graphql.data.GraphQlRepository;
|
||||||
|
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) -> {
|
||||||
|
GraphQlService graphQlService = context.getBean(GraphQlService.class);
|
||||||
|
GraphQlTester graphQlTester = GraphQlTester.create(graphQlService);
|
||||||
|
graphQlTester.query("{ 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,80 @@
|
|||||||
|
/*
|
||||||
|
* 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.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.GraphQlService;
|
||||||
|
import org.springframework.graphql.data.GraphQlRepository;
|
||||||
|
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) -> {
|
||||||
|
GraphQlService graphQlService = context.getBean(GraphQlService.class);
|
||||||
|
GraphQlTester graphQlTester = GraphQlTester.create(graphQlService);
|
||||||
|
graphQlTester.query("{ 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,182 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2021 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.autoconfigure.graphql.reactive;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import graphql.schema.idl.TypeRuntimeWiring;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||||
|
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.graphql.GraphQlTestDataFetchers;
|
||||||
|
import org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
|
||||||
|
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
|
||||||
|
import org.springframework.graphql.web.WebGraphQlHandler;
|
||||||
|
import org.springframework.graphql.web.WebInterceptor;
|
||||||
|
import org.springframework.graphql.web.webflux.GraphQlHttpHandler;
|
||||||
|
import org.springframework.graphql.web.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 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()
|
||||||
|
.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));
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
WebInterceptor customWebInterceptor() {
|
||||||
|
return (webInput, interceptorChain) -> interceptorChain.next(webInput)
|
||||||
|
.map((output) -> output.transform((builder) -> builder.responseHeader("X-Custom-Header", "42")));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,163 @@
|
|||||||
|
/*
|
||||||
|
* 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.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.RuntimeWiringConfigurer;
|
||||||
|
import org.springframework.graphql.security.ReactiveSecurityDataFetcherExceptionResolver;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
|
||||||
|
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||||
|
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
|
||||||
|
import org.springframework.security.core.userdetails.User;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||||
|
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.springframework.security.config.Customizer.withDefaults;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link GraphQlWebFluxSecurityAutoConfiguration}.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
*/
|
||||||
|
class GraphQlWebFluxSecurityAutoConfigurationTests {
|
||||||
|
|
||||||
|
private static final String BASE_URL = "https://spring.example.org/graphql";
|
||||||
|
|
||||||
|
private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
|
||||||
|
.withConfiguration(AutoConfigurations.of(HttpHandlerAutoConfiguration.class, WebFluxAutoConfiguration.class,
|
||||||
|
CodecsAutoConfiguration.class, JacksonAutoConfiguration.class, GraphQlAutoConfiguration.class,
|
||||||
|
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) throws Exception {
|
||||||
|
return http.csrf((spec) -> spec.disable())
|
||||||
|
// Demonstrate that method security works
|
||||||
|
// Best practice to use both for defense in depth
|
||||||
|
.authorizeExchange((requests) -> requests.anyExchange().permitAll()).httpBasic(withDefaults())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
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,179 @@
|
|||||||
|
/*
|
||||||
|
* 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.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.security.SecurityContextThreadLocalAccessor;
|
||||||
|
import org.springframework.graphql.security.SecurityDataFetcherExceptionResolver;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.core.userdetails.User;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||||
|
import org.springframework.security.web.DefaultSecurityFilterChain;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
import org.springframework.test.web.servlet.MvcResult;
|
||||||
|
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.springframework.security.config.Customizer.withDefaults;
|
||||||
|
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
|
||||||
|
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link GraphQlWebMvcSecurityAutoConfiguration}.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
*/
|
||||||
|
class GraphQlWebMvcSecurityAutoConfigurationTests {
|
||||||
|
|
||||||
|
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
|
||||||
|
.withConfiguration(
|
||||||
|
AutoConfigurations.of(DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class,
|
||||||
|
HttpMessageConvertersAutoConfiguration.class, JacksonAutoConfiguration.class,
|
||||||
|
GraphQlAutoConfiguration.class, 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)
|
||||||
|
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
|
||||||
|
static InMemoryUserDetailsManager userDetailsService() {
|
||||||
|
User.UserBuilder userBuilder = User.withDefaultPasswordEncoder();
|
||||||
|
UserDetails rob = userBuilder.username("rob").password("rob").roles("USER").build();
|
||||||
|
UserDetails admin = userBuilder.username("admin").password("admin").roles("USER", "ADMIN").build();
|
||||||
|
return new InMemoryUserDetailsManager(rob, admin);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,193 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2021 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.autoconfigure.graphql.servlet;
|
||||||
|
|
||||||
|
import graphql.schema.idl.TypeRuntimeWiring;
|
||||||
|
import org.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.graphql.execution.RuntimeWiringConfigurer;
|
||||||
|
import org.springframework.graphql.web.WebGraphQlHandler;
|
||||||
|
import org.springframework.graphql.web.WebInterceptor;
|
||||||
|
import org.springframework.graphql.web.webmvc.GraphQlHttpHandler;
|
||||||
|
import org.springframework.graphql.web.webmvc.GraphQlWebSocketHandler;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
import org.springframework.test.web.servlet.MvcResult;
|
||||||
|
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||||
|
|
||||||
|
import static org.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_JSON))
|
||||||
|
.andExpect(jsonPath("data.bookById.name").value("GraphQL for beginners"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void httpGetQueryShouldBeSupported() {
|
||||||
|
testWith((mockMvc) -> {
|
||||||
|
String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }";
|
||||||
|
mockMvc.perform(get("/graphql?query={query}", "{\"query\": \"" + query + "\"}"))
|
||||||
|
.andExpect(status().isMethodNotAllowed()).andExpect(header().string("Allow", "POST"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectMissingQuery() {
|
||||||
|
testWith((mockMvc) -> mockMvc.perform(post("/graphql").content("{}")).andExpect(status().isBadRequest()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectQueryWithInvalidJson() {
|
||||||
|
testWith((mockMvc) -> mockMvc.perform(post("/graphql").content(":)")).andExpect(status().isBadRequest()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldConfigureWebInterceptors() {
|
||||||
|
testWith((mockMvc) -> {
|
||||||
|
String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }";
|
||||||
|
MvcResult result = mockMvc.perform(post("/graphql").content("{\"query\": \"" + query + "\"}")).andReturn();
|
||||||
|
mockMvc.perform(asyncDispatch(result)).andExpect(status().isOk())
|
||||||
|
.andExpect(header().string("X-Custom-Header", "42"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@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));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testWith(MockMvcConsumer mockMvcConsumer) {
|
||||||
|
this.contextRunner.run((context) -> {
|
||||||
|
MediaType mediaType = MediaType.APPLICATION_JSON;
|
||||||
|
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(context)
|
||||||
|
.defaultRequest(post("/graphql").contentType(mediaType).accept(mediaType)).build();
|
||||||
|
mockMvcConsumer.accept(mockMvc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface MockMvcConsumer {
|
||||||
|
|
||||||
|
void accept(MockMvc mockMvc) throws Exception;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
static class DataFetchersConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
RuntimeWiringConfigurer bookDataFetcher() {
|
||||||
|
return (builder) -> builder.type(TypeRuntimeWiring.newTypeWiring("Query").dataFetcher("bookById",
|
||||||
|
GraphQlTestDataFetchers.getBookByIdDataFetcher()));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
static class CustomWebInterceptor {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
WebInterceptor customWebInterceptor() {
|
||||||
|
return (webInput, interceptorChain) -> interceptorChain.next(webInput)
|
||||||
|
.map((output) -> output.transform((builder) -> builder.responseHeader("X-Custom-Header", "42")));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
type Person {
|
||||||
|
id: ID
|
||||||
|
name: String
|
||||||
|
}
|
@ -0,0 +1,135 @@
|
|||||||
|
[[web.graphql]]
|
||||||
|
== Spring GraphQL
|
||||||
|
If you want to build GraphQL applications, you can take advantage of Spring Boot's auto-configuration for {spring-graphql}[Spring GraphQL].
|
||||||
|
The Spring 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
|
||||||
|
|
||||||
|
|===
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[[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[]
|
||||||
|
----
|
||||||
|
|
||||||
|
|
||||||
|
[[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 register `@Controller` classes with annotated handler methods and registers those as `DataFetcher`s.
|
||||||
|
Here's a sample implementation for our greeting query with a `@Controller` class:
|
||||||
|
|
||||||
|
[source,java,indent=0,subs="verbatim"]
|
||||||
|
----
|
||||||
|
include::{docs-java}/web/graphql/GreetingController.java[]
|
||||||
|
----
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[[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.web-endpoints]]
|
||||||
|
=== Web Endpoints
|
||||||
|
|
||||||
|
The GraphQL HTTP endpoint is at HTTP POST "/graphql" by default. The path can be customized with configprop:spring.graphql.path[].
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[[web.graphql.cors]]
|
||||||
|
=== CORS
|
||||||
|
|
||||||
|
{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.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,44 @@
|
|||||||
|
/*
|
||||||
|
* 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.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.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.query("{ greeting(name: \"Alice\") } ").execute().path("greeting").entity(String.class)
|
||||||
|
.isEqualTo("Hello, Alice!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldGreetWithDefaultName() {
|
||||||
|
this.graphQlTester.query("{ greeting } ").execute().path("greeting").entity(String.class)
|
||||||
|
.isEqualTo("Hello, Spring!");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2021 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docs.web.graphql;
|
||||||
|
|
||||||
|
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,29 @@
|
|||||||
|
type Query {
|
||||||
|
greeting(name: String! = "Spring"): String!
|
||||||
|
project(slug: ID!): Project
|
||||||
|
}
|
||||||
|
|
||||||
|
""" A Project in the Spring portfolio """
|
||||||
|
type Project {
|
||||||
|
""" Unique string id used in URLs """
|
||||||
|
slug: ID!
|
||||||
|
""" Project name """
|
||||||
|
name: String!
|
||||||
|
""" URL of the git repository """
|
||||||
|
repositoryUrl: String!
|
||||||
|
""" Current support status """
|
||||||
|
status: ProjectStatus!
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ProjectStatus {
|
||||||
|
""" Actively supported by the Spring team """
|
||||||
|
ACTIVE
|
||||||
|
""" Supported by the community """
|
||||||
|
COMMUNITY
|
||||||
|
""" Prototype, not officially supported yet """
|
||||||
|
INCUBATING
|
||||||
|
""" Project being retired, in maintenance mode """
|
||||||
|
ATTIC
|
||||||
|
""" End-Of-Lifed """
|
||||||
|
EOL
|
||||||
|
}
|
@ -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")
|
||||||
|
}
|
@ -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,152 @@
|
|||||||
|
/*
|
||||||
|
* 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.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.AutoConfigureWebGraphQlTester;
|
||||||
|
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}
|
||||||
|
* </ul>
|
||||||
|
* <p>
|
||||||
|
* The annotation does not automatically load {@code @Component}, {@code @Service},
|
||||||
|
* {@code @Repository}, and other beans.
|
||||||
|
* <p>
|
||||||
|
* By default, tests annotated with {@code @GraphQlTest} have a
|
||||||
|
* {@link org.springframework.graphql.test.tester.GraphQlTester} configured. For more
|
||||||
|
* fine-grained control of the GraphQlTester, use
|
||||||
|
* {@link AutoConfigureGraphQlTester @AutoConfigureGraphQlTester}.
|
||||||
|
* <p>
|
||||||
|
* Typically {@code @GraphQlTest} is used in combination with
|
||||||
|
* {@link org.springframework.boot.test.mock.mockito.MockBean @MockBean} or
|
||||||
|
* {@link org.springframework.context.annotation.Import @Import} to load any collaborators
|
||||||
|
* and other components required for the tests.
|
||||||
|
* <p>
|
||||||
|
* To load your full application configuration instead and test via
|
||||||
|
* {@code WebGraphQlTester}, consider using
|
||||||
|
* {@link org.springframework.boot.test.context.SpringBootTest @SpringBootTest} combined
|
||||||
|
* with {@link AutoConfigureWebGraphQlTester @AutoConfigureWebGraphQlTester}.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
* @since 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}, and {@code GenericConverter} beans are included.
|
||||||
|
* @see #includeFilters()
|
||||||
|
* @see #excludeFilters()
|
||||||
|
* @return if default filters should be used
|
||||||
|
*/
|
||||||
|
boolean useDefaultFilters() default true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A set of include filters which can be used to add otherwise filtered beans to the
|
||||||
|
* application context.
|
||||||
|
* @return include filters to apply
|
||||||
|
*/
|
||||||
|
ComponentScan.Filter[] includeFilters() default {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A set of exclude filters which can be used to filter beans that would otherwise be
|
||||||
|
* added to the application context.
|
||||||
|
* @return exclude filters to apply
|
||||||
|
*/
|
||||||
|
ComponentScan.Filter[] excludeFilters() default {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-configuration exclusions that should be applied for this test.
|
||||||
|
* @return auto-configuration exclusions to apply
|
||||||
|
*/
|
||||||
|
@AliasFor(annotation = ImportAutoConfiguration.class, attribute = "exclude")
|
||||||
|
Class<?>[] excludeAutoConfiguration() default {};
|
||||||
|
|
||||||
|
}
|
@ -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,93 @@
|
|||||||
|
/*
|
||||||
|
* 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.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.TypeExcludeFilter;
|
||||||
|
import org.springframework.boot.jackson.JsonComponent;
|
||||||
|
import org.springframework.boot.test.autoconfigure.filter.StandardAnnotationCustomizableTypeExcludeFilter;
|
||||||
|
import org.springframework.core.convert.converter.Converter;
|
||||||
|
import org.springframework.core.convert.converter.GenericConverter;
|
||||||
|
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.util.ClassUtils;
|
||||||
|
import org.springframework.util.ObjectUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link TypeExcludeFilter} for {@link GraphQlTest @GraphQlTest}.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
* @since 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);
|
||||||
|
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-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.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
|
||||||
|
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||||
|
import org.springframework.graphql.test.tester.WebGraphQlTester;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annotation that can be applied to a test class to enable a {@link WebGraphQlTester}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This annotation should be used with
|
||||||
|
* {@link org.springframework.boot.test.context.SpringBootTest @SpringBootTest} tests with
|
||||||
|
* Spring MVC or Spring WebFlux mock infrastructures.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
* @since 2.7.0
|
||||||
|
* @see WebGraphQlTesterAutoConfiguration
|
||||||
|
*/
|
||||||
|
@Target({ ElementType.TYPE, ElementType.METHOD })
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Documented
|
||||||
|
@Inherited
|
||||||
|
@AutoConfigureMockMvc
|
||||||
|
@AutoConfigureWebTestClient
|
||||||
|
@ImportAutoConfiguration
|
||||||
|
public @interface AutoConfigureWebGraphQlTester {
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* 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 graphql.GraphQL;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
|
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.graphql.GraphQlService;
|
||||||
|
import org.springframework.graphql.test.tester.GraphQlTester;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-configuration for {@link GraphQlTester}.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
* @since 2.7.0
|
||||||
|
*/
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@ConditionalOnClass({ GraphQL.class, GraphQlTester.class })
|
||||||
|
@AutoConfigureAfter(GraphQlAutoConfiguration.class)
|
||||||
|
public class GraphQlTesterAutoConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnBean(GraphQlService.class)
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
public GraphQlTester graphQlTester(GraphQlService graphQlService) {
|
||||||
|
return GraphQlTester.create(graphQlService);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* 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 org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
|
import org.springframework.boot.autoconfigure.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.context.annotation.Configuration;
|
||||||
|
import org.springframework.graphql.test.tester.WebGraphQlTester;
|
||||||
|
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||||
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-configuration for {@link WebGraphQlTester}.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
* @since 2.7.0
|
||||||
|
*/
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@ConditionalOnClass({ WebClient.class, WebTestClient.class, WebGraphQlTester.class })
|
||||||
|
@AutoConfigureAfter({ WebTestClientAutoConfiguration.class, MockMvcAutoConfiguration.class })
|
||||||
|
public class WebGraphQlTesterAutoConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnBean(WebTestClient.class)
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
public WebGraphQlTester webTestClientGraphQlTester(WebTestClient webTestClient, GraphQlProperties properties) {
|
||||||
|
WebTestClient mutatedWebTestClient = webTestClient.mutate().baseUrl(properties.getPath()).build();
|
||||||
|
return WebGraphQlTester.create(mutatedWebTestClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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,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-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.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.query(query).execute().path("data.bookById.id").entity(String.class).isEqualTo("42");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,184 @@
|
|||||||
|
/*
|
||||||
|
* 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.test.autoconfigure.graphql;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||||
|
import graphql.schema.idl.RuntimeWiring;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.ComponentScan;
|
||||||
|
import org.springframework.context.annotation.FilterType;
|
||||||
|
import org.springframework.core.type.classreading.MetadataReader;
|
||||||
|
import org.springframework.core.type.classreading.MetadataReaderFactory;
|
||||||
|
import org.springframework.core.type.classreading.SimpleMetadataReaderFactory;
|
||||||
|
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
|
||||||
|
import org.springframework.graphql.web.WebInput;
|
||||||
|
import org.springframework.graphql.web.WebInterceptor;
|
||||||
|
import org.springframework.graphql.web.WebInterceptorChain;
|
||||||
|
import org.springframework.graphql.web.WebOutput;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link GraphQlTypeExcludeFilter}
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
*/
|
||||||
|
class GraphQlTypeExcludeFilterTests {
|
||||||
|
|
||||||
|
private MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matchWhenHasNoControllers() throws Exception {
|
||||||
|
GraphQlTypeExcludeFilter filter = new GraphQlTypeExcludeFilter(WithNoControllers.class);
|
||||||
|
assertThat(excludes(filter, Controller1.class)).isFalse();
|
||||||
|
assertThat(excludes(filter, Controller2.class)).isFalse();
|
||||||
|
assertThat(excludes(filter, ExampleRuntimeWiringConfigurer.class)).isFalse();
|
||||||
|
assertThat(excludes(filter, ExampleService.class)).isTrue();
|
||||||
|
assertThat(excludes(filter, ExampleRepository.class)).isTrue();
|
||||||
|
assertThat(excludes(filter, ExampleWebInterceptor.class)).isTrue();
|
||||||
|
assertThat(excludes(filter, ExampleModule.class)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matchWhenHasController() throws Exception {
|
||||||
|
GraphQlTypeExcludeFilter filter = new GraphQlTypeExcludeFilter(WithController.class);
|
||||||
|
assertThat(excludes(filter, Controller1.class)).isFalse();
|
||||||
|
assertThat(excludes(filter, Controller2.class)).isTrue();
|
||||||
|
assertThat(excludes(filter, ExampleRuntimeWiringConfigurer.class)).isFalse();
|
||||||
|
assertThat(excludes(filter, ExampleService.class)).isTrue();
|
||||||
|
assertThat(excludes(filter, ExampleRepository.class)).isTrue();
|
||||||
|
assertThat(excludes(filter, ExampleWebInterceptor.class)).isTrue();
|
||||||
|
assertThat(excludes(filter, ExampleModule.class)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matchNotUsingDefaultFilters() throws Exception {
|
||||||
|
GraphQlTypeExcludeFilter filter = new GraphQlTypeExcludeFilter(NotUsingDefaultFilters.class);
|
||||||
|
assertThat(excludes(filter, Controller1.class)).isTrue();
|
||||||
|
assertThat(excludes(filter, Controller2.class)).isTrue();
|
||||||
|
assertThat(excludes(filter, ExampleRuntimeWiringConfigurer.class)).isTrue();
|
||||||
|
assertThat(excludes(filter, ExampleService.class)).isTrue();
|
||||||
|
assertThat(excludes(filter, ExampleRepository.class)).isTrue();
|
||||||
|
assertThat(excludes(filter, ExampleWebInterceptor.class)).isTrue();
|
||||||
|
assertThat(excludes(filter, ExampleModule.class)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matchWithIncludeFilter() throws Exception {
|
||||||
|
GraphQlTypeExcludeFilter filter = new GraphQlTypeExcludeFilter(WithIncludeFilter.class);
|
||||||
|
assertThat(excludes(filter, Controller1.class)).isFalse();
|
||||||
|
assertThat(excludes(filter, Controller2.class)).isFalse();
|
||||||
|
assertThat(excludes(filter, ExampleRuntimeWiringConfigurer.class)).isFalse();
|
||||||
|
assertThat(excludes(filter, ExampleService.class)).isTrue();
|
||||||
|
assertThat(excludes(filter, ExampleRepository.class)).isFalse();
|
||||||
|
assertThat(excludes(filter, ExampleWebInterceptor.class)).isTrue();
|
||||||
|
assertThat(excludes(filter, ExampleModule.class)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matchWithExcludeFilter() throws Exception {
|
||||||
|
GraphQlTypeExcludeFilter filter = new GraphQlTypeExcludeFilter(WithExcludeFilter.class);
|
||||||
|
assertThat(excludes(filter, Controller1.class)).isTrue();
|
||||||
|
assertThat(excludes(filter, Controller2.class)).isFalse();
|
||||||
|
assertThat(excludes(filter, ExampleRuntimeWiringConfigurer.class)).isFalse();
|
||||||
|
assertThat(excludes(filter, ExampleService.class)).isTrue();
|
||||||
|
assertThat(excludes(filter, ExampleRepository.class)).isTrue();
|
||||||
|
assertThat(excludes(filter, ExampleWebInterceptor.class)).isTrue();
|
||||||
|
assertThat(excludes(filter, ExampleModule.class)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean excludes(GraphQlTypeExcludeFilter filter, Class<?> type) throws IOException {
|
||||||
|
MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(type.getName());
|
||||||
|
return filter.match(metadataReader, this.metadataReaderFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GraphQlTest
|
||||||
|
static class WithNoControllers {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@GraphQlTest(Controller1.class)
|
||||||
|
static class WithController {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@GraphQlTest(useDefaultFilters = false)
|
||||||
|
static class NotUsingDefaultFilters {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@GraphQlTest(includeFilters = @ComponentScan.Filter(Repository.class))
|
||||||
|
static class WithIncludeFilter {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@GraphQlTest(excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = Controller1.class))
|
||||||
|
static class WithExcludeFilter {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
static class Controller1 {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
static class Controller2 {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Service
|
||||||
|
static class ExampleService {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
static class ExampleRepository {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static class ExampleRuntimeWiringConfigurer implements RuntimeWiringConfigurer {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure(RuntimeWiring.Builder builder) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static class ExampleWebInterceptor implements WebInterceptor {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<WebOutput> intercept(WebInput webInput, WebInterceptorChain chain) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
static class ExampleModule extends SimpleModule {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* 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.test.autoconfigure.graphql.tester;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||||
|
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.graphql.GraphQlService;
|
||||||
|
import org.springframework.graphql.test.tester.GraphQlTester;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link GraphQlTesterAutoConfiguration}.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
*/
|
||||||
|
class GraphQlTesterAutoConfigurationTests {
|
||||||
|
|
||||||
|
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||||
|
.withConfiguration(AutoConfigurations.of(GraphQlTesterAutoConfiguration.class));
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void 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
|
||||||
|
GraphQlService graphQlService() {
|
||||||
|
return mock(GraphQlService.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
type Query {
|
||||||
|
bookById(id: ID): Book
|
||||||
|
}
|
||||||
|
|
||||||
|
type Book {
|
||||||
|
id: ID
|
||||||
|
name: String
|
||||||
|
pageCount: Int
|
||||||
|
author: String
|
||||||
|
}
|
@ -0,0 +1,218 @@
|
|||||||
|
/*
|
||||||
|
* 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.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.GraphQlTester;
|
||||||
|
import org.springframework.graphql.test.tester.WebGraphQlTester;
|
||||||
|
import org.springframework.test.context.ContextCustomizer;
|
||||||
|
import org.springframework.test.context.MergedContextConfiguration;
|
||||||
|
import org.springframework.test.context.TestContextAnnotationUtils;
|
||||||
|
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||||
|
import org.springframework.util.ClassUtils;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.context.WebApplicationContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link ContextCustomizer} for {@link GraphQlTester}.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
*/
|
||||||
|
class GraphQlTesterContextCustomizer implements ContextCustomizer {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
|
||||||
|
SpringBootTest springBootTest = TestContextAnnotationUtils.findMergedAnnotation(mergedConfig.getTestClass(),
|
||||||
|
SpringBootTest.class);
|
||||||
|
if (springBootTest.webEnvironment().isEmbedded()) {
|
||||||
|
registerGraphQlTester(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerGraphQlTester(ConfigurableApplicationContext context) {
|
||||||
|
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
|
||||||
|
if (beanFactory instanceof BeanDefinitionRegistry) {
|
||||||
|
registerGraphQlTester((BeanDefinitionRegistry) beanFactory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerGraphQlTester(BeanDefinitionRegistry registry) {
|
||||||
|
RootBeanDefinition definition = new RootBeanDefinition(GraphQlTesterRegistrar.class);
|
||||||
|
definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
|
||||||
|
registry.registerBeanDefinition(GraphQlTesterRegistrar.class.getName(), definition);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
return (obj != null) && (obj.getClass() == getClass());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return getClass().hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class GraphQlTesterRegistrar
|
||||||
|
implements BeanDefinitionRegistryPostProcessor, Ordered, BeanFactoryAware {
|
||||||
|
|
||||||
|
private BeanFactory beanFactory;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
|
||||||
|
this.beanFactory = beanFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
|
||||||
|
if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors((ListableBeanFactory) this.beanFactory,
|
||||||
|
GraphQlTester.class, false, false).length == 0) {
|
||||||
|
registry.registerBeanDefinition(WebGraphQlTester.class.getName(),
|
||||||
|
new RootBeanDefinition(GraphQlTesterFactory.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOrder() {
|
||||||
|
return Ordered.LOWEST_PRECEDENCE - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class GraphQlTesterFactory implements FactoryBean<GraphQlTester>, ApplicationContextAware {
|
||||||
|
|
||||||
|
private 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 GraphQlTester object;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||||
|
this.applicationContext = applicationContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSingleton() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<?> getObjectType() {
|
||||||
|
return GraphQlTester.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GraphQlTester getObject() throws Exception {
|
||||||
|
if (this.object == null) {
|
||||||
|
this.object = createGraphQlTester();
|
||||||
|
}
|
||||||
|
return this.object;
|
||||||
|
}
|
||||||
|
|
||||||
|
private WebGraphQlTester createGraphQlTester() {
|
||||||
|
WebTestClient webTestClient = this.applicationContext.getBean(WebTestClient.class);
|
||||||
|
boolean sslEnabled = isSslEnabled(this.applicationContext);
|
||||||
|
String port = this.applicationContext.getEnvironment().getProperty("local.server.port", "8080");
|
||||||
|
WebTestClient mutatedWebClient = webTestClient.mutate().baseUrl(getBaseUrl(sslEnabled, port)).build();
|
||||||
|
return WebGraphQlTester.create(mutatedWebClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getBaseUrl(boolean sslEnabled, String port) {
|
||||||
|
String basePath = deduceBasePath();
|
||||||
|
return (sslEnabled ? "https" : "http") + "://localhost:" + port + basePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String deduceBasePath() {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* 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.graphql.tester;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.graphql.test.tester.GraphQlTester;
|
||||||
|
import org.springframework.test.context.ContextConfigurationAttributes;
|
||||||
|
import org.springframework.test.context.ContextCustomizer;
|
||||||
|
import org.springframework.test.context.ContextCustomizerFactory;
|
||||||
|
import org.springframework.test.context.TestContextAnnotationUtils;
|
||||||
|
import org.springframework.util.ClassUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link ContextCustomizerFactory} for {@link GraphQlTester}.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
* @see GraphQlTesterContextCustomizer
|
||||||
|
*/
|
||||||
|
class GraphQlTesterContextCustomizerFactory implements ContextCustomizerFactory {
|
||||||
|
|
||||||
|
private static final String GRAPHQLTESTER_CLASS = "org.springframework.graphql.test.tester.GraphQlTester";
|
||||||
|
|
||||||
|
private static final String WEBTESTCLIENT_CLASS = "org.springframework.test.web.reactive.server.WebTestClient";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ContextCustomizer createContextCustomizer(Class<?> testClass,
|
||||||
|
List<ContextConfigurationAttributes> configAttributes) {
|
||||||
|
SpringBootTest springBootTest = TestContextAnnotationUtils.findMergedAnnotation(testClass,
|
||||||
|
SpringBootTest.class);
|
||||||
|
return (springBootTest != null && isGraphQlTesterPresent()) ? new GraphQlTesterContextCustomizer() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isGraphQlTesterPresent() {
|
||||||
|
return ClassUtils.isPresent(WEBTESTCLIENT_CLASS, getClass().getClassLoader())
|
||||||
|
&& ClassUtils.isPresent(GRAPHQLTESTER_CLASS, getClass().getClassLoader());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link org.springframework.graphql.test.tester.GraphQlTester} utilities.
|
||||||
|
*/
|
||||||
|
package org.springframework.boot.test.graphql.tester;
|
@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* 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.test.graphql.tester;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.boot.web.embedded.tomcat.TomcatReactiveWebServerFactory;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||||
|
import org.springframework.graphql.test.tester.GraphQlTester;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.server.reactive.ContextPathCompositeHandler;
|
||||||
|
import org.springframework.http.server.reactive.HttpHandler;
|
||||||
|
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||||
|
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||||
|
import org.springframework.test.annotation.DirtiesContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration test for {@link GraphQlTesterContextCustomizer}.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
*/
|
||||||
|
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
|
||||||
|
properties = "spring.main.web-application-type=reactive")
|
||||||
|
@DirtiesContext
|
||||||
|
class GraphQlTesterContextCustomizerIntegrationTests {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
GraphQlTester graphQlTester;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandleGraphQlRequests() {
|
||||||
|
this.graphQlTester.query("{}").executeAndVerify();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
static class TestConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
TomcatReactiveWebServerFactory webServerFactory() {
|
||||||
|
return new TomcatReactiveWebServerFactory(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
HttpHandler httpHandler() {
|
||||||
|
TestHandler httpHandler = new TestHandler();
|
||||||
|
Map<String, HttpHandler> handlersMap = Collections.singletonMap("/graphql", httpHandler);
|
||||||
|
return new ContextPathCompositeHandler(handlersMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static class TestHandler implements HttpHandler {
|
||||||
|
|
||||||
|
private static final DefaultDataBufferFactory factory = new DefaultDataBufferFactory();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
|
||||||
|
response.setStatusCode(HttpStatus.OK);
|
||||||
|
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
|
||||||
|
return response.writeWith(Mono.just(factory.wrap("{\"data\":{}}".getBytes())));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* 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.test.graphql.tester;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.boot.web.embedded.tomcat.TomcatReactiveWebServerFactory;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||||
|
import org.springframework.graphql.test.tester.GraphQlTester;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.server.reactive.ContextPathCompositeHandler;
|
||||||
|
import org.springframework.http.server.reactive.HttpHandler;
|
||||||
|
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||||
|
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||||
|
import org.springframework.test.context.TestPropertySource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link GraphQlTesterContextCustomizer} with a custom context path for a
|
||||||
|
* Reactive web application.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
*/
|
||||||
|
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||||
|
@TestPropertySource(properties = { "spring.main.web-application-type=reactive", "spring.webflux.base-path=/test" })
|
||||||
|
class GraphQlTesterContextCustomizerWithCustomBasePathTests {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
GraphQlTester graphQlTester;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandleGraphQlRequests() {
|
||||||
|
this.graphQlTester.query("{}").executeAndVerify();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
static class TestConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
TomcatReactiveWebServerFactory webServerFactory() {
|
||||||
|
return new TomcatReactiveWebServerFactory(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
HttpHandler httpHandler() {
|
||||||
|
TestHandler httpHandler = new TestHandler();
|
||||||
|
Map<String, HttpHandler> handlersMap = Collections.singletonMap("/test/graphql", httpHandler);
|
||||||
|
return new ContextPathCompositeHandler(handlersMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static class TestHandler implements HttpHandler {
|
||||||
|
|
||||||
|
private static final DefaultDataBufferFactory factory = new DefaultDataBufferFactory();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
|
||||||
|
response.setStatusCode(HttpStatus.OK);
|
||||||
|
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
|
||||||
|
return response.writeWith(Mono.just(factory.wrap("{\"data\":{}}".getBytes())));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* 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.test.graphql.tester;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Import;
|
||||||
|
import org.springframework.graphql.test.tester.GraphQlTester;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.context.TestPropertySource;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.servlet.DispatcherServlet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link GraphQlTesterContextCustomizer} with a custom context path for a
|
||||||
|
* Servlet web application.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
*/
|
||||||
|
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||||
|
@TestPropertySource(properties = "server.servlet.context-path=/test")
|
||||||
|
class GraphQlTesterContextCustomizerWithCustomContextPathTests {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
GraphQlTester graphQlTester;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandleGraphQlRequests() {
|
||||||
|
this.graphQlTester.query("{}").executeAndVerify();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@Import(TestController.class)
|
||||||
|
static class TestConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
TomcatServletWebServerFactory webServerFactory() {
|
||||||
|
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(0);
|
||||||
|
factory.setContextPath("/test");
|
||||||
|
return factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
DispatcherServlet dispatcherServlet() {
|
||||||
|
return new DispatcherServlet();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
static class TestController {
|
||||||
|
|
||||||
|
@PostMapping(path = "/graphql", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
String graphql() {
|
||||||
|
return "{}";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
plugins {
|
||||||
|
id "java"
|
||||||
|
id "org.springframework.boot.conventions"
|
||||||
|
}
|
||||||
|
|
||||||
|
description = "Spring Boot GraphQL smoke test"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-graphql"))
|
||||||
|
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-web"))
|
||||||
|
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-security"))
|
||||||
|
|
||||||
|
testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test"))
|
||||||
|
testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-webflux"))
|
||||||
|
testImplementation('org.springframework.graphql:spring-graphql-test')
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* 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 smoketest.graphql;
|
||||||
|
|
||||||
|
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 {
|
||||||
|
|
||||||
|
private final GreetingService greetingService;
|
||||||
|
|
||||||
|
public GreetingController(GreetingService greetingService) {
|
||||||
|
this.greetingService = greetingService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@QueryMapping
|
||||||
|
public String greeting(@Argument String name) {
|
||||||
|
return this.greetingService.greet(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* 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 smoketest.graphql;
|
||||||
|
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class GreetingService {
|
||||||
|
|
||||||
|
@PreAuthorize("hasRole('ADMIN')")
|
||||||
|
public String greet(String name) {
|
||||||
|
return "Hello, " + name + "!";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* 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 smoketest.graphql;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class Project {
|
||||||
|
|
||||||
|
private String slug;
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
public Project(String slug, String name) {
|
||||||
|
this.slug = slug;
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSlug() {
|
||||||
|
return this.slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSlug(String slug) {
|
||||||
|
this.slug = slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (o == null || getClass() != o.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Project project = (Project) o;
|
||||||
|
return this.slug.equals(project.slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(this.slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue