Pagination test with controller method

See gh-620
This commit is contained in:
rstoyanchev
2023-03-17 21:46:35 +00:00
parent 6630ca0701
commit d017506d43
4 changed files with 282 additions and 4 deletions

View File

@@ -53,8 +53,8 @@ public class PaginationRequestMethodArgumentResolver<P> 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);
}

View File

@@ -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<String, Object>> MAP_TYPE_REFERENCE =
new TypeReference<Map<String, Object>>() {};
private static final TypeReference<Map<String, Object>> MAP_TYPE_REFERENCE = new TypeReference<>() {};
private final ObjectMapper mapper = new ObjectMapper();

View File

@@ -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<MyPosition> 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<String, Object> 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<String, Object> 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<MyPosition> request = (PaginationRequest<MyPosition>) result;
assertThat(request.position().get().index()).isEqualTo(index);
assertThat(request.count().get()).isEqualTo(count);
assertThat(request.forward()).isEqualTo(forward);
}
private static DataFetchingEnvironment environment(Map<String, Object> arguments) {
return DataFetchingEnvironmentImpl.newDataFetchingEnvironment().arguments(arguments).build();
}
@SuppressWarnings("unused")
@Controller
private static class BookController {
@QueryMapping
public Window<Book> getBooks(PaginationRequest<MyPosition> request) {
return null;
}
@QueryMapping
public Window<Book> getBooksWithUnknownPosition(PaginationRequest<UnknownPosition> request) {
return null;
}
}
private static class MyPositionCursorStrategy implements CursorStrategy<MyPosition> {
@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 {
}
}

View File

@@ -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<AnnotatedControllerConfigurer, GraphQlSetup> 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<Book> books(ScrollRequest request) {
int offset = (int) ((OffsetScrollPosition) request.position().get()).getOffset();
int count = request.count().get();
List<Book> books = BookSource.books().subList(offset, offset + count);
return Window.from(books, OffsetScrollPosition::of);
}
}
}