# ============================================ # Projet : API-KA # Fichier : tests/test_stats.py # Node : m3u96b # Author : Simon-Pierre Boucher # Contact : contact@spboucher.ai # Date : 2026-08-17 # ============================================ """Tests des routes /api/stats : dashboard (contrat SPEC), rapport PDF, page /stats.""" from __future__ import annotations import datetime import pytest from fastapi.testclient import TestClient from src.api import reqstats from src.api.main import app from src.api.routes import stats as stats_module from src.database.db import session_scope from src.database.models import ApiRequest, CollectionRun @pytest.fixture(autouse=True) def _clear_stats_cache(): """Le cache 5 min du dashboard ne doit pas fuiter entre les tests.""" stats_module._cache.clear() yield stats_module._cache.clear() def _seed_requests_and_run() -> None: now = datetime.datetime.now(tz=datetime.UTC) today = datetime.datetime.now(tz=stats_module.TZ).date() with session_scope() as session: if session.query(ApiRequest).count() == 0: for i in range(5): session.add( ApiRequest( ts=now - datetime.timedelta(hours=i + 1), method="GET", endpoint="/api/v1/louka", status=200 if i else 404, duration_ms=10.0 + i, ) ) session.add( CollectionRun( service="louka", date_key=today, status="success", records_count=42, duration_seconds=2.0, error_message=None, node="m3u96b", started_at=now, finished_at=now, ) ) def test_dashboard_contract() -> None: _seed_requests_and_run() with TestClient(app) as client: response = client.get("/api/stats/dashboard?period=7j") assert response.status_code == 200 body = response.json() assert body["success"] is True dash = body["data"] assert set(dash) >= {"updated", "period", "kpis", "series", "tables", "records"} kpis = {k["id"]: k for k in dash["kpis"]} assert kpis["calls"]["value"] >= 5 assert kpis["records"]["value"] >= 42 assert any(s["id"] == "calls" for s in dash["series"]) def test_dashboard_invalid_period() -> None: with TestClient(app) as client: assert client.get("/api/stats/dashboard?period=xx").status_code == 422 def test_report_pdf() -> None: _seed_requests_and_run() with TestClient(app) as client: response = client.get("/api/stats/report?period=7j&mode=synthese") assert response.status_code == 200 assert response.headers["content-type"] == "application/pdf" assert "groupe-ka_api-ka_stats_7j_" in response.headers["content-disposition"] assert response.content.startswith(b"%PDF") def test_stats_page_and_middleware_recording() -> None: with TestClient(app) as client: response = client.get("/stats") assert response.status_code == 200 assert "tableau de bord" in response.text.lower() or "stats" in response.text.lower() # Le middleware a empilé la requête ; flush() la persiste en base. reqstats.flush() with session_scope() as session: assert ( session.query(ApiRequest).filter(ApiRequest.endpoint == "/stats").count() >= 1 ) def test_ecosystem_pdf_builder() -> None: """EcosystemReport : PDF consolidé hors-ligne, avec une plateforme disponible et une injoignable (section « données indisponibles »).""" from src.api import ecopdf platforms = [ { "site": { "id": "lou-ka", "wordmark": "Lou·Ka", "accent": "#ff6a00", "domain": "www.lou-ka.com", "tagline": "Tous les logements à louer", }, "dashboard": { "updated": "2026-08-17T12:00:00-04:00", "period": {"from": "2026-08-11", "to": "2026-08-17", "label": "7 jours"}, "kpis": [ {"id": "actives", "label": "Annonces actives", "value": 33907}, {"id": "nouvelles", "label": "Nouvelles annonces (période)", "value": 29428}, ], "records": [{"label": "Jour record", "value": "412 annonces", "date": "2026-08-09"}], }, "error": None, }, { "site": { "id": "immo-ka", "wordmark": "Immo·Ka", "accent": "#e23744", "domain": "www.immo-ka.com", "tagline": "Toutes les propriétés à vendre", }, "dashboard": None, "error": "délai dépassé (15 s)", }, ] for mode in ("complet", "synthese"): pdf = ecopdf.EcosystemReport( platforms=platforms, period={"label": "7 jours"}, mode=mode ).build() assert pdf.startswith(b"%PDF") assert ecopdf.main_kpi(platforms[0]["dashboard"])["id"] == "actives" assert ecopdf.new_kpi(platforms[0]["dashboard"])["id"] == "nouvelles" def test_normalize_endpoint() -> None: assert reqstats.normalize_endpoint("/api/v1/louka/date/2026-08-16") == ( "/api/v1/louka/date/{date}" ) assert reqstats.normalize_endpoint("/api/v1/louka/latest") == "/api/v1/louka/latest" assert reqstats.normalize_endpoint("/ka/tokens.css") == "/ka/*" assert reqstats.normalize_endpoint("/wp-admin/setup.php") == "(autre)"