Files
rpa_vision_v3/tests/unit/test_dashboard_auth_p0a.py
Dom bb4ed2a75d feat(dashboard): session cleaner intégré + auth + nettoyage UI
- Onglet "🧹 Nettoyage" dans le dashboard (iframe vers port 5006)
- Indicateur d'état + bouton de démarrage si cleaner down
- Service systemd rpa-session-cleaner intégré au target rpa-vision
- svc.sh et services.conf incluent session-cleaner (port 5006)

P0-A — Auth dashboard Flask :
- HTTP Basic obligatoire sur tous les endpoints (sauf /health, /healthz)
- Credentials via DASHBOARD_USER + DASHBOARD_PASSWORD
- 13 tests

Nettoyage UI :
- Section "Détection Visuelle" OWL retirée (modèle remplacé par pipeline VLM)
- Dashboard préfère auto shot_*_blurred.png (avec ?raw=1 pour brut)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:48:36 +02:00

161 lines
6.0 KiB
Python

"""
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"])