mirror of
https://github.com/graphql-python/graphene-django.git
synced 2024-11-10 19:57:15 +03:00
Add support for validation rules (#1475)
* Add support for validation rules * Enable customizing validate max_errors through settings * Add tests for validation rules * Add examples for validation rules * Allow setting validation_rules in class def * Add tests for validation_rules inherited from parent class * Make tests for validation rules stricter
This commit is contained in:
parent
3a64994e52
commit
feb7252b8a
|
@ -33,5 +33,6 @@ For more advanced use, check out the Relay tutorial.
|
|||
authorization
|
||||
debug
|
||||
introspection
|
||||
validation
|
||||
testing
|
||||
settings
|
||||
|
|
|
@ -269,3 +269,14 @@ Default: ``False``
|
|||
|
||||
|
||||
.. _GraphiQLDocs: https://graphiql-test.netlify.app/typedoc/modules/graphiql_react#graphiqlprovider-2
|
||||
|
||||
|
||||
``MAX_VALIDATION_ERRORS``
|
||||
------------------------------------
|
||||
|
||||
In case ``validation_rules`` are provided to ``GraphQLView``, if this is set to a non-negative ``int`` value,
|
||||
``graphql.validation.validate`` will stop validation after this number of errors has been reached.
|
||||
If not set or set to ``None``, the maximum number of errors will follow ``graphql.validation.validate`` default
|
||||
*i.e.* 100.
|
||||
|
||||
Default: ``None``
|
||||
|
|
29
docs/validation.rst
Normal file
29
docs/validation.rst
Normal file
|
@ -0,0 +1,29 @@
|
|||
Query Validation
|
||||
================
|
||||
|
||||
Graphene-Django supports query validation by allowing passing a list of validation rules (subclasses of `ValidationRule <https://github.com/graphql-python/graphql-core/blob/v3.2.3/src/graphql/validation/rules/__init__.py>`_ from graphql-core) to the ``validation_rules`` option in ``GraphQLView``.
|
||||
|
||||
.. code:: python
|
||||
|
||||
from django.urls import path
|
||||
from graphene.validation import DisableIntrospection
|
||||
from graphene_django.views import GraphQLView
|
||||
|
||||
urlpatterns = [
|
||||
path("graphql", GraphQLView.as_view(validation_rules=(DisableIntrospection,))),
|
||||
]
|
||||
|
||||
or
|
||||
|
||||
.. code:: python
|
||||
|
||||
from django.urls import path
|
||||
from graphene.validation import DisableIntrospection
|
||||
from graphene_django.views import GraphQLView
|
||||
|
||||
class View(GraphQLView):
|
||||
validation_rules = (DisableIntrospection,)
|
||||
|
||||
urlpatterns = [
|
||||
path("graphql", View.as_view()),
|
||||
]
|
|
@ -43,6 +43,7 @@ DEFAULTS = {
|
|||
"GRAPHIQL_INPUT_VALUE_DEPRECATION": False,
|
||||
"ATOMIC_MUTATIONS": False,
|
||||
"TESTING_ENDPOINT": "/graphql",
|
||||
"MAX_VALIDATION_ERRORS": None,
|
||||
}
|
||||
|
||||
if settings.DEBUG:
|
||||
|
|
|
@ -827,3 +827,97 @@ def test_query_errors_atomic_request(set_rollback_mock, client):
|
|||
def test_query_errors_non_atomic(set_rollback_mock, client):
|
||||
client.get(url_string(query="force error"))
|
||||
set_rollback_mock.assert_not_called()
|
||||
|
||||
|
||||
VALIDATION_URLS = [
|
||||
"/graphql/validation/",
|
||||
"/graphql/validation/alternative/",
|
||||
"/graphql/validation/inherited/",
|
||||
]
|
||||
|
||||
QUERY_WITH_TWO_INTROSPECTIONS = """
|
||||
query Instrospection {
|
||||
queryType: __schema {
|
||||
queryType {name}
|
||||
}
|
||||
mutationType: __schema {
|
||||
mutationType {name}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
N_INTROSPECTIONS = 2
|
||||
|
||||
INTROSPECTION_DISALLOWED_ERROR_MESSAGE = "introspection is disabled"
|
||||
MAX_VALIDATION_ERRORS_EXCEEDED_MESSAGE = "too many validation errors"
|
||||
|
||||
|
||||
@pytest.mark.urls("graphene_django.tests.urls_validation")
|
||||
def test_allow_introspection(client):
|
||||
response = client.post(
|
||||
url_string("/graphql/", query="{__schema {queryType {name}}}")
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
assert response_json(response) == {
|
||||
"data": {"__schema": {"queryType": {"name": "QueryRoot"}}}
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("url", VALIDATION_URLS)
|
||||
@pytest.mark.urls("graphene_django.tests.urls_validation")
|
||||
def test_validation_disallow_introspection(client, url):
|
||||
response = client.post(url_string(url, query="{__schema {queryType {name}}}"))
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
json_response = response_json(response)
|
||||
assert "data" not in json_response
|
||||
assert "errors" in json_response
|
||||
assert len(json_response["errors"]) == 1
|
||||
|
||||
error_message = json_response["errors"][0]["message"]
|
||||
assert INTROSPECTION_DISALLOWED_ERROR_MESSAGE in error_message
|
||||
|
||||
|
||||
@pytest.mark.parametrize("url", VALIDATION_URLS)
|
||||
@pytest.mark.urls("graphene_django.tests.urls_validation")
|
||||
@patch(
|
||||
"graphene_django.settings.graphene_settings.MAX_VALIDATION_ERRORS", N_INTROSPECTIONS
|
||||
)
|
||||
def test_within_max_validation_errors(client, url):
|
||||
response = client.post(url_string(url, query=QUERY_WITH_TWO_INTROSPECTIONS))
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
json_response = response_json(response)
|
||||
assert "data" not in json_response
|
||||
assert "errors" in json_response
|
||||
assert len(json_response["errors"]) == N_INTROSPECTIONS
|
||||
|
||||
error_messages = [error["message"].lower() for error in json_response["errors"]]
|
||||
|
||||
n_introspection_error_messages = sum(
|
||||
INTROSPECTION_DISALLOWED_ERROR_MESSAGE in msg for msg in error_messages
|
||||
)
|
||||
assert n_introspection_error_messages == N_INTROSPECTIONS
|
||||
|
||||
assert all(
|
||||
MAX_VALIDATION_ERRORS_EXCEEDED_MESSAGE not in msg for msg in error_messages
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("url", VALIDATION_URLS)
|
||||
@pytest.mark.urls("graphene_django.tests.urls_validation")
|
||||
@patch("graphene_django.settings.graphene_settings.MAX_VALIDATION_ERRORS", 1)
|
||||
def test_exceeds_max_validation_errors(client, url):
|
||||
response = client.post(url_string(url, query=QUERY_WITH_TWO_INTROSPECTIONS))
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
json_response = response_json(response)
|
||||
assert "data" not in json_response
|
||||
assert "errors" in json_response
|
||||
|
||||
error_messages = (error["message"].lower() for error in json_response["errors"])
|
||||
assert any(MAX_VALIDATION_ERRORS_EXCEEDED_MESSAGE in msg for msg in error_messages)
|
||||
|
|
26
graphene_django/tests/urls_validation.py
Normal file
26
graphene_django/tests/urls_validation.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
from django.urls import path
|
||||
|
||||
from graphene.validation import DisableIntrospection
|
||||
|
||||
from ..views import GraphQLView
|
||||
from .schema_view import schema
|
||||
|
||||
|
||||
class View(GraphQLView):
|
||||
schema = schema
|
||||
|
||||
|
||||
class NoIntrospectionView(View):
|
||||
validation_rules = (DisableIntrospection,)
|
||||
|
||||
|
||||
class NoIntrospectionViewInherited(NoIntrospectionView):
|
||||
pass
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("graphql/", View.as_view()),
|
||||
path("graphql/validation/", View.as_view(validation_rules=(DisableIntrospection,))),
|
||||
path("graphql/validation/alternative/", NoIntrospectionView.as_view()),
|
||||
path("graphql/validation/inherited/", NoIntrospectionViewInherited.as_view()),
|
||||
]
|
|
@ -96,6 +96,7 @@ class GraphQLView(View):
|
|||
batch = False
|
||||
subscription_path = None
|
||||
execution_context_class = None
|
||||
validation_rules = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
@ -107,6 +108,7 @@ class GraphQLView(View):
|
|||
batch=False,
|
||||
subscription_path=None,
|
||||
execution_context_class=None,
|
||||
validation_rules=None,
|
||||
):
|
||||
if not schema:
|
||||
schema = graphene_settings.SCHEMA
|
||||
|
@ -135,6 +137,8 @@ class GraphQLView(View):
|
|||
), "A Schema is required to be provided to GraphQLView."
|
||||
assert not all((graphiql, batch)), "Use either graphiql or batch processing"
|
||||
|
||||
self.validation_rules = validation_rules or self.validation_rules
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
def get_root_value(self, request):
|
||||
return self.root_value
|
||||
|
@ -332,7 +336,12 @@ class GraphQLView(View):
|
|||
)
|
||||
)
|
||||
|
||||
validation_errors = validate(schema, document)
|
||||
validation_errors = validate(
|
||||
schema,
|
||||
document,
|
||||
self.validation_rules,
|
||||
graphene_settings.MAX_VALIDATION_ERRORS,
|
||||
)
|
||||
|
||||
if validation_errors:
|
||||
return ExecutionResult(data=None, errors=validation_errors)
|
||||
|
|
Loading…
Reference in New Issue
Block a user