c764cf73c6
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
203 lines
8.1 KiB
Python
203 lines
8.1 KiB
Python
"""Tests for auth blueprint (client/blueprints/auth.py).
|
|
|
|
Covers login GET/POST, logout, unauthenticated redirect, and language/theme switch.
|
|
"""
|
|
import pytest
|
|
from unittest.mock import patch
|
|
|
|
|
|
class TestLoginPage:
|
|
"""GET /auth/login tests."""
|
|
|
|
def test_login_page_renders(self, client):
|
|
"""Login page renders successfully with 200 status."""
|
|
resp = client.get("/auth/login")
|
|
assert resp.status_code == 200
|
|
assert b"login" in resp.data.lower() or b"Login" in resp.data
|
|
|
|
def test_login_page_redirects_when_already_logged_in(self, logged_in_client):
|
|
"""Already authenticated user is redirected away from login."""
|
|
resp = logged_in_client.get("/auth/login")
|
|
assert resp.status_code == 302
|
|
|
|
|
|
class TestLoginPost:
|
|
"""POST /auth/login tests."""
|
|
|
|
def test_login_post_success_redirects(self, client, mock_api_client):
|
|
"""Successful login POST sets session and redirects."""
|
|
mock_api_client.post.return_value = {
|
|
"api_key": "new-api-key-abc",
|
|
"user": {
|
|
"id": 1,
|
|
"username": "testuser",
|
|
"display_name": "Test User",
|
|
"roles": ["MeasurementTec"],
|
|
"is_admin": False,
|
|
"language_pref": "en",
|
|
"theme_pref": "light",
|
|
"active": True,
|
|
},
|
|
}
|
|
# Mock settings call made after login
|
|
mock_api_client.get.return_value = {}
|
|
|
|
resp = client.post(
|
|
"/auth/login",
|
|
data={"username": "testuser", "password": "pass123"},
|
|
follow_redirects=False,
|
|
)
|
|
assert resp.status_code == 302
|
|
mock_api_client.post.assert_called_once()
|
|
|
|
# Verify session was populated
|
|
with client.session_transaction() as sess:
|
|
assert sess.get("api_key") == "new-api-key-abc"
|
|
assert sess["user"]["username"] == "testuser"
|
|
# #33: without this, PERMANENT_SESSION_LIFETIME (8h) never applied
|
|
# and Flask used a browser-length session instead.
|
|
assert sess.permanent is True
|
|
|
|
def test_login_post_error_shows_error(self, client, mock_api_client):
|
|
"""Failed login returns the login page with error flash."""
|
|
mock_api_client.post.return_value = {
|
|
"error": True,
|
|
"detail": "Credenziali non valide",
|
|
}
|
|
|
|
resp = client.post(
|
|
"/auth/login",
|
|
data={"username": "bad", "password": "wrong"},
|
|
follow_redirects=True,
|
|
)
|
|
assert resp.status_code == 200
|
|
# Error message should appear in the response (flashed)
|
|
assert b"Credenziali non valide" in resp.data or b"login" in resp.data.lower()
|
|
|
|
def test_login_post_empty_fields(self, client, mock_api_client):
|
|
"""Login with empty username/password shows validation error."""
|
|
resp = client.post(
|
|
"/auth/login",
|
|
data={"username": "", "password": ""},
|
|
follow_redirects=True,
|
|
)
|
|
assert resp.status_code == 200
|
|
# Should render login again (not crash)
|
|
|
|
|
|
class TestLogout:
|
|
"""POST /auth/logout tests (#33: GET is refused - logout must carry CSRF)."""
|
|
|
|
def test_logout_clears_session(self, logged_in_client, mock_api_client):
|
|
"""Logout clears session and redirects to login."""
|
|
mock_api_client.post.return_value = {}
|
|
|
|
resp = logged_in_client.post("/auth/logout", follow_redirects=False)
|
|
assert resp.status_code == 302
|
|
assert "/auth/login" in resp.headers["Location"]
|
|
|
|
# Session should be cleared
|
|
with logged_in_client.session_transaction() as sess:
|
|
assert "api_key" not in sess
|
|
assert "user" not in sess
|
|
|
|
def test_logout_get_is_rejected(self, logged_in_client):
|
|
"""#33: a bare GET (link, prefetch, forged <img>) no longer logs anyone out."""
|
|
resp = logged_in_client.get("/auth/logout", follow_redirects=False)
|
|
assert resp.status_code == 405
|
|
|
|
|
|
class TestUnauthenticatedRedirect:
|
|
"""Protected routes redirect unauthenticated users."""
|
|
|
|
def test_profile_requires_login(self, client):
|
|
"""Profile page redirects to login when not authenticated."""
|
|
resp = client.get("/auth/profile", follow_redirects=False)
|
|
assert resp.status_code == 302
|
|
assert "/auth/login" in resp.headers["Location"]
|
|
|
|
|
|
class TestLanguageSwitch:
|
|
"""Language and theme switching via app routes."""
|
|
|
|
def test_set_language_stores_in_session(self, client):
|
|
"""Setting language updates session and redirects back."""
|
|
resp = client.get("/set-language/en", follow_redirects=False)
|
|
assert resp.status_code == 302
|
|
|
|
with client.session_transaction() as sess:
|
|
assert sess.get("language") == "en"
|
|
|
|
def test_set_language_invalid_ignored(self, client):
|
|
"""Setting an unsupported language does not crash and still redirects."""
|
|
resp = client.get("/set-language/xx", follow_redirects=False)
|
|
assert resp.status_code == 302
|
|
|
|
with client.session_transaction() as sess:
|
|
# Invalid language should NOT be stored
|
|
assert sess.get("language") != "xx"
|
|
|
|
|
|
class TestPinLogin:
|
|
"""Login Operatore con nome + PIN (#59)."""
|
|
|
|
USER = {
|
|
"id": 7, "username": "mario", "display_name": "Mario Rossi",
|
|
"roles": ["MeasurementTec"], "is_admin": False,
|
|
"language_pref": "it", "theme_pref": "light", "active": True,
|
|
}
|
|
|
|
def test_login_page_has_pin_view_and_password_link(self, client):
|
|
resp = client.get("/auth/login")
|
|
assert b"pin-pad.js" in resp.data
|
|
assert b"pin-pad" in resp.data # tastierino presente nel markup
|
|
assert b'name="password"' in resp.data # form password ancora presente
|
|
|
|
def test_pin_users_listed(self, client, mock_api_client):
|
|
mock_api_client.get.return_value = [
|
|
{"id": 7, "display_name": "Mario Rossi", "secret": "x"},
|
|
]
|
|
resp = client.get("/auth/pin-users")
|
|
assert resp.status_code == 200
|
|
assert resp.get_json() == [{"id": 7, "display_name": "Mario Rossi"}]
|
|
|
|
def test_pin_users_empty_on_error(self, client, mock_api_client):
|
|
mock_api_client.get.return_value = {"error": True, "detail": "boom"}
|
|
assert client.get("/auth/pin-users").get_json() == []
|
|
|
|
def test_pin_users_empty_list_falls_back(self, client, mock_api_client):
|
|
mock_api_client.get.return_value = []
|
|
assert client.get("/auth/pin-users").get_json() == []
|
|
|
|
def test_login_pin_success_sets_session(self, client, mock_api_client):
|
|
mock_api_client.post.return_value = {"api_key": "pin-key", "user": self.USER}
|
|
mock_api_client.get.return_value = {}
|
|
resp = client.post("/auth/login-pin", json={"user_id": 7, "pin": "1234"})
|
|
assert resp.status_code == 200
|
|
assert resp.get_json()["ok"] is True
|
|
assert resp.get_json()["redirect"]
|
|
mock_api_client.post.assert_called_once_with(
|
|
"/api/auth/login-pin", data={"user_id": 7, "pin": "1234"})
|
|
with client.session_transaction() as sess:
|
|
assert sess["api_key"] == "pin-key"
|
|
assert sess["user_id"] == 7
|
|
assert sess.permanent is True
|
|
|
|
def test_login_pin_wrong_pin_401(self, client, mock_api_client):
|
|
mock_api_client.post.return_value = {"error": True, "status_code": 401, "detail": "x"}
|
|
resp = client.post("/auth/login-pin", json={"user_id": 7, "pin": "0000"})
|
|
assert resp.status_code == 401
|
|
assert resp.get_json()["detail"] == "PIN errato"
|
|
|
|
def test_login_pin_locked_423_shows_detail(self, client, mock_api_client):
|
|
mock_api_client.post.return_value = {
|
|
"error": True, "status_code": 423, "detail": "Utente bloccato fino alle 10:15"}
|
|
resp = client.post("/auth/login-pin", json={"user_id": 7, "pin": "0000"})
|
|
assert resp.status_code == 423
|
|
assert "10:15" in resp.get_json()["detail"]
|
|
|
|
def test_login_pin_invalid_pin_422(self, client, mock_api_client):
|
|
resp = client.post("/auth/login-pin", json={"user_id": 7, "pin": "12"})
|
|
assert resp.status_code == 422
|
|
mock_api_client.post.assert_not_called()
|