gwoe-antragspruefer/tests/test_antrag_repository.py

159 lines
5.8 KiB
Python
Raw Normal View History

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