Skip to content

Commit feb7252

Browse files
authored
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
1 parent 3a64994 commit feb7252

File tree

7 files changed

+172
-1
lines changed

7 files changed

+172
-1
lines changed

docs/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,6 @@ For more advanced use, check out the Relay tutorial.
3333
authorization
3434
debug
3535
introspection
36+
validation
3637
testing
3738
settings

docs/settings.rst

+11
Original file line numberDiff line numberDiff line change
@@ -269,3 +269,14 @@ Default: ``False``
269269
270270
271271
.. _GraphiQLDocs: https://graphiql-test.netlify.app/typedoc/modules/graphiql_react#graphiqlprovider-2
272+
273+
274+
``MAX_VALIDATION_ERRORS``
275+
------------------------------------
276+
277+
In case ``validation_rules`` are provided to ``GraphQLView``, if this is set to a non-negative ``int`` value,
278+
``graphql.validation.validate`` will stop validation after this number of errors has been reached.
279+
If not set or set to ``None``, the maximum number of errors will follow ``graphql.validation.validate`` default
280+
*i.e.* 100.
281+
282+
Default: ``None``

docs/validation.rst

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
Query Validation
2+
================
3+
4+
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``.
5+
6+
.. code:: python
7+
8+
from django.urls import path
9+
from graphene.validation import DisableIntrospection
10+
from graphene_django.views import GraphQLView
11+
12+
urlpatterns = [
13+
path("graphql", GraphQLView.as_view(validation_rules=(DisableIntrospection,))),
14+
]
15+
16+
or
17+
18+
.. code:: python
19+
20+
from django.urls import path
21+
from graphene.validation import DisableIntrospection
22+
from graphene_django.views import GraphQLView
23+
24+
class View(GraphQLView):
25+
validation_rules = (DisableIntrospection,)
26+
27+
urlpatterns = [
28+
path("graphql", View.as_view()),
29+
]

graphene_django/settings.py

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"GRAPHIQL_INPUT_VALUE_DEPRECATION": False,
4444
"ATOMIC_MUTATIONS": False,
4545
"TESTING_ENDPOINT": "/graphql",
46+
"MAX_VALIDATION_ERRORS": None,
4647
}
4748

4849
if settings.DEBUG:

graphene_django/tests/test_views.py

