From 698562b1f586d5c9bbedf80f60dc70feeae2deb5 Mon Sep 17 00:00:00 2001 From: Dotty Dotter Date: Tue, 28 Apr 2026 10:54:28 +0200 Subject: [PATCH] test(#134): Coverage-Backfill auswertungen + Repositories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- tests/test_abonnement_repository.py | 78 ++++++++++++++++++++++++++ tests/test_antrag_repository.py | 52 +++++++++++++++++ tests/test_auswertungen.py | 87 +++++++++++++++++++++++++++++ tests/test_bewertung_repository.py | 19 +++++++ 4 files changed, 236 insertions(+) diff --git a/tests/test_abonnement_repository.py b/tests/test_abonnement_repository.py index 22f3380..957ee55 100644 --- a/tests/test_abonnement_repository.py +++ b/tests/test_abonnement_repository.py @@ -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 diff --git a/tests/test_antrag_repository.py b/tests/test_antrag_repository.py index 4a8d81f..bc1725c 100644 --- a/tests/test_antrag_repository.py +++ b/tests/test_antrag_repository.py @@ -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() diff --git a/tests/test_auswertungen.py b/tests/test_auswertungen.py index 032df91..4b4fea9 100644 --- a/tests/test_auswertungen.py +++ b/tests/test_auswertungen.py @@ -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) diff --git a/tests/test_bewertung_repository.py b/tests/test_bewertung_repository.py index 364ba67..b5d5316 100644 --- a/tests/test_bewertung_repository.py +++ b/tests/test_bewertung_repository.py @@ -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()