"""Tests for the 401 -> session-expired handling (#25, frontend half). Backend half (auth_service key reuse, constant-time auth) is lot A2 and lives in src/backend - not covered here. """ from unittest.mock import MagicMock, patch from flask import g, session def _mock_401_response(detail="Invalid or expired API key"): resp = MagicMock() resp.ok = False resp.status_code = 401 resp.json.return_value = {"detail": detail} resp.text = detail return resp class TestApiClientHandlesResponse: """Unit-level: services/api_client.py clears the session on 401.""" def test_401_clears_session_and_flags_g(self, flask_app): from services.api_client import api_client with flask_app.test_request_context("/"): session["api_key"] = "stale-key" session["user"] = {"id": 1, "username": "op1"} result = api_client._handle_response(_mock_401_response()) assert result["error"] is True assert result["status_code"] == 401 assert result["session_expired"] is True assert "api_key" not in session assert "user" not in session assert g.session_expired is True def test_non_401_error_does_not_touch_session(self, flask_app): from services.api_client import api_client with flask_app.test_request_context("/"): session["api_key"] = "still-good" resp = MagicMock(ok=False, status_code=404) resp.json.return_value = {"detail": "not found"} result = api_client._handle_response(resp) assert "session_expired" not in result assert session.get("api_key") == "still-good" assert not getattr(g, "session_expired", False) class TestHtmlPageRedirectsOnSessionExpired: """Integration: a page view that got a 401 ends up redirected to login.""" def test_recipe_list_401_redirects_to_login(self, logged_in_client): with patch("services.api_client.requests.get", return_value=_mock_401_response()): resp = logged_in_client.get("/maker/recipes", follow_redirects=False) assert resp.status_code == 302 assert "/auth/login" in resp.headers["Location"] def test_recipe_list_401_empties_the_session(self, logged_in_client): with patch("services.api_client.requests.get", return_value=_mock_401_response()): logged_in_client.get("/maker/recipes", follow_redirects=False) with logged_in_client.session_transaction() as sess: assert "api_key" not in sess assert "user" not in sess def test_recipe_list_401_flashes_session_expired_message(self, logged_in_client): with patch("services.api_client.requests.get", return_value=_mock_401_response()): resp = logged_in_client.get("/maker/recipes", follow_redirects=True) assert "Sessione scaduta" in resp.get_data(as_text=True) class TestJsonProxyReturns401WithFlag: """Integration: an AJAX proxy route surfaces session_expired in its JSON body.""" def test_save_measurement_401_returns_session_expired_flag(self, logged_in_client): with patch("services.api_client.requests.post", return_value=_mock_401_response()): resp = logged_in_client.post( "/measure/save-measurement", json={"subtask_id": 1, "version_id": 1, "value": 1.0}, ) assert resp.status_code == 401 body = resp.get_json() assert body["error"] is True assert body["session_expired"] is True def test_statistics_summary_401_returns_session_expired_flag(self, logged_in_client): with patch("services.api_client.requests.get", return_value=_mock_401_response()): resp = logged_in_client.get("/statistics/api/summary") assert resp.status_code == 401 body = resp.get_json() assert body["session_expired"] is True