Files
bambuddy/backend/tests/integration/test_settings_ui_preferences.py
T
maziggy 8a6fcf5cbd fix(settings): expose UI rendering fields without requiring SETTINGS_READ (#1293)
The Clear Plate button (and 4 other features on the Printers page) read
  their state from /settings, which requires SETTINGS_READ. Granting that
  permission also adds the Settings nav item and leaks SMTP/LDAP/MQTT
  credentials — exactly what users were trying to avoid by giving an
  operator only printers:clear_plate.

  New /settings/ui-preferences endpoint returns a curated, opt-in subset
  of non-sensitive fields. Matches the existing /default-sidebar-order
  precedent. PrintersPage switched to the new endpoint; admin pages still
  use /settings for full access.
2026-05-12 16:30:28 +02:00

120 lines
4.7 KiB
Python

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