@@ -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