""" Tests du Fix P0-A : authentification HTTP Basic sur le dashboard Flask (port 5001). Avant ce fix, 71 endpoints étaient exposés sans auth. Le middleware `_dashboard_basic_auth_middleware` ajoute un challenge 401 sur toutes les routes HTTP sauf les healthchecks publics. Contrôles : - Sans Authorization header → 401 avec WWW-Authenticate - Avec mauvais credentials → 401 - Avec bons credentials → passage normal (200) - /health, /healthz, /api/health restent publics (monitoring externe) - Mode TESTING sans DASHBOARD_AUTH_ENABLED → bypass (compat tests existants) - DASHBOARD_AUTH_DISABLED=true → bypass global (dev local) """ from __future__ import annotations import base64 import importlib import sys from pathlib import Path import pytest # Ajouter le répertoire racine au path sys.path.insert(0, str(Path(__file__).parent.parent.parent)) @pytest.fixture def auth_enabled_client(monkeypatch): """Client Flask avec l'auth activée (TESTING + DASHBOARD_AUTH_ENABLED). On recharge le module pour forcer la relecture des variables d'env. """ monkeypatch.setenv("DASHBOARD_USER", "lea") monkeypatch.setenv("DASHBOARD_PASSWORD", "secret-test-pwd") monkeypatch.delenv("DASHBOARD_AUTH_DISABLED", raising=False) # Recharger le module pour que les constantes soient relues if "web_dashboard.app" in sys.modules: importlib.reload(sys.modules["web_dashboard.app"]) from web_dashboard.app import app app.config["TESTING"] = True app.config["DASHBOARD_AUTH_ENABLED"] = True with app.test_client() as c: yield c @pytest.fixture def auth_disabled_client(monkeypatch): """Client Flask avec l'auth désactivée (bypass global).""" monkeypatch.setenv("DASHBOARD_AUTH_DISABLED", "true") if "web_dashboard.app" in sys.modules: importlib.reload(sys.modules["web_dashboard.app"]) from web_dashboard.app import app app.config["TESTING"] = True with app.test_client() as c: yield c def _basic_auth_header(user: str, password: str) -> str: token = base64.b64encode(f"{user}:{password}".encode()).decode() return f"Basic {token}" class TestDashboardAuthP0A: """Fix P0-A : auth HTTP Basic obligatoire sur le dashboard.""" def test_no_auth_header_returns_401(self, auth_enabled_client): """Sans header Authorization → 401 + challenge WWW-Authenticate.""" resp = auth_enabled_client.get("/api/system/status") assert resp.status_code == 401 assert "WWW-Authenticate" in resp.headers assert "Basic" in resp.headers["WWW-Authenticate"] def test_wrong_password_returns_401(self, auth_enabled_client): """Mauvais mot de passe → 401.""" resp = auth_enabled_client.get( "/api/system/status", headers={"Authorization": _basic_auth_header("lea", "wrong")}, ) assert resp.status_code == 401 def test_wrong_user_returns_401(self, auth_enabled_client): """Mauvais utilisateur → 401.""" resp = auth_enabled_client.get( "/api/system/status", headers={"Authorization": _basic_auth_header("intruder", "secret-test-pwd")}, ) assert resp.status_code == 401 def test_malformed_header_returns_401(self, auth_enabled_client): """Header mal formé (pas de Basic) → 401.""" resp = auth_enabled_client.get( "/api/system/status", headers={"Authorization": "Bearer tototoken"}, ) assert resp.status_code == 401 def test_valid_credentials_pass(self, auth_enabled_client): """Bons credentials → 200.""" resp = auth_enabled_client.get( "/api/system/status", headers={"Authorization": _basic_auth_header("lea", "secret-test-pwd")}, ) assert resp.status_code == 200 def test_healthz_public(self, auth_enabled_client): """/healthz reste public (systemd healthcheck).""" resp = auth_enabled_client.get("/healthz") assert resp.status_code == 200 def test_health_public(self, auth_enabled_client): """/health reste public (monitoring externe).""" resp = auth_enabled_client.get("/health") assert resp.status_code == 200 def test_api_health_public(self, auth_enabled_client): """/api/health reste public (NPM reverse proxy).""" resp = auth_enabled_client.get("/api/health") assert resp.status_code == 200 def test_auth_disabled_bypass(self, auth_disabled_client): """DASHBOARD_AUTH_DISABLED=true → pas d'auth requise.""" resp = auth_disabled_client.get("/api/system/status") assert resp.status_code == 200 def test_config_endpoint_requires_auth(self, auth_enabled_client): """L'endpoint sensible /api/config exige l'auth.""" resp = auth_enabled_client.get("/api/config") assert resp.status_code == 401 def test_services_endpoint_requires_auth(self, auth_enabled_client): """L'endpoint sensible /api/services exige l'auth.""" resp = auth_enabled_client.get("/api/services") assert resp.status_code == 401 def test_services_start_all_requires_auth(self, auth_enabled_client): """Un endpoint POST destructeur exige l'auth.""" resp = auth_enabled_client.post("/api/services/start-all") assert resp.status_code == 401 def test_index_page_requires_auth(self, auth_enabled_client): """Même la page HTML d'accueil exige l'auth (pas de leak côté public).""" resp = auth_enabled_client.get("/") assert resp.status_code == 401 @pytest.fixture(autouse=True) def _restore_module(monkeypatch): """Recharge web_dashboard.app après chaque test pour que les autres tests (TestDashboardRoutes sans auth explicite) continuent de passer.""" yield monkeypatch.delenv("DASHBOARD_AUTH_DISABLED", raising=False) monkeypatch.delenv("DASHBOARD_USER", raising=False) monkeypatch.delenv("DASHBOARD_PASSWORD", raising=False) if "web_dashboard.app" in sys.modules: importlib.reload(sys.modules["web_dashboard.app"])