+94
Original file line numberDiff line numberDiff line change
@@ -827,3 +827,97 @@ def test_query_errors_atomic_request(set_rollback_mock, client):
827827
def test_query_errors_non_atomic(set_rollback_mock, client):
828828
client.get(url_string(query="force error"))
829829
set_rollback_mock.assert_not_called()
830+
831+
832+
VALIDATION_URLS = [
833+
"/graphql/validation/",
834+
"/graphql/validation/alternative/",
835+
"/graphql/validation/inherited/",
836+
]
837+
838+
QUERY_WITH_TWO_INTROSPECTIONS = """
839+
query Instrospection {
840+
queryType: __schema {
841+
queryType {name}
842+
}
843+
mutationType: __schema {
844+
mutationType {name}
845+
}
846+
}
847+
"""
848+
849+
N_INTROSPECTIONS = 2
850+
851+
INTROSPECTION_DISALLOWED_ERROR_MESSAGE = "introspection is disabled"
852+
MAX_VALIDATION_ERRORS_EXCEEDED_MESSAGE = "too many validation errors"
853+
854+
855+
@pytest.mark.urls("graphene_django.tests.urls_validation")
856+
def test_allow_introspection(client):
857+
response = client.post(
858+
url_string("/graphql/", query="{__schema {queryType {name}}}")
859+
)
860+
assert response.status_code == 200
861+
862+
assert response_json(response) == {
863+
"data": {"__schema": {"queryType": {"name": "QueryRoot"}}}
864+
}
865+
866+
867+
@pytest.mark.parametrize("url", VALIDATION_URLS)
868+
@pytest.mark.urls("graphene_django.tests.urls_validation")
869+
def test_validation_disallow_introspection(client, url):
870+
response = client.post(url_string(url, query="{__schema {queryType {name}}}"))
871+
872+
assert response.status_code == 400
873+
874+
json_response = response_json(response)
875+
assert "data" not in json_response
876+
assert "errors" in json_response
877+
assert len(json_response["errors"]) == 1
878+
879+
error_message = json_response["errors"][0]["message"]
880+
assert INTROSPECTION_DISALLOWED_ERROR_MESSAGE in error_message
881+
882+
883+
@pytest.mark.parametrize("url", VALIDATION_URLS)
884+
@pytest.mark.urls("graphene_django.tests.urls_validation")
885+
@patch(
886+
"graphene_django.settings.graphene_settings.MAX_VALIDATION_ERRORS", N_INTROSPECTIONS
887+
)
888+
def test_within_max_validation_errors(client, url):
889+
response = client.post(url_string(url, query=QUERY_WITH_TWO_INTROSPECTIONS))
890+
891+
assert response.status_code == 400
892+
893+
json_response = response_json(response)
894+
assert "data" not in json_response
895+
assert "errors" in json_response
896+
assert len(json_response["errors"]) == N_INTROSPECTIONS
897+
898+
error_messages = [error["message"].lower() for error in json_response["errors"]]
899+
900+
n_introspection_error_messages = sum(
901+
INTROSPECTION_DISALLOWED_ERROR_MESSAGE in msg for msg in error_messages
902+
)
903+
assert n_introspection_error_messages == N_INTROSPECTIONS
904+
905+
assert all(
906+
MAX_VALIDATION_ERRORS_EXCEEDED_MESSAGE not in msg for msg in error_messages
907+
)
908+
909+
910+
@pytest.mark.parametrize("url", VALIDATION_URLS)
911+
@pytest.mark.urls("graphene_django.tests.urls_validation")
912+
@patch("graphene_django.settings.graphene_settings.MAX_VALIDATION_ERRORS", 1)
913+
def test_exceeds_max_validation_errors(client, url):
914+
response = client.post(url_string(url, query=QUERY_WITH_TWO_INTROSPECTIONS))
915+
916+
assert response.status_code == 400
917+
918+
json_response = response_json(response)
919+
assert "data" not in json_response
920+
assert "errors" in json_response
921+
922+
error_messages = (error["message"].lower() for error in json_response["errors"])
923+
assert any(MAX_VALIDATION_ERRORS_EXCEEDED_MESSAGE in msg for msg in error_messages)
+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from django.urls import path
2+
3+
from graphene.validation import DisableIntrospection
4+
5+
from ..views import GraphQLView
6+
from .schema_view import schema
7+
8+
9+
class View(GraphQLView):
10+
schema = schema
11+
12+
13+
class NoIntrospectionView(View):
14+
validation_rules = (DisableIntrospection,)
15+
16+
17+
class NoIntrospectionViewInherited(NoIntrospectionView):
18+
pass
19+
20+
21+
urlpatterns = [
22+
path("graphql/", View.as_view()),
23+
path("graphql/validation/", View.as_view(validation_rules=(DisableIntrospection,))),
24+
path("graphql/validation/alternative/", NoIntrospectionView.as_view()),
25+
path("graphql/validation/inherited/", NoIntrospectionViewInherited.as_view()),
26+
]

graphene_django/views.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ class GraphQLView(View):
9696
batch = False
9797
subscription_path = None
9898
execution_context_class = None
99+
validation_rules = None
99100

100101
def __init__(
101102
self,
@@ -107,6 +108,7 @@ def __init__(
107108
batch=False,
108109
subscription_path=None,
109110
execution_context_class=None,
111+
validation_rules=None,
110112
):
111113
if not schema:
112114
schema = graphene_settings.SCHEMA
@@ -135,6 +137,8 @@ def __init__(
135137
), "A Schema is required to be provided to GraphQLView."
136138
assert not all((graphiql, batch)), "Use either graphiql or batch processing"
137139

140+
self.validation_rules = validation_rules or self.validation_rules
141+
138142
# noinspection PyUnusedLocal
139143
def get_root_value(self, request):
140144
return self.root_value
@@ -332,7 +336,12 @@ def execute_graphql_request(
332336
)
333337
)
334338

335-
validation_errors = validate(schema, document)
339+
validation_errors = validate(
340+
schema,
341+
document,
342+
self.validation_rules,
343+
graphene_settings.MAX_VALIDATION_ERRORS,
344+
)
336345

337346
if validation_errors:
338347
return ExecutionResult(data=None, errors=validation_errors)

0 commit comments

Comments
 (0)