You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Closes#1360.
`DRFMutation.IOSettings` declared `model: django.db.models.Model = None`,
`graphene_model: DjangoObjectType = None` and `serializer = None` - the
annotations typed instances, not classes, and there was no runtime guard
on the `None` defaults. A subclass that forgot to override any of these
crashed deep inside `mutate()` with `AttributeError` / `TypeError` that
the broad `except Exception` masks as a generic "internal error".
`DRFDeletion.IOSettings` had the inverse bug: `model` was never declared,
even though `DRFDeletion.mutate` dereferences `cls.IOSettings.model.objects`.
- Retype both `IOSettings` classes with
`ClassVar[Optional[type[...]]] = None`, narrow `pk_fields` to `list[str]`
(only used as a dict key).
- Add `_require_io_setting(cls, name)` helper that raises
`NotImplementedError("<Subclass>.IOSettings.<name> must be set by the
subclass.")` on missing / None attributes. `DRFMutation.mutate` checks
`model` / `serializer` / `graphene_model`; `DRFDeletion.mutate` checks
`model` and also raises a descriptive `ValueError` when the
`required=False` `id` argument is absent.
- Graduate `config.graphql.base` out of the mypy baseline: remove the
`[mypy-config.graphql.base]` section from `mypy.ini` and prune the ten
corresponding lines from `docs/typing/mypy_baseline.txt`.
- Regression tests in
`opencontractserver/tests/test_security_hardening.py::TestIOSettingsRequiredFieldsGuard`.
Copy file name to clipboardExpand all lines: CHANGELOG.md
+7Lines changed: 7 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -34,6 +34,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
34
34
35
35
### Fixed
36
36
37
+
- **DRFMutation/DRFDeletion `IOSettings` misconfiguration surfaces as a generic internal error** (Issue #1360, `config/graphql/base.py`): `DRFMutation.IOSettings` declared `model: django.db.models.Model = None`, `graphene_model: DjangoObjectType = None` and `serializer = None` — the type annotations were wrong (instance types, not class types, holding `None`), and there was no runtime guard. A subclass that forgot to override one of those would fail deep inside `mutate()` with an `AttributeError` (`'NoneType' object has no attribute 'objects'`) or `TypeError` (`'NoneType' object is not callable`), which the broad `except Exception` then masks as a generic "internal error" message back to the API caller. `DRFDeletion.IOSettings` had the inverse problem — it didn't declare `model` at all, even though `DRFDeletion.mutate` dereferences `cls.IOSettings.model.objects`. Discovered while working on #1332 (typing): mypy flagged four errors on `base.py:128/204/238/252` plus `base.py:93 "type[IOSettings]" has no attribute "model"`.
38
+
- Retyped both `IOSettings` declarations with `ClassVar[Optional[type[...]]] = None` so the annotation matches reality (holding a class, not an instance, and nullable by default). `pk_fields` narrowed from `list[str | int]` to `list[str]` — the only call sites use it as a dict key.
39
+
- Added `_require_io_setting(mutation_cls, name)` helper that fails fast with `NotImplementedError("<Subclass>.IOSettings.<name> must be set by the subclass.")` when a required attribute is `None` or missing. Called for `model`/`serializer`/`graphene_model` in `DRFMutation.mutate` and for `model` in `DRFDeletion.mutate`. The error is still caught by the broad `except Exception` and surfaced as "internal error" to the API, but the full descriptive message lands in the server log via `logger.error(traceback.format_exc())` — developer gets an obvious misconfiguration signal instead of an opaque `AttributeError`.
40
+
- Tightened `DRFDeletion.mutate` to raise `ValueError` when `kwargs[lookup_field]` is missing (the `id` GraphQL argument is `required=False`), instead of passing `None` into `from_global_id` and getting an opaque `AttributeError`.
41
+
- Graduated `config.graphql.base` out of the mypy baseline: removed the `[mypy-config.graphql.base]` section from `mypy.ini` and pruned the ten corresponding lines from `docs/typing/mypy_baseline.txt`. `mypy --config-file mypy.ini` is now clean on this module.
42
+
- Regression tests in `opencontractserver/tests/test_security_hardening.py::TestIOSettingsRequiredFieldsGuard`: the helper raises `NotImplementedError` for each of `model`/`serializer`/`graphene_model` when missing or `None`, returns the configured value when present, and the base classes expose the `None` defaults the guard relies on.
43
+
37
44
- **Frontend coverage badge stuck on "unknown"** (`README.md:12`, `.github/workflows/codecov-notify.yml`, `.github/workflows/frontend.yml`, `.github/workflows/frontend-e2e.yml`, `frontend/package.json`, `frontend/yarn.lock`): PR #1322 pointed the README badge at a new merged `frontend` flag fed by a cross-workflow `lcov-result-merger@5` step inside `codecov-notify.yml`. Every `frontend-merged-coverage` upload since has landed at Codecov with `state: error` / `totals: null`, so the badge rendered "unknown" even though `frontend-unit` (31%), `frontend-component` (61%), and `frontend-e2e` (24%) were all processing correctly. Two defects in the merged lcov confirmed by local repro of the CI merge step: (1) `lcov-result-merger@5` emits a stripped lcov containing only `SF:`, `DA:`, `BRDA:`, `end_of_record` — it drops `TN:`, `FN`, `FNDA`, `FNF`, `FNH`, `LF`, `LH`, `BRF`, `BRH`, so line-summary fields required by Codecov's parser are absent; (2) Vitest v8 emits `src/...` (relative to `frontend/`) while `vite-plugin-istanbul` + `nyc report` emit `/home/runner/work/OpenContracts/OpenContracts/frontend/src/...` (absolute), and the merger keys on the literal path string so the same file appears as two records with conflicting hit counts. `codecov-notify.yml` also ran without `actions/checkout`, which Codecov's action docs explicitly recommend against. Fix: stop merging client-side and let Codecov aggregate server-side, since that is what flags are for. Each per-suite upload now declares two flags — `frontend-unit,frontend`, `frontend-component,frontend`, `frontend-e2e,frontend` — so the `frontend` flag total is the union of the three uploads computed by Codecov. `codecov-notify.yml` is reduced to its original gate-and-notify role (no artifact downloads, no `lcov-result-merger`, no merged upload). Deleted the `frontend-{unit,ct,e2e}-lcov` artifact publishes in `frontend.yml` / `frontend-e2e.yml`, removed the `lcov-result-merger@^5.0.1` devDep and the `coverage:merge` script from `frontend/package.json`, and pruned the orphaned entries from `frontend/yarn.lock`. README badge URL unchanged (`flag=frontend`).
Copy file name to clipboardExpand all lines: docs/typing/mypy_baseline.txt
-9Lines changed: 0 additions & 9 deletions
Original file line number
Diff line number
Diff line change
@@ -31,15 +31,6 @@ config/graphql/annotation_queries.py:641: error: Argument 1 to "from_global_id"
31
31
config/graphql/annotation_queries.py:659: error: Argument 1 to "from_global_id" has incompatible type "Any | None"; expected "str" [arg-type]
32
32
config/graphql/annotation_queries.py:719: error: Argument 1 to "from_global_id" has incompatible type "Any | None"; expected "str" [arg-type]
33
33
config/graphql/badge_mutations.py:154: error: Argument 2 to "set_permissions_for_obj_to_user" has incompatible type "Badge"; expected "type[Model]" [arg-type]
34
-
config/graphql/base.py:128: error: Incompatible types in assignment (expression has type "None", variable has type "Model") [assignment]
35
-
config/graphql/base.py:190: error: Argument 1 to "from_global_id" has incompatible type "Any | None"; expected "str" [arg-type]
36
-
config/graphql/base.py:190: error: Incompatible types in assignment (expression has type "str", variable has type "list[str]") [assignment]
37
-
config/graphql/base.py:204: error: "Model" has no attribute "objects" [attr-defined]
38
-
config/graphql/base.py:207: error: Argument 1 to "from_global_id" has incompatible type "Any | None"; expected "str" [arg-type]
39
-
config/graphql/base.py:238: error: "None" not callable [misc]
40
-
config/graphql/base.py:252: error: "None" not callable [misc]
41
-
config/graphql/base.py:90: error: Argument 1 to "from_global_id" has incompatible type "Any | None"; expected "str" [arg-type]
42
-
config/graphql/base.py:93: error: "type[IOSettings]" has no attribute "model" [attr-defined]
43
34
config/graphql/base_types.py:28: error: Need type annotation for "id_to_children" (hint: "id_to_children: dict[<type>, <type>] = ...") [var-annotated]
44
35
config/graphql/conversation_mutations.py:148: error: Argument 2 to "set_permissions_for_obj_to_user" has incompatible type "Conversation"; expected "type[Model]" [arg-type]
45
36
config/graphql/conversation_mutations.py:242: error: Argument 2 to "set_permissions_for_obj_to_user" has incompatible type "ChatMessage"; expected "type[Model]" [arg-type]
0 commit comments