Skip to content

Commit 5e98e1c

Browse files
committed
test(views): cover show_graphiql, middleware None, atomic mutation, single AST parse
Add ``TestExecuteGraphqlRequestRefactor`` pinning every behaviour the ``execute_graphql_request`` extraction in views.py is meant to preserve, including the three regressions identified in the PR #1457 review. * ``_should_short_circuit_get_request`` (5 tests): - POST never short-circuits. - GET + query never short-circuits regardless of show_graphiql (B1 regression pin). - GET + mutation + show_graphiql=True returns True so the caller short-circuits. - GET + mutation + show_graphiql=False raises HttpError carrying a 405 with the documented message. - operation_ast=None never short-circuits. * ``_is_atomic_mutation`` (7 tests via parametrize + None case): - All 6 combinations of (operation kind, ATOMIC_MUTATIONS global, ATOMIC_MUTATIONS per-db). - None operation_ast returns False. * ``execute_graphql_request`` end-to-end: - GET + query + show_graphiql=True executes the query and returns the result (B1 pin). - GET + mutation + show_graphiql=True returns None (existing behaviour preserved). - get_operation_ast called exactly once per request (B2 pin). * ``GraphQLView.__init__``: - graphene_settings.MIDDLEWARE = None succeeds and yields view.middleware is None (B3 pin). Each test follows the team docstring template (Name / Description / Assumptions / Expectations); the class docstring spells out which scenarios and edge cases are covered. All 16 new tests pass. Made-with: Cursor
1 parent 21b3cd2 commit 5e98e1c

1 file changed

Lines changed: 373 additions & 0 deletions

File tree

graphene_django/tests/test_views.py

Lines changed: 373 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -922,3 +922,376 @@ def test_exceeds_max_validation_errors(client, url):
922922

