From d7b474751d59b9c94279283295057f224b7688a7 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+aryan340@users.noreply.github.com> Date: Sat, 14 Aug 2021 07:45:34 +0530 Subject: [PATCH] add depth limit validator tests --- docs/execution/validators.rst | 24 +- graphene/utils/is_introspection_key.py | 2 +- .../tests/test_depth_limit_validator.py | 279 ++++++++++++++++++ .../tests/test_disable_introspection.py | 4 - 4 files changed, 297 insertions(+), 12 deletions(-) create mode 100644 graphene/validation/tests/test_depth_limit_validator.py diff --git a/docs/execution/validators.rst b/docs/execution/validators.rst index 92b8ecd2..d7e1310b 100644 --- a/docs/execution/validators.rst +++ b/docs/execution/validators.rst @@ -20,16 +20,26 @@ Example Here is how you would implement depth-limiting on your schema. .. code:: python + from graphql import validate + from graphene import ObjectType, Schema, String from graphene.validation import depth_limit_validator - # The following schema doesn't execute queries - # which have a depth more than 20. - result = schema.execute( - 'THE QUERY', - validation_rules=[ + class MyQuery(ObjectType): + name = String(required=True) + + + schema = Schema(query=MyQuery) + + # Queries which have a depth more than 20 + # will not be executed. + + validation_errors = validate( + schema=schema, + document='THE QUERY', + rules=( depth_limit_validator( max_depth=20 - ) - ] + ), + ) ) diff --git a/graphene/utils/is_introspection_key.py b/graphene/utils/is_introspection_key.py index 68951940..59d72b24 100644 --- a/graphene/utils/is_introspection_key.py +++ b/graphene/utils/is_introspection_key.py @@ -3,4 +3,4 @@ def is_introspection_key(key): # > All types and directives defined within a schema must not have a name which # > begins with "__" (two underscores), as this is used exclusively # > by GraphQL’s introspection system. - return str(node.name.value).startswith("__") + return str(key).startswith("__") diff --git a/graphene/validation/tests/test_depth_limit_validator.py b/graphene/validation/tests/test_depth_limit_validator.py new file mode 100644 index 00000000..3eea3a32 --- /dev/null +++ b/graphene/validation/tests/test_depth_limit_validator.py @@ -0,0 +1,279 @@ +import re + +from pytest import raises +from graphql import parse, get_introspection_query, validate + +from ...types import Schema, ObjectType, Interface +from ...types import String, Int, List, Field +from ..depth_limit import depth_limit_validator + + +class PetType(Interface): + name = String(required=True) + + class meta: + name = "Pet" + + +class CatType(ObjectType): + class meta: + name = "Cat" + interfaces = (PetType,) + + +class DogType(ObjectType): + class meta: + name = "Dog" + interfaces = (PetType,) + + +class AddressType(ObjectType): + street = String(required=True) + number = Int(required=True) + city = String(required=True) + country = String(required=True) + + class Meta: + name = "Address" + + +class HumanType(ObjectType): + name = String(required=True) + email = String(required=True) + address = Field(AddressType, required=True) + pets = List(PetType, required=True) + + class Meta: + name = "Human" + + +class Query(ObjectType): + user = Field( + HumanType, + required=True, + name=String() + ) + version = String( + required=True + ) + user1 = Field( + HumanType, + required=True + ) + user2 = Field( + HumanType, + required=True + ) + user3 = Field( + HumanType, + required=True + ) + + @staticmethod + def resolve_user(root, info, name=None): + pass + + +schema = Schema(query=Query) + + +def run_query(query: str, max_depth: int, ignore=None): + document = parse(query) + + result = None + + def callback(query_depths): + nonlocal result + result = query_depths + + errors = validate( + schema.graphql_schema, + document, + rules=( + depth_limit_validator( + max_depth=max_depth, + ignore=ignore, + callback=callback + ), + ), + ) + + return errors, result + + +def test_should_count_depth_without_fragment(): + query = """ + query read0 { + version + } + query read1 { + version + user { + name + } + } + query read2 { + matt: user(name: "matt") { + email + } + andy: user(name: "andy") { + email + address { + city + } + } + } + query read3 { + matt: user(name: "matt") { + email + } + andy: user(name: "andy") { + email + address { + city + } + pets { + name + owner { + name + } + } + } + } + """ + + expected = {"read0": 0, "read1": 1, "read2": 2, "read3": 3} + + errors, result = run_query(query, 10) + assert not errors + assert result == expected + + +def test_should_count_with_fragments(): + query = """ + query read0 { + ... on Query { + version + } + } + query read1 { + version + user { + ... on Human { + name + } + } + } + fragment humanInfo on Human { + email + } + fragment petInfo on Pet { + name + owner { + name + } + } + query read2 { + matt: user(name: "matt") { + ...humanInfo + } + andy: user(name: "andy") { + ...humanInfo + address { + city + } + } + } + query read3 { + matt: user(name: "matt") { + ...humanInfo + } + andy: user(name: "andy") { + ... on Human { + email + } + address { + city + } + pets { + ...petInfo + } + } + } + """ + + expected = {"read0": 0, "read1": 1, "read2": 2, "read3": 3} + + errors, result = run_query(query, 10) + assert not errors + assert result == expected + + +def test_should_ignore_the_introspection_query(): + errors, result = run_query(get_introspection_query(), 10) + assert not errors + assert result == {"IntrospectionQuery": 0} + + +def test_should_catch_very_deep_query(): + query = """{ + user { + pets { + owner { + pets { + owner { + pets { + name + } + } + } + } + } + } + } + """ + errors, result = run_query(query, 4) + + assert len(errors) == 1 + assert errors[0].message == "'anonymous' exceeds maximum operation depth of 4" + + +def test_should_ignore_field(): + query = """ + query read1 { + user { address { city } } + } + query read2 { + user1 { address { city } } + user2 { address { city } } + user3 { address { city } } + } + """ + + errors, result = run_query( + query, + 10, + ignore=[ + "user1", + re.compile("user2"), + lambda field_name: field_name == "user3", + ], + ) + + expected = {"read1": 2, "read2": 0} + assert not errors + assert result == expected + + +def test_should_raise_invalid_ignore(): + query = """ + query read1 { + user { address { city } } + } + """ + with raises(ValueError, match="Invalid ignore option:"): + run_query( + query, + 10, + ignore=[True], + ) diff --git a/graphene/validation/tests/test_disable_introspection.py b/graphene/validation/tests/test_disable_introspection.py index 4d1faa7d..c13786ed 100644 --- a/graphene/validation/tests/test_disable_introspection.py +++ b/graphene/validation/tests/test_disable_introspection.py @@ -18,10 +18,6 @@ def run_query(query: str): result = None - def callback(query_depths): - nonlocal result - result = query_depths - errors = validate( schema.graphql_schema, document,