Files
TieMeasureFlow/src/backend/tests/test_settings.py
T

310 lines
10 KiB
Python

"""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'<svg xmlns="http://www.w3.org/2000/svg"><script>alert(1)</script></svg>'
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"<html><body><script>alert(1)</script></body></html>"
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