diff --git a/src/main/java/graphql/validation/ValidationErrorType.java b/src/main/java/graphql/validation/ValidationErrorType.java index 5710a1b0b9..e701a5d778 100644 --- a/src/main/java/graphql/validation/ValidationErrorType.java +++ b/src/main/java/graphql/validation/ValidationErrorType.java @@ -43,5 +43,6 @@ public enum ValidationErrorType implements ValidationErrorClassification { NullValueForNonNullArgument, SubscriptionMultipleRootFields, SubscriptionIntrospectionRootField, - UniqueObjectFieldName + UniqueObjectFieldName, + UnknownOperation } diff --git a/src/main/java/graphql/validation/Validator.java b/src/main/java/graphql/validation/Validator.java index d7c3db2fdc..52709109d6 100644 --- a/src/main/java/graphql/validation/Validator.java +++ b/src/main/java/graphql/validation/Validator.java @@ -1,7 +1,6 @@ package graphql.validation; -import graphql.ExperimentalApi; import graphql.Internal; import graphql.i18n.I18n; import graphql.language.Document; @@ -10,6 +9,7 @@ import graphql.validation.rules.DeferDirectiveLabel; import graphql.validation.rules.DeferDirectiveOnRootLevel; import graphql.validation.rules.DeferDirectiveOnValidOperation; +import graphql.validation.rules.KnownOperationTypes; import graphql.validation.rules.UniqueObjectFieldName; import graphql.validation.rules.ExecutableDefinitions; import graphql.validation.rules.FieldsOnCorrectType; @@ -52,7 +52,7 @@ public class Validator { * `graphql-java` will stop validation after a maximum number of validation messages has been reached. Attackers * can send pathologically invalid queries to induce a Denial of Service attack and fill memory with 10000s of errors * and burn CPU in process. - * + *

* By default, this is set to 100 errors. You can set a new JVM wide value as the maximum allowed validation errors. * * @param maxValidationErrors the maximum validation errors allow JVM wide @@ -169,6 +169,10 @@ public List createRules(ValidationContext validationContext, Valid DeferDirectiveLabel deferDirectiveLabel = new DeferDirectiveLabel(validationContext, validationErrorCollector); rules.add(deferDirectiveLabel); + + KnownOperationTypes knownOperationTypes = new KnownOperationTypes(validationContext, validationErrorCollector); + rules.add(knownOperationTypes); + return rules; } } diff --git a/src/main/java/graphql/validation/rules/KnownOperationTypes.java b/src/main/java/graphql/validation/rules/KnownOperationTypes.java new file mode 100644 index 0000000000..2ef9af48fe --- /dev/null +++ b/src/main/java/graphql/validation/rules/KnownOperationTypes.java @@ -0,0 +1,48 @@ +package graphql.validation.rules; + +import graphql.Internal; +import graphql.language.OperationDefinition; +import graphql.schema.GraphQLSchema; +import graphql.util.StringKit; +import graphql.validation.AbstractRule; +import graphql.validation.ValidationContext; +import graphql.validation.ValidationErrorCollector; + +import static graphql.validation.ValidationErrorType.UnknownOperation; + +/** + * Unique variable names + *

+ * A GraphQL operation is only valid if all its variables are uniquely named. + */ +@Internal +public class KnownOperationTypes extends AbstractRule { + + public KnownOperationTypes(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { + super(validationContext, validationErrorCollector); + } + + @Override + public void checkOperationDefinition(OperationDefinition operationDefinition) { + OperationDefinition.Operation documentOperation = operationDefinition.getOperation(); + GraphQLSchema graphQLSchema = getValidationContext().getSchema(); + if (documentOperation == OperationDefinition.Operation.MUTATION + && graphQLSchema.getMutationType() == null) { + String message = i18n(UnknownOperation, "KnownOperationTypes.noOperation", formatOperation(documentOperation)); + addError(UnknownOperation, operationDefinition.getSourceLocation(), message); + } else if (documentOperation == OperationDefinition.Operation.SUBSCRIPTION + && graphQLSchema.getSubscriptionType() == null) { + String message = i18n(UnknownOperation, "KnownOperationTypes.noOperation", formatOperation(documentOperation)); + addError(UnknownOperation, operationDefinition.getSourceLocation(), message); + } else if (documentOperation == OperationDefinition.Operation.QUERY + && graphQLSchema.getQueryType() == null) { + // This is unlikely to happen, as a validated GraphQLSchema must have a Query type by definition + String message = i18n(UnknownOperation, "KnownOperationTypes.noOperation", formatOperation(documentOperation)); + addError(UnknownOperation, operationDefinition.getSourceLocation(), message); + } + } + + private String formatOperation(OperationDefinition.Operation operation) { + return StringKit.capitalize(operation.name().toLowerCase()); + } +} diff --git a/src/main/resources/i18n/Validation.properties b/src/main/resources/i18n/Validation.properties index 4f5c42ab25..a9403bea5b 100644 --- a/src/main/resources/i18n/Validation.properties +++ b/src/main/resources/i18n/Validation.properties @@ -38,6 +38,8 @@ KnownFragmentNames.undefinedFragment=Validation error ({0}) : Undefined fragment # KnownTypeNames.unknownType=Validation error ({0}) : Unknown type ''{1}'' # +KnownOperationTypes.noOperation=Validation error ({0}): The ''{1}'' operation is not supported by the schema +# LoneAnonymousOperation.withOthers=Validation error ({0}) : Anonymous operation with other operations LoneAnonymousOperation.namedOperation=Validation error ({0}) : Operation ''{1}'' is following anonymous operation # diff --git a/src/main/resources/i18n/Validation_de.properties b/src/main/resources/i18n/Validation_de.properties index fec637643c..7823c9d511 100644 --- a/src/main/resources/i18n/Validation_de.properties +++ b/src/main/resources/i18n/Validation_de.properties @@ -30,6 +30,8 @@ KnownFragmentNames.undefinedFragment=Validierungsfehler ({0}) : Undefiniertes Fr # KnownTypeNames.unknownType=Validierungsfehler ({0}) : Unbekannter Typ ''{1}'' # +KnownOperationTypes.noOperation=Validierungsfehler ({0}): ''{1}'' Operation wird vom Schema nicht unterstützt +# LoneAnonymousOperation.withOthers=Validierungsfehler ({0}) : Anonyme Operation mit anderen Operationen LoneAnonymousOperation.namedOperation=Validierungsfehler ({0}) : Operation ''{1}'' folgt der anonymen Operation # diff --git a/src/test/groovy/graphql/GraphQLTest.groovy b/src/test/groovy/graphql/GraphQLTest.groovy index 8235ac5235..51b0fe9ce3 100644 --- a/src/test/groovy/graphql/GraphQLTest.groovy +++ b/src/test/groovy/graphql/GraphQLTest.groovy @@ -350,7 +350,7 @@ class GraphQLTest extends Specification { thrown(GraphQLException) } - def "null mutation type does not throw an npe re: #345 but returns and error"() { + def "null mutation type does not throw an npe but returns and error"() { given: GraphQLSchema schema = newSchema().query( @@ -370,7 +370,7 @@ class GraphQLTest extends Specification { then: result.errors.size() == 1 - result.errors[0].class == MissingRootTypeException + ((ValidationError) result.errors[0]).validationErrorType == ValidationErrorType.UnknownOperation } def "#875 a subscription query against a schema that doesn't support subscriptions should result in a GraphQL error"() { @@ -393,7 +393,7 @@ class GraphQLTest extends Specification { then: result.errors.size() == 1 - result.errors[0].class == MissingRootTypeException + ((ValidationError) result.errors[0]).validationErrorType == ValidationErrorType.UnknownOperation } def "query with int literal too large"() { diff --git a/src/test/groovy/graphql/ParseAndValidateTest.groovy b/src/test/groovy/graphql/ParseAndValidateTest.groovy index 949b4aeb5e..fa66c3cbed 100644 --- a/src/test/groovy/graphql/ParseAndValidateTest.groovy +++ b/src/test/groovy/graphql/ParseAndValidateTest.groovy @@ -1,6 +1,11 @@ package graphql +import graphql.language.Document +import graphql.language.SourceLocation import graphql.parser.InvalidSyntaxException +import graphql.parser.Parser +import graphql.schema.idl.SchemaParser +import graphql.schema.idl.UnExecutableSchemaGenerator import graphql.validation.ValidationError import graphql.validation.ValidationErrorType import graphql.validation.rules.NoUnusedFragments @@ -155,4 +160,79 @@ class ParseAndValidateTest extends Specification { then: !rs.errors.isEmpty() // all rules apply - we have errors } + + def "validation error raised if mutation operation does not exist in schema"() { + def sdl = ''' + type Query { + myQuery : String! + } + ''' + + def registry = new SchemaParser().parse(sdl) + def schema = UnExecutableSchemaGenerator.makeUnExecutableSchema(registry) + String request = "mutation MyMutation { myMutation }" + + when: + Document inputDocument = new Parser().parseDocument(request) + List errors = ParseAndValidate.validate(schema, inputDocument) + + then: + errors.size() == 1 + def error = errors.first() + error.validationErrorType == ValidationErrorType.UnknownOperation + error.message == "Validation error (UnknownOperation): The 'Mutation' operation is not supported by the schema" + error.locations == [new SourceLocation(1, 1)] + } + + def "validation error raised if subscription operation does not exist in schema"() { + def sdl = ''' + type Query { + myQuery : String! + } + ''' + + def registry = new SchemaParser().parse(sdl) + def schema = UnExecutableSchemaGenerator.makeUnExecutableSchema(registry) + + String request = "subscription MySubscription { mySubscription }" + + when: + Document inputDocument = new Parser().parseDocument(request) + List errors = ParseAndValidate.validate(schema, inputDocument) + + then: + errors.size() == 1 + def error = errors.first() + error.validationErrorType == ValidationErrorType.UnknownOperation + error.message == "Validation error (UnknownOperation): The 'Subscription' operation is not supported by the schema" + error.locations == [new SourceLocation(1, 1)] + } + + def "known operation validation rule checks all operations in document"() { + def sdl = ''' + type Query { + myQuery : String! + } + ''' + + def registry = new SchemaParser().parse(sdl) + def schema = UnExecutableSchemaGenerator.makeUnExecutableSchema(registry) + String request = "mutation MyMutation { myMutation } subscription MySubscription { mySubscription }" + + when: + Document inputDocument = new Parser().parseDocument(request) + List errors = ParseAndValidate.validate(schema, inputDocument) + + then: + errors.size() == 2 + def error1 = errors.get(0) + error1.validationErrorType == ValidationErrorType.UnknownOperation + error1.message == "Validation error (UnknownOperation): The 'Mutation' operation is not supported by the schema" + error1.locations == [new SourceLocation(1, 1)] + + def error2 = errors.get(1) + error2.validationErrorType == ValidationErrorType.UnknownOperation + error2.message == "Validation error (UnknownOperation): The 'Subscription' operation is not supported by the schema" + error2.locations == [new SourceLocation(1, 36)] + } } diff --git a/src/test/groovy/graphql/validation/rules/KnownDirectivesTest.groovy b/src/test/groovy/graphql/validation/rules/KnownDirectivesTest.groovy index 2149a0a171..8ac2b0d037 100644 --- a/src/test/groovy/graphql/validation/rules/KnownDirectivesTest.groovy +++ b/src/test/groovy/graphql/validation/rules/KnownDirectivesTest.groovy @@ -246,6 +246,10 @@ class KnownDirectivesTest extends Specification { field: String } + type Subscription { + field: String + } + ''' def schema = TestUtil.schema(sdl)