Neue Tests in dieser Migration: - test_database.py (Merkliste-CRUD, Subscriptions, abgeordnetenwatch-Joins) - test_clustering.py (82% Coverage) - test_drucksache_typen.py (100%) - test_mail.py (86%) - test_monitoring.py (23 Tests) - test_abgeordnetenwatch.py (23 Tests, inkl. Drucksache-Extraction) - test_redline_parser.py (20 Tests fuer §INS§/§DEL§-Marker) - test_bug_regressions.py (PRAGMA, JWT-azp, CDU-PDF, PFLICHT-FRAKTIONEN, NRW-Titel) - test_embeddings_v3_v4.py (WRITE/READ-Pattern) - test_wahlprogramm_check.py (#128) - test_wahlprogramm_fetch.py (#138) - test_antrag/bewertung/abonnement_repository.py + test_llm_bewerter.py (DDD) - test_domain_behavior.py (5 Domain-Methoden boundary tests) - tests/e2e/test_ui.py (Playwright) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
159 lines
5.8 KiB
Python
159 lines
5.8 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
|