diff --git a/src/main/java/graphql/schema/validation/DefaultValuesAreValid.java b/src/main/java/graphql/schema/validation/DefaultValuesAreValid.java index 452797346..fb373ba91 100644 --- a/src/main/java/graphql/schema/validation/DefaultValuesAreValid.java +++ b/src/main/java/graphql/schema/validation/DefaultValuesAreValid.java @@ -47,7 +47,7 @@ public TraversalControl visitGraphQLInputObjectField(GraphQLInputObjectField inp !validationUtil.isValidLiteralValue((Value) defaultValue.getValue(), inputObjectField.getType(), schema, graphQLContext, Locale.getDefault())) { invalid = true; } else if (defaultValue.isExternal() && - !isValidExternalValue(schema, defaultValue.getValue(), inputObjectField.getType(), graphQLContext)) { + !isValidExternalValue(schema, defaultValue.getValue(), inputObjectField.getType(), graphQLContext, errorCollector)) { invalid = true; } if (invalid) { @@ -70,7 +70,7 @@ public TraversalControl visitGraphQLArgument(GraphQLArgument argument, Traverser !validationUtil.isValidLiteralValue((Value) defaultValue.getValue(), argument.getType(), schema, graphQLContext, Locale.getDefault())) { invalid = true; } else if (defaultValue.isExternal() && - !isValidExternalValue(schema, defaultValue.getValue(), argument.getType(), graphQLContext)) { + !isValidExternalValue(schema, defaultValue.getValue(), argument.getType(), graphQLContext, errorCollector)) { invalid = true; } if (invalid) { @@ -80,7 +80,17 @@ public TraversalControl visitGraphQLArgument(GraphQLArgument argument, Traverser return TraversalControl.CONTINUE; } - private boolean isValidExternalValue(GraphQLSchema schema, Object externalValue, GraphQLInputType type, GraphQLContext graphQLContext) { + private boolean isValidExternalValue( + GraphQLSchema schema, + Object externalValue, + GraphQLInputType type, + GraphQLContext graphQLContext, + SchemaValidationErrorCollector errorCollector + ) { + // Coercion expands nested field defaults. Avoid recursing into a cycle that has already made the schema invalid. + if (errorCollector.containsValidationError(SchemaValidationErrorType.DefaultValueCircularRef)) { + return true; + } try { ValuesResolver.externalValueToInternalValue(schema.getCodeRegistry().getFieldVisibility(), externalValue, type, graphQLContext, Locale.getDefault()); return true; diff --git a/src/main/java/graphql/schema/validation/NoDefaultValueCircularRefs.java b/src/main/java/graphql/schema/validation/NoDefaultValueCircularRefs.java index d33d4526a..4e39dfe6f 100644 --- a/src/main/java/graphql/schema/validation/NoDefaultValueCircularRefs.java +++ b/src/main/java/graphql/schema/validation/NoDefaultValueCircularRefs.java @@ -5,14 +5,17 @@ import graphql.language.ObjectField; import graphql.language.ObjectValue; import graphql.language.Value; +import graphql.schema.GraphQLArgument; import graphql.schema.GraphQLInputObjectField; import graphql.schema.GraphQLInputObjectType; import graphql.schema.GraphQLSchemaElement; import graphql.schema.GraphQLType; import graphql.schema.GraphQLTypeVisitorStub; import graphql.schema.InputValueWithState; +import graphql.util.FpKit; import graphql.util.TraversalControl; import graphql.util.TraverserContext; +import org.jspecify.annotations.Nullable; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -42,198 +45,146 @@ @Internal public class NoDefaultValueCircularRefs extends GraphQLTypeVisitorStub { - // Coordinates already fully traversed without finding a cycle, used to avoid duplicate error reports - // when the same coordinate is reachable from multiple input object types. - private final Set fullyExplored = new LinkedHashSet<>(); - - // The spec's "visitedFields" set, tracked as coordinate strings ("Type.field"). - // The spec creates a new immutable set at each step; this implementation mutates and backtracks - // for the same effect. - private final LinkedHashSet visitedFields = new LinkedHashSet<>(); + private final Set checkedFields = new LinkedHashSet<>(); + private final LinkedHashSet fieldPath = new LinkedHashSet<>(); @Override public TraversalControl visitGraphQLInputObjectType(GraphQLInputObjectType type, TraverserContext context) { - SchemaValidationErrorCollector errorCollector = context.getVarFromParents(SchemaValidationErrorCollector.class); - - // Implements InputObjectDefaultValueHasCycle(inputObject) from the spec: - // "If defaultValue is not provided, initialize it to an empty unordered map." - inputObjectDefaultValueHasCycle(type, ObjectValue.newObjectValue().build(), errorCollector); - + checkType(type, getErrorCollector(context)); return TraversalControl.CONTINUE; } - /** - * Implements {@code InputObjectDefaultValueHasCycle(inputObject, defaultValue, visitedFields)} - * from the spec, for literal (AST) default values. - */ - private void inputObjectDefaultValueHasCycle( - GraphQLInputObjectType inputObject, - Value defaultValue, - SchemaValidationErrorCollector errorCollector - ) { - // "If defaultValue is a list: for each itemValue in defaultValue..." - if (defaultValue instanceof ArrayValue) { - for (Value itemValue : ((ArrayValue) defaultValue).getValues()) { - inputObjectDefaultValueHasCycle(inputObject, itemValue, errorCollector); - } - return; - } - - // "Otherwise, if defaultValue is an unordered map..." - if (!(defaultValue instanceof ObjectValue)) { - return; - } - - ObjectValue objectValue = (ObjectValue) defaultValue; - Map> defaultValueMap = new LinkedHashMap<>(); - for (ObjectField field : objectValue.getObjectFields()) { - defaultValueMap.put(field.getName(), field.getValue()); + @Override + public TraversalControl visitGraphQLArgument(GraphQLArgument argument, TraverserContext context) { + GraphQLType namedType = unwrapAll(argument.getType()); + if (namedType instanceof GraphQLInputObjectType) { + checkType((GraphQLInputObjectType) namedType, getErrorCollector(context)); } + return TraversalControl.CONTINUE; + } - // "For each field in inputObject: if InputFieldDefaultValueHasCycle(...)" - for (GraphQLInputObjectField field : inputObject.getFieldDefinitions()) { - String fieldName = field.getName(); - boolean hasDefaultValue = defaultValueMap.containsKey(fieldName); - if (!hasDefaultValue && field.getInputFieldDefaultValue().isNotSet()) { - continue; - } - - GraphQLType namedFieldType = unwrapAll(field.getType()); - if (!(namedFieldType instanceof GraphQLInputObjectType)) { + private void checkType(GraphQLInputObjectType type, SchemaValidationErrorCollector errorCollector) { + for (GraphQLInputObjectField field : type.getFieldDefinitions()) { + GraphQLInputObjectType fieldType = getInputObjectType(field); + if (fieldType == null) { continue; } - - GraphQLInputObjectType fieldInputObject = (GraphQLInputObjectType) namedFieldType; - if (hasDefaultValue) { - // "Let fieldDefaultValue be the value for fieldName in defaultValue. - // If fieldDefaultValue exists: InputObjectDefaultValueHasCycle(namedFieldType, fieldDefaultValue, visitedFields)" - inputObjectDefaultValueHasCycle(fieldInputObject, defaultValueMap.get(fieldName), errorCollector); - } else { - // "Otherwise: let fieldDefaultValue be the default value of field..." - inputFieldDefaultValueHasCycle(field, fieldInputObject, inputObject.getName(), errorCollector); - } + checkFieldDefaultValue(field, fieldType, type.getName(), errorCollector); } } - /** - * Implements {@code InputObjectDefaultValueHasCycle(inputObject, defaultValue, visitedFields)} - * from the spec, for external (programmatic Map/List) default values. - */ - private void inputObjectDefaultValueHasCycle( + private void checkValue( GraphQLInputObjectType inputObject, - Object defaultValue, + @Nullable Object value, SchemaValidationErrorCollector errorCollector ) { - // "If defaultValue is a list: for each itemValue in defaultValue..." - if (defaultValue instanceof Iterable) { - for (Object itemValue : (Iterable) defaultValue) { - if (itemValue != null) { - inputObjectDefaultValueHasCycle(inputObject, itemValue, errorCollector); - } + if (value == null) { + return; + } + if (value instanceof ArrayValue) { + for (Value itemValue : ((ArrayValue) value).getValues()) { + checkValue(inputObject, itemValue, errorCollector); } return; } - - // "Otherwise, if defaultValue is an unordered map..." - if (!(defaultValue instanceof Map)) { + if (FpKit.isIterable(value)) { + for (Object itemValue : FpKit.toIterable(value)) { + checkValue(inputObject, itemValue, errorCollector); + } return; } - @SuppressWarnings("unchecked") - Map defaultValueMap = (Map) defaultValue; + Map valueMap = getValueMap(value); + if (valueMap == null) { + return; + } + checkObjectValue(inputObject, valueMap, errorCollector); + } - // "For each field in inputObject: if InputFieldDefaultValueHasCycle(...)" + private void checkObjectValue( + GraphQLInputObjectType inputObject, + Map valueMap, + SchemaValidationErrorCollector errorCollector + ) { for (GraphQLInputObjectField field : inputObject.getFieldDefinitions()) { - String fieldName = field.getName(); - boolean hasDefaultValue = defaultValueMap.containsKey(fieldName); - if (!hasDefaultValue && field.getInputFieldDefaultValue().isNotSet()) { + boolean hasValue = valueMap.containsKey(field.getName()); + if (!hasValue && field.getInputFieldDefaultValue().isNotSet()) { continue; } - GraphQLType namedFieldType = unwrapAll(field.getType()); - if (!(namedFieldType instanceof GraphQLInputObjectType)) { + GraphQLInputObjectType fieldType = getInputObjectType(field); + if (fieldType == null) { continue; } - - GraphQLInputObjectType fieldInputObject = (GraphQLInputObjectType) namedFieldType; - if (hasDefaultValue) { - // "Let fieldDefaultValue be the value for fieldName in defaultValue. - // If fieldDefaultValue exists: InputObjectDefaultValueHasCycle(namedFieldType, fieldDefaultValue, visitedFields)" - Object fieldDefaultValue = defaultValueMap.get(fieldName); - if (fieldDefaultValue != null) { - inputObjectDefaultValueHasCycle(fieldInputObject, fieldDefaultValue, errorCollector); - } - } else { - // "Otherwise: let fieldDefaultValue be the default value of field..." - inputFieldDefaultValueHasCycle(field, fieldInputObject, inputObject.getName(), errorCollector); + if (hasValue) { + checkValue(fieldType, valueMap.get(field.getName()), errorCollector); + continue; } + checkFieldDefaultValue(field, fieldType, inputObject.getName(), errorCollector); } } - /** - * Implements the "Otherwise" branch of {@code InputFieldDefaultValueHasCycle(field, defaultValue, visitedFields)} - * from the spec — called when the field is not present in the parent's default value, - * so the field's own default will be used at runtime. - */ - private void inputFieldDefaultValueHasCycle( + private void checkFieldDefaultValue( GraphQLInputObjectField field, - GraphQLInputObjectType namedFieldType, + GraphQLInputObjectType fieldType, String parentTypeName, SchemaValidationErrorCollector errorCollector ) { - // "Let fieldDefaultValue be the default value of field. - // If fieldDefaultValue does not exist: return false." - InputValueWithState fieldDefaultValue = field.getInputFieldDefaultValue(); - if (fieldDefaultValue.isNotSet()) { + InputValueWithState defaultValue = field.getInputFieldDefaultValue(); + if (!defaultValue.isLiteral() && !defaultValue.isExternal()) { return; } String coordinate = parentTypeName + "." + field.getName(); + if (fieldPath.contains(coordinate)) { + addError(coordinate, errorCollector); + return; + } + if (!checkedFields.add(coordinate)) { + return; + } - // "If field is within visitedFields: return true." - if (visitedFields.contains(coordinate)) { - // Cycle found — collect intermediate nodes (everything after the coordinate itself) - List intermediaries = new ArrayList<>(); - boolean found = false; - for (String entry : visitedFields) { - if (found) { - intermediaries.add(entry); - } - if (entry.equals(coordinate)) { - found = true; - } - } + fieldPath.add(coordinate); + checkValue(fieldType, defaultValue.getValue(), errorCollector); + fieldPath.remove(coordinate); + } - String message; - if (intermediaries.isEmpty()) { - message = "Invalid circular reference. The default value of Input Object field " - + coordinate + " references itself."; - } else { - message = "Invalid circular reference. The default value of Input Object field " - + coordinate + " references itself via the default values of: " - + String.join(", ", intermediaries) + "."; - } + private void addError(String coordinate, SchemaValidationErrorCollector errorCollector) { + List path = new ArrayList<>(fieldPath); + List intermediaries = path.subList(path.indexOf(coordinate) + 1, path.size()); + String via = intermediaries.isEmpty() + ? "" + : " via the default values of: " + String.join(", ", intermediaries); + String message = "Invalid circular reference. The default value of Input Object field " + + coordinate + " references itself" + via + "."; + errorCollector.addError(new SchemaValidationError( + SchemaValidationErrorType.DefaultValueCircularRef, message)); + } - errorCollector.addError(new SchemaValidationError( - SchemaValidationErrorType.DefaultValueCircularRef, message)); - return; + private @Nullable GraphQLInputObjectType getInputObjectType(GraphQLInputObjectField field) { + GraphQLType type = unwrapAll(field.getType()); + if (type instanceof GraphQLInputObjectType) { + return (GraphQLInputObjectType) type; } + return null; + } - if (fullyExplored.contains(coordinate)) { - return; + private @Nullable Map getValueMap(Object value) { + if (value instanceof Map) { + return (Map) value; + } + if (!(value instanceof ObjectValue)) { + return null; } - fullyExplored.add(coordinate); - - // "Let nextVisitedFields be a new set containing field and everything from visitedFields. - // Return InputObjectDefaultValueHasCycle(namedFieldType, fieldDefaultValue, nextVisitedFields)." - visitedFields.add(coordinate); - if (fieldDefaultValue.isLiteral() && fieldDefaultValue.getValue() instanceof Value) { - inputObjectDefaultValueHasCycle(namedFieldType, (Value) fieldDefaultValue.getValue(), errorCollector); - } else if (fieldDefaultValue.isExternal() && fieldDefaultValue.getValue() != null) { - inputObjectDefaultValueHasCycle(namedFieldType, fieldDefaultValue.getValue(), errorCollector); + Map> valueMap = new LinkedHashMap<>(); + for (ObjectField field : ((ObjectValue) value).getObjectFields()) { + valueMap.put(field.getName(), field.getValue()); } + return valueMap; + } - visitedFields.remove(coordinate); + private SchemaValidationErrorCollector getErrorCollector(TraverserContext context) { + return context.getVarFromParents(SchemaValidationErrorCollector.class); } } diff --git a/src/main/java/graphql/schema/validation/SchemaValidator.java b/src/main/java/graphql/schema/validation/SchemaValidator.java index fac409378..5e8432b0a 100644 --- a/src/main/java/graphql/schema/validation/SchemaValidator.java +++ b/src/main/java/graphql/schema/validation/SchemaValidator.java @@ -14,10 +14,8 @@ @Internal public class SchemaValidator { - - private final List rules = new ArrayList<>(); - - public SchemaValidator() { + public List getRules() { + List rules = new ArrayList<>(); rules.add(new NoUnbrokenInputCycles()); rules.add(new NoDefaultValueCircularRefs()); rules.add(new TypesImplementInterfaces()); @@ -28,9 +26,6 @@ public SchemaValidator() { rules.add(new InputAndOutputTypesUsedAppropriately()); rules.add(new OneOfInputObjectRules()); rules.add(new DeprecatedInputObjectAndArgumentsAreValid()); - } - - public List getRules() { return rules; } @@ -39,7 +34,7 @@ public Set validateSchema(GraphQLSchema schema) { Map, Object> rootVars = new LinkedHashMap<>(); rootVars.put(GraphQLSchema.class, schema); rootVars.put(SchemaValidationErrorCollector.class, validationErrorCollector); - new SchemaTraverser().depthFirstFullSchema(rules, schema, rootVars); + new SchemaTraverser().depthFirstFullSchema(getRules(), schema, rootVars); return validationErrorCollector.getErrors(); } diff --git a/src/test/groovy/graphql/schema/validation/NoDefaultValueCircularRefsTest.groovy b/src/test/groovy/graphql/schema/validation/NoDefaultValueCircularRefsTest.groovy index bd35006c2..29b376236 100644 --- a/src/test/groovy/graphql/schema/validation/NoDefaultValueCircularRefsTest.groovy +++ b/src/test/groovy/graphql/schema/validation/NoDefaultValueCircularRefsTest.groovy @@ -1,115 +1,74 @@ package graphql.schema.validation import graphql.TestUtil +import graphql.schema.GraphQLSchema import spock.lang.Specification +import static graphql.Scalars.GraphQLString +import static graphql.schema.GraphQLArgument.newArgument +import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition +import static graphql.schema.GraphQLInputObjectField.newInputObjectField +import static graphql.schema.GraphQLInputObjectType.newInputObject +import static graphql.schema.GraphQLList.list +import static graphql.schema.GraphQLObjectType.newObject +import static graphql.schema.GraphQLSchema.newSchema +import static graphql.schema.GraphQLTypeReference.typeRef + class NoDefaultValueCircularRefsTest extends Specification { - def "self-referential default value is rejected"() { + + def "circular SDL defaults are rejected: #description"() { when: - TestUtil.schema(''' - type Query { test(arg: A): String } - input A { x: A = {} } - ''') + TestUtil.schema(sdl) then: - def e = thrown(InvalidSchemaException) - e.message.contains("Invalid circular reference. The default value of Input Object field A.x references itself.") - } + def exception = thrown(InvalidSchemaException) + exception.message.contains(expectedMessage) - def "mutual recursion through defaults is rejected"() { - when: - TestUtil.schema(''' + where: + description | sdl | expectedMessage + "self reference" | ''' + type Query { test(arg: A): String } + input A { x: A = {} } + ''' | "Invalid circular reference. The default value of Input Object field A.x references itself." + "mutual types" | ''' type Query { test(arg: A): String } input A { b: B = {} } input B { a: A = {} } - ''') - - then: - def e = thrown(InvalidSchemaException) - e.message.contains("Invalid circular reference") - e.message.contains("A.b") - } - - def "transitive cycle through three types is rejected"() { - when: - TestUtil.schema(''' + ''' | "Invalid circular reference" + "three types" | ''' type Query { test(arg: B): String } input B { x: B2 = {} } input B2 { x: B3 = {} } input B3 { x: B = {} } - ''') - - then: - def e = thrown(InvalidSchemaException) - e.message.contains("Invalid circular reference. The default value of Input Object field B.x references itself via the default values of: B2.x, B3.x.") - } - - def "self-reference through list wrapping"() { - when: - TestUtil.schema(''' + ''' | "Invalid circular reference. The default value of Input Object field B.x references itself via the default values of: B2.x, B3.x." + "list wrapping" | ''' type Query { test(arg: C): String } input C { x: [C] = [{}] } - ''') - - then: - def e = thrown(InvalidSchemaException) - e.message.contains("Invalid circular reference. The default value of Input Object field C.x references itself.") - } - - def "nested default value that eventually cycles"() { - when: - TestUtil.schema(''' + ''' | "Invalid circular reference. The default value of Input Object field C.x references itself." + "nested value" | ''' type Query { test(arg: D): String } input D { x: D = { x: { x: {} } } } - ''') - - then: - def e = thrown(InvalidSchemaException) - e.message.contains("Invalid circular reference. The default value of Input Object field D.x references itself.") - } - - def "cross-field cycle through defaults"() { - when: - TestUtil.schema(''' + ''' | "Invalid circular reference. The default value of Input Object field D.x references itself." + "cross field" | ''' type Query { test(arg: E): String } input E { x: E = { x: null } y: E = { y: null } } - ''') - - then: - def e = thrown(InvalidSchemaException) - e.message.contains("Invalid circular reference. The default value of Input Object field E.x references itself via the default values of: E.y.") - } - - def "cycle through non-null wrapping"() { - when: - TestUtil.schema(''' + ''' | "Invalid circular reference. The default value of Input Object field E.x references itself via the default values of: E.y." + "non-null type" | ''' type Query { test(arg: F): String } input F { x: F2! = {} } input F2 { x: F = { x: {} } } - ''') - - then: - def e = thrown(InvalidSchemaException) - e.message.contains("Invalid circular reference. The default value of Input Object field F2.x references itself.") - } - - def "partial default with non-provided recursive field"() { - when: - TestUtil.schema(''' + ''' | "Invalid circular reference. The default value of Input Object field F2.x references itself." + "omitted field" | ''' type Query { test(arg: A): String } input A { x: B = {name: "hi"} } input B { name: String a: A = {} } - ''') - - then: - def e = thrown(InvalidSchemaException) - e.message.contains("Invalid circular reference. The default value of Input Object field A.x references itself via the default values of: B.a.") + ''' | "Invalid circular reference. The default value of Input Object field A.x references itself via the default values of: B.a." } def "multiple independent cycles are reported"() { @@ -121,82 +80,88 @@ class NoDefaultValueCircularRefsTest extends Specification { ''') then: - def e = thrown(InvalidSchemaException) - e.message.contains("A.x references itself") - e.message.contains("P.x references itself") + def exception = thrown(InvalidSchemaException) + exception.message.contains("A.x references itself") + exception.message.contains("P.x references itself") } - def "explicit field in default breaks cycle"() { + def "non-circular SDL defaults are accepted: #description"() { when: - def schema = TestUtil.schema(''' - type Query { test(arg: A): String } - input A { b: B = {a: null} } - input B { a: A = {} } - ''') + def schema = TestUtil.schema(sdl) then: - noExceptionThrown() schema.getType("A") != null - } - def "recursive field without default does not cycle"() { - when: - def schema = TestUtil.schema(''' + where: + description | sdl + "explicit nested null" | ''' + type Query { test(arg: A): String } + input A { b: B = {a: null} } + input B { a: A = {} } + ''' + "recursive field unset" | ''' type Query { test(arg: A): String } input A { b: B = {} } input B { a: A } - ''') - - then: - noExceptionThrown() - schema.getType("A") != null - } - - def "scalar default value does not cycle"() { - when: - def schema = TestUtil.schema(''' + ''' + "scalar default" | ''' type Query { test(arg: A): String } input A { name: String = "hi" } - ''') - - then: - noExceptionThrown() - schema.getType("A") != null - } - - def "null literal default does not cycle"() { - when: - def schema = TestUtil.schema(''' + ''' + "null default" | ''' type Query { test(arg: A): String } input A { x: A = null } - ''') - - then: - noExceptionThrown() - schema.getType("A") != null + ''' + "empty list" | ''' + type Query { test(arg: A): String } + input A { x: [A] = [] } + ''' + "explicit self field null" | ''' + type Query { test(arg: A): String } + input A { x: A = {x: null} } + ''' } - def "empty list default does not cycle"() { + def "circular programmatic defaults are rejected: #description"() { when: - def schema = TestUtil.schema(''' - type Query { test(arg: A): String } - input A { x: [A] = [] } - ''') + buildProgrammaticSchema(defaultValue, listType) then: - noExceptionThrown() - schema.getType("A") != null + def exception = thrown(InvalidSchemaException) + exception.message.contains("Invalid circular reference") + + where: + description | defaultValue | listType + "map" | [:] | false + "list" | [[:]] | true + "Java array" | ([[:]] as Object[]) | true } - def "explicit null on recursive field breaks self-reference"() { - when: - def schema = TestUtil.schema(''' - type Query { test(arg: A): String } - input A { x: A = {x: null} } - ''') + def "explicit null breaks a programmatic default cycle"() { + expect: + buildProgrammaticSchema([x: null], false).getType("A") != null + } - then: - noExceptionThrown() - schema.getType("A") != null + private static GraphQLSchema buildProgrammaticSchema(Object defaultValue, boolean listType) { + def recursiveType = typeRef("A") + def fieldType = listType ? list(recursiveType) : recursiveType + def inputType = newInputObject() + .name("A") + .field(newInputObjectField() + .name("x") + .type(fieldType) + .defaultValueProgrammatic(defaultValue)) + .build() + def queryType = newObject() + .name("Query") + .field(newFieldDefinition() + .name("test") + .type(GraphQLString) + .argument(newArgument() + .name("arg") + .type(inputType) + .defaultValueProgrammatic([:]))) + .build() + return newSchema().query(queryType).build() } } diff --git a/src/test/groovy/graphql/schema/validation/SchemaValidatorTest.groovy b/src/test/groovy/graphql/schema/validation/SchemaValidatorTest.groovy index a854129d4..7a9404f93 100644 --- a/src/test/groovy/graphql/schema/validation/SchemaValidatorTest.groovy +++ b/src/test/groovy/graphql/schema/validation/SchemaValidatorTest.groovy @@ -10,17 +10,21 @@ class SchemaValidatorTest extends Specification { when: def validator = new SchemaValidator() def rules = validator.rules + def nextRules = validator.rules + then: - rules.size() == 10 - rules[0] instanceof NoUnbrokenInputCycles - rules[1] instanceof NoDefaultValueCircularRefs - rules[2] instanceof TypesImplementInterfaces - rules[3] instanceof TypeAndFieldRule - rules[4] instanceof DefaultValuesAreValid - rules[5] instanceof AppliedDirectivesAreValid - rules[6] instanceof AppliedDirectiveArgumentsAreValid - rules[7] instanceof InputAndOutputTypesUsedAppropriately - rules[8] instanceof OneOfInputObjectRules - rules[9] instanceof DeprecatedInputObjectAndArgumentsAreValid + rules*.class == [ + NoUnbrokenInputCycles, + NoDefaultValueCircularRefs, + TypesImplementInterfaces, + TypeAndFieldRule, + DefaultValuesAreValid, + AppliedDirectivesAreValid, + AppliedDirectiveArgumentsAreValid, + InputAndOutputTypesUsedAppropriately, + OneOfInputObjectRules, + DeprecatedInputObjectAndArgumentsAreValid, + ] + rules[1] !== nextRules[1] } }