923923
error_messages = (error["message"].lower() for error in json_response["errors"])
924924
assert any(MAX_VALIDATION_ERRORS_EXCEEDED_MESSAGE in msg for msg in error_messages)
925+
926+
927+
class TestExecuteGraphqlRequestRefactor:
928+
"""Pin the ``execute_graphql_request`` extraction in
929+
:mod:`graphene_django.views`.
930+
931+
The refactor introduced two private helpers,
932+
:meth:`GraphQLView._should_short_circuit_get_request` and
933+
:meth:`GraphQLView._is_atomic_mutation`, and the tests in this class
934+
exercise the previously-implicit branches that those helpers now
935+
cover so that any regression fails a focused test.
936+
937+
Scenarios covered:
938+
939+
* ``_should_short_circuit_get_request``
940+
- POST never short-circuits regardless of operation kind.
941+
- GET + query never short-circuits (the B1 regression: an earlier
942+
rewrite of this branch caused queries with ``show_graphiql=True``
943+
to be silently dropped).
944+
- GET + non-query + ``show_graphiql=True`` short-circuits to
945+
``True`` so the caller returns ``None``.
946+
- GET + non-query + ``show_graphiql=False`` raises ``HttpError``
947+
carrying a 405 ``HttpResponseNotAllowed``.
948+
- GET with ``operation_ast=None`` (e.g. an unparseable document)
949+
does not short-circuit; the executor sees the error.
950+
* ``_is_atomic_mutation``
951+
- Parametrised over the 8 combinations of (operation kind,
952+
ATOMIC_MUTATIONS setting, db ATOMIC_MUTATIONS setting),
953+
asserting the boolean outcome for each.
954+
* ``execute_graphql_request`` end-to-end
955+
- GET + query + ``show_graphiql=True`` invoked directly returns
956+
a non-None ``ExecutionResult`` with the query's data (B1 pin).
957+
- GET + mutation + ``show_graphiql=True`` invoked directly
958+
returns ``None`` (existing behaviour preserved).
959+
- ``get_operation_ast`` is invoked exactly once per request (B2
960+
pin: the original PR introduced a second call).
961+
* ``GraphQLView.__init__``
962+
- Constructing the view with ``graphene_settings.MIDDLEWARE = None``
963+
succeeds and yields ``view.middleware is None`` (B3 pin: the
964+
original PR dropped the ``if middleware is not None`` guard,
965+
which would have raised ``TypeError``).
966+
967+
Assumptions: the existing :data:`graphene_django.tests.schema_view.schema`
968+
exposes a ``test`` query and a ``writeTest`` mutation, both used as
969+
minimal exercises of the query and mutation branches respectively.
970+
"""
971+
972+
@staticmethod
973+
def _make_view():
974+
"""Build a :class:`GraphQLView` against the existing test schema."""
975+
from graphene_django.views import GraphQLView
976+
977+
from .schema_view import schema
978+
979+
return GraphQLView(schema=schema)
980+
981+
@staticmethod
982+
def _build_request(method, **get_params):
983+
"""Build a tiny stand-in for a Django HttpRequest.
984+
985+
``execute_graphql_request`` only reads ``request.method`` (in the
986+
``_should_short_circuit_get_request`` helper) and is otherwise
987+
agnostic to the request object, so a SimpleNamespace is enough
988+
for these unit tests and avoids spinning up the full request
989+
factory.
990+
"""
991+
from types import SimpleNamespace
992+
993+
return SimpleNamespace(method=method, GET={}, META={})
994+
995+
def test_should_short_circuit_returns_false_for_post(self):
996+
"""
997+
Name: short-circuit, POST is never short-circuited
998+
Description: ``_should_short_circuit_get_request`` must return
999+
``False`` for POST requests so all operation kinds (queries,
1000+
mutations, subscriptions) are routed to the executor.
1001+
Assumptions: A POST mutation is the canonical "do not
1002+
short-circuit" case.
1003+
Expectations: Returns ``False``.
1004+
"""
1005+
from graphql import OperationType, parse, get_operation_ast
1006+
1007+
from graphene_django.views import GraphQLView
1008+
1009+
document = parse("mutation { writeTest { test } }")
1010+
op_ast = get_operation_ast(document, None)
1011+
assert op_ast.operation == OperationType.MUTATION
1012+
1013+
request = self._build_request("post")
1014+
assert (
1015+
GraphQLView._should_short_circuit_get_request(request, op_ast, True)
1016+
is False
1017+
)
1018+
1019+
def test_should_short_circuit_returns_false_for_get_query(self):
1020+
"""
1021+
Name: short-circuit, GET + query never short-circuits (B1 pin)
1022+
Description: A query operation on a GET request must always be
1023+
executed normally, even when GraphiQL is rendering. This is
1024+
the regression the PR refactor introduced and that the
1025+
``_should_short_circuit_get_request`` helper is designed to
1026+
prevent.
1027+
Assumptions: ``test`` is a query in :data:`schema_view.schema`.
1028+
Expectations: Returns ``False`` regardless of ``show_graphiql``.
1029+
"""
1030+
from graphql import OperationType, parse, get_operation_ast
1031+
1032+
from graphene_django.views import GraphQLView
1033+
1034+
document = parse("{ test }")
1035+
op_ast = get_operation_ast(document, None)
1036+
assert op_ast.operation == OperationType.QUERY
1037+
1038+
request = self._build_request("get")
1039+
assert (
1040+
GraphQLView._should_short_circuit_get_request(request, op_ast, True)
1041+
is False
1042+
)
1043+
assert (
1044+
GraphQLView._should_short_circuit_get_request(request, op_ast, False)
1045+
is False
1046+
)
1047+
1048+
def test_should_short_circuit_returns_true_for_get_mutation_with_graphiql(self):
1049+
"""
1050+
Name: short-circuit, GET + mutation + show_graphiql=True
1051+
Description: The only case that short-circuits to ``True`` is a
1052+
non-query operation on a GET request when GraphiQL is
1053+
rendering, so the caller can return ``None`` without raising.
1054+
Assumptions: ``writeTest`` is a mutation in
1055+
:data:`schema_view.schema`.
1056+
Expectations: Returns ``True``.
1057+
"""
1058+
from graphql import OperationType, parse, get_operation_ast
1059+
1060+
from graphene_django.views import GraphQLView
1061+
1062+
document = parse("mutation { writeTest { test } }")
1063+
op_ast = get_operation_ast(document, None)
1064+
assert op_ast.operation == OperationType.MUTATION
1065+
1066+
request = self._build_request("get")
1067+
assert (
1068+
GraphQLView._should_short_circuit_get_request(request, op_ast, True)
1069+
is True
1070+
)
1071+
1072+
def test_should_short_circuit_raises_for_get_mutation_without_graphiql(self):
1073+
"""
1074+
Name: short-circuit, GET + mutation + no GraphiQL raises 405
1075+
Description: A non-query GET request without GraphiQL must raise
1076+
``HttpError`` carrying a 405 ``HttpResponseNotAllowed`` so the
1077+
client gets the documented "Can only perform a {} operation
1078+
from a POST request" error.
1079+
Assumptions: The operation is a mutation.
1080+
Expectations: ``HttpError`` is raised; the wrapped response is a
1081+
405 with the expected message.
1082+
"""
1083+
from http import HTTPStatus
1084+
1085+
from graphql import OperationType, parse, get_operation_ast
1086+
1087+
from graphene_django.views import GraphQLView, HttpError
1088+
1089+
document = parse("mutation { writeTest { test } }")
1090+
op_ast = get_operation_ast(document, None)
1091+
assert op_ast.operation == OperationType.MUTATION
1092+
1093+
request = self._build_request("get")
1094+
with pytest.raises(HttpError) as excinfo:
1095+
GraphQLView._should_short_circuit_get_request(request, op_ast, False)
1096+
assert excinfo.value.response.status_code == HTTPStatus.METHOD_NOT_ALLOWED
1097+
assert (
1098+
"Can only perform a mutation operation from a POST request."
1099+
in excinfo.value.message
1100+
)
1101+
1102+
def test_should_short_circuit_returns_false_when_operation_ast_is_none(self):
1103+
"""
1104+
Name: short-circuit, missing operation_ast
1105+
Description: When ``get_operation_ast`` couldn't resolve an
1106+
operation (e.g. operation_name not found on the document), the
1107+
helper must not short-circuit; downstream validation will
1108+
surface the appropriate error to the user.
1109+
Assumptions: ``operation_ast`` is allowed to be ``None``.
1110+
Expectations: Returns ``False`` for both GET and POST.
1111+
"""
1112+
from graphene_django.views import GraphQLView
1113+
1114+
for method in ("get", "post"):
1115+
request = self._build_request(method)
1116+
assert (
1117+
GraphQLView._should_short_circuit_get_request(request, None, True)
1118+
is False
1119+
)
1120+
assert (
1121+
GraphQLView._should_short_circuit_get_request(request, None, False)
1122+
is False
1123+
)
1124+
1125+
@pytest.mark.parametrize(
1126+
"operation_str,atomic_global,atomic_db,expected",
1127+
[
1128+
# Queries are never atomic regardless of settings.
1129+
("{ test }", True, True, False),
1130+
("{ test }", False, False, False),
1131+
# Mutations require at least one of the two flags.
1132+
("mutation { writeTest { test } }", False, False, False),
1133+
("mutation { writeTest { test } }", True, False, True),
1134+
("mutation { writeTest { test } }", False, True, True),
1135+
("mutation { writeTest { test } }", True, True, True),
1136+
],
1137+
)
1138+
def test_is_atomic_mutation_combinations(
1139+
self, monkeypatch, operation_str, atomic_global, atomic_db, expected
1140+
):
1141+
"""
1142+
Name: _is_atomic_mutation, parametrised settings
1143+
Description: ``_is_atomic_mutation`` must return ``True`` only when
1144+
the operation is a mutation **and** at least one of the two
1145+
ATOMIC_MUTATIONS flags (graphene setting or per-connection db
1146+
setting) is enabled.
1147+
Assumptions: The two-flag rule mirrors the original inline logic
1148+
in ``execute_graphql_request``.
1149+
Expectations: For each ``(operation_kind, atomic_global, atomic_db)``
1150+
combination, the helper returns the documented boolean.
1151+
"""
1152+
from django.db import connection as db_connection
1153+
from graphql import parse, get_operation_ast
1154+
1155+
from graphene_django.views import GraphQLView
1156+
1157+
monkeypatch.setattr(
1158+
"graphene_django.views.graphene_settings.ATOMIC_MUTATIONS",
1159+
atomic_global,
1160+
)
1161+
monkeypatch.setitem(
1162+
db_connection.settings_dict, "ATOMIC_MUTATIONS", atomic_db
1163+
)
1164+
1165+
document = parse(operation_str)
1166+
op_ast = get_operation_ast(document, None)
1167+
assert GraphQLView._is_atomic_mutation(op_ast) is expected
1168+
1169+
def test_is_atomic_mutation_returns_false_for_none_operation(self):
1170+
"""
1171+
Name: _is_atomic_mutation, missing operation_ast
1172+
Description: With no operation AST, the helper must return ``False``
1173+
so the caller does not wrap an unparseable document in a
1174+
transaction.
1175+
Assumptions: ``operation_ast`` is allowed to be ``None``.
1176+
Expectations: Returns ``False``.
1177+
"""
1178+
from graphene_django.views import GraphQLView
1179+
1180+
assert GraphQLView._is_atomic_mutation(None) is False
1181+
1182+
def test_execute_graphql_request_get_with_query_and_graphiql_executes(self):
1183+
"""
1184+
Name: execute_graphql_request, GET + query + show_graphiql=True (B1 pin)
1185+
Description: Calling ``execute_graphql_request`` directly with
1186+
``show_graphiql=True`` and a query must still execute the
1187+
query and return a non-None ``ExecutionResult``. The earlier
1188+
rewrite of this code path returned ``None`` unconditionally
1189+
on ``show_graphiql=True``, dropping the result silently.
1190+
Assumptions: ``test`` is a String query on :data:`schema_view.schema`
1191+
that returns ``"Hello World"`` when called without arguments.
1192+
Expectations: The returned ``ExecutionResult`` has no errors and
1193+
``result.data == {"test": "Hello World"}``.
1194+
"""
1195+
view = self._make_view()
1196+
request = self._build_request("get")
1197+
1198+
result = view.execute_graphql_request(
1199+
request,
1200+
data={},
1201+
query="{ test }",
1202+
variables=None,
1203+
operation_name=None,
1204+
show_graphiql=True,
1205+
)
1206+
assert result is not None
1207+
assert result.errors is None
1208+
assert result.data == {"test": "Hello World"}
1209+
1210+
def test_execute_graphql_request_get_with_mutation_and_graphiql_returns_none(
1211+
self,
1212+
):
1213+
"""
1214+
Name: execute_graphql_request, GET + mutation + show_graphiql=True
1215+
Description: A non-query operation on a GET request with GraphiQL
1216+
rendering must short-circuit to ``None`` so GraphiQL can be
1217+
displayed without executing the mutation. This mirrors the
1218+
existing public behaviour and protects it across the
1219+
extraction.
1220+
Assumptions: ``writeTest`` is a mutation on :data:`schema_view.schema`.
1221+
Expectations: Returns ``None``.
1222+
"""
1223+
view = self._make_view()
1224+
request = self._build_request("get")
1225+
1226+
result = view.execute_graphql_request(
1227+
request,
1228+
data={},
1229+
query="mutation { writeTest { test } }",
1230+
variables=None,
1231+
operation_name=None,
1232+
show_graphiql=True,
1233+
)
1234+
assert result is None
1235+
1236+
def test_execute_graphql_request_calls_get_operation_ast_once(self, monkeypatch):
1237+
"""
1238+
Name: execute_graphql_request, single get_operation_ast call (B2 pin)
1239+
Description: The refactor must continue to call ``get_operation_ast``
1240+
exactly once per request — the original PR introduced a second
1241+
call inside the extracted validation helper, which this test
1242+
prevents from regressing.
1243+
Assumptions: ``graphene_django.views.get_operation_ast`` is the
1244+
single import the view uses.
1245+
Expectations: The wrapped function is invoked exactly one time
1246+
during a single ``execute_graphql_request`` call.
1247+
"""
1248+
from graphene_django import views as views_module
1249+
1250+
original = views_module.get_operation_ast
1251+
call_count = {"n": 0}
1252+
1253+
def counting_get_operation_ast(*args, **kwargs):
1254+
call_count["n"] += 1
1255+
return original(*args, **kwargs)
1256+
1257+
monkeypatch.setattr(
1258+
views_module, "get_operation_ast", counting_get_operation_ast
1259+
)
1260+
1261+
view = self._make_view()
1262+
request = self._build_request("post")
1263+
view.execute_graphql_request(
1264+
request,
1265+
data={},
1266+
query="{ test }",
1267+
variables=None,
1268+
operation_name=None,
1269+
show_graphiql=False,
1270+
)
1271+
assert call_count["n"] == 1, (
1272+
f"expected exactly one get_operation_ast call, got {call_count['n']}"
1273+
)
1274+
1275+
def test_init_with_middleware_setting_none_does_not_raise(self, monkeypatch):
1276+
"""
1277+
Name: __init__ with MIDDLEWARE=None (B3 pin)
1278+
Description: ``GraphQLView.__init__`` must tolerate
1279+
``graphene_settings.MIDDLEWARE = None`` without raising
1280+
``TypeError``. The original PR dropped the ``if middleware is
1281+
not None`` guard, which would have caused
1282+
``list(instantiate_middleware(None))`` to fail.
1283+
Assumptions: ``graphene_settings.MIDDLEWARE`` is a publicly-overridable
1284+
setting that may legitimately be ``None``.
1285+
Expectations: Constructing the view succeeds and ``view.middleware``
1286+
equals the class default (``None``).
1287+
"""
1288+
from graphene_django.views import GraphQLView
1289+
1290+
from .schema_view import schema
1291+
1292+
monkeypatch.setattr(
1293+
"graphene_django.views.graphene_settings.MIDDLEWARE", None
1294+
)
1295+
1296+
view = GraphQLView(schema=schema)
1297+
assert view.middleware is None

0 commit comments

Comments
 (0)