diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/graphql/GraphQlTest.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/graphql/GraphQlTest.java new file mode 100644 index 0000000000..d0cfa2d55e --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/graphql/GraphQlTest.java @@ -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. + *

+ * The annotation disables full auto-configuration and instead loads only components + * relevant to GraphQL tests, including the following: + *

+ *

+ * The annotation does not automatically load {@code @Component}, {@code @Service}, + * {@code @Repository}, and other beans. + *

+ * 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}. + *

+ * 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. + *

+ * 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 {}; + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/graphql/GraphQlTypeExcludeFilter.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/graphql/GraphQlTypeExcludeFilter.java new file mode 100644 index 0000000000..1ae5b52f60 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/graphql/GraphQlTypeExcludeFilter.java @@ -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 { + + private static final Class[] NO_CONTROLLERS = {}; + + private static final String[] OPTIONAL_INCLUDES = { "com.fasterxml.jackson.databind.Module" }; + + private static final Set> DEFAULT_INCLUDES; + + static { + Set> 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> DEFAULT_INCLUDES_AND_CONTROLLER; + + static { + Set> 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> getDefaultIncludes() { + if (ObjectUtils.isEmpty(this.controllers)) { + return DEFAULT_INCLUDES_AND_CONTROLLER; + } + return DEFAULT_INCLUDES; + } + + @Override + protected Set> getComponentIncludes() { + return new LinkedHashSet<>(Arrays.asList(this.controllers)); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/graphql/Book.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/graphql/Book.java new file mode 100644 index 0000000000..9f1cb863f0 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/graphql/Book.java @@ -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; + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/graphql/BookController.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/graphql/BookController.java new file mode 100644 index 0000000000..9e9fc970a4 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/graphql/BookController.java @@ -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"); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/graphql/ExampleGraphQlApplication.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/graphql/ExampleGraphQlApplication.java new file mode 100644 index 0000000000..798592a56c --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/graphql/ExampleGraphQlApplication.java @@ -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 { + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/graphql/GraphQlTestIntegrationTest.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/graphql/GraphQlTestIntegrationTest.java new file mode 100644 index 0000000000..18807b332b --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/graphql/GraphQlTestIntegrationTest.java @@ -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"); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/graphql/GraphQlTypeExcludeFilterTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/graphql/GraphQlTypeExcludeFilterTests.java new file mode 100644 index 0000000000..53fa09adcb --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/graphql/GraphQlTypeExcludeFilterTests.java @@ -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 intercept(WebInput webInput, WebInterceptorChain chain) { + return null; + } + + } + + @SuppressWarnings("serial") + static class ExampleModule extends SimpleModule { + + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/resources/graphql/schema.graphqls b/spring-boot-project/spring-boot-test-autoconfigure/src/test/resources/graphql/schema.graphqls new file mode 100644 index 0000000000..975b7ca60d --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/resources/graphql/schema.graphqls @@ -0,0 +1,10 @@ +type Query { + bookById(id: ID): Book +} + +type Book { + id: ID + name: String + pageCount: Int + author: String +} \ No newline at end of file