"""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()