"""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" )