Capoturno: autorizzazione con PIN (nome + tastierino) o password nella modale (#59)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
"""Capoturno authorisation by PIN (#59): proxies of the measure blueprint.
|
||||
|
||||
The modal in task_execute.html sends either ``supervisor_user_id`` + ``supervisor_pin``
|
||||
or the old ``supervisor_username`` + ``supervisor_password``; the proxies forward
|
||||
whichever pair is present and let the PIN/lockout refusals through to the modal.
|
||||
"""
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from flask import session
|
||||
|
||||
RUN = {"id": 7, "status": "paused"}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def measure_mod():
|
||||
from blueprints import measure
|
||||
return measure
|
||||
|
||||
|
||||
def test_pin_users_proxy_returns_only_id_and_name(logged_in_client, measure_mod):
|
||||
with patch.object(measure_mod, "api_client") as mock_api:
|
||||
mock_api.get.return_value = [
|
||||
{"id": 3, "display_name": "Anna Rossi", "pin_hash": "x"},
|
||||
{"id": 4, "display_name": "bruno"},
|
||||
]
|
||||
resp = logged_in_client.get("/measure/api/pin-users")
|
||||
assert resp.status_code == 200
|
||||
assert resp.get_json() == [
|
||||
{"id": 3, "display_name": "Anna Rossi"},
|
||||
{"id": 4, "display_name": "bruno"},
|
||||
]
|
||||
assert mock_api.get.call_args[0][0] == "/api/auth/pin-users"
|
||||
|
||||
|
||||
def test_pin_users_proxy_empty_on_backend_error(logged_in_client, measure_mod):
|
||||
"""The modal opens the Password tab when the list is empty."""
|
||||
with patch.object(measure_mod, "api_client") as mock_api:
|
||||
mock_api.get.return_value = {"error": True, "status_code": 502, "detail": "down"}
|
||||
resp = logged_in_client.get("/measure/api/pin-users")
|
||||
assert resp.status_code == 200
|
||||
assert resp.get_json() == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("action", ["pause", "resume", "close"])
|
||||
def test_supervised_action_forwards_user_id_and_pin(logged_in_client, measure_mod, action):
|
||||
with patch.object(measure_mod, "api_client") as mock_api:
|
||||
mock_api.post.return_value = RUN
|
||||
resp = logged_in_client.post(
|
||||
f"/measure/api/production/7/{action}",
|
||||
json={"supervisor_user_id": 5, "supervisor_pin": "1234"},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
endpoint, kwargs = mock_api.post.call_args
|
||||
assert endpoint[0] == f"/api/production-runs/7/{action}"
|
||||
assert kwargs["data"]["supervisor_user_id"] == 5
|
||||
assert kwargs["data"]["supervisor_pin"] == "1234"
|
||||
assert "supervisor_username" not in kwargs["data"]
|
||||
assert "supervisor_password" not in kwargs["data"]
|
||||
|
||||
|
||||
def test_supervised_action_password_pair_unchanged(logged_in_client, measure_mod):
|
||||
with patch.object(measure_mod, "api_client") as mock_api:
|
||||
mock_api.post.return_value = RUN
|
||||
resp = logged_in_client.post(
|
||||
"/measure/api/production/7/pause",
|
||||
json={"supervisor_username": "capo", "supervisor_password": "segreta"},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
kwargs = mock_api.post.call_args[1]
|
||||
assert kwargs["data"]["supervisor_username"] == "capo"
|
||||
assert kwargs["data"]["supervisor_password"] == "segreta"
|
||||
assert "supervisor_pin" not in kwargs["data"]
|
||||
|
||||
|
||||
def test_supervised_action_pin_without_user_is_refused(logged_in_client, measure_mod):
|
||||
with patch.object(measure_mod, "api_client") as mock_api:
|
||||
resp = logged_in_client.post(
|
||||
"/measure/api/production/7/pause", json={"supervisor_pin": "1234"},
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
mock_api.post.assert_not_called()
|
||||
|
||||
|
||||
def test_authorise_measurement_forwards_user_id_and_pin(logged_in_client, measure_mod):
|
||||
with patch.object(measure_mod, "api_client") as mock_api:
|
||||
mock_api.post.return_value = {"id": 9}
|
||||
resp = logged_in_client.post(
|
||||
"/measure/api/measurements/9/authorise",
|
||||
json={"supervisor_user_id": 5, "supervisor_pin": "4321"},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
endpoint, kwargs = mock_api.post.call_args
|
||||
assert endpoint[0] == "/api/measurements/9/authorise"
|
||||
assert kwargs["data"] == {"supervisor_user_id": 5, "supervisor_pin": "4321"}
|
||||
|
||||
|
||||
def test_wrong_pin_401_stays_in_the_modal_and_keeps_the_session(logged_in_client, measure_mod):
|
||||
"""api_client empties the session on any 401; a wrong PIN must not log the operator out."""
|
||||
def refuse(*args, **kwargs):
|
||||
session.clear() # what api_client._handle_session_expired does
|
||||
return {
|
||||
"error": True, "status_code": 401, "session_expired": True,
|
||||
"detail": "Invalid supervisor credentials",
|
||||
}
|
||||
|
||||
with patch.object(measure_mod, "api_client") as mock_api:
|
||||
mock_api.post.side_effect = refuse
|
||||
resp = logged_in_client.post(
|
||||
"/measure/api/production/7/close",
|
||||
json={"supervisor_user_id": 5, "supervisor_pin": "0000"},
|
||||
)
|
||||
assert resp.status_code == 401
|
||||
body = resp.get_json()
|
||||
assert body["detail"] == "PIN errato"
|
||||
assert "session_expired" not in body
|
||||
with logged_in_client.session_transaction() as sess:
|
||||
assert sess.get("api_key") == "test-api-key-12345"
|
||||
|
||||
|
||||
def test_real_expired_session_401_still_clears_the_session(logged_in_client, measure_mod):
|
||||
"""A 401 that is not about the capoturno keeps its session_expired flag."""
|
||||
def expired(*args, **kwargs):
|
||||
session.clear()
|
||||
return {
|
||||
"error": True, "status_code": 401, "session_expired": True,
|
||||
"detail": "Invalid API key",
|
||||
}
|
||||
|
||||
with patch.object(measure_mod, "api_client") as mock_api:
|
||||
mock_api.post.side_effect = expired
|
||||
resp = logged_in_client.post(
|
||||
"/measure/api/production/7/close",
|
||||
json={"supervisor_user_id": 5, "supervisor_pin": "0000"},
|
||||
)
|
||||
assert resp.status_code == 401
|
||||
assert resp.get_json()["session_expired"] is True
|
||||
with logged_in_client.session_transaction() as sess:
|
||||
assert "api_key" not in sess
|
||||
|
||||
|
||||
def test_locked_user_423_passes_the_detail(logged_in_client, measure_mod):
|
||||
with patch.object(measure_mod, "api_client") as mock_api:
|
||||
mock_api.post.return_value = {
|
||||
"error": True, "status_code": 423,
|
||||
"detail": "Utente bloccato fino alle 10:15",
|
||||
}
|
||||
resp = logged_in_client.post(
|
||||
"/measure/api/production/7/pause",
|
||||
json={"supervisor_user_id": 5, "supervisor_pin": "1111"},
|
||||
)
|
||||
assert resp.status_code == 423
|
||||
assert resp.get_json()["detail"] == "Utente bloccato fino alle 10:15"
|
||||
|
||||
|
||||
def test_password_401_detail_is_kept(logged_in_client, measure_mod):
|
||||
"""Password mode: the refusal text is the backend's, as before."""
|
||||
with patch.object(measure_mod, "api_client") as mock_api:
|
||||
mock_api.post.return_value = {
|
||||
"error": True, "status_code": 403, "detail": "User is not authorised as supervisor",
|
||||
}
|
||||
resp = logged_in_client.post(
|
||||
"/measure/api/production/7/pause",
|
||||
json={"supervisor_username": "op", "supervisor_password": "x"},
|
||||
)
|
||||
assert resp.status_code == 403
|
||||
assert resp.get_json()["detail"] == "User is not authorised as supervisor"
|
||||
Reference in New Issue
Block a user