# ============================================ # Projet : API-KA # Fichier : tests/test_api.py # Node : m3u96b # Author : Simon-Pierre Boucher # Contact : contact@spboucher.ai # Date : 2026-08-16 # ============================================ """Tests des routes de l'API publique : format uniforme, pagination, 404, /health.""" from __future__ import annotations import datetime import hashlib import json import pytest from fastapi.testclient import TestClient from src.api.main import app from src.database.db import session_scope from src.database.models import DATA_MODELS, CollectionRun SEED_DATE = datetime.date(2026, 8, 10) def _seed_louka() -> None: model = DATA_MODELS["louka"] now = datetime.datetime.now(tz=datetime.UTC) with session_scope() as session: for i in range(3): payload = {"id": i, "nom": f"louka-{i}"} checksum = hashlib.sha256( json.dumps(payload, sort_keys=True, separators=(",", ":")).encode() ).hexdigest() exists = ( session.query(model) .filter_by(source="louka", date_key=SEED_DATE, checksum=checksum) .first() ) if not exists: session.add( model( payload=payload, source="louka", collected_at=now, date_key=SEED_DATE, checksum=checksum, ) ) session.add( CollectionRun( service="louka", date_key=SEED_DATE, status="success", records_count=3, duration_seconds=1.2, error_message=None, node="m3u96b", started_at=now, finished_at=now, ) ) @pytest.fixture(scope="module") def client(): """Client de test FastAPI avec cycle de vie (lifespan) actif.""" _seed_louka() with TestClient(app) as test_client: yield test_client def test_root(client: TestClient) -> None: """GET / retourne le statut de la plateforme et la version.""" response = client.get("/") assert response.status_code == 200 body = response.json() assert body["success"] is True assert body["data"]["platform"] == "API-KA" assert body["data"]["version"] assert "node" in body["meta"] def test_root_serves_web_platform_to_browsers(client: TestClient) -> None: """GET / avec Accept: text/html sert la plateforme web (docs + playground).""" response = client.get("/", headers={"Accept": "text/html"}) assert response.status_code == 200 assert response.headers["content-type"].startswith("text/html") assert "Playground" in response.text assert "API-KA" in response.text def test_health(client: TestClient) -> None: """GET /health expose le node, l'état DB et les dernières collectes.""" response = client.get("/health") assert response.status_code == 200 data = response.json()["data"] assert data["database"] == "ok" assert data["required_node"] == "m3u96b" assert data["last_collections"]["louka"] is not None assert data["last_collections"]["louka"]["records_count"] == 3 def test_list_service_data(client: TestClient) -> None: """GET /api/v1/louka pagine les données au format uniforme.""" response = client.get("/api/v1/louka", params={"page": 1, "limit": 2}) assert response.status_code == 200 body = response.json() assert body["success"] is True assert len(body["data"]) == 2 assert body["meta"]["page"] == 1 assert body["meta"]["limit"] == 2 assert body["meta"]["total"] >= 3 def test_latest(client: TestClient) -> None: """GET /api/v1/louka/latest retourne la collecte la plus récente.""" response = client.get("/api/v1/louka/latest") assert response.status_code == 200 body = response.json() assert body["meta"]["date_key"] == SEED_DATE.isoformat() assert len(body["data"]) == 3 def test_by_date(client: TestClient) -> None: """GET /api/v1/louka/date/{date} filtre par date logique.""" response = client.get(f"/api/v1/louka/date/{SEED_DATE.isoformat()}") assert response.status_code == 200 assert response.json()["meta"]["total"] == 3 empty = client.get("/api/v1/louka/date/2020-01-01") assert empty.status_code == 200 assert empty.json()["meta"]["total"] == 0 invalid = client.get("/api/v1/louka/date/pas-une-date") assert invalid.status_code == 422 def test_stats(client: TestClient) -> None: """GET /api/v1/louka/stats agrège par jour et expose la dernière réussite.""" response = client.get("/api/v1/louka/stats") assert response.status_code == 200 data = response.json()["data"] assert data["total_records"] >= 3 assert any(day["date_key"] == SEED_DATE.isoformat() for day in data["days"]) assert data["last_success"]["status"] == "success" def test_unknown_service_is_404(client: TestClient) -> None: """Tout service hors de la liste officielle → 404.""" assert client.get("/api/v1/inconnu").status_code == 404 assert client.get("/api/v1/inconnu/latest").status_code == 404 assert client.get("/api/v1/inconnu/stats").status_code == 404 def test_limit_is_capped_at_500(client: TestClient) -> None: """limit > 500 est rejeté (422).""" response = client.get("/api/v1/louka", params={"limit": 501}) assert response.status_code == 422 def test_runs_endpoint(client: TestClient) -> None: """GET /api/v1/runs liste l'historique, filtrable par service et statut.""" response = client.get("/api/v1/runs", params={"service": "louka"}) assert response.status_code == 200 body = response.json() assert body["meta"]["total"] >= 1 assert all(run["service"] == "louka" for run in body["data"]) assert client.get("/api/v1/runs", params={"service": "inconnu"}).status_code == 404 assert client.get("/api/v1/runs", params={"status": "invalide"}).status_code == 422