Vollständige Pipeline zur Analyse kommunaler Vorlagen aus ALLRIS: - OParl-Import: 20.149 Vorlagen - PDF-Extraktion: 10.045 Volltexte (adaptives Throttling) - KI-Zusammenfassungen: 10.026 via Qwen Plus (parallelisiert) - Beratungsfolge-Scraper: Beschlusstexte + Wortprotokolle - Abstimmungs-Analyse mit Koalitionsmatrix - Georeferenzierung (Nominatim) Stack: FastAPI + SvelteKit + SQLite Deployment: Docker + Traefik auf VServer Daten (DB, Logs) nicht im Repo — siehe Restic-Backup. Repo-Setup: scripts/setup.sh für Neuaufbau aus OParl-API.
114 lines
3.4 KiB
Python
114 lines
3.4 KiB
Python
"""Tests for API endpoints against the real database."""
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
from tracker.main import app
|
|
from tracker.db.session import get_connection
|
|
|
|
|
|
@pytest.fixture
|
|
def client():
|
|
return TestClient(app)
|
|
|
|
|
|
@pytest.fixture
|
|
def db():
|
|
conn = get_connection()
|
|
yield conn
|
|
conn.close()
|
|
|
|
|
|
# --- Sanity checks on the DB ---
|
|
|
|
class TestDatabaseSanity:
|
|
def test_vorlagen_count(self, db):
|
|
count = db.execute("SELECT COUNT(*) as cnt FROM vorlagen").fetchone()["cnt"]
|
|
assert count >= 6000, f"Expected >=6000 Vorlagen, got {count}"
|
|
|
|
def test_vorlagen_types(self, db):
|
|
types = db.execute(
|
|
"SELECT DISTINCT typ FROM vorlagen WHERE typ IS NOT NULL"
|
|
).fetchall()
|
|
type_names = {r["typ"] for r in types}
|
|
assert "antrag" in type_names
|
|
assert "anfrage" in type_names
|
|
|
|
def test_beratungen_exist(self, db):
|
|
count = db.execute("SELECT COUNT(*) as cnt FROM beratungen").fetchone()["cnt"]
|
|
assert count > 0, "No Beratungen in DB"
|
|
|
|
def test_suffix_vorlagen_exist(self, db):
|
|
count = db.execute(
|
|
"SELECT COUNT(*) as cnt FROM vorlagen WHERE aktenzeichen_suffix IS NOT NULL"
|
|
).fetchone()["cnt"]
|
|
assert count > 0, "No suffix Vorlagen in DB"
|
|
|
|
|
|
# --- API: Health ---
|
|
|
|
class TestHealth:
|
|
def test_health(self, client):
|
|
resp = client.get("/api/health")
|
|
assert resp.status_code == 200
|
|
assert resp.json()["status"] == "ok"
|
|
|
|
|
|
# --- API: Vorlagen ---
|
|
|
|
class TestVorlagenAPI:
|
|
def test_list_vorlagen(self, client):
|
|
resp = client.get("/api/vorlagen?page=1&page_size=10")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["total"] >= 6000
|
|
assert len(data["items"]) == 10
|
|
assert data["page"] == 1
|
|
|
|
def test_list_vorlagen_filter_typ(self, client):
|
|
resp = client.get("/api/vorlagen?typ=antrag&page_size=5")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["total"] > 0
|
|
for item in data["items"]:
|
|
assert item["typ"] == "antrag"
|
|
|
|
def test_list_vorlagen_filter_suche(self, client):
|
|
resp = client.get("/api/vorlagen?suche=Klimaschutz&page_size=5")
|
|
assert resp.status_code == 200
|
|
# May or may not find results, but should not error
|
|
|
|
def test_get_vorlage_detail(self, client, db):
|
|
# Get first vorlage with aktenzeichen
|
|
row = db.execute(
|
|
"SELECT id FROM vorlagen WHERE aktenzeichen IS NOT NULL LIMIT 1"
|
|
).fetchone()
|
|
assert row is not None
|
|
|
|
resp = client.get(f"/api/vorlagen/{row['id']}")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["id"] == row["id"]
|
|
assert data["aktenzeichen"] is not None
|
|
|
|
def test_get_vorlage_not_found(self, client):
|
|
resp = client.get("/api/vorlagen/999999")
|
|
assert resp.status_code == 404
|
|
|
|
|
|
# --- API: Ketten ---
|
|
|
|
class TestKettenAPI:
|
|
def test_list_ketten_empty_initially(self, client):
|
|
"""Before building chains, the list may be empty."""
|
|
resp = client.get("/api/ketten?page_size=5")
|
|
assert resp.status_code == 200
|
|
|
|
def test_list_ketten_filter(self, client):
|
|
resp = client.get("/api/ketten?status=eingereicht&page_size=5")
|
|
assert resp.status_code == 200
|
|
|
|
def test_get_kette_not_found(self, client):
|
|
resp = client.get("/api/ketten/999999")
|
|
assert resp.status_code == 404
|