From d017506d4317f7c853a69c718f59b7d01b459975 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 17 Mar 2023 21:46:35 +0000 Subject: [PATCH] Pagination test with controller method See gh-620 --- ...ginationRequestMethodArgumentResolver.java | 2 +- .../support/ArgumentResolverTestSupport.java | 5 +- ...ionRequestMethodArgumentResolverTests.java | 133 ++++++++++++++++ .../support/SchemaMappingPaginationTests.java | 146 ++++++++++++++++++ 4 files changed, 282 insertions(+), 4 deletions(-) create mode 100644 spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/PaginationRequestMethodArgumentResolverTests.java create mode 100644 spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/SchemaMappingPaginationTests.java diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/PaginationRequestMethodArgumentResolver.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/PaginationRequestMethodArgumentResolver.java index 17294956..5a7d493a 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/PaginationRequestMethodArgumentResolver.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/PaginationRequestMethodArgumentResolver.java @@ -53,8 +53,8 @@ public class PaginationRequestMethodArgumentResolver

implements HandlerMethod @Override public Object resolveArgument(MethodParameter parameter, DataFetchingEnvironment environment) throws Exception { boolean forward = !environment.getArguments().containsKey("last"); - String cursor = environment.getArgument(forward ? "before" : "after"); Integer count = environment.getArgument(forward ? "first" : "last"); + String cursor = environment.getArgument(forward ? "after" : "before"); P position = (cursor != null ? this.cursorStrategy.fromCursor(cursor) : null); return createRequest(position, count, forward); } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ArgumentResolverTestSupport.java b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ArgumentResolverTestSupport.java index b5ee5937..8ad3b76b 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ArgumentResolverTestSupport.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ArgumentResolverTestSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 the original author or authors. + * Copyright 2020-2023 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. @@ -39,8 +39,7 @@ import org.springframework.util.ClassUtils; */ class ArgumentResolverTestSupport { - private static final TypeReference> MAP_TYPE_REFERENCE = - new TypeReference>() {}; + private static final TypeReference> MAP_TYPE_REFERENCE = new TypeReference<>() {}; private final ObjectMapper mapper = new ObjectMapper(); diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/PaginationRequestMethodArgumentResolverTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/PaginationRequestMethodArgumentResolverTests.java new file mode 100644 index 00000000..98abfe43 --- /dev/null +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/PaginationRequestMethodArgumentResolverTests.java @@ -0,0 +1,133 @@ +/* + * Copyright 2020-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.graphql.data.method.annotation.support; + +import java.util.Map; + +import graphql.schema.DataFetchingEnvironment; +import graphql.schema.DataFetchingEnvironmentImpl; +import org.junit.jupiter.api.Test; + +import org.springframework.core.MethodParameter; +import org.springframework.data.domain.Window; +import org.springframework.graphql.Book; +import org.springframework.graphql.data.method.annotation.QueryMapping; +import org.springframework.graphql.data.pagination.CursorStrategy; +import org.springframework.graphql.data.pagination.PaginationRequest; +import org.springframework.stereotype.Controller; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link PaginationRequestMethodArgumentResolver}. + * @author Rossen Stoyanchev + */ +public class PaginationRequestMethodArgumentResolverTests extends ArgumentResolverTestSupport { + + private final PaginationRequestMethodArgumentResolver resolver = + new PaginationRequestMethodArgumentResolver<>(new MyPositionCursorStrategy()); + + private final MethodParameter param = + methodParam(BookController.class, "getBooks", PaginationRequest.class); + + + @Test + void supports() { + assertThat(this.resolver.supportsParameter(this.param)).isTrue(); + + MethodParameter param = methodParam(BookController.class, "getBooksWithUnknownPosition", PaginationRequest.class); + assertThat(this.resolver.supportsParameter(param)).isFalse(); + } + + @Test + void forwardPagination() throws Exception { + int count = 10; + int index = 25; + Map arguments = Map.of("first", count, "after", String.valueOf(index)); + Object result = this.resolver.resolveArgument(this.param, environment(arguments)); + + testRequest(count, index, result, true); + } + + @Test + void backwardPagination() throws Exception { + int count = 20; + int index = 100; + Map arguments = Map.of("last", count, "before", String.valueOf(index)); + Object result = this.resolver.resolveArgument(this.param, environment(arguments)); + + testRequest(count, index, result, false); + } + + private static void testRequest(int count, int index, Object result, boolean forward) { + PaginationRequest request = (PaginationRequest) result; + assertThat(request.position().get().index()).isEqualTo(index); + assertThat(request.count().get()).isEqualTo(count); + assertThat(request.forward()).isEqualTo(forward); + } + + private static DataFetchingEnvironment environment(Map arguments) { + return DataFetchingEnvironmentImpl.newDataFetchingEnvironment().arguments(arguments).build(); + } + + + @SuppressWarnings("unused") + @Controller + private static class BookController { + + @QueryMapping + public Window getBooks(PaginationRequest request) { + return null; + } + + @QueryMapping + public Window getBooksWithUnknownPosition(PaginationRequest request) { + return null; + } + + } + + + private static class MyPositionCursorStrategy implements CursorStrategy { + + + @Override + public boolean supports(Class targetType) { + return targetType.equals(MyPosition.class); + } + + @Override + public String toCursor(MyPosition position) { + return String.valueOf(position.index()); + } + + @Override + public MyPosition fromCursor(String cursor) { + return new MyPosition(Integer.parseInt(cursor)); + } + + } + + + private record MyPosition(int index) { + } + + + private static class UnknownPosition { + } + +} diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/SchemaMappingPaginationTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/SchemaMappingPaginationTests.java new file mode 100644 index 00000000..19d95f37 --- /dev/null +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/SchemaMappingPaginationTests.java @@ -0,0 +1,146 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.graphql.data.method.annotation.support; + +import java.util.List; +import java.util.function.BiConsumer; + +import org.junit.jupiter.api.Test; +import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.data.domain.OffsetScrollPosition; +import org.springframework.data.domain.Window; +import org.springframework.graphql.Book; +import org.springframework.graphql.BookSource; +import org.springframework.graphql.ExecutionGraphQlResponse; +import org.springframework.graphql.ExecutionGraphQlService; +import org.springframework.graphql.GraphQlSetup; +import org.springframework.graphql.TestExecutionRequest; +import org.springframework.graphql.data.method.annotation.QueryMapping; +import org.springframework.graphql.data.pagination.ConnectionFieldTypeVisitor; +import org.springframework.graphql.data.query.ScrollPositionCursorStrategy; +import org.springframework.graphql.data.query.ScrollRequest; +import org.springframework.graphql.data.query.WindowConnectionAdapter; +import org.springframework.graphql.execution.ConnectionTypeGenerator; +import org.springframework.stereotype.Controller; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * GraphQL paginated requests handled through {@code @SchemaMapping} methods. + * + * @author Rossen Stoyanchev + */ +public class SchemaMappingPaginationTests { + + private static final String SCHEMA = """ + type Query { + books(first:Int, after:String): BookConnection + } + type Book { + id: ID + name: String + } + """; + + + @Test + void forwardPagination() throws Exception { + + String document = """ + { + books(first:2, after:"O_3") { + edges { + cursor, + node { + id + name + } + } + pageInfo { + startCursor, + endCursor, + hasPreviousPage, + hasNextPage + } + } + } + """; + + ExecutionGraphQlService graphQlService = graphQlService((configurer, setup) -> { + + ConnectionTypeGenerator typeGenerator = new ConnectionTypeGenerator(); + setup.typeDefinitionRegistryConfigurer(typeGenerator::generateConnectionTypes); + + ScrollPositionCursorStrategy cursorStrategy = new ScrollPositionCursorStrategy(); + WindowConnectionAdapter connectionAdapter = new WindowConnectionAdapter(cursorStrategy); + setup.typeVisitor(ConnectionFieldTypeVisitor.create(List.of(connectionAdapter))); + + configurer.setCursorStrategy(cursorStrategy); + }); + + ExecutionGraphQlResponse response = + graphQlService.execute(TestExecutionRequest.forDocument(document)).block(); + + assertThat(new ObjectMapper().writeValueAsString(response.getData())) + .as("Errors: " + response.getErrors()).isEqualTo( + "{\"books\":{" + + "\"edges\":[" + + "{\"cursor\":\"O_0\",\"node\":{\"id\":\"4\",\"name\":\"To The Lighthouse\"}}," + + "{\"cursor\":\"O_1\",\"node\":{\"id\":\"5\",\"name\":\"Animal Farm\"}}" + + "]," + + "\"pageInfo\":{" + + "\"startCursor\":\"O_0\"," + + "\"endCursor\":\"O_1\"," + + "\"hasPreviousPage\":false," + + "\"hasNextPage\":false" + + "}}}"); + } + + private ExecutionGraphQlService graphQlService(BiConsumer consumer) { + + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(BookController.class); + context.refresh(); + + AnnotatedControllerConfigurer configurer = new AnnotatedControllerConfigurer(); + configurer.setApplicationContext(context); + + GraphQlSetup setup = GraphQlSetup.schemaContent(this.SCHEMA).runtimeWiring(configurer); + consumer.accept(configurer, setup); + + configurer.afterPropertiesSet(); + + return setup.toGraphQlService(); + } + + + @SuppressWarnings("unused") + @Controller + private static class BookController { + + @QueryMapping + public Window books(ScrollRequest request) { + int offset = (int) ((OffsetScrollPosition) request.position().get()).getOffset(); + int count = request.count().get(); + List books = BookSource.books().subList(offset, offset + count); + return Window.from(books, OffsetScrollPosition::of); + } + + } + +}