| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119 |
- """Tests for the public /settings/ui-preferences endpoint (#1293).
- Reporter @Tivonfeng: granting `printers:clear_plate` alone wasn't enough to
- make the Clear Plate button work — the frontend also needs `require_plate_clear`
- from /settings, which requires SETTINGS_READ (and also surfaces SMTP/LDAP/MQTT
- secrets). The fix is a public subset endpoint that returns only UI rendering
- fields, so the frontend doesn't have to demand SETTINGS_READ for non-admin UX.
- The two guarantees pinned here:
- 1. The endpoint is accessible without SETTINGS_READ.
- 2. The endpoint NEVER returns sensitive fields (SMTP/LDAP/MQTT credentials,
- API tokens, HA bearer token, etc.) — even if a future commit accidentally
- adds one of those keys to _UI_PREFERENCE_FIELDS, this test fails loudly.
- """
- import pytest
- from httpx import AsyncClient
- # Anything in this list MUST NOT appear in the /ui-preferences response.
- # Mirror of _SENSITIVE_FIELDS_FOR_API_KEY in backend/app/api/routes/settings.py
- # plus a wider net for any *_password / *_token / *_key suffix.
- _SENSITIVE_KEYS = {
- "smtp_password",
- "smtp_username",
- "smtp_from_email",
- "smtp_host",
- "smtp_port",
- "mqtt_password",
- "mqtt_username",
- "mqtt_broker",
- "ha_token",
- "ha_url",
- "prometheus_token",
- "virtual_printer_access_code",
- "ldap_bind_password",
- "ldap_bind_dn",
- "ldap_server_url",
- "external_url",
- "bambu_studio_api_url",
- "orcaslicer_api_url",
- "local_backup_path",
- "github_token",
- "gitea_token",
- "obico_api_key",
- "obico_endpoint_url",
- }
- class TestUiPreferencesEndpoint:
- """The new public endpoint must work without SETTINGS_READ and must
- never return sensitive fields."""
- @pytest.mark.asyncio
- async def test_endpoint_returns_200_without_auth(self, async_client: AsyncClient):
- """No SETTINGS_READ required — that's the whole point of the endpoint."""
- response = await async_client.get("/api/v1/settings/ui-preferences")
- assert response.status_code == 200
- data = response.json()
- assert isinstance(data, dict)
- @pytest.mark.asyncio
- async def test_returns_require_plate_clear(self, async_client: AsyncClient):
- """The field that drove #1293: PrintersPage gates the Clear Plate button
- on this. Must be present in the response."""
- response = await async_client.get("/api/v1/settings/ui-preferences")
- assert response.status_code == 200
- data = response.json()
- assert "require_plate_clear" in data
- # Type must be bool (frontend does === true checks)
- assert isinstance(data["require_plate_clear"], bool)
- @pytest.mark.asyncio
- async def test_returns_expected_field_set(self, async_client: AsyncClient):
- """Pin the exact set of fields the endpoint exposes — adding a sensitive
- field to _UI_PREFERENCE_FIELDS by accident should fail this assert and
- force the author to reconsider."""
- response = await async_client.get("/api/v1/settings/ui-preferences")
- data = response.json()
- expected = {
- "require_plate_clear",
- "check_printer_firmware",
- "camera_view_mode",
- "time_format",
- "date_format",
- "drying_presets",
- "ams_humidity_good",
- "ams_humidity_fair",
- "ams_temp_good",
- "ams_temp_fair",
- "bed_cooled_threshold",
- }
- assert set(data.keys()) == expected
- @pytest.mark.asyncio
- async def test_response_excludes_sensitive_fields(self, async_client: AsyncClient, db_session):
- """Even with sensitive fields seeded in the DB, none of them must
- appear in the response — the endpoint is opt-in, not opt-out."""
- from backend.app.models.settings import Settings
- # Seed every sensitive field with a unique recognizable value so a leak
- # would be obvious in failure output.
- for i, key in enumerate(_SENSITIVE_KEYS):
- db_session.add(Settings(key=key, value=f"SECRET_VALUE_{i}_DO_NOT_LEAK"))
- await db_session.commit()
- response = await async_client.get("/api/v1/settings/ui-preferences")
- assert response.status_code == 200
- data = response.json()
- # No sensitive key should appear in the response keys
- leaked_keys = _SENSITIVE_KEYS & set(data.keys())
- assert leaked_keys == set(), f"Leaked sensitive fields: {leaked_keys}"
- # And the recognizable values shouldn't appear in any value either
- response_text = response.text
- for i in range(len(_SENSITIVE_KEYS)):
- assert f"SECRET_VALUE_{i}_DO_NOT_LEAK" not in response_text, (
- f"Sensitive value index {i} leaked into response body"
- )
|