From 11e04df9c1d3ab9144eb700d83330ba39ac202be Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Sat, 27 Jun 2020 13:33:30 +0100 Subject: [PATCH] Add some validation to input arguments --- graphene/experimental/decorators/mutation.py | 38 +++++- .../decorators/tests/test_mutation.py | 125 +++++++++++++++++- 2 files changed, 159 insertions(+), 4 deletions(-) diff --git a/graphene/experimental/decorators/mutation.py b/graphene/experimental/decorators/mutation.py index 5bf3e3bd..140a0acb 100644 --- a/graphene/experimental/decorators/mutation.py +++ b/graphene/experimental/decorators/mutation.py @@ -1,9 +1,33 @@ +from typing import List + from graphene.types.field import Field +from graphene.types.inputobjecttype import InputObjectType +from graphene.types.scalars import Scalar from graphene.utils.str_converters import to_camel_case +class MutationInvalidArgumentsError(Exception): + def __init__(self, mutation_name: str, invalid_arguments: List[str]): + invalid_arguments = sorted(invalid_arguments) + + if len(invalid_arguments) == 1: + message = ( + f"Argument `{invalid_arguments[0]}` is not a valid type " + f"in mutation `{mutation_name}`. " + ) + else: + head = ", ".join(invalid_arguments[:-1]) + message = ( + f"Arguments `{head}` and `{invalid_arguments[-1]}` are not valid types " + f"in mutation `{mutation_name}`. " + ) + + message += "Arguments to a mutation need to be either a Scalar type or an InputObjectType." + + super().__init__(message) + + def mutation(return_type, arguments=None, **kwargs): - # TODO: validate input arguments if arguments is None: arguments = {} @@ -11,13 +35,23 @@ def mutation(return_type, arguments=None, **kwargs): name = kwargs.pop("name", None) or resolver_function.__name__ description = kwargs.pop("description", None) or resolver_function.__doc__ + invalid_arguments = [] + for argument_name, argument in arguments.items(): + if not ( + isinstance(argument, Scalar) or isinstance(argument, InputObjectType) + ): + invalid_arguments.append(argument_name) + + if len(invalid_arguments) > 0: + raise MutationInvalidArgumentsError(name, invalid_arguments) + return Field( return_type, args=arguments, name=to_camel_case(name), resolver=resolver_function, description=description, - **kwargs + **kwargs, ) return decorate diff --git a/graphene/experimental/decorators/tests/test_mutation.py b/graphene/experimental/decorators/tests/test_mutation.py index e4c03fd9..8ca3b89c 100644 --- a/graphene/experimental/decorators/tests/test_mutation.py +++ b/graphene/experimental/decorators/tests/test_mutation.py @@ -1,8 +1,10 @@ from textwrap import dedent -from graphene import String, ObjectType, Schema, Union, Field +import pytest -from ..mutation import mutation +from graphene import Boolean, Field, InputObjectType, ObjectType, Schema, String, Union + +from ..mutation import mutation, MutationInvalidArgumentsError def test_mutation_basic(): @@ -160,3 +162,122 @@ def test_mutation_complex_return(): } """ ) + + +def test_mutation_complex_input(): + class User(ObjectType): + name = String(required=True) + email = String(required=True) + + class CreateUserSuccess(ObjectType): + user = Field(User, required=True) + + class CreateUserError(ObjectType): + error_message = String(required=True) + + class CreateUserOutput(Union): + class Meta: + types = [ + CreateUserSuccess, + CreateUserError, + ] + + class CreateUserInput(InputObjectType): + name = String(required=True) + email = String(required=True) + + @mutation( + CreateUserOutput, + required=True, + arguments={"user": CreateUserInput(required=True)}, + ) + def create_user(root, info, user): + return CreateUserSuccess(user=User(**user)) + + class Query(ObjectType): + a = String() + + schema = Schema(query=Query, mutations=[create_user]) + result = schema.execute( + """ + mutation CreateUserMutation { + createUser(user: { name: "Kate", email: "kate@example.com" }) { + __typename + ... on CreateUserSuccess { + user { + name + } + } + } + } + """ + ) + + assert not result.errors + assert result.data == { + "createUser": {"__typename": "CreateUserSuccess", "user": {"name": "Kate"}} + } + + assert str(schema) == dedent( + """\ + type Query { + a: String + } + + type Mutation { + createUser(user: CreateUserInput!): CreateUserOutput! + } + + union CreateUserOutput = CreateUserSuccess | CreateUserError + + type CreateUserSuccess { + user: User! + } + + type User { + name: String! + email: String! + } + + type CreateUserError { + errorMessage: String! + } + + input CreateUserInput { + name: String! + email: String! + } + """ + ) + + +def test_raises_error_invalid_input(): + class User(ObjectType): + name = String(required=True) + email = String(required=True) + + with pytest.raises(MutationInvalidArgumentsError) as validation_error: + + @mutation( + Boolean, required=True, arguments={"user": User}, + ) + def create_user(root, info, user): + return True + + assert str(validation_error.value) == ( + "Argument `user` is not a valid type in mutation `create_user`. " + "Arguments to a mutation need to be either a Scalar type or an InputObjectType." + ) + + with pytest.raises(MutationInvalidArgumentsError) as validation_error: + + @mutation( + Boolean, required=True, arguments={"user": User, "user2": User}, + ) + def create_user2(root, info, user): + return True + + assert str(validation_error.value) == ( + "Arguments `user` and `user2` are not valid types in mutation `create_user2`. " + "Arguments to a mutation need to be either a Scalar type or an InputObjectType." + )