Utenti: PIN impostabile/togliibile dall'admin, link Monitor nel menu (#51 #59 #57)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 12:38:58 +02:00
parent c8dd5eecfd
commit 3c55550d93
5 changed files with 305 additions and 0 deletions
@@ -0,0 +1,110 @@
"""#59/#51/#57: PIN nella gestione utenti (proxy admin, pannello) e link Monitor nel menu."""
from unittest.mock import MagicMock, patch
import pytest
from tests.conftest import api_get_router
USER = {
"id": 2, "username": "mario", "display_name": "Mario", "email": None,
"roles": ["MeasurementTec"], "is_admin": False, "language_pref": "it",
"theme_pref": "light", "active": True, "has_pin": True,
}
@pytest.fixture
def mock_admin_api():
mock = MagicMock()
mock.get.side_effect = api_get_router({
"/api/users": [USER],
"/api/settings": {"auto_logout_minutes": "30"},
})
with patch("blueprints.admin.api_client", mock):
yield mock
def test_pin_proxy_forwards_four_digits(logged_in_client, mock_admin_api):
mock_admin_api.put.return_value = {"message": "ok"}
resp = logged_in_client.put("/admin/api/users/2/pin", json={"pin": "1234"})
assert resp.status_code == 200
mock_admin_api.put.assert_called_once_with("/api/users/2/pin", data={"pin": "1234"})
def test_pin_proxy_forwards_null_to_remove(logged_in_client, mock_admin_api):
mock_admin_api.put.return_value = {"message": "ok"}
resp = logged_in_client.put("/admin/api/users/2/pin", json={"pin": None})
assert resp.status_code == 200
mock_admin_api.put.assert_called_once_with("/api/users/2/pin", data={"pin": None})
def test_pin_proxy_passes_422_detail(logged_in_client, mock_admin_api):
mock_admin_api.put.return_value = {
"error": True, "status_code": 422, "detail": "Gli admin non hanno il PIN",
}
resp = logged_in_client.put("/admin/api/users/1/pin", json={"pin": "1234"})
assert resp.status_code == 422
assert resp.get_json()["detail"] == "Gli admin non hanno il PIN"
def test_pin_proxy_requires_admin(client, mock_admin_api):
with client.session_transaction() as sess:
sess["api_key"] = "k"
sess["user"] = {"id": 3, "username": "op", "roles": ["MeasurementTec"],
"is_admin": False, "language_pref": "it", "theme_pref": "light"}
resp = client.put("/admin/api/users/2/pin", json={"pin": "1234"})
assert resp.status_code != 200
mock_admin_api.put.assert_not_called()
def test_users_panel_has_pin_controls(logged_in_client, mock_admin_api):
html = logged_in_client.get("/admin/settings?tab=utenti").data.decode("utf-8")
assert "openPin(user)" in html # pulsante Imposta/Reset PIN
assert "user.has_pin === true" in html # indicatore PIN impostato
assert "removePin()" in html # Togli PIN
assert "form.pin" in html # campo PIN alla creazione
assert "!user.is_admin" in html # niente PIN per gli admin
def _session_client(client, roles, is_admin):
with client.session_transaction() as sess:
sess["api_key"] = "k"
sess["user"] = {"id": 5, "username": "u", "display_name": "U", "roles": roles,
"is_admin": is_admin, "language_pref": "it", "theme_pref": "light"}
return client
@pytest.fixture
def mock_monitor_api():
mock = MagicMock()
mock.get.return_value = []
with patch("blueprints.monitor.api_client", mock), \
patch("blueprints.measure.api_client", mock):
yield mock
def test_monitor_link_for_admin(logged_in_client, mock_monitor_api):
html = logged_in_client.get("/monitor").data.decode("utf-8")
assert 'href="/monitor"' in html
def test_monitor_link_for_supervisor(client, mock_monitor_api):
_session_client(client, ["Supervisor"], False)
html = client.get("/monitor").data.decode("utf-8")
assert 'href="/monitor"' in html
def test_monitor_link_absent_for_operator(client, mock_api_client, monkeypatch):
# Stessa preparazione di test_measure.py: la pagina vuole STATION_CODE a config ricaricata
monkeypatch.setenv("STATION_CODE", "ST-TEST")
import importlib
import config
importlib.reload(config)
import blueprints.measure
importlib.reload(blueprints.measure)
mock_api_client.get_station_recipes.return_value = []
_session_client(client, ["MeasurementTec"], False)
resp = client.get("/measure/select")
assert resp.status_code == 200
html = resp.data.decode("utf-8")
assert "/measure/select" in html or "Misure" in html or "Measure" in html # menu reso
assert 'href="/monitor"' not in html