Skip to content

Commit 48fc6ce

Browse files
committed
test(types): cover validate_fields branches with focused unit tests
Add ``TestValidateFieldsHelpers`` exercising the new ``_validate_only_fields`` / ``_validate_exclude_fields`` helpers and the ``validate_fields`` orchestrator directly (i.e. without going through ``DjangoObjectType.__init_subclass_with_meta__``), so each warning branch fails a focused test rather than only being detected via integration coverage. Coverage: * ``_validate_only_fields``: known field (no warning), model attribute (attribute warning), unknown name (missing-field warning), ``None`` (no-op). * ``_validate_exclude_fields``: real field (no warning), custom field (no-effect warning), unknown name (missing-attribute warning), ``None`` (no-op). * ``validate_fields`` orchestrator: ``ALL_FIELDS`` sentinel normalisation; combined call delegating to both helpers. Each test follows the team docstring template (Name / Description / Assumptions / Expectations) and asserts on the warning message text via ``pytest.warns(..., match=...)`` so a regression in either the extraction or the message wording surfaces a focused failure. Made-with: Cursor
1 parent d6f9828 commit 48fc6ce

1 file changed

Lines changed: 263 additions & 0 deletions

File tree

graphene_django/tests/test_types.py

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -832,3 +832,266 @@ class Query(ObjectType):
832832
assert "type Reporter implements Node {" not in schema
833833
assert "type ReporterConnection {" not in schema
834834
assert "type ReporterEdge {" not in schema
835+
836+
837+
class TestValidateFieldsHelpers:
838+
"""Direct unit tests for the ``validate_fields`` extraction in
839+
:mod:`graphene_django.types`.
840+
841+
These tests exercise :func:`_validate_only_fields`,
842+
:func:`_validate_exclude_fields`, and the
843+
:func:`validate_fields` orchestrator in isolation — without going
844+
through ``DjangoObjectType.__init_subclass_with_meta__`` — so each
845+
warning case fails a focused test rather than only being detected
846+
via integration coverage on type construction.
847+
848+
Scenarios covered:
849+
850+
* ``_validate_only_fields``
851+
- happy path: requested name is in ``all_field_names`` (no warning)
852+
- requested name resolves to a non-field model attribute (warning
853+
about "matches an attribute")
854+
- requested name is unknown to the model (warning about "doesn't
855+
exist on Django model")
856+
- ``only_fields=None`` is a no-op
857+
- ``only_fields=ALL_FIELDS`` is normalised to a no-op by the
858+
orchestrator and is therefore exercised through ``validate_fields``
859+
* ``_validate_exclude_fields``
860+
- happy path: excluded name corresponds to a real model field
861+
(no warning)
862+
- excluded name is a custom field on the type (warning about
863+
"Excluding the custom field")
864+
- excluded name is unknown to the model (warning about "does not
865+
have a field or attribute")
866+
- ``exclude_fields=None`` is a no-op
867+
* ``validate_fields`` orchestrator
868+
- ``ALL_FIELDS`` sentinel for ``only_fields`` is normalised to
869+
empty iteration (and therefore emits no warning)
870+
- delegates to both helpers (combined-warning happy path)
871+
872+
The tests assert on the message text via ``pytest.warns(..., match=...)``
873+
so a regression in the error wording also fails a focused test.
874+
"""
875+
876+
@staticmethod
877+
def _all_field_names():
878+
"""Reusable set of "real" converted-field names for ReporterModel."""
879+
return {"first_name", "last_name", "email", "pets", "custom_field"}
880+
881+
def test_only_fields_known_field_emits_no_warning(self):
882+
"""
883+
Name: only_fields, known field, no warning
884+
Description: A name present in ``all_field_names`` short-circuits via
885+
``continue`` and emits no warning.
886+
Assumptions: ``first_name`` is one of the converted fields supplied
887+
in ``all_field_names``.
888+
Expectations: ``warnings.simplefilter("error")`` does not raise — i.e.
889+
no warning is emitted.
890+
"""
891+
from ..types import _validate_only_fields
892+
893+
with warnings.catch_warnings():
894+
warnings.simplefilter("error")
895+
_validate_only_fields(
896+
["first_name"], self._all_field_names(), ReporterModel, "TestType"
897+
)
898+
899+
def test_only_fields_model_attribute_warns_about_attribute(self):
900+
"""
901+
Name: only_fields, attribute-not-field, attribute warning
902+
Description: A name that exists as a Python attribute on the model but
903+
is not a model field (e.g. ``Reporter.some_method``) must produce
904+
the "matches an attribute" warning so the user knows to declare
905+
the field on the type.
906+
Assumptions: ``Reporter.some_method`` is defined on the model and is
907+
not in ``all_field_names``.
908+
Expectations: A ``UserWarning`` whose message matches the
909+
"matches an attribute on Django model" wording is emitted.
910+
"""
911+
from ..types import _validate_only_fields
912+
913+
with pytest.warns(
914+
UserWarning,
915+
match=r"matches an attribute on Django model .* but it's not a model field",
916+
):
917+
_validate_only_fields(
918+
["some_method"], self._all_field_names(), ReporterModel, "TestType"
919+
)
920+
921+
def test_only_fields_unknown_name_warns_about_missing_field(self):
922+
"""
923+
Name: only_fields, unknown name, missing-field warning
924+
Description: A name that is neither a converted field nor any
925+
attribute on the model must produce the "doesn't exist on
926+
Django model" warning so the user knows it is a typo or stale
927+
entry in ``Meta.fields``.
928+
Assumptions: ``"foo"`` is not a field of ReporterModel and is not
929+
in ``all_field_names``.
930+
Expectations: A ``UserWarning`` whose message matches the
931+
"doesn't exist on Django model" wording is emitted.
932+
"""
933+
from ..types import _validate_only_fields
934+
935+
with pytest.warns(
936+
UserWarning,
937+
match=r"Field name \"foo\" doesn't exist on Django model",
938+
):
939+
_validate_only_fields(
940+
["foo"], self._all_field_names(), ReporterModel, "TestType"
941+
)
942+
943+
def test_only_fields_none_is_a_noop(self):
944+
"""
945+
Name: only_fields, None argument, no-op
946+
Description: Passing ``None`` for ``only_fields`` must iterate over
947+
an empty sequence and emit no warning.
948+
Assumptions: The helper uses ``only_fields or ()`` so ``None`` is
949+
normalised at call site.
950+
Expectations: ``warnings.simplefilter("error")`` does not raise.
951+
"""
952+
from ..types import _validate_only_fields
953+
954+
with warnings.catch_warnings():
955+
warnings.simplefilter("error")
956+
_validate_only_fields(
957+
None, self._all_field_names(), ReporterModel, "TestType"
958+
)
959+
960+
def test_exclude_fields_real_model_field_emits_no_warning(self):
961+
"""
962+
Name: exclude_fields, real model field, no warning
963+
Description: Excluding a name that exists on the model and is not
964+
also a custom field on the type is the happy path and produces
965+
no warning.
966+
Assumptions: ``"first_name"`` is on ReporterModel and is not in the
967+
``all_field_names`` set passed in (we deliberately use a set
968+
without it to model "this is a real field, not a custom one").
969+
Expectations: ``warnings.simplefilter("error")`` does not raise.
970+
"""
971+
from ..types import _validate_exclude_fields
972+
973+
with warnings.catch_warnings():
974+
warnings.simplefilter("error")
975+
_validate_exclude_fields(
976+
["first_name"],
977+
{"custom_field"},
978+
ReporterModel,
979+
"TestType",
980+
)
981+
982+
def test_exclude_fields_custom_field_warns_about_no_effect(self):
983+
"""
984+
Name: exclude_fields, custom field, "no effect" warning
985+
Description: Excluding a name that maps to a custom field declared
986+
directly on the ``DjangoObjectType`` has no effect (since
987+
``Meta.exclude`` only filters auto-converted model fields), so
988+
a warning must be emitted.
989+
Assumptions: ``"custom_field"`` is in the supplied ``all_field_names``
990+
(i.e. it was added by the user as a custom field on the type).
991+
Expectations: A ``UserWarning`` whose message matches the
992+
"Excluding the custom field" wording is emitted.
993+
"""
994+
from ..types import _validate_exclude_fields
995+
996+
with pytest.warns(
997+
UserWarning,
998+
match=r"Excluding the custom field \"custom_field\" on DjangoObjectType",
999+
):
1000+
_validate_exclude_fields(
1001+
["custom_field"],
1002+
self._all_field_names(),
1003+
ReporterModel,
1004+
"TestType",
1005+
)
1006+
1007+
def test_exclude_fields_unknown_name_warns_about_missing_attribute(self):
1008+
"""
1009+
Name: exclude_fields, unknown name, missing-attribute warning
1010+
Description: Excluding a name that is neither a custom field nor any
1011+
attribute of the model must produce the "does not have a field
1012+
or attribute" warning to flag the typo or stale entry.
1013+
Assumptions: ``"foo"`` does not exist on ReporterModel and is not in
1014+
``all_field_names``.
1015+
Expectations: A ``UserWarning`` matching the "does not have a field
1016+
or attribute" wording is emitted.
1017+
"""
1018+
from ..types import _validate_exclude_fields
1019+
1020+
with pytest.warns(
1021+
UserWarning,
1022+
match=r"does not have a field or attribute named \"foo\"",
1023+
):
1024+
_validate_exclude_fields(
1025+
["foo"], self._all_field_names(), ReporterModel, "TestType"
1026+
)
1027+
1028+
def test_exclude_fields_none_is_a_noop(self):
1029+
"""
1030+
Name: exclude_fields, None argument, no-op
1031+
Description: Passing ``None`` for ``exclude_fields`` must iterate
1032+
over an empty sequence and emit no warning.
1033+
Assumptions: The helper uses ``exclude_fields or ()`` so ``None`` is
1034+
normalised at call site.
1035+
Expectations: ``warnings.simplefilter("error")`` does not raise.
1036+
"""
1037+
from ..types import _validate_exclude_fields
1038+
1039+
with warnings.catch_warnings():
1040+
warnings.simplefilter("error")
1041+
_validate_exclude_fields(
1042+
None, self._all_field_names(), ReporterModel, "TestType"
1043+
)
1044+
1045+
def test_validate_fields_normalises_all_fields_sentinel(self):
1046+
"""
1047+
Name: validate_fields, ALL_FIELDS sentinel
1048+
Description: The orchestrator must treat ``only_fields=ALL_FIELDS``
1049+
as an empty iteration so that the request "all fields" never
1050+
triggers a per-name warning loop.
1051+
Assumptions: :data:`graphene_django.types.ALL_FIELDS` is the sentinel
1052+
used to mean "all fields".
1053+
Expectations: No warning is emitted even though the
1054+
``ALL_FIELDS`` string itself is not in ``all_field_names``.
1055+
"""
1056+
from ..types import ALL_FIELDS, validate_fields
1057+
1058+
fake_fields = {"first_name": object(), "last_name": object()}
1059+
with warnings.catch_warnings():
1060+
warnings.simplefilter("error")
1061+
validate_fields(
1062+
"TestType",
1063+
ReporterModel,
1064+
fake_fields,
1065+
only_fields=ALL_FIELDS,
1066+
exclude_fields=None,
1067+
)
1068+
1069+
def test_validate_fields_delegates_to_both_helpers(self):
1070+
"""
1071+
Name: validate_fields, delegates to both helpers
1072+
Description: The orchestrator must forward both ``only_fields`` and
1073+
``exclude_fields`` to their respective helpers, so an invalid
1074+
entry in either list still surfaces a warning.
1075+
Assumptions: ``"foo"`` is unknown to the model in both contexts.
1076+
Expectations: Two warnings are emitted in a single call: one for
1077+
the missing-from-fields name and one for the
1078+
missing-from-exclude name.
1079+
"""
1080+
from ..types import validate_fields
1081+
1082+
fake_fields = {"first_name": object()}
1083+
with warnings.catch_warnings(record=True) as recorded:
1084+
warnings.simplefilter("always")
1085+
validate_fields(
1086+
"TestType",
1087+
ReporterModel,
1088+
fake_fields,
1089+
only_fields=["foo"],
1090+
exclude_fields=["bar"],
1091+
)
1092+
1093+
messages = [str(w.message) for w in recorded]
1094+
assert any("Field name \"foo\" doesn't exist" in m for m in messages), messages
1095+
assert any(
1096+
"does not have a field or attribute named \"bar\"" in m for m in messages
1097+
), messages

0 commit comments

Comments
 (0)