notification_toggles.py 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
  1. """The per-event notification toggles, derived from the model's own columns (#2945).
  2. A hardcoded list only covers the toggles someone remembered to add to it, so it
  3. carries the same hazard as the field maps it is meant to police, one layer up.
  4. Derived, a toggle added tomorrow is exercised the day its column lands.
  5. This lives here rather than in either test module because the unit parity checks
  6. and the integration round-trips need the same two answers, and a derivation
  7. duplicated across files is a list with extra steps.
  8. """
  9. from __future__ import annotations
  10. from sqlalchemy import Boolean
  11. from sqlalchemy.sql.schema import Column
  12. from backend.app.models.notification import NotificationProvider
  13. # The Boolean check is not decoration. An on_* column of some other type would
  14. # turn `{field: True}` into a 422 and break every test parametrised off this
  15. # list, in whichever unrelated PR happened to add that column -- and a spurious
  16. # failure is how a derivation gets edited until it passes.
  17. _EVENT_TOGGLES: list[Column] = [
  18. column
  19. for column in NotificationProvider.__table__.columns
  20. if column.name.startswith("on_") and isinstance(column.type, Boolean)
  21. ]
  22. EVENT_TOGGLE_COLUMNS: list[str] = sorted(column.name for column in _EVENT_TOGGLES)
  23. # Every column a client is meant to be able to set. The hazard #2945 describes is
  24. # per-column -- a field that exists everywhere except one of the maps it has to
  25. # cross -- and `enabled`, the quiet-hours and digest fields cross the same two
  26. # maps as the on_* toggles. The prefix is a naming convention, not the boundary.
  27. # What is left out is what the server owns: the key, the timestamps, and the
  28. # delivery status it records itself.
  29. _SERVER_OWNED = {"id", "created_at", "updated_at", "last_success", "last_error", "last_error_at"}
  30. SETTABLE_COLUMNS: list[str] = sorted(
  31. column.name for column in NotificationProvider.__table__.columns if column.name not in _SERVER_OWNED
  32. )
  33. def _model_default(column: Column) -> bool:
  34. """The value a row gets for this column when the caller sends nothing.
  35. Reads the Python-side default only. That is exact while every toggle
  36. declares one, which ``test_every_event_column_has_a_python_default`` holds
  37. in place: a column added with only a ``server_default`` would read as False
  38. here, get a target of True, and for a server-default-true column the round
  39. trip would send True and assert True -- vacuous again, and silently.
  40. """
  41. if column.default is None:
  42. return False
  43. return bool(column.default.arg)
  44. # The value each toggle has to be driven to for the assertion to mean anything.
  45. #
  46. # Nine of these columns default to True on the model and on the response schema,
  47. # so a test that sends True and asserts True is answered by the default alone:
  48. # a field dropped by a hand-maintained map still reads back True, and the
  49. # mutation that would prove the map is load-bearing survives. Driving each
  50. # toggle to whatever its default is not makes the round trip the only thing that
  51. # can supply the answer.
  52. TOGGLE_TARGET: dict[str, bool] = {column.name: not _model_default(column) for column in _EVENT_TOGGLES}