diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/query/DefaultGraphQLTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/query/DefaultGraphQLTester.java index 628b8520..50487ffc 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/query/DefaultGraphQLTester.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/query/DefaultGraphQLTester.java @@ -26,6 +26,7 @@ import java.util.List; import java.util.Map; import java.util.function.Consumer; import java.util.function.Predicate; +import java.util.stream.Collectors; import com.jayway.jsonpath.Configuration; import com.jayway.jsonpath.DocumentContext; @@ -33,7 +34,6 @@ import com.jayway.jsonpath.JsonPath; import com.jayway.jsonpath.PathNotFoundException; import com.jayway.jsonpath.TypeRef; import com.jayway.jsonpath.spi.json.JacksonJsonProvider; -import com.jayway.jsonpath.spi.json.JsonProvider; import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider; import graphql.ExecutionResult; import graphql.GraphQL; @@ -154,7 +154,7 @@ class DefaultGraphQLTester implements GraphQLTester { @Override public SubscriptionSpec executeSubscription(RequestInput queryInput) { - FluxExchangeResult result = this.client.post() + FluxExchangeResult exchangeResult = this.client.post() .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.TEXT_EVENT_STREAM) .bodyValue(queryInput) @@ -164,9 +164,8 @@ class DefaultGraphQLTester implements GraphQLTester { .returnResult(TestExecutionResult.class); return new DefaultSubscriptionSpec( - result.getResponseBody().cast(ExecutionResult.class), - Collections.emptyList(), this.jsonPathConfig, - result::assertWithDiagnostics); + exchangeResult.getResponseBody().cast(ExecutionResult.class), + this.jsonPathConfig, exchangeResult::assertWithDiagnostics); } } @@ -203,8 +202,13 @@ class DefaultGraphQLTester implements GraphQLTester { public SubscriptionSpec executeSubscription(RequestInput input) { ExecutionResult result = executeInternal(input); AssertionErrors.assertTrue("Subscription did not return Publisher", result.getData() instanceof Publisher); - return new DefaultSubscriptionSpec( - result.getData(), result.getErrors(), this.jsonPathConfig, assertDecorator(input)); + + List errors = result.getErrors(); + Consumer assertDecorator = assertDecorator(input); + assertDecorator.accept(() -> AssertionErrors.assertTrue( + "Response has " + errors.size() + " unexpected error(s).", CollectionUtils.isEmpty(errors))); + + return new DefaultSubscriptionSpec(result.getData(), this.jsonPathConfig, assertDecorator); } private ExecutionResult executeInternal(RequestInput input) { @@ -283,38 +287,91 @@ class DefaultGraphQLTester implements GraphQLTester { } - /** - * Base for a {@link ResponseSpec} implementations. - */ - private static class ResponseSpecSupport { + private static class ErrorsContainer { - private final List errors; + private static final Predicate MATCH_ALL_PREDICATE = error -> true; - private boolean errorsChecked; + private final List errors; private final Consumer assertDecorator; - private ResponseSpecSupport(List errors, Consumer assertDecorator) { + ErrorsContainer(List errors, Consumer assertDecorator) { + Assert.notNull(errors, "`errors` is required"); + Assert.notNull(assertDecorator, "`assertDecorator` is required"); this.errors = errors; this.assertDecorator = assertDecorator; } - protected Consumer getAssertDecorator() { - return this.assertDecorator; + public void doAssert(Runnable task) { + this.assertDecorator.accept(task); } - protected void consumeErrors(Consumer> errorConsumer) { - this.errorsChecked = true; - errorConsumer.accept(this.errors); + public void filterErrors(Predicate errorPredicate) { + this.errors.forEach(error -> error.filter(errorPredicate)); } - protected void assertErrorsEmptyOrConsumed() { - if (!this.errorsChecked) { - this.assertDecorator.accept(() -> AssertionErrors.assertTrue( - "Response contains GraphQL errors. " + - "To avoid this message, please use ResponseSpec#errorsSatisfy to check them.", - CollectionUtils.isEmpty(this.errors))); + public void consumeErrors(Consumer> consumer) { + filterErrors(MATCH_ALL_PREDICATE); + consumer.accept(new ArrayList<>(this.errors)); + } + + public void verifyErrors() { + + List unexpected = + this.errors.stream().filter(error -> !error.isExpected()).collect(Collectors.toList()); + + this.assertDecorator.accept(() -> AssertionErrors.assertTrue( + "Response has " + unexpected.size() + " unexpected error(s)" + + (unexpected.size() != this.errors.size() ? " of " + this.errors.size() + " total" : "") + ". " + + "If expected, please use ResponseSpec#errors to filter them out: " + unexpected, + CollectionUtils.isEmpty(unexpected))); + } + } + + + /** + * Container for a GraphQL response with access to data and errors. + */ + private static class ResponseContainer extends ErrorsContainer { + + private static final JsonPath ERRORS_PATH = JsonPath.compile("$.errors"); + + private final DocumentContext documentContext; + + private final String jsonContent; + + ResponseContainer(DocumentContext documentContext, Consumer assertDecorator) { + super(readErrors(documentContext), assertDecorator); + this.documentContext = documentContext; + this.jsonContent = this.documentContext.jsonString(); + } + + private static List readErrors(DocumentContext documentContext) { + Assert.notNull(documentContext, "DocumentContext is required"); + try { + return documentContext.read(ERRORS_PATH, new TypeRef>() {}); } + catch (PathNotFoundException ex) { + return Collections.emptyList(); + } + } + + public String jsonContent() { + return this.jsonContent; + } + + public String jsonContent(JsonPath jsonPath) { + try { + Object content = this.documentContext.read(jsonPath); + return this.documentContext.configuration().jsonProvider().toJson(content); + } + catch (Exception ex) { + throw new AssertionError("JSON parsing error", ex); + } + } + + public T read(JsonPath jsonPath, TypeRef typeRef) { + return this.documentContext.read(jsonPath, typeRef); } } @@ -322,12 +379,9 @@ class DefaultGraphQLTester implements GraphQLTester { /** * {@link ResponseSpec} that operates on the response from a GraphQL HTTP request. */ - private static class DefaultResponseSpec extends ResponseSpecSupport implements ResponseSpec { + private static class DefaultResponseSpec implements ResponseSpec, ErrorSpec { - private static final JsonPath ERRORS_PATH = JsonPath.compile("$.errors"); - - - private final DocumentContext documentContext; + private final ResponseContainer responseContainer; /** * Class constructor. @@ -337,33 +391,36 @@ class DefaultGraphQLTester implements GraphQLTester { * body details */ private DefaultResponseSpec(DocumentContext documentContext, Consumer assertDecorator) { - super(initErrors(documentContext), assertDecorator); - Assert.notNull(documentContext, "DocumentContext is required"); - Assert.notNull(assertDecorator, "`assertDecorator` is required"); - this.documentContext = documentContext; - } - - private static List initErrors(DocumentContext documentContext) { - try { - return new ArrayList<>(documentContext.read( - ERRORS_PATH, new TypeRef>() {})); - } - catch (PathNotFoundException ex) { - return Collections.emptyList(); - } - } - - - @Override - public ResponseSpec errorsSatisfy(Consumer> errorConsumer) { - consumeErrors(errorConsumer); - return this; + this.responseContainer = new ResponseContainer(documentContext, assertDecorator); } @Override public PathSpec path(String path) { - assertErrorsEmptyOrConsumed(); - return new DefaultPathSpec(path, this.documentContext, getAssertDecorator()); + this.responseContainer.verifyErrors(); + return new DefaultPathSpec(path, this.responseContainer); + } + + @Override + public ErrorSpec errors() { + return this; + } + + @Override + public ErrorSpec filter(Predicate predicate) { + this.responseContainer.filterErrors(predicate); + return this; + } + + @Override + public TraverseSpec verify() { + this.responseContainer.verifyErrors(); + return this; + } + + @Override + public TraverseSpec satisfy(Consumer> consumer) { + this.responseContainer.consumeErrors(consumer); + return this; } } @@ -375,28 +432,23 @@ class DefaultGraphQLTester implements GraphQLTester { private final String inputPath; - private final DocumentContext documentContext; - - private final Consumer assertDecorator; + private final ResponseContainer responseContainer; private final JsonPath jsonPath; private final JsonPathExpectationsHelper pathHelper; - private final String content; - - DefaultPathSpec(String path, DocumentContext documentContext, Consumer assertDecorator) { + DefaultPathSpec(String path, ResponseContainer responseContainer) { Assert.notNull(path, "`path` is required"); + Assert.notNull(responseContainer, "ResponseContainer is required"); this.inputPath = path; - this.documentContext = documentContext; - this.assertDecorator = assertDecorator; - this.jsonPath = initPath(path); + this.responseContainer = responseContainer; + this.jsonPath = initJsonPath(path); this.pathHelper = new JsonPathExpectationsHelper(this.jsonPath.getPath()); - this.content = documentContext.jsonString(); } - private static JsonPath initPath(String path) { + private static JsonPath initJsonPath(String path) { if (!StringUtils.hasText(path)) { path = "$.data"; } @@ -409,38 +461,42 @@ class DefaultGraphQLTester implements GraphQLTester { @Override public PathSpec path(String path) { - return new DefaultPathSpec(path, this.documentContext, this.assertDecorator); + return new DefaultPathSpec(path, this.responseContainer); } @Override public PathSpec pathExists() { - this.assertDecorator.accept(() -> this.pathHelper.hasJsonPath(this.content)); + this.responseContainer.doAssert( + () -> this.pathHelper.hasJsonPath(this.responseContainer.jsonContent())); return this; } @Override public PathSpec pathDoesNotExist() { - this.assertDecorator.accept(() -> this.pathHelper.doesNotHaveJsonPath(this.content)); + this.responseContainer.doAssert( + () -> this.pathHelper.doesNotHaveJsonPath(this.responseContainer.jsonContent())); return this; } @Override public PathSpec valueExists() { - this.assertDecorator.accept(() -> this.pathHelper.exists(this.content)); + this.responseContainer.doAssert( + () -> this.pathHelper.exists(this.responseContainer.jsonContent())); return this; } @Override public PathSpec valueDoesNotExist() { - this.assertDecorator.accept(() -> this.pathHelper.doesNotExist(this.content)); + this.responseContainer.doAssert( + () -> this.pathHelper.doesNotExist(this.responseContainer.jsonContent())); return this; } @Override public PathSpec valueIsEmpty() { - this.assertDecorator.accept(() -> { + this.responseContainer.doAssert(() -> { try { - this.pathHelper.assertValueIsEmpty(this.content); + this.pathHelper.assertValueIsEmpty(this.responseContainer.jsonContent()); } catch (AssertionError ex) { // ignore @@ -451,32 +507,33 @@ class DefaultGraphQLTester implements GraphQLTester { @Override public PathSpec valueIsNotEmpty() { - this.assertDecorator.accept(() -> this.pathHelper.assertValueIsNotEmpty(this.content)); + this.responseContainer.doAssert( + () -> this.pathHelper.assertValueIsNotEmpty(this.responseContainer.jsonContent())); return this; } @Override public EntitySpec entity(Class entityType) { - D entity = this.documentContext.read(this.jsonPath, new TypeRefAdapter<>(entityType)); - return new DefaultEntitySpec<>(entity, this.documentContext, assertDecorator, this.inputPath); + D entity = this.responseContainer.read(this.jsonPath, new TypeRefAdapter<>(entityType)); + return new DefaultEntitySpec<>(entity, this.responseContainer, this.inputPath); } @Override public EntitySpec entity(ParameterizedTypeReference entityType) { - D entity = this.documentContext.read(this.jsonPath, new TypeRefAdapter<>(entityType)); - return new DefaultEntitySpec<>(entity, this.documentContext, assertDecorator, this.inputPath); + D entity = this.responseContainer.read(this.jsonPath, new TypeRefAdapter<>(entityType)); + return new DefaultEntitySpec<>(entity, this.responseContainer, this.inputPath); } @Override public ListEntitySpec entityList(Class elementType) { - List entity = this.documentContext.read(this.jsonPath, new TypeRefAdapter<>(List.class, elementType)); - return new DefaultListEntitySpec<>(entity, this.documentContext, assertDecorator, this.inputPath); + List entity = this.responseContainer.read(this.jsonPath, new TypeRefAdapter<>(List.class, elementType)); + return new DefaultListEntitySpec<>(entity, this.responseContainer, this.inputPath); } @Override public ListEntitySpec entityList(ParameterizedTypeReference elementType) { - List entity = this.documentContext.read(this.jsonPath, new TypeRefAdapter<>(List.class, elementType)); - return new DefaultListEntitySpec<>(entity, this.documentContext, assertDecorator, this.inputPath); + List entity = this.responseContainer.read(this.jsonPath, new TypeRefAdapter<>(List.class, elementType)); + return new DefaultListEntitySpec<>(entity, this.responseContainer, this.inputPath); } @Override @@ -492,16 +549,8 @@ class DefaultGraphQLTester implements GraphQLTester { } private void matchesJson(String expected, boolean strict) { - this.assertDecorator.accept(() -> { - String actual; - try { - JsonProvider jsonProvider = this.documentContext.configuration().jsonProvider(); - Object content = this.documentContext.read(this.jsonPath); - actual = jsonProvider.toJson(content); - } - catch (Exception ex) { - throw new AssertionError("JSON parsing error", ex); - } + this.responseContainer.doAssert(() -> { + String actual = this.responseContainer.jsonContent(this.jsonPath); try { new JsonExpectationsHelper().assertJsonEqual(expected, actual, strict); } @@ -526,16 +575,13 @@ class DefaultGraphQLTester implements GraphQLTester { private final D entity; - private final DocumentContext documentContext; - - private final Consumer assertDecorator; + private final ResponseContainer responseContainer; private final String inputPath; - DefaultEntitySpec(D entity, DocumentContext context, Consumer decorator, String path) { + DefaultEntitySpec(D entity, ResponseContainer responseContainer, String path) { this.entity = entity; - this.documentContext = context; - this.assertDecorator = decorator; + this.responseContainer = responseContainer; this.inputPath = path; } @@ -543,52 +589,56 @@ class DefaultGraphQLTester implements GraphQLTester { return this.entity; } + protected void doAssert(Runnable task) { + this.responseContainer.doAssert(task); + } + protected String getInputPath() { return this.inputPath; } - protected Consumer getAssertDecorator() { - return this.assertDecorator; - } - @Override public PathSpec path(String path) { - return new DefaultPathSpec(path, this.documentContext, this.assertDecorator); + return new DefaultPathSpec(path, this.responseContainer); } @Override public T isEqualTo(Object expected) { - this.assertDecorator.accept(() -> AssertionErrors.assertEquals(this.inputPath, expected, this.entity)); + this.responseContainer.doAssert( + () -> AssertionErrors.assertEquals(this.inputPath, expected, this.entity)); return self(); } @Override public T isNotEqualTo(Object other) { - this.assertDecorator.accept(() -> AssertionErrors.assertNotEquals(this.inputPath, other, this.entity)); + this.responseContainer.doAssert( + () -> AssertionErrors.assertNotEquals(this.inputPath, other, this.entity)); return self(); } @Override public T isSameAs(Object expected) { - this.assertDecorator.accept(() -> AssertionErrors.assertTrue(this.inputPath, expected == this.entity)); + this.responseContainer.doAssert( + () -> AssertionErrors.assertTrue(this.inputPath, expected == this.entity)); return self(); } @Override public T isNotSameAs(Object other) { - this.assertDecorator.accept(() -> AssertionErrors.assertTrue(this.inputPath, other != this.entity)); + this.responseContainer.doAssert(() -> AssertionErrors.assertTrue(this.inputPath, other != this.entity)); return self(); } @Override public T matches(Predicate predicate) { - this.assertDecorator.accept(() -> AssertionErrors.assertTrue(this.inputPath, predicate.test(this.entity))); + this.responseContainer.doAssert( + () -> AssertionErrors.assertTrue(this.inputPath, predicate.test(this.entity))); return self(); } @Override public T satisfies(Consumer consumer) { - this.assertDecorator.accept(() -> consumer.accept(this.entity)); + this.responseContainer.doAssert(() -> consumer.accept(this.entity)); return self(); } @@ -610,14 +660,14 @@ class DefaultGraphQLTester implements GraphQLTester { private static class DefaultListEntitySpec extends DefaultEntitySpec, ListEntitySpec> implements ListEntitySpec { - DefaultListEntitySpec(List entity, DocumentContext context, Consumer decorator, String path) { - super(entity, context, decorator, path); + DefaultListEntitySpec(List entity, ResponseContainer responseContainer, String path) { + super(entity, responseContainer, path); } @Override @SuppressWarnings("unchecked") public ListEntitySpec contains(E... elements) { - getAssertDecorator().accept(() -> { + doAssert(() -> { List expected = Arrays.asList(elements); AssertionErrors.assertTrue( "List at path '" + getInputPath() + "' does not contain " + expected, @@ -629,7 +679,7 @@ class DefaultGraphQLTester implements GraphQLTester { @Override @SuppressWarnings("unchecked") public ListEntitySpec doesNotContain(E... elements) { - getAssertDecorator().accept(() -> { + doAssert(() -> { List expected = Arrays.asList(elements); AssertionErrors.assertTrue( "List at path '" + getInputPath() + "' should not have contained " + expected, @@ -641,7 +691,7 @@ class DefaultGraphQLTester implements GraphQLTester { @Override @SuppressWarnings("unchecked") public ListEntitySpec containsExactly(E... elements) { - getAssertDecorator().accept(() -> { + doAssert(() -> { List expected = Arrays.asList(elements); AssertionErrors.assertTrue( "List at path '" + getInputPath() + "' should have contained exactly " + expected, @@ -652,31 +702,28 @@ class DefaultGraphQLTester implements GraphQLTester { @Override public ListEntitySpec hasSize(int size) { - getAssertDecorator().accept(() -> { - AssertionErrors.assertTrue( - "List at path '" + getInputPath() + "' should have size " + size, - (getEntity() != null && getEntity().size() == size)); - }); + doAssert(() -> + AssertionErrors.assertTrue( + "List at path '" + getInputPath() + "' should have size " + size, + (getEntity() != null && getEntity().size() == size))); return this; } @Override public ListEntitySpec hasSizeLessThan(int boundary) { - getAssertDecorator().accept(() -> { - AssertionErrors.assertTrue( - "List at path '" + getInputPath() + "' should have size less than " + boundary, - (getEntity() != null && getEntity().size() < boundary)); - }); + doAssert(() -> + AssertionErrors.assertTrue( + "List at path '" + getInputPath() + "' should have size less than " + boundary, + (getEntity() != null && getEntity().size() < boundary))); return this; } @Override public ListEntitySpec hasSizeGreaterThan(int boundary) { - getAssertDecorator().accept(() -> { - AssertionErrors.assertTrue( - "List at path '" + getInputPath() + "' should have size greater than " + boundary, - (getEntity() != null && getEntity().size() > boundary)); - }); + doAssert(() -> + AssertionErrors.assertTrue( + "List at path '" + getInputPath() + "' should have size greater than " + boundary, + (getEntity() != null && getEntity().size() > boundary))); return this; } } @@ -686,32 +733,27 @@ class DefaultGraphQLTester implements GraphQLTester { * {@link SubscriptionSpec} implementation that operates on a * {@link Publisher} of {@link ExecutionResult}. */ - private static class DefaultSubscriptionSpec extends ResponseSpecSupport implements SubscriptionSpec { + private static class DefaultSubscriptionSpec implements SubscriptionSpec { private final Publisher publisher; private final Configuration jsonPathConfig; - DefaultSubscriptionSpec( - Publisher publisher, List errors, Configuration jsonPathConfig, - Consumer assertDecorator) { + private final Consumer assertDecorator; + + DefaultSubscriptionSpec( + Publisher publisher, Configuration jsonPathConfig, Consumer decorator) { - super(errors, assertDecorator); this.publisher = publisher; this.jsonPathConfig = jsonPathConfig; - } - - @Override - public SubscriptionSpec errorsSatisfy(Consumer> errorConsumer) { - consumeErrors(errorConsumer); - return this; + this.assertDecorator = decorator; } @Override public Flux toFlux() { return Flux.from(this.publisher).map(result -> { DocumentContext context = JsonPath.parse(result.toSpecification(), this.jsonPathConfig); - return new DefaultResponseSpec(context, getAssertDecorator()); + return new DefaultResponseSpec(context, this.assertDecorator); }); } } diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/query/GraphQLTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/query/GraphQLTester.java index fe9ba99f..4587803f 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/query/GraphQLTester.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/query/GraphQLTester.java @@ -129,7 +129,7 @@ public interface GraphQLTester { /** * Execute the GraphQL request and return a spec for further inspection - * of the response data and errors. + * of response data and errors. * * @return options for asserting the response * @throws AssertionError if the request is performed over HTTP and the @@ -138,16 +138,15 @@ public interface GraphQLTester { ResponseSpec execute(); /** - * Perform the GraphQL request and then verify the GraphQL response does - * not contain any errors. To assert the errors, use {@link #execute()} - * instead. + * Execute the GraphQL request and verify the response contains no errors. */ void executeAndVerify(); /** - * Perform the GraphQL subscription request. + * Execute the GraphQL request as a subscription and return a spec with + * options to transform the result stream. * - * @return options for assertions on subscription events + * @return spec with options to transform the subscription result stream * @throws AssertionError if the request is performed over HTTP and the * response status is not 200 (OK). */ @@ -193,34 +192,29 @@ public interface GraphQLTester { * @return spec for asserting the content under the given path * @throws AssertionError if the GraphQL response contains * errors - * that have not be checked via {@link ResponseSpec#errorsSatisfy(Consumer)} + * that have not be checked via {@link ResponseSpec#errors()} */ PathSpec path(String path); } /** - * Declare the first options available to insecpt a GraphQL response. + * Declare options to check the data and errors of a GraphQL response. */ interface ResponseSpec extends TraverseSpec { /** - * Inspect errors - * in the response, if any. - *

If this method is not used first, any attempts to check the data - * will result in an {@link AssertionError}. Therefore for GraphQL - * responses that are expected to have both data and errors, be sure - * to use this method first. - * @param errorConsumer the consumer to inspect errors with - * @return the same spec for further assertions on the data + * Return a spec to filter out or inspect errors. This must be used + * before traversing to a {@link #path(String)} if some errors are + * expected and need to be filtered out. */ - ResponseSpec errorsSatisfy(Consumer> errorConsumer); + ErrorSpec errors(); } /** - * Assertions available for the data at a given path. + * Declare options available to assert data at a given path. */ interface PathSpec extends TraverseSpec { @@ -384,7 +378,8 @@ public interface GraphQLTester { /** - * Extension of {@link EntitySpec} for a List of entities. + * Extension of {@link EntitySpec} with options available to assert data + * converted to a List of entities. * @param the type of elements in the list */ interface ListEntitySpec extends EntitySpec, ListEntitySpec> { @@ -438,21 +433,44 @@ public interface GraphQLTester { /** - * Declare options available to assert a GraphQL Subscription response. + * Declare options to filter out expected errors or inspect all errors and + * verify there are no unexpected errors. */ - interface SubscriptionSpec { + interface ErrorSpec { + + /** + * Add a filter for expected errors. All errors that match the predicate + * are treated as expected and ignored on {@link #verify()} or when + * {@link TraverseSpec#path(String) traversing} to a data path. + * @param errorPredicate the predicate to add + * @return the same spec to add more filters before {@link #verify()} + */ + ErrorSpec filter(Predicate errorPredicate); + + /** + * Verify there are either no errors or that there no unexpected errors + * that have not been {@link #filter(Predicate) filtered out}. + * @return a spec to switch to a data path + */ + TraverseSpec verify(); /** * Inspect errors - * in the response, if any. - *

If this method is not used first, any attempts to check event data - * will result in an {@link AssertionError}. Therefore for a GraphQL - * subscription that are expected to have both errors and events, be sure - * to use this method first. - * @param errorConsumer the consumer to inspect errors with - * @return the same spec for further assertions on the data + * in the response, if any. Use of this method effectively suppresses + * all errors and allows {@link TraverseSpec#path(String) traversing} to a + * data path. + * @param errorsConsumer to inspect errors with + * @return a spec to switch to a data path */ - SubscriptionSpec errorsSatisfy(Consumer> errorConsumer); + TraverseSpec satisfy(Consumer> errorsConsumer); + + } + + + /** + * Declare options available to assert a GraphQL Subscription response. + */ + interface SubscriptionSpec { /** * Return a {@link Flux} of entities converted from some part of the data diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/query/TestGraphQLError.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/query/TestGraphQLError.java index 3134dde0..33af2b31 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/query/TestGraphQLError.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/query/TestGraphQLError.java @@ -17,6 +17,7 @@ package org.springframework.graphql.test.query; import java.util.List; import java.util.Map; +import java.util.function.Predicate; import java.util.stream.Collectors; import graphql.ErrorClassification; @@ -40,6 +41,9 @@ class TestGraphQLError implements GraphQLError { private Map extensions; + private boolean expected; + + public void setMessage(String message) { this.message = message; @@ -86,6 +90,13 @@ class TestGraphQLError implements GraphQLError { return this.extensions; } + /** + * Whether the error is marked as filtered out as expected. + */ + public boolean isExpected() { + return this.expected; + } + @Override public Map toSpecification() { GraphqlErrorBuilder builder = GraphqlErrorBuilder.newError(); @@ -104,6 +115,13 @@ class TestGraphQLError implements GraphQLError { return builder.build().toSpecification(); } + /** + * Mark this error as expected if it matches the predicate. + */ + void filter(Predicate predicate) { + this.expected |= predicate.test(this); + } + @Override public String toString() { return toSpecification().toString(); diff --git a/spring-graphql-test/src/test/java/org/springframework/graphql/test/query/GraphQLTesterTests.java b/spring-graphql-test/src/test/java/org/springframework/graphql/test/query/GraphQLTesterTests.java index 664082c7..eb9c2f27 100644 --- a/spring-graphql-test/src/test/java/org/springframework/graphql/test/query/GraphQLTesterTests.java +++ b/spring-graphql-test/src/test/java/org/springframework/graphql/test/query/GraphQLTesterTests.java @@ -215,34 +215,13 @@ public class GraphQLTesterTests { @ParameterizedTest @MethodSource("argumentSource") - void errorsAssertedIfNotChecked(GraphQLTesterSetup setup) throws Exception { + void errorsCheckedOnExecuteAndVerify(GraphQLTesterSetup setup) throws Exception { String query = "{me {name, friends}}"; - setup.response(GraphqlErrorBuilder.newError() - .message("Invalid query") - .location(new SourceLocation(1, 2)) - .build()); - - GraphQLTester.ResponseSpec spec = setup.graphQLTester().query(query).execute(); - - assertThatThrownBy(() -> spec.path("me")).hasMessageContaining("Response contains GraphQL errors."); - - setup.verifyRequest(input -> assertThat(input.getQuery()).contains(query)); - setup.shutdown(); - } - - @ParameterizedTest - @MethodSource("argumentSource") - void errorsAssertedOnExecuteAndVerify(GraphQLTesterSetup setup) throws Exception { - - String query = "{me {name, friends}}"; - setup.response(GraphqlErrorBuilder.newError() - .message("Invalid query") - .location(new SourceLocation(1, 2)) - .build()); + setup.response(GraphqlErrorBuilder.newError().message("Invalid query").build()); assertThatThrownBy(() -> setup.graphQLTester().query(query).executeAndVerify()) - .hasMessageContaining("Response contains GraphQL errors."); + .hasMessageContaining("Response has 1 unexpected error(s)."); setup.verifyRequest(input -> assertThat(input.getQuery()).contains(query)); setup.shutdown(); @@ -250,7 +229,60 @@ public class GraphQLTesterTests { @ParameterizedTest @MethodSource("argumentSource") - void errorsAllowedIfChecked(GraphQLTesterSetup setup) throws Exception { + void errorsCheckedOnTraverse(GraphQLTesterSetup setup) throws Exception { + + String query = "{me {name, friends}}"; + setup.response(GraphqlErrorBuilder.newError().message("Invalid query").build()); + + assertThatThrownBy(() -> setup.graphQLTester().query(query).execute().path("me")) + .hasMessageContaining("Response has 1 unexpected error(s)."); + + setup.verifyRequest(input -> assertThat(input.getQuery()).contains(query)); + setup.shutdown(); + } + + @ParameterizedTest + @MethodSource("argumentSource") + void errorsPartiallyFiltered(GraphQLTesterSetup setup) throws Exception { + + String query = "{me {name, friends}}"; + setup.response( + GraphqlErrorBuilder.newError().message("some error").build(), + GraphqlErrorBuilder.newError().message("some other error").build()); + + assertThatThrownBy(() -> + setup.graphQLTester().query(query).execute() + .errors() + .filter(error -> error.getMessage().equals("some error")) + .verify()) + .hasMessageContaining("Response has 1 unexpected error(s) of 2 total."); + + setup.verifyRequest(input -> assertThat(input.getQuery()).contains(query)); + setup.shutdown(); + } + + @ParameterizedTest + @MethodSource("argumentSource") + void errorsFiltered(GraphQLTesterSetup setup) throws Exception { + + String query = "{me {name, friends}}"; + setup.response( + GraphqlErrorBuilder.newError().message("some error").build(), + GraphqlErrorBuilder.newError().message("some other error").build()); + + setup.graphQLTester().query(query).execute() + .errors() + .filter(error -> error.getMessage().startsWith("some ")) + .verify() + .path("me").pathDoesNotExist(); + + setup.verifyRequest(input -> assertThat(input.getQuery()).contains(query)); + setup.shutdown(); + } + + @ParameterizedTest + @MethodSource("argumentSource") + void errorsConsumed(GraphQLTesterSetup setup) throws Exception { String query = "{me {name, friends}}"; setup.response(GraphqlErrorBuilder.newError() @@ -259,7 +291,7 @@ public class GraphQLTesterTests { .build()); setup.graphQLTester().query(query).execute() - .errorsSatisfy(errors -> { + .errors().satisfy(errors -> { assertThat(errors).hasSize(1); assertThat(errors.get(0).getMessage()).isEqualTo("Invalid query"); assertThat(errors.get(0).getLocations()).hasSize(1);