Spaces:
Sleeping
Sleeping
| """Tests des endpoints : succès, vide, erreur amont, timeout, cache périmé, | |
| rate-limit du refresh, santé.""" | |
| import time | |
| import pytest | |
| from fastapi.testclient import TestClient | |
| import app as app_module | |
| import sources | |
| FIRE_OK = { | |
| "id": "abc123", "lat": 43.3, "lon": 5.4, | |
| "acquired_at": "2026-07-27T01:37:00Z", | |
| "source": "NASA FIRMS VIIRS S-NPP (Europe 24 h)", "satellite": "N", | |
| "instrument": "VIIRS", "frp": 12.3, "brightness": 312.5, | |
| "confidence": "nominal", "daynight": "N", "type": "thermal_anomaly", | |
| } | |
| def _result(fires, used=None): | |
| sts = [sources.SourceStatus(name=u or "src", ok=True, latency_ms=42, | |
| last_success="2026-07-27T10:00:00Z") | |
| for u in (used or ["src"])] | |
| return sources.FetchResult(fires=fires, sources_used=used or ["src"], statuses=sts) | |
| def client(monkeypatch): | |
| monkeypatch.setattr(app_module, "_cache", {"payload": None, "fetched_at": 0.0}) | |
| monkeypatch.setattr(app_module, "_last_forced", 0.0) | |
| monkeypatch.setattr(app_module, "_last_statuses", []) | |
| return TestClient(app_module.app) | |
| def test_fires_succes(client, monkeypatch): | |
| monkeypatch.setattr(sources, "fetch_all", lambda: _result([FIRE_OK])) | |
| r = client.get("/api/fires") | |
| assert r.status_code == 200 | |
| body = r.json() | |
| assert body["status"] == "ok" | |
| assert body["count"] == 1 | |
| assert body["fires"][0]["id"] == "abc123" | |
| assert body["data_observed_at"] == "2026-07-27T01:37:00Z" | |
| assert body["stale"] is False | |
| assert body["errors"] == [] | |
| assert body["attribution"] | |
| def test_fires_vide_honnete(client, monkeypatch): | |
| """Amont OK mais zéro feu : état vide honnête, pas de données fabriquées.""" | |
| monkeypatch.setattr(sources, "fetch_all", lambda: _result([])) | |
| body = client.get("/api/fires").json() | |
| assert body["status"] == "ok" | |
| assert body["count"] == 0 | |
| assert body["fires"] == [] | |
| assert body["data_observed_at"] is None | |
| def test_erreur_amont_sans_cache(client, monkeypatch): | |
| def boom(): | |
| raise sources.UpstreamError("toutes les sources ont échoué") | |
| monkeypatch.setattr(sources, "fetch_all", boom) | |
| body = client.get("/api/fires").json() | |
| assert body["status"] == "error" | |
| assert body["fires"] == [] | |
| assert body["errors"] | |
| def test_cache_frais_pas_de_nouvel_appel(client, monkeypatch): | |
| appels = [] | |
| def fake(): | |
| appels.append(1) | |
| return _result([FIRE_OK]) | |
| monkeypatch.setattr(sources, "fetch_all", fake) | |
| client.get("/api/fires") | |
| client.get("/api/fires") | |
| assert len(appels) == 1 # 2e appel servi par le cache | |
| def test_cache_perime_apres_echec(client, monkeypatch): | |
| monkeypatch.setattr(sources, "fetch_all", lambda: _result([FIRE_OK])) | |
| first = client.get("/api/fires").json() | |
| assert first["stale"] is False | |
| # Cache expiré + amont en panne -> repli sur cache marqué périmé | |
| app_module._cache["fetched_at"] = time.time() - (app_module.TTL_S + 10) | |
| def boom(): | |
| raise sources.UpstreamError("timeout amont") | |
| monkeypatch.setattr(sources, "fetch_all", boom) | |
| body = client.get("/api/fires").json() | |
| assert body["status"] == "ok" | |
| assert body["stale"] is True | |
| assert body["count"] == 1 | |
| assert body["errors"] | |
| def test_timeout_traite_comme_erreur(client, monkeypatch): | |
| def boom(): | |
| raise sources.UpstreamError("ReadTimeout: délai dépassé") | |
| monkeypatch.setattr(sources, "fetch_all", boom) | |
| body = client.get("/api/fires").json() | |
| assert body["status"] == "error" | |
| def test_refresh_force_et_rate_limit(client, monkeypatch): | |
| appels = [] | |
| def fake(): | |
| appels.append(1) | |
| return _result([FIRE_OK]) | |
| monkeypatch.setattr(sources, "fetch_all", fake) | |
| client.get("/api/fires") | |
| app_module._cache["fetched_at"] = time.time() - (app_module.TTL_S + 10) | |
| r = client.post("/api/refresh") | |
| assert r.status_code == 200 | |
| assert len(appels) == 2 | |
| # Second refresh dans les 30 s : pas de nouvel appel amont | |
| app_module._cache["fetched_at"] = time.time() - (app_module.TTL_S + 10) | |
| client.post("/api/refresh") | |
| assert len(appels) == 2 | |
| def test_health_apres_succes(client, monkeypatch): | |
| monkeypatch.setattr(sources, "fetch_all", lambda: _result([FIRE_OK], ["NASA FIRMS test"])) | |
| client.get("/api/fires") | |
| r = client.get("/api/health") | |
| assert r.status_code == 200 | |
| body = r.json() | |
| assert body["status"] in ("ok", "degraded") | |
| assert body["version"] | |
| assert body["time"].endswith("Z") | |
| assert body["sources"][0]["name"] == "NASA FIRMS test" | |
| assert body["sources"][0]["ok"] is True | |
| assert body["cache"]["ttl_s"] == app_module.TTL_S | |
| assert body["cache"]["age_s"] is not None | |
| def test_health_sans_donnees(client): | |
| body = client.get("/api/health").json() | |
| assert body["status"] == "degraded" | |
| assert body["cache"]["age_s"] is None | |