feat: FASE 7 - Polish & Testing (security, i18n, test suite, docs)
Security hardening: CORS lockdown, rate limiting middleware con sliding window e eviction IP stale, security headers (CSP, HSTS, X-Frame-Options), session cookie hardening, filename sanitization upload. i18n completion: internazionalizzati barcode.js e csv-export.js con bridge window.BARCODE_I18N/CSV_I18N, aggiornati .po IT/EN con 27 nuove stringhe. Tablet UX: touch target 44px per dispositivi coarse pointer. Test suite: 101 test totali (76 server + 25 client), copertura completa di tutti i router API, autenticazione, ruoli, CRUD, SPC, file upload, security integration. Infrastruttura SQLite async in-memory con fixtures. Fix critici: MissingGreenlet in recipe_service (selectinload eager), route ordering tasks.py, auth_service bcrypt diretto, Measurement.id Integer per SQLite. Documentazione: API.md (riferimento completo 40+ endpoint), DEPLOYMENT.md (guida produzione con Docker/Nginx/SSL), USER_GUIDE.md (manuale utente per ruolo). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
"""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"
|
||||
|
||||
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:
|
||||
"""GET/POST /auth/logout tests."""
|
||||
|
||||
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.get("/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
|
||||
|
||||
|
||||
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"
|
||||
Reference in New Issue
Block a user