"""Tests for settings router (/api/settings).""" import io from pathlib import Path from unittest.mock import patch import pytest from httpx import AsyncClient from sqlalchemy.ext.asyncio import AsyncSession from src.backend.models.orm.setting import SystemSetting from src.backend.models.orm.user import User from src.backend.tests.conftest import auth_headers class TestGetSettings: """GET /api/settings/ tests.""" async def test_get_settings( self, client: AsyncClient, admin_user: User, db_session: AsyncSession, ): """Authenticated user can retrieve settings.""" # Seed a setting setting = SystemSetting( setting_key="test_setting", setting_value="test_value", setting_type="string", ) db_session.add(setting) await db_session.flush() resp = await client.get( "/api/settings/", headers=auth_headers(admin_user) ) assert resp.status_code == 200 data = resp.json() assert isinstance(data, dict) assert data.get("test_setting") == "test_value" async def test_get_settings_empty( self, client: AsyncClient, maker_user: User ): """Empty settings returns empty dict.""" resp = await client.get( "/api/settings/", headers=auth_headers(maker_user) ) assert resp.status_code == 200 assert resp.json() == {} or isinstance(resp.json(), dict) async def test_get_setting_by_key( self, client: AsyncClient, admin_user: User, db_session: AsyncSession, ): """Settings dict contains specific key after creation.""" setting = SystemSetting( setting_key="csv_delimiter", setting_value=";", setting_type="string", ) db_session.add(setting) await db_session.flush() resp = await client.get( "/api/settings/", headers=auth_headers(admin_user) ) assert resp.status_code == 200 assert resp.json()["csv_delimiter"] == ";" class TestUpdateSettings: """PUT /api/settings/ tests.""" async def test_update_settings( self, client: AsyncClient, admin_user: User ): """Admin can update/create settings.""" resp = await client.put( "/api/settings/", headers=auth_headers(admin_user), json={ "company_name": "Tielogic Srl", "csv_delimiter": ",", }, ) assert resp.status_code == 200 data = resp.json() assert data["message"].startswith("Updated") assert "company_name" in data["updated_keys"] assert "csv_delimiter" in data["updated_keys"] # Verify persistence get_resp = await client.get( "/api/settings/", headers=auth_headers(admin_user) ) settings = get_resp.json() assert settings["company_name"] == "Tielogic Srl" async def test_settings_require_admin( self, client: AsyncClient, maker_user: User ): """Non-admin user cannot update settings.""" resp = await client.put( "/api/settings/", headers=auth_headers(maker_user), json={"some_key": "some_value"}, ) assert resp.status_code == 403 class TestUpdateSettingsValidation: """gitea#23: an invalid csv_delimiter must be rejected, not accepted and left to blow up close_run later with a csv.writer TypeError.""" async def test_csv_delimiter_two_chars_rejected( self, client: AsyncClient, admin_user: User ): resp = await client.put( "/api/settings/", headers=auth_headers(admin_user), json={"csv_delimiter": ";;"}, ) assert resp.status_code == 422 async def test_csv_delimiter_empty_rejected( self, client: AsyncClient, admin_user: User ): resp = await client.put( "/api/settings/", headers=auth_headers(admin_user), json={"csv_delimiter": ""}, ) assert resp.status_code == 422 async def test_csv_delimiter_tab_accepted( self, client: AsyncClient, admin_user: User ): resp = await client.put( "/api/settings/", headers=auth_headers(admin_user), json={"csv_delimiter": "\t"}, ) assert resp.status_code == 200 async def test_csv_decimal_separator_invalid_rejected( self, client: AsyncClient, admin_user: User ): resp = await client.put( "/api/settings/", headers=auth_headers(admin_user), json={"csv_decimal_separator": "x"}, ) assert resp.status_code == 422 async def test_auto_logout_minutes_out_of_range_rejected( self, client: AsyncClient, admin_user: User ): resp = await client.put( "/api/settings/", headers=auth_headers(admin_user), json={"auto_logout_minutes": "0"}, ) assert resp.status_code == 422 async def test_auto_logout_minutes_empty_disables( self, client: AsyncClient, admin_user: User ): """Empty string is what the settings page sends to disable auto-logout.""" resp = await client.put( "/api/settings/", headers=auth_headers(admin_user), json={"auto_logout_minutes": ""}, ) assert resp.status_code == 200 # --- #47/#48/#49: chiavi Impostazioni di Trà filo --- @pytest.mark.parametrize("key,value", [ ("default_measurement_interval_minutes", "1"), ("default_measurement_interval_minutes", "1440"), ("default_measurement_interval_minutes", ""), ("general_tolerance_plus", "0"), ("general_tolerance_plus", "0.05"), ("general_tolerance_minus", ""), ("list_measurement_types", '["Lunghezza", "Diametro"]'), ("list_units", "[]"), ("list_units", ""), ]) async def test_new_keys_valid_accepted( self, client: AsyncClient, admin_user: User, key: str, value: str ): resp = await client.put( "/api/settings/", headers=auth_headers(admin_user), json={key: value} ) assert resp.status_code == 200, resp.text @pytest.mark.parametrize("key,value", [ ("default_measurement_interval_minutes", "0"), ("default_measurement_interval_minutes", "1441"), ("default_measurement_interval_minutes", "abc"), ("general_tolerance_plus", "-0.1"), ("general_tolerance_plus", "0,05"), ("general_tolerance_minus", "NaN"), ("general_tolerance_minus", "abc"), ("list_measurement_types", "not json"), ("list_measurement_types", '{"a": 1}'), ("list_units", '["mm", ""]'), ("list_units", '["mm", 3]'), ("list_units", "[" + ",".join(f'"v{i}"' for i in range(51)) + "]"), ]) async def test_new_keys_invalid_rejected( self, client: AsyncClient, admin_user: User, key: str, value: str ): resp = await client.put( "/api/settings/", headers=auth_headers(admin_user), json={key: value} ) assert resp.status_code == 422, resp.text assert key in resp.json()["detail"] async def test_unknown_key_not_validated( self, client: AsyncClient, admin_user: User ): """Keys with no known validator pass through (generic settings store).""" resp = await client.put( "/api/settings/", headers=auth_headers(admin_user), json={"some_future_key": "anything"}, ) assert resp.status_code == 200 class TestUploadLogo: """POST /api/settings/logo tests (gitea#36 point 2).""" _PNG_BYTES = b"\x89PNG\r\n\x1a\n" + b"\x00" * 20 async def test_valid_png_accepted( self, client: AsyncClient, admin_user: User, tmp_path: Path ): with patch("src.backend.api.routers.settings.settings") as mock_settings: mock_settings.upload_path = tmp_path mock_settings.max_upload_size_mb = 50 resp = await client.post( "/api/settings/logo", headers=auth_headers(admin_user), files={"file": ("logo.png", io.BytesIO(self._PNG_BYTES), "image/png")}, ) assert resp.status_code == 200 assert resp.json()["logo_path"].endswith(".png") async def test_svg_refused_even_with_svg_content_type( self, client: AsyncClient, admin_user: User, tmp_path: Path ): """SVG is refused outright, not just when it lies about its type.""" svg_bytes = b'' with patch("src.backend.api.routers.settings.settings") as mock_settings: mock_settings.upload_path = tmp_path mock_settings.max_upload_size_mb = 50 resp = await client.post( "/api/settings/logo", headers=auth_headers(admin_user), files={"file": ("logo.svg", io.BytesIO(svg_bytes), "image/svg+xml")}, ) assert resp.status_code == 400 assert not any(tmp_path.rglob("*.svg")) async def test_pdf_refused_for_logo( self, client: AsyncClient, admin_user: User, tmp_path: Path ): """The logo slot only accepts images: files.py's PDF signature must not slip through settings.py's narrower ALLOWED_IMAGE_TYPES check.""" pdf_bytes = b"%PDF-1.4\n" + b"\x00" * 20 with patch("src.backend.api.routers.settings.settings") as mock_settings: mock_settings.upload_path = tmp_path mock_settings.max_upload_size_mb = 50 resp = await client.post( "/api/settings/logo", headers=auth_headers(admin_user), files={"file": ("logo.pdf", io.BytesIO(pdf_bytes), "application/pdf")}, ) assert resp.status_code == 400 async def test_html_disguised_as_png_refused( self, client: AsyncClient, admin_user: User, tmp_path: Path ): html_bytes = b"
" with patch("src.backend.api.routers.settings.settings") as mock_settings: mock_settings.upload_path = tmp_path mock_settings.max_upload_size_mb = 50 resp = await client.post( "/api/settings/logo", headers=auth_headers(admin_user), files={"file": ("logo.png", io.BytesIO(html_bytes), "image/png")}, ) assert resp.status_code == 400