mirror of
https://github.com/graphql-python/graphene-django.git
synced 2024-11-25 02:54:06 +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
|
authorization
|
||||||
debug
|
debug
|
||||||
introspection
|
introspection
|
||||||
|
validation
|
||||||
testing
|
testing
|
||||||
settings
|
settings
|
||||||
|
|
|
@ -269,3 +269,14 @@ Default: ``False``
|
||||||
|
|
||||||
|
|
||||||
.. _GraphiQLDocs: https://graphiql-test.netlify.app/typedoc/modules/graphiql_react#graphiqlprovider-2
|
.. _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,
|
"GRAPHIQL_INPUT_VALUE_DEPRECATION": False,
|
||||||
"ATOMIC_MUTATIONS": False,
|
"ATOMIC_MUTATIONS": False,
|
||||||
"TESTING_ENDPOINT": "/graphql",
|
"TESTING_ENDPOINT": "/graphql",
|
||||||
|
"MAX_VALIDATION_ERRORS": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.DEBUG:
|
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):
|
def test_query_errors_non_atomic(set_rollback_mock, client):
|
||||||
client.get(url_string(query="force error"))
|
client.get(url_string(query="force error"))
|
||||||
set_rollback_mock.assert_not_called()
|
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
|
batch = False
|
||||||
subscription_path = None
|
subscription_path = None
|
||||||
execution_context_class = None
|
execution_context_class = None
|
||||||
|
validation_rules = None
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -107,6 +108,7 @@ class GraphQLView(View):
|
||||||
batch=False,
|
batch=False,
|
||||||
subscription_path=None,
|
subscription_path=None,
|
||||||
execution_context_class=None,
|
execution_context_class=None,
|
||||||
|
validation_rules=None,
|
||||||
):
|
):
|
||||||
if not schema:
|
if not schema:
|
||||||
schema = graphene_settings.SCHEMA
|
schema = graphene_settings.SCHEMA
|
||||||
|
@ -135,6 +137,8 @@ class GraphQLView(View):
|
||||||
), "A Schema is required to be provided to GraphQLView."
|
), "A Schema is required to be provided to GraphQLView."
|
||||||
assert not all((graphiql, batch)), "Use either graphiql or batch processing"
|
assert not all((graphiql, batch)), "Use either graphiql or batch processing"
|
||||||
|
|
||||||
|
self.validation_rules = validation_rules or self.validation_rules
|
||||||
|
|
||||||
# noinspection PyUnusedLocal
|
# noinspection PyUnusedLocal
|
||||||
def get_root_value(self, request):
|
def get_root_value(self, request):
|
||||||
return self.root_value
|
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:
|
if validation_errors:
|
||||||
return ExecutionResult(data=None, errors=validation_errors)
|
return ExecutionResult(data=None, errors=validation_errors)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user