gwoe-antragspruefer/tests/test_endpoints_smoke.py
Dotty Dotter d552582a0c test: gesamte Test-Suite gruen (1294/1294) vor v2.0.0
- conftest: pymupdf-Alias-Loading robuster, fuer echte Render-Tests
- test_v2_pdf_consistency: fehlende_programme deserialisieren
- test_endpoints_smoke: Auth-Tests skippen wenn Keycloak nicht aktiv;
  queue/status-Schema auf workers_running aktualisiert
- test_inline_styles_baseline: skippen wenn tools/-Dir fehlt (Container)
- test_presse_generator_style: Mock-Body lang genug fuer kein Re-Generate;
  neuer event-loop pro Test (3.10+-Lifecycle)
- test_bug_regressions: EMBEDDINGS_DB-Patch auch im analyzer_mod;
  raising=False bei fitz/pymupdf raus (zerstoerte Folge-Tests)
- test_icons: macOS AppleDouble-Files (._*) ueberspringen
- test_protokoll_parsers_nrw: raising=False raus (Test-Isolation)
2026-05-09 22:29:37 +02:00

338 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Smoke-Tests für die neuen API-Endpoints.
Diese Tests prüfen nur ob die Endpoints antworten und das richtige
Format zurückgeben — keine echten LLM-Calls oder DB-Writes.
"""
import pytest
# Skip wenn die App nicht importierbar ist (missing dependencies lokal)
try:
from fastapi.testclient import TestClient
from app.main import app
from app.auth import _is_auth_enabled
client = TestClient(app)
_HAS_APP = True
_AUTH_ON = _is_auth_enabled()
except ImportError:
_HAS_APP = False
_AUTH_ON = False
client = None
pytestmark = pytest.mark.skipif(not _HAS_APP, reason="app.main not importable")
auth_required = pytest.mark.skipif(
not _AUTH_ON,
reason="Keycloak nicht konfiguriert — Auth-Wall ist deaktiviert (Dev-Modus)",
)
class TestQueueStatus:
def test_returns_json(self):
resp = client.get("/api/queue/status")
assert resp.status_code == 200
data = resp.json()
assert "pending" in data
assert "workers_running" in data
def test_pending_starts_at_zero(self):
data = client.get("/api/queue/status").json()
assert data["pending"] == 0
class TestAuthMe:
def test_unauthenticated_returns_false(self):
resp = client.get("/api/auth/me")
assert resp.status_code == 200
data = resp.json()
assert data["authenticated"] is False
class TestAuthLoginUrl:
def test_returns_enabled_flag(self):
resp = client.get("/api/auth/login-url")
assert resp.status_code == 200
data = resp.json()
assert "enabled" in data
class TestBundeslaender:
def test_returns_list(self):
resp = client.get("/api/bundeslaender")
assert resp.status_code == 200
data = resp.json()
assert isinstance(data, list)
assert len(data) > 10 # mindestens 10 BLs
class TestProgramme:
def test_returns_list(self):
resp = client.get("/api/programme")
assert resp.status_code == 200
data = resp.json()
assert isinstance(data, list)
assert len(data) > 50 # mindestens 50 Programme
def test_status_returns_indexed_count(self):
resp = client.get("/api/programme/status")
assert resp.status_code == 200
data = resp.json()
assert "indexed" in data
assert "total" in data
class TestHealth:
def test_health(self):
resp = client.get("/health")
assert resp.status_code == 200
class TestVoteOrphansEndpoint:
"""GET /api/auswertungen/vote-orphans (öffentlich)."""
def test_returns_json_structure(self):
resp = client.get("/api/auswertungen/vote-orphans?limit=5")
assert resp.status_code == 200
data = resp.json()
assert "count" in data
assert "items" in data
assert "by_bundesland" in data
assert isinstance(data["items"], list)
def test_filter_bundesland_param(self):
resp = client.get("/api/auswertungen/vote-orphans?bundesland=NRW&limit=3")
assert resp.status_code == 200
data = resp.json()
# Wenn items vorhanden, alle aus NRW
for it in data["items"]:
assert it["bundesland"] == "NRW"
@auth_required
class TestVoteOrphansAutoRateAuth:
"""POST /api/auswertungen/vote-orphans/auto-rate erfordert Admin."""
def test_unauthenticated_rejected(self):
resp = client.post(
"/api/auswertungen/vote-orphans/auto-rate",
data={"limit": 5},
)
# Auth-Wall greift entweder direkt 401, 403 oder Redirect (307/302)
assert resp.status_code in (401, 403, 307, 302)
@auth_required
class TestBatchAnalyzeAuth:
"""POST /api/batch-analyze erfordert Admin."""
def test_unauthenticated_rejected(self):
resp = client.post(
"/api/batch-analyze",
data={"bundesland": "NRW", "limit": 5},
)
assert resp.status_code in (401, 403, 307, 302)
def test_all_bl_unauthenticated_also_rejected(self):
resp = client.post(
"/api/batch-analyze",
data={"bundesland": "ALL", "limit": 10},
)
assert resp.status_code in (401, 403, 307, 302)
class TestAktuelleThemenEndpoints:
"""GET /api/aktuelle-themen/* sind oeffentlich."""
def test_top_returns_buckets(self):
resp = client.get("/api/aktuelle-themen/top?days=7&top_k=3")
assert resp.status_code == 200
data = resp.json()
assert "buckets" in data
assert "n_total_news" in data
assert "filter" in data
def test_top_with_single_date(self):
resp = client.get("/api/aktuelle-themen/top?date=2026-05-01")
assert resp.status_code == 200
data = resp.json()
assert data["filter"]["single_date"] == "2026-05-01"
def test_top_with_only_relevant(self):
resp = client.get("/api/aktuelle-themen/top?only_relevant=true&top_k=5")
assert resp.status_code == 200
data = resp.json()
assert data["filter"]["only_relevant"] is True
def test_zeitreihe(self):
resp = client.get("/api/aktuelle-themen/zeitreihe?days=14")
assert resp.status_code == 200
data = resp.json()
assert "buckets" in data
assert "sources" in data
assert "series" in data
def test_top_antraege(self):
resp = client.get("/api/aktuelle-themen/top-antraege?min_gwoe_score=8.0")
assert resp.status_code == 200
data = resp.json()
assert "antraege" in data
def test_cluster(self):
resp = client.get("/api/aktuelle-themen/cluster?days=7")
assert resp.status_code == 200
data = resp.json()
assert "clusters" in data
def test_drafts_list(self):
resp = client.get("/api/aktuelle-themen/drafts?limit=5")
assert resp.status_code == 200
data = resp.json()
assert "drafts" in data
def test_drafts_versions(self):
resp = client.get(
"/api/aktuelle-themen/drafts-versions"
"?drucksache=missing&news_url=https://example.com/x"
)
assert resp.status_code == 200
data = resp.json()
assert "versions" in data
assert isinstance(data["versions"], list)
class TestScoreHistogramEndpoint:
"""GET /api/auswertungen/score-histogram (öffentlich)."""
def test_returns_11_buckets(self):
resp = client.get("/api/auswertungen/score-histogram")
assert resp.status_code == 200
data = resp.json()
# 11 Buckets: 01, 12, ..., 1011
assert len(data["buckets"]) == 11
for b in data["buckets"]:
assert "score_min" in b
assert "score_max" in b
assert "count" in b
assert b["count"] >= 0
def test_total_matches_sum_of_buckets(self):
resp = client.get("/api/auswertungen/score-histogram")
data = resp.json()
total_sum = sum(b["count"] for b in data["buckets"])
assert data["total"] == total_sum
def test_filter_bundesland(self):
resp = client.get("/api/auswertungen/score-histogram?bundesland=NRW")
assert resp.status_code == 200
data = resp.json()
assert data["filter"]["bundesland"] == "NRW"
def test_filter_wahlperiode(self):
resp = client.get("/api/auswertungen/score-histogram?wahlperiode=NRW-WP18")
assert resp.status_code == 200
data = resp.json()
assert data["filter"]["wahlperiode"] == "NRW-WP18"
@auth_required
class TestAdminStandAuth:
"""/v2/admin/stand + /api/admin/stand erfordern Admin."""
def test_page_unauthenticated_rejected(self):
resp = client.get("/v2/admin/stand")
assert resp.status_code in (401, 403, 307, 302)
def test_api_unauthenticated_rejected(self):
resp = client.get("/api/admin/stand")
assert resp.status_code in (401, 403, 307, 302)
class TestStimmverhaltenAggregateEndpoints:
"""Die 7 Stimmverhalten-Aggregat-Endpoints liefern strukturierte JSONs (siehe ADR 0010)."""
def test_stimm_index_returns_fraktionen(self):
resp = client.get("/api/auswertungen/stimm-index")
assert resp.status_code == 200
data = resp.json()
assert "fraktionen" in data
assert isinstance(data["fraktionen"], list)
assert "n_assessments_matched" in data
def test_stimm_index_min_n_filter(self):
resp = client.get("/api/auswertungen/stimm-index?min_n=1")
assert resp.status_code == 200
data = resp.json()
assert data["filter"]["min_n"] == 1
def test_stimm_index_exclude_antragsteller_default(self):
"""Default `exclude_antragsteller=True`."""
resp = client.get("/api/auswertungen/stimm-index")
data = resp.json()
assert data["filter"]["exclude_antragsteller"] in (True, "true", 1)
def test_heuchelei_returns_fraktionen(self):
resp = client.get("/api/auswertungen/heuchelei")
assert resp.status_code == 200
data = resp.json()
assert "fraktionen" in data
def test_empfehlungs_konsistenz(self):
resp = client.get("/api/auswertungen/empfehlungs-konsistenz")
assert resp.status_code == 200
data = resp.json()
assert "fraktionen" in data or "items" in data
def test_stimm_index_pro_wert_5_werte(self):
resp = client.get("/api/auswertungen/stimm-index-pro-wert")
assert resp.status_code == 200
data = resp.json()
assert "werte" in data
# 5 GWÖ-Werte: Würde, Solidarität, Nachhaltigkeit, Gerechtigkeit, Demokratie
assert len(data["werte"]) == 5
def test_stimm_index_cross_bl(self):
resp = client.get("/api/auswertungen/stimm-index-cross-bl")
assert resp.status_code == 200
data = resp.json()
assert "fraktionen" in data
assert "bundeslaender" in data
assert "cells" in data
def test_stimm_index_zeitreihe(self):
resp = client.get("/api/auswertungen/stimm-index-zeitreihe")
assert resp.status_code == 200
data = resp.json()
# Erwartete Felder: buckets (Zeit-Achse), fraktionen, series
assert "buckets" in data
assert "fraktionen" in data
assert "series" in data
class TestStimmverhaltenCsvExport:
"""CSV-Export Long-Format aus #168."""
def test_csv_returns_text(self):
resp = client.get("/api/auswertungen/stimmverhalten.csv")
assert resp.status_code == 200
ctype = resp.headers.get("content-type", "")
assert "text/csv" in ctype or "text/plain" in ctype
def test_csv_has_header_row(self):
resp = client.get("/api/auswertungen/stimmverhalten.csv")
body = resp.text
# Header-Zeile mit den erwarteten Long-Format-Spalten
first_line = body.split("\n", 1)[0]
# Mindestens drei Pflicht-Spalten erwartet — Partei statt Fraktion in CSV
for required in ("drucksache", "bundesland", "partei", "vote"):
assert required in first_line, f"Spalte {required!r} fehlt im Header: {first_line!r}"
def test_csv_data_rows_have_consistent_columns(self):
"""Datenzeilen haben gleiche Spalten-Anzahl wie der Header."""
resp = client.get("/api/auswertungen/stimmverhalten.csv")
body = resp.text.strip()
if not body:
pytest.skip("kein CSV-Body — DB enthält evtl. keine Vote-Matches")
lines = body.split("\n")
n_cols = lines[0].count(",") + 1
for line in lines[1:6]: # Samples
assert line.count(",") + 1 == n_cols, f"Spaltenzahl-Mismatch: {line!r}"