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>
This commit is contained in:
160
tests/unit/test_dashboard_auth_p0a.py
Normal file
160
tests/unit/test_dashboard_auth_p0a.py
Normal file
@@ -0,0 +1,160 @@
|
||||
"""
|
||||
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"])
|
||||
Reference in New Issue
Block a user