- 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.
211 lines
8.7 KiB
Python
211 lines
8.7 KiB
Python
"""Tests für AntragRepository (#136, ADR 0008).
|
|
|
|
Das Protocol definiert den Vertrag — beide Implementationen
|
|
(``Sqlite*`` und ``InMemory*``) müssen sich daran halten. Die
|
|
SqliteAntragRepository-Implementation wird hier nur gegen die
|
|
Protocol-Konformität geprüft, nicht gegen echte DB-I/O; dafür sind
|
|
die bestehenden DB-Tests zuständig.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
|
|
import pytest
|
|
|
|
from app.repositories import (
|
|
AntragRepository,
|
|
InMemoryAntragRepository,
|
|
SqliteAntragRepository,
|
|
)
|
|
|
|
|
|
# ─── Protocol-Konformität ───────────────────────────────────────────────────
|
|
|
|
class TestProtocolConformance:
|
|
def test_in_memory_implements_protocol(self):
|
|
repo = InMemoryAntragRepository()
|
|
assert isinstance(repo, AntragRepository)
|
|
|
|
def test_sqlite_implements_protocol(self):
|
|
repo = SqliteAntragRepository()
|
|
assert isinstance(repo, AntragRepository)
|
|
|
|
|
|
# ─── InMemoryAntragRepository — Vertrag ─────────────────────────────────────
|
|
|
|
def _run(coro):
|
|
return asyncio.get_event_loop().run_until_complete(coro)
|
|
|
|
|
|
def _make_assessment(drucksache: str = "18/1", bundesland: str = "NRW",
|
|
gwoe_score: float = 5.0, title: str = "Test-Antrag",
|
|
fraktionen=None, themen=None) -> dict:
|
|
return {
|
|
"drucksache": drucksache,
|
|
"title": title,
|
|
"bundesland": bundesland,
|
|
"gwoe_score": gwoe_score,
|
|
"fraktionen": fraktionen or ["SPD"],
|
|
"themen": themen or ["Bildung"],
|
|
}
|
|
|
|
|
|
class TestInMemoryRepoSaveAndGet:
|
|
def test_save_and_get_round_trip(self):
|
|
repo = InMemoryAntragRepository()
|
|
a = _make_assessment(drucksache="18/42", gwoe_score=7.5)
|
|
assert _run(repo.save(a)) is True
|
|
stored = _run(repo.get("18/42"))
|
|
assert stored is not None
|
|
assert stored["gwoe_score"] == 7.5
|
|
|
|
def test_get_returns_none_for_missing(self):
|
|
repo = InMemoryAntragRepository()
|
|
assert _run(repo.get("18/999")) is None
|
|
|
|
def test_save_requires_drucksache(self):
|
|
repo = InMemoryAntragRepository()
|
|
with pytest.raises(ValueError):
|
|
_run(repo.save({"title": "oh no"}))
|
|
|
|
def test_save_twice_overwrites_last_wins(self):
|
|
"""UPSERT-Semantik: zweites save überschreibt das erste — wie in SQL."""
|
|
repo = InMemoryAntragRepository()
|
|
_run(repo.save(_make_assessment("18/1", gwoe_score=3.0)))
|
|
_run(repo.save(_make_assessment("18/1", gwoe_score=8.0)))
|
|
stored = _run(repo.get("18/1"))
|
|
assert stored["gwoe_score"] == 8.0
|
|
|
|
def test_get_returns_independent_copy(self):
|
|
"""Mutation des zurückgegebenen Dicts darf den Store nicht verändern."""
|
|
repo = InMemoryAntragRepository()
|
|
_run(repo.save(_make_assessment("18/1", gwoe_score=5.0)))
|
|
r1 = _run(repo.get("18/1"))
|
|
r1["gwoe_score"] = 999.0
|
|
r2 = _run(repo.get("18/1"))
|
|
assert r2["gwoe_score"] == 5.0
|
|
|
|
|
|
class TestInMemoryRepoList:
|
|
def test_list_empty(self):
|
|
repo = InMemoryAntragRepository()
|
|
assert _run(repo.list()) == []
|
|
|
|
def test_list_sorted_by_gwoe_score_desc(self):
|
|
repo = InMemoryAntragRepository()
|
|
_run(repo.save(_make_assessment("18/1", gwoe_score=3.0)))
|
|
_run(repo.save(_make_assessment("18/2", gwoe_score=9.0)))
|
|
_run(repo.save(_make_assessment("18/3", gwoe_score=6.0)))
|
|
rows = _run(repo.list())
|
|
assert [r["drucksache"] for r in rows] == ["18/2", "18/3", "18/1"]
|
|
|
|
def test_list_filter_by_bundesland(self):
|
|
repo = InMemoryAntragRepository()
|
|
_run(repo.save(_make_assessment("18/1", bundesland="NRW")))
|
|
_run(repo.save(_make_assessment("18/2", bundesland="HE")))
|
|
rows = _run(repo.list(bundesland="HE"))
|
|
assert len(rows) == 1
|
|
assert rows[0]["bundesland"] == "HE"
|
|
|
|
def test_list_all_pseudo_bundesland_is_noop(self):
|
|
repo = InMemoryAntragRepository()
|
|
_run(repo.save(_make_assessment("18/1", bundesland="NRW")))
|
|
_run(repo.save(_make_assessment("18/2", bundesland="HE")))
|
|
assert len(_run(repo.list(bundesland="ALL"))) == 2
|
|
|
|
|
|
class TestInMemoryRepoSearch:
|
|
def test_search_by_title(self):
|
|
repo = InMemoryAntragRepository()
|
|
_run(repo.save(_make_assessment("18/1", title="Klimaschutz für alle")))
|
|
_run(repo.save(_make_assessment("18/2", title="Steuerreform")))
|
|
rows = _run(repo.search("Klima"))
|
|
assert len(rows) == 1
|
|
assert rows[0]["drucksache"] == "18/1"
|
|
|
|
def test_search_by_themen(self):
|
|
repo = InMemoryAntragRepository()
|
|
_run(repo.save(_make_assessment("18/1", themen=["Verkehr"])))
|
|
_run(repo.save(_make_assessment("18/2", themen=["Bildung"])))
|
|
rows = _run(repo.search("bildung"))
|
|
assert len(rows) == 1
|
|
assert rows[0]["drucksache"] == "18/2"
|
|
|
|
def test_search_respects_limit(self):
|
|
repo = InMemoryAntragRepository()
|
|
for i in range(10):
|
|
_run(repo.save(_make_assessment(f"18/{i}", title="Klimaschutz", gwoe_score=i)))
|
|
rows = _run(repo.search("Klimaschutz", limit=3))
|
|
assert len(rows) == 3
|
|
|
|
|
|
class TestInMemoryRepoDelete:
|
|
def test_delete_existing(self):
|
|
repo = InMemoryAntragRepository()
|
|
_run(repo.save(_make_assessment("18/1")))
|
|
assert _run(repo.delete("18/1")) is True
|
|
assert _run(repo.get("18/1")) is None
|
|
|
|
def test_delete_missing_returns_false(self):
|
|
repo = InMemoryAntragRepository()
|
|
assert _run(repo.delete("18/999")) is False
|
|
|
|
|
|
class TestInitialSeed:
|
|
def test_initial_seed_fills_store(self):
|
|
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()
|