Simplify filtered error handling in GraphQlTester
See gh-317
This commit is contained in:
@@ -16,27 +16,26 @@
|
||||
|
||||
package org.springframework.graphql.test.tester;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.jayway.jsonpath.Configuration;
|
||||
import com.jayway.jsonpath.DocumentContext;
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
import com.jayway.jsonpath.PathNotFoundException;
|
||||
import com.jayway.jsonpath.TypeRef;
|
||||
import graphql.ExecutionResult;
|
||||
import graphql.GraphQLError;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.graphql.GraphQlRequest;
|
||||
import org.springframework.graphql.client.GraphQlTransport;
|
||||
import org.springframework.graphql.support.DocumentSource;
|
||||
@@ -201,18 +200,13 @@ final class DefaultGraphQlTester implements GraphQlTester {
|
||||
*/
|
||||
private final static class ResponseDelegate {
|
||||
|
||||
private static final TypeRef<List<TestGraphQlError>> ERROR_LIST_TYPE = new TypeRef<List<TestGraphQlError>>() {};
|
||||
|
||||
private static final JsonPath ERRORS_PATH = JsonPath.compile("$.errors");
|
||||
|
||||
private static final Predicate<GraphQLError> MATCH_ALL_PREDICATE = (error) -> true;
|
||||
|
||||
|
||||
private final DocumentContext jsonDoc;
|
||||
|
||||
private final Supplier<String> jsonContent;
|
||||
|
||||
private final List<TestGraphQlError> errors;
|
||||
private final List<GraphQLError> errors;
|
||||
|
||||
private final List<GraphQLError> unexpectedErrors;
|
||||
|
||||
private final Consumer<Runnable> assertDecorator;
|
||||
|
||||
@@ -223,18 +217,12 @@ final class DefaultGraphQlTester implements GraphQlTester {
|
||||
|
||||
this.jsonDoc = JsonPath.parse(result.toSpecification(), jsonPathConfig);
|
||||
this.jsonContent = this.jsonDoc::jsonString;
|
||||
this.errors = readErrors(this.jsonDoc);
|
||||
this.errors = result.getErrors();
|
||||
this.unexpectedErrors = new ArrayList<>(this.errors);
|
||||
this.assertDecorator = assertDecorator;
|
||||
|
||||
filterErrors(errorFilter);
|
||||
}
|
||||
|
||||
private static List<TestGraphQlError> readErrors(DocumentContext documentContext) {
|
||||
try {
|
||||
return documentContext.read(ERRORS_PATH, ERROR_LIST_TYPE);
|
||||
}
|
||||
catch (PathNotFoundException ex) {
|
||||
return Collections.emptyList();
|
||||
if (errorFilter != null) {
|
||||
filterErrors(errorFilter);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,39 +248,34 @@ final class DefaultGraphQlTester implements GraphQlTester {
|
||||
this.assertDecorator.accept(task);
|
||||
}
|
||||
|
||||
boolean filterErrors(@Nullable Predicate<GraphQLError> predicate) {
|
||||
boolean filterErrors(Predicate<GraphQLError> predicate) {
|
||||
boolean filtered = false;
|
||||
if (predicate != null) {
|
||||
for (TestGraphQlError error : this.errors) {
|
||||
filtered |= error.apply(predicate);
|
||||
for (GraphQLError error : this.errors) {
|
||||
if (predicate.test(error)) {
|
||||
this.unexpectedErrors.remove(error);
|
||||
filtered = true;
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
void expectErrors(@Nullable Predicate<GraphQLError> predicate) {
|
||||
void expectErrors(Predicate<GraphQLError> predicate) {
|
||||
boolean filtered = filterErrors(predicate);
|
||||
this.assertDecorator.accept(() -> AssertionErrors.assertTrue("No matching errors.", filtered));
|
||||
}
|
||||
|
||||
void consumeErrors(Consumer<List<GraphQLError>> consumer) {
|
||||
filterErrors(MATCH_ALL_PREDICATE);
|
||||
consumer.accept(new ArrayList<>(this.errors));
|
||||
filterErrors(error -> true);
|
||||
consumer.accept(this.errors);
|
||||
}
|
||||
|
||||
void verifyErrors() {
|
||||
List<TestGraphQlError> 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)));
|
||||
this.assertDecorator.accept(() ->
|
||||
AssertionErrors.assertTrue(
|
||||
"Response has " + this.unexpectedErrors.size() + " unexpected error(s) " +
|
||||
"of " + this.errors.size() + " total. " +
|
||||
"If expected, please filter them out: " + this.unexpectedErrors,
|
||||
CollectionUtils.isEmpty(this.unexpectedErrors)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -630,4 +613,35 @@ final class DefaultGraphQlTester implements GraphQlTester {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adapt JSONPath {@link TypeRef} to {@link ParameterizedTypeReference}.
|
||||
*/
|
||||
private static final class TypeRefAdapter<T> extends TypeRef<T> {
|
||||
|
||||
private final Type type;
|
||||
|
||||
TypeRefAdapter(Class<T> clazz) {
|
||||
this.type = clazz;
|
||||
}
|
||||
|
||||
TypeRefAdapter(ParameterizedTypeReference<T> typeReference) {
|
||||
this.type = typeReference.getType();
|
||||
}
|
||||
|
||||
TypeRefAdapter(Class<?> clazz, Class<?> generic) {
|
||||
this.type = ResolvableType.forClassWithGenerics(clazz, generic).getType();
|
||||
}
|
||||
|
||||
TypeRefAdapter(Class<?> clazz, ParameterizedTypeReference<?> generic) {
|
||||
this.type = ResolvableType.forClassWithGenerics(clazz, ResolvableType.forType(generic)).getType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
/*
|
||||
* 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.graphql.test.tester;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import graphql.ErrorClassification;
|
||||
import graphql.GraphQLError;
|
||||
import graphql.GraphqlErrorBuilder;
|
||||
import graphql.execution.ResultPath;
|
||||
import graphql.language.SourceLocation;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* {@link GraphQLError} with setters, for internal use to deserialize from a response.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
final class TestGraphQlError implements GraphQLError {
|
||||
|
||||
@Nullable
|
||||
private String message;
|
||||
|
||||
@Nullable
|
||||
private List<SourceLocation> locations;
|
||||
|
||||
@Nullable
|
||||
private List<Object> path;
|
||||
|
||||
@Nullable
|
||||
private Map<String, Object> extensions;
|
||||
|
||||
private boolean expected;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public String getMessage() {
|
||||
return this.message;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
void setLocations(List<TestSourceLocation> locations) {
|
||||
this.locations = TestSourceLocation.toSourceLocations(locations);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public List<SourceLocation> getLocations() {
|
||||
return this.locations;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public ErrorClassification getErrorType() {
|
||||
// Attempt the reverse of how errorType is serialized in GraphqlErrorHelper.toSpecification.
|
||||
// However we can only do that for ErrorClassification enums that we know of.
|
||||
String value = (getExtensions() != null ? (String) getExtensions().get("classification") : null);
|
||||
if (value != null) {
|
||||
try {
|
||||
return graphql.ErrorType.valueOf(value);
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
return org.springframework.graphql.execution.ErrorType.valueOf(value);
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
void setPath(List<Object> path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public List<Object> getPath() {
|
||||
return this.path;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
void setExtensions(Map<String, Object> extensions) {
|
||||
this.extensions = extensions;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Map<String, Object> getExtensions() {
|
||||
return this.extensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the error is marked as filtered out as expected.
|
||||
* @return whether the error is marked as expected
|
||||
*/
|
||||
boolean isExpected() {
|
||||
return this.expected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> toSpecification() {
|
||||
GraphqlErrorBuilder builder = GraphqlErrorBuilder.newError();
|
||||
if (this.message != null) {
|
||||
builder.message(this.message);
|
||||
}
|
||||
if (this.locations != null) {
|
||||
this.locations.forEach(builder::location);
|
||||
}
|
||||
if (this.path != null) {
|
||||
builder.path(ResultPath.fromList(this.path));
|
||||
}
|
||||
if (this.extensions != null) {
|
||||
builder.extensions(this.extensions);
|
||||
}
|
||||
return builder.build().toSpecification();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this error as expected if it matches the predicate.
|
||||
* @param predicate the error predicate
|
||||
* @return whether the predicate matched
|
||||
*/
|
||||
boolean apply(Predicate<GraphQLError> predicate) {
|
||||
boolean match = predicate.test(this);
|
||||
this.expected |= match;
|
||||
return match;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toSpecification().toString();
|
||||
}
|
||||
|
||||
private static final class TestSourceLocation {
|
||||
|
||||
private int line;
|
||||
|
||||
private int column;
|
||||
|
||||
@Nullable
|
||||
private String sourceName;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
void setLine(int line) {
|
||||
this.line = line;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
int getLine() {
|
||||
return this.line;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
void setColumn(int column) {
|
||||
this.column = column;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
int getColumn() {
|
||||
return this.column;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
void setSourceName(String sourceName) {
|
||||
this.sourceName = sourceName;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@SuppressWarnings("unused")
|
||||
String getSourceName() {
|
||||
return this.sourceName;
|
||||
}
|
||||
|
||||
static List<SourceLocation> toSourceLocations(List<TestSourceLocation> locations) {
|
||||
return locations.stream()
|
||||
.map(loc -> new SourceLocation(loc.line, loc.column, loc.sourceName))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
/*
|
||||
* 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.graphql.test.tester;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
import com.jayway.jsonpath.TypeRef;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.ResolvableType;
|
||||
|
||||
/**
|
||||
* Adapt a JSONPath {@link TypeRef} to {@link ParameterizedTypeReference} and
|
||||
* {@link ResolvableType} for classes with generics.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class TypeRefAdapter<T> extends TypeRef<T> {
|
||||
|
||||
private final Type type;
|
||||
|
||||
|
||||
TypeRefAdapter(Class<T> clazz) {
|
||||
this.type = clazz;
|
||||
}
|
||||
|
||||
TypeRefAdapter(ParameterizedTypeReference<T> typeReference) {
|
||||
this.type = typeReference.getType();
|
||||
}
|
||||
|
||||
TypeRefAdapter(Class<?> clazz, Class<?> generic) {
|
||||
this.type = ResolvableType.forClassWithGenerics(clazz, generic).getType();
|
||||
}
|
||||
|
||||
TypeRefAdapter(Class<?> clazz, ParameterizedTypeReference<?> generic) {
|
||||
this.type = ResolvableType.forClassWithGenerics(clazz, ResolvableType.forType(generic)).getType();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Type getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -193,7 +193,7 @@ public class GraphQlTesterTests extends GraphQlTesterTestSupport {
|
||||
setMockResponse(GraphqlErrorBuilder.newError().message("Invalid query").build());
|
||||
|
||||
assertThatThrownBy(() -> graphQlTester().document(document).executeAndVerify())
|
||||
.hasMessageContaining("Response has 1 unexpected error(s).");
|
||||
.hasMessageContaining("Response has 1 unexpected error(s)");
|
||||
|
||||
assertThat(requestInput().getDocument()).contains(document);
|
||||
}
|
||||
@@ -205,7 +205,7 @@ public class GraphQlTesterTests extends GraphQlTesterTestSupport {
|
||||
setMockResponse(GraphqlErrorBuilder.newError().message("Invalid query").build());
|
||||
|
||||
assertThatThrownBy(() -> graphQlTester().document(document).execute().path("me"))
|
||||
.hasMessageContaining("Response has 1 unexpected error(s).");
|
||||
.hasMessageContaining("Response has 1 unexpected error(s)");
|
||||
|
||||
assertThat(requestInput().getDocument()).contains(document);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.springframework.graphql.client;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -23,12 +24,14 @@ import java.util.function.Consumer;
|
||||
import com.jayway.jsonpath.Configuration;
|
||||
import com.jayway.jsonpath.DocumentContext;
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
import com.jayway.jsonpath.TypeRef;
|
||||
import graphql.ExecutionResult;
|
||||
import graphql.GraphQLError;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.graphql.GraphQlRequest;
|
||||
import org.springframework.graphql.support.DocumentSource;
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -229,4 +232,34 @@ final class DefaultGraphQlClient implements GraphQlClient {
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adapt JSONPath {@link TypeRef} to {@link ParameterizedTypeReference}.
|
||||
*/
|
||||
private static final class TypeRefAdapter<T> extends TypeRef<T> {
|
||||
|
||||
private final Type type;
|
||||
|
||||
TypeRefAdapter(Class<T> clazz) {
|
||||
this.type = clazz;
|
||||
}
|
||||
|
||||
TypeRefAdapter(ParameterizedTypeReference<T> typeReference) {
|
||||
this.type = typeReference.getType();
|
||||
}
|
||||
|
||||
TypeRefAdapter(Class<?> clazz, Class<?> generic) {
|
||||
this.type = ResolvableType.forClassWithGenerics(clazz, generic).getType();
|
||||
}
|
||||
|
||||
TypeRefAdapter(Class<?> clazz, ParameterizedTypeReference<?> generic) {
|
||||
this.type = ResolvableType.forClassWithGenerics(clazz, ResolvableType.forType(generic)).getType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2022 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.client;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
import com.jayway.jsonpath.TypeRef;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.ResolvableType;
|
||||
|
||||
/**
|
||||
* Adapt a JSONPath {@link TypeRef} to {@link ParameterizedTypeReference} and
|
||||
* {@link ResolvableType} for classes with generics.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class TypeRefAdapter<T> extends TypeRef<T> {
|
||||
|
||||
private final Type type;
|
||||
|
||||
|
||||
TypeRefAdapter(Class<T> clazz) {
|
||||
this.type = clazz;
|
||||
}
|
||||
|
||||
TypeRefAdapter(ParameterizedTypeReference<T> typeReference) {
|
||||
this.type = typeReference.getType();
|
||||
}
|
||||
|
||||
TypeRefAdapter(Class<?> clazz, Class<?> generic) {
|
||||
this.type = ResolvableType.forClassWithGenerics(clazz, generic).getType();
|
||||
}
|
||||
|
||||
TypeRefAdapter(Class<?> clazz, ParameterizedTypeReference<?> generic) {
|
||||
this.type = ResolvableType.forClassWithGenerics(clazz, ResolvableType.forType(generic)).getType();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Type getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -29,7 +29,6 @@ import graphql.language.SourceLocation;
|
||||
import org.springframework.graphql.execution.ErrorType;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* Implementation of {@link GraphQLError} backed by a {@link Map}.
|
||||
@@ -63,9 +62,9 @@ public final class MapGraphQlError implements GraphQLError {
|
||||
(int) map.getOrDefault("column", 0),
|
||||
(String) map.get("sourceName")))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public String getMessage() {
|
||||
|
||||
Reference in New Issue
Block a user