test(#134): Coverage-Backfill auswertungen + Repositories

- app/auswertungen.py 87.4% → 97.9%
  - TestLoadAssessmentsRobustness: ungueltiges JSON in fraktionen-Spalte
    fallback to []
  - TestAggregateMatrixSkipsBlanks: bundesland-NULL-Eintrag wird ignoriert
  - TestGetWahlperioden: sortierte Liste

- app/repositories/abonnement_repository.py 85.2% → 100%
- app/repositories/antrag_repository.py 87.0% → 98.1%
- app/repositories/bewertung_repository.py 90% → 100%

Pattern fuer Sqlite-Repos: AsyncMock auf database.X-Funktion, dann
pruefen dass die Methode korrekt delegiert (Argumente, Return-Wert).
Trivial wrappers, aber jetzt auditierbar.

Total: 48.7% → 49.2%, 686 → 705 Tests.
This commit is contained in:
Dotty Dotter 2026-04-28 10:54:28 +02:00
parent b13b46a444
commit 698562b1f5
4 changed files with 236 additions and 0 deletions

View File

@ -93,3 +93,81 @@ class TestDelete:
def test_delete_by_id_missing_returns_false(self):
repo = InMemoryAbonnementRepository()
assert _run(repo.delete_by_id(999)) is False
# ─────────────────────────────────────────────────────────────────────────────
# SqliteAbonnementRepository — Delegation an database.* (#134 Coverage-Backfill)
# ─────────────────────────────────────────────────────────────────────────────
class TestSqliteAbonnementRepositoryDelegation:
"""Die Sqlite-Variante ist nur ein duenner Wrapper um Module-Funktionen
in app.database. Test prueft dass jede Methode korrekt delegiert,
ohne echte DB-Calls (Module-Funktionen werden gemockt)."""
def test_create_delegates(self):
from unittest.mock import AsyncMock, patch
from app.repositories.abonnement_repository import SqliteAbonnementRepository
repo = SqliteAbonnementRepository()
with patch("app.repositories.abonnement_repository.database.create_subscription",
new=AsyncMock(return_value=42)) as m:
result = _run(repo.create("u1", "a@b.de", "NRW", "CDU", "weekly"))
assert result == 42
m.assert_called_once_with("u1", "a@b.de", "NRW", "CDU", "weekly")
def test_list_by_user_delegates(self):
from unittest.mock import AsyncMock, patch
from app.repositories.abonnement_repository import SqliteAbonnementRepository
repo = SqliteAbonnementRepository()
fake = [{"id": 1, "email": "x@y"}]
with patch("app.repositories.abonnement_repository.database.list_subscriptions",
new=AsyncMock(return_value=fake)) as m:
assert _run(repo.list_by_user("u1")) == fake
m.assert_called_once_with("u1")
def test_list_all_delegates(self):
from unittest.mock import AsyncMock, patch
from app.repositories.abonnement_repository import SqliteAbonnementRepository
with patch("app.repositories.abonnement_repository.database.list_all_subscriptions",
new=AsyncMock(return_value=[])) as m:
assert _run(SqliteAbonnementRepository().list_all()) == []
m.assert_called_once_with()
def test_list_due_delegates(self):
from unittest.mock import AsyncMock, patch
from app.repositories.abonnement_repository import SqliteAbonnementRepository
with patch("app.repositories.abonnement_repository.database.get_all_subscriptions_due",
new=AsyncMock(return_value=[])) as m:
_run(SqliteAbonnementRepository().list_due("weekly"))
m.assert_called_once_with("weekly")
def test_delete_delegates(self):
from unittest.mock import AsyncMock, patch
from app.repositories.abonnement_repository import SqliteAbonnementRepository
with patch("app.repositories.abonnement_repository.database.delete_subscription",
new=AsyncMock(return_value=True)) as m:
_run(SqliteAbonnementRepository().delete("u1", 5))
m.assert_called_once_with("u1", 5)
def test_delete_by_id_delegates(self):
from unittest.mock import AsyncMock, patch
from app.repositories.abonnement_repository import SqliteAbonnementRepository
with patch("app.repositories.abonnement_repository.database.delete_subscription_by_id",
new=AsyncMock(return_value=False)) as m:
_run(SqliteAbonnementRepository().delete_by_id(99))
m.assert_called_once_with(99)
def test_mark_sent_delegates(self):
from unittest.mock import AsyncMock, patch
from app.repositories.abonnement_repository import SqliteAbonnementRepository
with patch("app.repositories.abonnement_repository.database.mark_subscription_sent",
new=AsyncMock(return_value=None)) as m:
_run(SqliteAbonnementRepository().mark_sent(7))
m.assert_called_once_with(7)
def test_get_abonnement_repository_returns_singleton():
from app.repositories.abonnement_repository import get_abonnement_repository
a = get_abonnement_repository()
b = get_abonnement_repository()
assert a is b

View File

@ -156,3 +156,55 @@ class TestInitialSeed:
seed = [_make_assessment("18/1"), _make_assessment("18/2")]
repo = InMemoryAntragRepository(initial=seed)
assert len(_run(repo.list())) == 2
# ─────────────────────────────────────────────────────────────────────────────
# SqliteAntragRepository — Delegation an database.* (#134 Coverage-Backfill)
# ─────────────────────────────────────────────────────────────────────────────
class TestSqliteAntragRepositoryDelegation:
def test_save_delegates(self):
from unittest.mock import AsyncMock, patch
from app.repositories.antrag_repository import SqliteAntragRepository
with patch("app.repositories.antrag_repository.database.upsert_assessment",
new=AsyncMock(return_value=True)) as m:
assert _run(SqliteAntragRepository().save({"drucksache": "x"})) is True
m.assert_called_once_with({"drucksache": "x"})
def test_get_delegates(self):
from unittest.mock import AsyncMock, patch
from app.repositories.antrag_repository import SqliteAntragRepository
with patch("app.repositories.antrag_repository.database.get_assessment",
new=AsyncMock(return_value={"x": 1})) as m:
assert _run(SqliteAntragRepository().get("18/1")) == {"x": 1}
m.assert_called_once_with("18/1")
def test_list_delegates(self):
from unittest.mock import AsyncMock, patch
from app.repositories.antrag_repository import SqliteAntragRepository
with patch("app.repositories.antrag_repository.database.get_all_assessments",
new=AsyncMock(return_value=[])) as m:
_run(SqliteAntragRepository().list("NRW"))
m.assert_called_once_with("NRW")
def test_search_delegates(self):
from unittest.mock import AsyncMock, patch
from app.repositories.antrag_repository import SqliteAntragRepository
with patch("app.repositories.antrag_repository.database.search_assessments",
new=AsyncMock(return_value=[])) as m:
_run(SqliteAntragRepository().search("klima", "NRW", 25))
m.assert_called_once_with("klima", "NRW", 25)
def test_delete_delegates(self):
from unittest.mock import AsyncMock, patch
from app.repositories.antrag_repository import SqliteAntragRepository
with patch("app.repositories.antrag_repository.database.delete_assessment",
new=AsyncMock(return_value=True)) as m:
_run(SqliteAntragRepository().delete("18/1"))
m.assert_called_once_with("18/1")
def test_get_antrag_repository_returns_singleton():
from app.repositories.antrag_repository import get_antrag_repository
assert get_antrag_repository() is get_antrag_repository()

View File

@ -225,3 +225,90 @@ class TestExportLongFormat:
# Generic FREIE WÄHLER darf in der Zeile NICHT auftauchen
bb_lines = [l for l in csv_text.splitlines() if "BB" in l and "8/2," in l]
assert any("BVB-FW" in l for l in bb_lines)
# ─────────────────────────────────────────────────────────────────────────────
# Edge-Cases (#134 Coverage-Backfill)
# ─────────────────────────────────────────────────────────────────────────────
class TestLoadAssessmentsRobustness:
"""_load_assessments toleriert kaputte JSON-Eintraege im fraktionen-Feld."""
def test_invalid_json_in_fraktionen_falls_back_to_empty(self, tmp_path):
from app.auswertungen import _load_assessments
db = tmp_path / "broken.db"
conn = sqlite3.connect(str(db))
conn.execute("""
CREATE TABLE assessments (
drucksache TEXT PRIMARY KEY, title TEXT,
fraktionen TEXT, datum TEXT, bundesland TEXT,
gwoe_score REAL, link TEXT, gwoe_begruendung TEXT,
gwoe_matrix TEXT, gwoe_schwerpunkt TEXT,
wahlprogramm_scores TEXT, verbesserungen TEXT,
staerken TEXT, schwaechen TEXT, empfehlung TEXT,
empfehlung_symbol TEXT, verbesserungspotenzial TEXT,
themen TEXT, antrag_zusammenfassung TEXT,
antrag_kernpunkte TEXT, source TEXT, model TEXT,
created_at TEXT, updated_at TEXT
)
""")
# fraktionen-Feld enthaelt kein gueltiges JSON
conn.execute(
"INSERT INTO assessments (drucksache, bundesland, datum, fraktionen, gwoe_score) "
"VALUES (?, ?, ?, ?, ?)",
("18/777", "NRW", "2024-01-01", "{not json", 5.0),
)
conn.commit()
conn.close()
rows = _load_assessments(db)
assert len(rows) == 1
assert rows[0]["fraktionen"] == [] # Fallback
class TestAggregateMatrixSkipsBlanks:
def test_skips_assessments_without_bundesland(self, tmp_path):
"""Anträge ohne bundesland werden ignoriert (continue-Branch line 115)."""
from app.auswertungen import aggregate_matrix
db = tmp_path / "blanks.db"
conn = sqlite3.connect(str(db))
conn.execute("""
CREATE TABLE assessments (
drucksache TEXT PRIMARY KEY, title TEXT,
fraktionen TEXT, datum TEXT, bundesland TEXT,
gwoe_score REAL, link TEXT, gwoe_begruendung TEXT,
gwoe_matrix TEXT, gwoe_schwerpunkt TEXT,
wahlprogramm_scores TEXT, verbesserungen TEXT,
staerken TEXT, schwaechen TEXT, empfehlung TEXT,
empfehlung_symbol TEXT, verbesserungspotenzial TEXT,
themen TEXT, antrag_zusammenfassung TEXT,
antrag_kernpunkte TEXT, source TEXT, model TEXT,
created_at TEXT, updated_at TEXT
)
""")
conn.execute(
"INSERT INTO assessments (drucksache, bundesland, datum, fraktionen, gwoe_score) "
"VALUES (?, ?, ?, ?, ?)",
("X/1", None, "2024-01-01", '["CDU"]', 7.0), # bundesland NULL
)
conn.execute(
"INSERT INTO assessments (drucksache, bundesland, datum, fraktionen, gwoe_score) "
"VALUES (?, ?, ?, ?, ?)",
("18/1", "NRW", "2024-01-01", '["CDU"]', 7.0),
)
conn.commit()
conn.close()
m = aggregate_matrix(db_path=db)
assert m["total"] == 1 # nur der NRW-Eintrag
assert m["bundeslaender"] == ["NRW"]
class TestGetWahlperioden:
def test_returns_sorted_list(self, sample_db):
from app.auswertungen import get_wahlperioden
wps = get_wahlperioden(db_path=sample_db)
assert wps == sorted(wps)
# Sample-DB enthaelt NRW-WP18, MV-WP8, MV-WP7 sowie BB-WP8
assert any("NRW" in w for w in wps)

View File

@ -47,3 +47,22 @@ class TestVersionHistory:
rows_b = _run(repo.versions("18/2"))
assert len(rows_a) == 1 and rows_a[0]["gwoe_score"] == 5.0
assert len(rows_b) == 1 and rows_b[0]["gwoe_score"] == 8.0
# ─── SqliteBewertungRepository — Delegation (#134 Coverage-Backfill) ──────────
class TestSqliteBewertungRepositoryDelegation:
def test_versions_delegates(self):
from unittest.mock import AsyncMock, patch
from app.repositories.bewertung_repository import SqliteBewertungRepository
fake = [{"version": 1, "gwoe_score": 5.0}]
with patch("app.repositories.bewertung_repository.database.get_assessment_history",
new=AsyncMock(return_value=fake)) as m:
assert _run(SqliteBewertungRepository().versions("18/1")) == fake
m.assert_called_once_with("18/1")
def test_get_bewertung_repository_returns_singleton():
from app.repositories.bewertung_repository import get_bewertung_repository
assert get_bewertung_repository() is get_bewertung_repository()