test_settings_ui_preferences.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. """Tests for the public /settings/ui-preferences endpoint (#1293).
  2. Reporter @Tivonfeng: granting `printers:clear_plate` alone wasn't enough to
  3. make the Clear Plate button work — the frontend also needs `require_plate_clear`
  4. from /settings, which requires SETTINGS_READ (and also surfaces SMTP/LDAP/MQTT
  5. secrets). The fix is a public subset endpoint that returns only UI rendering
  6. fields, so the frontend doesn't have to demand SETTINGS_READ for non-admin UX.
  7. The two guarantees pinned here:
  8. 1. The endpoint is accessible without SETTINGS_READ.
  9. 2. The endpoint NEVER returns sensitive fields (SMTP/LDAP/MQTT credentials,
  10. API tokens, HA bearer token, etc.) — even if a future commit accidentally
  11. adds one of those keys to _UI_PREFERENCE_FIELDS, this test fails loudly.
  12. """
  13. import pytest
  14. from httpx import AsyncClient
  15. # Anything in this list MUST NOT appear in the /ui-preferences response.
  16. # Mirror of _SENSITIVE_FIELDS_FOR_API_KEY in backend/app/api/routes/settings.py
  17. # plus a wider net for any *_password / *_token / *_key suffix.
  18. _SENSITIVE_KEYS = {
  19. "smtp_password",
  20. "smtp_username",
  21. "smtp_from_email",
  22. "smtp_host",
  23. "smtp_port",
  24. "mqtt_password",
  25. "mqtt_username",
  26. "mqtt_broker",
  27. "ha_token",
  28. "ha_url",
  29. "prometheus_token",
  30. "virtual_printer_access_code",
  31. "ldap_bind_password",
  32. "ldap_bind_dn",
  33. "ldap_server_url",
  34. "external_url",
  35. "bambu_studio_api_url",
  36. "orcaslicer_api_url",
  37. "local_backup_path",
  38. "github_token",
  39. "gitea_token",
  40. "obico_api_key",
  41. "obico_endpoint_url",
  42. }
  43. class TestUiPreferencesEndpoint:
  44. """The new public endpoint must work without SETTINGS_READ and must
  45. never return sensitive fields."""
  46. @pytest.mark.asyncio
  47. async def test_endpoint_returns_200_without_auth(self, async_client: AsyncClient):
  48. """No SETTINGS_READ required — that's the whole point of the endpoint."""
  49. response = await async_client.get("/api/v1/settings/ui-preferences")
  50. assert response.status_code == 200
  51. data = response.json()
  52. assert isinstance(data, dict)
  53. @pytest.mark.asyncio
  54. async def test_returns_require_plate_clear(self, async_client: AsyncClient):
  55. """The field that drove #1293: PrintersPage gates the Clear Plate button
  56. on this. Must be present in the response."""
  57. response = await async_client.get("/api/v1/settings/ui-preferences")
  58. assert response.status_code == 200
  59. data = response.json()
  60. assert "require_plate_clear" in data
  61. # Type must be bool (frontend does === true checks)
  62. assert isinstance(data["require_plate_clear"], bool)
  63. @pytest.mark.asyncio
  64. async def test_returns_expected_field_set(self, async_client: AsyncClient):
  65. """Pin the exact set of fields the endpoint exposes — adding a sensitive
  66. field to _UI_PREFERENCE_FIELDS by accident should fail this assert and
  67. force the author to reconsider."""
  68. response = await async_client.get("/api/v1/settings/ui-preferences")
  69. data = response.json()
  70. expected = {
  71. "require_plate_clear",
  72. "check_printer_firmware",
  73. "camera_view_mode",
  74. "time_format",
  75. "date_format",
  76. "drying_presets",
  77. "ams_humidity_good",
  78. "ams_humidity_fair",
  79. "ams_temp_good",
  80. "ams_temp_fair",
  81. "bed_cooled_threshold",
  82. }
  83. assert set(data.keys()) == expected
  84. @pytest.mark.asyncio
  85. async def test_response_excludes_sensitive_fields(self, async_client: AsyncClient, db_session):
  86. """Even with sensitive fields seeded in the DB, none of them must
  87. appear in the response — the endpoint is opt-in, not opt-out."""
  88. from backend.app.models.settings import Settings
  89. # Seed every sensitive field with a unique recognizable value so a leak
  90. # would be obvious in failure output.
  91. for i, key in enumerate(_SENSITIVE_KEYS):
  92. db_session.add(Settings(key=key, value=f"SECRET_VALUE_{i}_DO_NOT_LEAK"))
  93. await db_session.commit()
  94. response = await async_client.get("/api/v1/settings/ui-preferences")
  95. assert response.status_code == 200
  96. data = response.json()
  97. # No sensitive key should appear in the response keys
  98. leaked_keys = _SENSITIVE_KEYS & set(data.keys())
  99. assert leaked_keys == set(), f"Leaked sensitive fields: {leaked_keys}"
  100. # And the recognizable values shouldn't appear in any value either
  101. response_text = response.text
  102. for i in range(len(_SENSITIVE_KEYS)):
  103. assert f"SECRET_VALUE_{i}_DO_NOT_LEAK" not in response_text, (
  104. f"Sensitive value index {i} leaked into response body"
  105. )