test: 467 -> 574 Tests (+107) — DDD, abgeordnetenwatch, monitoring, v2, Bug-Regressions
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>
2026-04-25 20:55:57 +02:00
|
|
|
|
"""Tests für app/abgeordnetenwatch.py und die DB-Funktionen (#106 Phase 1)."""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import asyncio
|
|
|
|
|
|
import json
|
|
|
|
|
|
import sys
|
|
|
|
|
|
import types
|
|
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─── aiosqlite-Stub-Schutz (analog test_database.py) ────────────────────────
|
|
|
|
|
|
_aio = sys.modules.get("aiosqlite")
|
|
|
|
|
|
if _aio is not None and not hasattr(_aio, "connect"):
|
|
|
|
|
|
del sys.modules["aiosqlite"]
|
|
|
|
|
|
|
|
|
|
|
|
import aiosqlite as _real_aiosqlite # noqa: E402
|
|
|
|
|
|
import importlib as _importlib
|
|
|
|
|
|
|
|
|
|
|
|
for _mod in ("app.database", "app.abgeordnetenwatch"):
|
|
|
|
|
|
if _mod in sys.modules:
|
|
|
|
|
|
_db_mod = sys.modules[_mod]
|
|
|
|
|
|
if _mod == "app.database" and not hasattr(
|
|
|
|
|
|
getattr(_db_mod, "aiosqlite", None), "connect"
|
|
|
|
|
|
):
|
|
|
|
|
|
del sys.modules[_mod]
|
|
|
|
|
|
_importlib.import_module(_mod)
|
|
|
|
|
|
else:
|
|
|
|
|
|
_importlib.import_module(_mod)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run(coro):
|
|
|
|
|
|
return asyncio.get_event_loop().run_until_complete(coro)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─── Fixtures ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
|
|
|
|
|
def db_path(tmp_path, monkeypatch):
|
|
|
|
|
|
path = tmp_path / "test_aw.db"
|
|
|
|
|
|
from app.config import settings
|
|
|
|
|
|
monkeypatch.setattr(settings, "db_path", str(path))
|
|
|
|
|
|
return str(path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
|
|
|
|
|
def initialized_db(db_path):
|
|
|
|
|
|
from app import database
|
|
|
|
|
|
run(database.init_db())
|
|
|
|
|
|
return db_path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─── extract_drucksache_from_intro ───────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
class TestExtractDrucksache:
|
|
|
|
|
|
def test_simple_match(self):
|
|
|
|
|
|
from app.abgeordnetenwatch import extract_drucksache_from_intro
|
|
|
|
|
|
html = "<p>Beratung des Antrags 18/1234 der Fraktion SPD.</p>"
|
|
|
|
|
|
assert extract_drucksache_from_intro(html) == "18/1234"
|
|
|
|
|
|
|
|
|
|
|
|
def test_match_in_link(self):
|
|
|
|
|
|
from app.abgeordnetenwatch import extract_drucksache_from_intro
|
|
|
|
|
|
html = '<a href="...">Drucksache 7/98765</a>'
|
|
|
|
|
|
assert extract_drucksache_from_intro(html) == "7/98765"
|
|
|
|
|
|
|
|
|
|
|
|
def test_first_match_wins(self):
|
|
|
|
|
|
from app.abgeordnetenwatch import extract_drucksache_from_intro
|
|
|
|
|
|
html = "Antrag 17/100 und 18/200"
|
|
|
|
|
|
assert extract_drucksache_from_intro(html) == "17/100"
|
|
|
|
|
|
|
|
|
|
|
|
def test_no_match_returns_none(self):
|
|
|
|
|
|
from app.abgeordnetenwatch import extract_drucksache_from_intro
|
|
|
|
|
|
html = "<p>Kein Bezug auf eine Drucksache hier.</p>"
|
|
|
|
|
|
assert extract_drucksache_from_intro(html) is None
|
|
|
|
|
|
|
|
|
|
|
|
def test_empty_string_returns_none(self):
|
|
|
|
|
|
from app.abgeordnetenwatch import extract_drucksache_from_intro
|
|
|
|
|
|
assert extract_drucksache_from_intro("") is None
|
|
|
|
|
|
|
|
|
|
|
|
def test_none_input_returns_none(self):
|
|
|
|
|
|
from app.abgeordnetenwatch import extract_drucksache_from_intro
|
|
|
|
|
|
assert extract_drucksache_from_intro(None) is None
|
|
|
|
|
|
|
|
|
|
|
|
def test_too_short_sequence_not_matched(self):
|
|
|
|
|
|
"""Zwei Ziffern reichen nicht aus (min. 3 nach Slash)."""
|
|
|
|
|
|
from app.abgeordnetenwatch import extract_drucksache_from_intro
|
|
|
|
|
|
html = "Seite 3/12 — nicht relevant"
|
|
|
|
|
|
assert extract_drucksache_from_intro(html) is None
|
|
|
|
|
|
|
test(#134): Coverage-Backfill drei Module
- app/ingest_votes.py 39.2% → 100%
- TestDownloadPdf: schreibt Bytes, propagiert HTTP-Fehler
- TestCli: --supported, kein-arg-error, fehlender PDF-Pfad,
pdf-Pfad-Run, --url-Download-Pfad, exit-Code 2 bei null Resultaten,
Errors-Liste im Output
- DB-Error-Collection in ingest_pdf
- app/wahlprogramme.py 90.7% → 100%
- TestLoadWahlprogrammText: paged-Datei, Normal-Datei-Fallback,
fehlende Datei
- TestSearchWahlprogramm: leere Returns
- TestFindRelevantQuotes: ValueError bei unbekanntem BL
- TestFormatQuoteForPrompt: leeres Dict
- app/abgeordnetenwatch.py 95.2% → 97.6%
- test_rp_pattern_nr_wp_swap: '/538-18.pdf' → '18/538'
- test_sn_pattern_dok_nr_leg_per_swap: 'dok_nr=2150&leg_per=8' → '8/2150'
Total: 47.59% → 48.69%, 666 → 686 Tests, 0 Failures.
2026-04-28 10:50:26 +02:00
|
|
|
|
def test_rp_pattern_nr_wp_swap(self):
|
|
|
|
|
|
"""RP-URL '/538-18.pdf' → drucksache-Format 'wp/nr' = '18/538'.
|
|
|
|
|
|
Wir vermeiden im HTML jegliche 'wp/nr'-Notation, sonst greift der
|
|
|
|
|
|
generische 'Drucksache (\\d+)/(\\d+)'-Match zuerst."""
|
|
|
|
|
|
from app.abgeordnetenwatch import extract_drucksache_from_intro
|
|
|
|
|
|
html = '<a href="https://landtag.rlp.de/dokumente/538-18.pdf">Antrag</a>'
|
|
|
|
|
|
result = extract_drucksache_from_intro(html)
|
|
|
|
|
|
assert result == "18/538"
|
|
|
|
|
|
|
|
|
|
|
|
def test_sn_pattern_dok_nr_leg_per_swap(self):
|
|
|
|
|
|
"""SN-URL 'dok_nr=2150&...&leg_per=8' → '8/2150'."""
|
|
|
|
|
|
from app.abgeordnetenwatch import extract_drucksache_from_intro
|
|
|
|
|
|
html = '<a href="/cgi-bin/foo?dok_nr=2150&extra=x&leg_per=8">DS</a>'
|
|
|
|
|
|
assert extract_drucksache_from_intro(html) == "8/2150"
|
|
|
|
|
|
|
test: 467 -> 574 Tests (+107) — DDD, abgeordnetenwatch, monitoring, v2, Bug-Regressions
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>
2026-04-25 20:55:57 +02:00
|
|
|
|
def test_two_digit_wp_number(self):
|
|
|
|
|
|
from app.abgeordnetenwatch import extract_drucksache_from_intro
|
|
|
|
|
|
html = "Bezug: 19/12345"
|
|
|
|
|
|
assert extract_drucksache_from_intro(html) == "19/12345"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─── PARLIAMENT_ID-Mapping ────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
class TestParliamentIdMapping:
|
|
|
|
|
|
def test_bt_maps_to_5(self):
|
|
|
|
|
|
from app.abgeordnetenwatch import PARLIAMENT_ID
|
|
|
|
|
|
assert PARLIAMENT_ID["BT"] == 5
|
|
|
|
|
|
|
|
|
|
|
|
def test_bund_alias_maps_to_5(self):
|
|
|
|
|
|
from app.abgeordnetenwatch import PARLIAMENT_ID
|
|
|
|
|
|
assert PARLIAMENT_ID["BUND"] == 5
|
|
|
|
|
|
|
|
|
|
|
|
def test_nrw_maps_to_4(self):
|
|
|
|
|
|
from app.abgeordnetenwatch import PARLIAMENT_ID
|
|
|
|
|
|
assert PARLIAMENT_ID["NRW"] == 4
|
|
|
|
|
|
|
|
|
|
|
|
def test_all_16_bundeslaender_plus_bt_present(self):
|
|
|
|
|
|
from app.abgeordnetenwatch import PARLIAMENT_ID
|
|
|
|
|
|
expected_codes = {
|
|
|
|
|
|
"BT", "NRW", "BE", "HH", "BW", "RP", "LSA", "MV",
|
|
|
|
|
|
"HB", "HE", "NI", "BY", "SL", "TH", "BB", "SN", "SH",
|
|
|
|
|
|
}
|
|
|
|
|
|
assert expected_codes <= set(PARLIAMENT_ID.keys())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─── fetch_polls — Stub-Test via httpx mock ───────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
class TestFetchPollsStub:
|
|
|
|
|
|
def test_fetch_polls_parses_response(self, monkeypatch):
|
|
|
|
|
|
"""Stub-Test: httpx.AsyncClient.get gibt eine Fake-API-Antwort zurück."""
|
|
|
|
|
|
import httpx
|
|
|
|
|
|
from app import abgeordnetenwatch as aw_mod
|
|
|
|
|
|
|
|
|
|
|
|
fake_polls = [
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": 42,
|
|
|
|
|
|
"label": "Testabstimmung 1",
|
|
|
|
|
|
"field_poll_date": "2026-04-01",
|
|
|
|
|
|
"field_accepted": True,
|
|
|
|
|
|
"field_topics": [{"label": "Klimaschutz"}],
|
|
|
|
|
|
"field_intro": "<p>Antrag 18/999 der Fraktion GRÜNE</p>",
|
|
|
|
|
|
"field_legislature": {"label": "18. Wahlperiode"},
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
class FakeResponse:
|
|
|
|
|
|
def raise_for_status(self): pass
|
|
|
|
|
|
def json(self): return {"data": fake_polls}
|
|
|
|
|
|
|
|
|
|
|
|
class FakeClient:
|
|
|
|
|
|
async def __aenter__(self): return self
|
|
|
|
|
|
async def __aexit__(self, *a): pass
|
|
|
|
|
|
async def get(self, url, params=None): return FakeResponse()
|
|
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(httpx, "AsyncClient", lambda **kw: FakeClient())
|
|
|
|
|
|
|
|
|
|
|
|
polls = run(aw_mod.fetch_polls("NRW", limit=10))
|
|
|
|
|
|
assert len(polls) == 1
|
|
|
|
|
|
assert polls[0]["id"] == 42
|
|
|
|
|
|
assert polls[0]["drucksache"] == "18/999"
|
|
|
|
|
|
assert polls[0]["field_accepted"] is True
|
|
|
|
|
|
|
|
|
|
|
|
def test_fetch_polls_unknown_bundesland_raises(self):
|
|
|
|
|
|
from app.abgeordnetenwatch import fetch_polls
|
|
|
|
|
|
with pytest.raises(ValueError, match="Unbekannter BL-Code"):
|
|
|
|
|
|
run(fetch_polls("XX"))
|
|
|
|
|
|
|
|
|
|
|
|
def test_fetch_votes_parses_response(self, monkeypatch):
|
|
|
|
|
|
"""Stub-Test: Votes werden korrekt geparst."""
|
|
|
|
|
|
import httpx
|
|
|
|
|
|
from app import abgeordnetenwatch as aw_mod
|
|
|
|
|
|
|
|
|
|
|
|
fake_votes = [
|
|
|
|
|
|
{
|
|
|
|
|
|
"mandate": {
|
|
|
|
|
|
"id": 101,
|
|
|
|
|
|
"label": "Erika Mustermann",
|
|
|
|
|
|
"party": {"label": "SPD"},
|
|
|
|
|
|
},
|
|
|
|
|
|
"vote": "yes",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"mandate": {
|
|
|
|
|
|
"id": 102,
|
|
|
|
|
|
"label": "Max Muster",
|
|
|
|
|
|
"party": {"label": "CDU"},
|
|
|
|
|
|
},
|
|
|
|
|
|
"vote": "no",
|
|
|
|
|
|
},
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
class FakeResponse:
|
|
|
|
|
|
def raise_for_status(self): pass
|
|
|
|
|
|
def json(self): return {"data": fake_votes}
|
|
|
|
|
|
|
|
|
|
|
|
class FakeClient:
|
|
|
|
|
|
async def __aenter__(self): return self
|
|
|
|
|
|
async def __aexit__(self, *a): pass
|
|
|
|
|
|
async def get(self, url, params=None): return FakeResponse()
|
|
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(httpx, "AsyncClient", lambda **kw: FakeClient())
|
|
|
|
|
|
|
|
|
|
|
|
votes = run(aw_mod.fetch_votes_for_poll(42))
|
|
|
|
|
|
assert len(votes) == 2
|
|
|
|
|
|
assert votes[0]["politician_id"] == 101
|
|
|
|
|
|
assert votes[0]["vote"] == "yes"
|
|
|
|
|
|
assert votes[0]["partei"] == "SPD"
|
|
|
|
|
|
assert votes[1]["vote"] == "no"
|
|
|
|
|
|
|
|
|
|
|
|
def test_fetch_votes_unknown_vote_value_becomes_no_show(self, monkeypatch):
|
|
|
|
|
|
import httpx
|
|
|
|
|
|
from app import abgeordnetenwatch as aw_mod
|
|
|
|
|
|
|
|
|
|
|
|
fake_votes = [{"mandate": {"id": 1, "label": "X"}, "vote": "gibberish"}]
|
|
|
|
|
|
|
|
|
|
|
|
class FakeResponse:
|
|
|
|
|
|
def raise_for_status(self): pass
|
|
|
|
|
|
def json(self): return {"data": fake_votes}
|
|
|
|
|
|
|
|
|
|
|
|
class FakeClient:
|
|
|
|
|
|
async def __aenter__(self): return self
|
|
|
|
|
|
async def __aexit__(self, *a): pass
|
|
|
|
|
|
async def get(self, url, params=None): return FakeResponse()
|
|
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(httpx, "AsyncClient", lambda **kw: FakeClient())
|
|
|
|
|
|
|
|
|
|
|
|
votes = run(aw_mod.fetch_votes_for_poll(99))
|
|
|
|
|
|
assert votes[0]["vote"] == "no_show"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─── DB-Upsert-Round-Trip ─────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
class TestDbUpsertRoundTrip:
|
|
|
|
|
|
def test_upsert_poll_new(self, initialized_db):
|
|
|
|
|
|
from app import database
|
|
|
|
|
|
is_new = run(database.upsert_aw_poll(
|
|
|
|
|
|
poll_id=1001,
|
|
|
|
|
|
parliament_id=4,
|
|
|
|
|
|
bundesland="NRW",
|
|
|
|
|
|
drucksache="18/500",
|
|
|
|
|
|
titel="Test-Abstimmung",
|
|
|
|
|
|
datum="2026-04-01",
|
|
|
|
|
|
accepted=True,
|
|
|
|
|
|
topics=["Klimaschutz"],
|
|
|
|
|
|
legislature_label="18. Wahlperiode",
|
|
|
|
|
|
synced_at="2026-04-20T10:00:00",
|
|
|
|
|
|
))
|
|
|
|
|
|
assert is_new is True
|
|
|
|
|
|
|
|
|
|
|
|
def test_upsert_poll_update_returns_false(self, initialized_db):
|
|
|
|
|
|
from app import database
|
|
|
|
|
|
run(database.upsert_aw_poll(
|
|
|
|
|
|
poll_id=1002, parliament_id=4, bundesland="NRW",
|
|
|
|
|
|
drucksache="18/501", titel="Alt", datum="2026-03-01",
|
|
|
|
|
|
accepted=False, topics=[], legislature_label="",
|
|
|
|
|
|
synced_at="2026-04-20T10:00:00",
|
|
|
|
|
|
))
|
|
|
|
|
|
is_new = run(database.upsert_aw_poll(
|
|
|
|
|
|
poll_id=1002, parliament_id=4, bundesland="NRW",
|
|
|
|
|
|
drucksache="18/501", titel="Neu", datum="2026-03-01",
|
|
|
|
|
|
accepted=True, topics=[], legislature_label="",
|
|
|
|
|
|
synced_at="2026-04-20T11:00:00",
|
|
|
|
|
|
))
|
|
|
|
|
|
assert is_new is False
|
|
|
|
|
|
|
|
|
|
|
|
def test_upsert_vote_new(self, initialized_db):
|
|
|
|
|
|
from app import database
|
|
|
|
|
|
run(database.upsert_aw_poll(
|
|
|
|
|
|
poll_id=2001, parliament_id=4, bundesland="NRW",
|
|
|
|
|
|
drucksache=None, titel="V-Test", datum="2026-04-01",
|
|
|
|
|
|
accepted=True, topics=[], legislature_label="",
|
|
|
|
|
|
synced_at="2026-04-20T10:00:00",
|
|
|
|
|
|
))
|
|
|
|
|
|
is_new = run(database.upsert_aw_vote(
|
|
|
|
|
|
poll_id=2001, politician_id=999,
|
|
|
|
|
|
politician_name="Test Politiker",
|
|
|
|
|
|
partei="SPD",
|
|
|
|
|
|
vote="yes",
|
|
|
|
|
|
))
|
|
|
|
|
|
assert is_new is True
|
|
|
|
|
|
|
|
|
|
|
|
def test_upsert_vote_update_returns_false(self, initialized_db):
|
|
|
|
|
|
from app import database
|
|
|
|
|
|
run(database.upsert_aw_poll(
|
|
|
|
|
|
poll_id=2002, parliament_id=4, bundesland="NRW",
|
|
|
|
|
|
drucksache=None, titel="V-Test2", datum="2026-04-01",
|
|
|
|
|
|
accepted=True, topics=[], legislature_label="",
|
|
|
|
|
|
synced_at="2026-04-20T10:00:00",
|
|
|
|
|
|
))
|
|
|
|
|
|
run(database.upsert_aw_vote(2002, 888, "Name", "CDU", "no"))
|
|
|
|
|
|
is_new = run(database.upsert_aw_vote(2002, 888, "Name", "CDU", "yes"))
|
|
|
|
|
|
assert is_new is False
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_abstimmungsverhalten_returns_none_for_missing(self, initialized_db):
|
|
|
|
|
|
from app import database
|
|
|
|
|
|
result = run(database.get_abstimmungsverhalten("99/9999"))
|
|
|
|
|
|
assert result is None
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_abstimmungsverhalten_aggregates(self, initialized_db):
|
|
|
|
|
|
from app import database
|
|
|
|
|
|
# Poll anlegen
|
|
|
|
|
|
run(database.upsert_aw_poll(
|
|
|
|
|
|
poll_id=3001, parliament_id=4, bundesland="NRW",
|
|
|
|
|
|
drucksache="18/3001", titel="AggTest", datum="2026-04-10",
|
|
|
|
|
|
accepted=True, topics=[], legislature_label="",
|
|
|
|
|
|
synced_at="2026-04-20T10:00:00",
|
|
|
|
|
|
))
|
|
|
|
|
|
# Votes: 2× SPD yes, 1× CDU no, 1× CDU abstain
|
|
|
|
|
|
run(database.upsert_aw_vote(3001, 1, "A", "SPD", "yes"))
|
|
|
|
|
|
run(database.upsert_aw_vote(3001, 2, "B", "SPD", "yes"))
|
|
|
|
|
|
run(database.upsert_aw_vote(3001, 3, "C", "CDU", "no"))
|
|
|
|
|
|
run(database.upsert_aw_vote(3001, 4, "D", "CDU", "abstain"))
|
|
|
|
|
|
|
|
|
|
|
|
result = run(database.get_abstimmungsverhalten("18/3001"))
|
|
|
|
|
|
assert result is not None
|
|
|
|
|
|
assert result["accepted"] is True
|
|
|
|
|
|
fraktionen = {f["partei"]: f for f in result["fraktionen"]}
|
|
|
|
|
|
assert fraktionen["SPD"]["yes"] == 2
|
|
|
|
|
|
assert fraktionen["CDU"]["no"] == 1
|
|
|
|
|
|
assert fraktionen["CDU"]["abstain"] == 1
|
|
|
|
|
|
|
|
|
|
|
|
def test_aw_tables_created_by_init_db(self, db_path):
|
|
|
|
|
|
"""init_db legt abgeordnetenwatch_polls und _votes an."""
|
|
|
|
|
|
import aiosqlite
|
|
|
|
|
|
from app import database
|
|
|
|
|
|
run(database.init_db())
|
|
|
|
|
|
|
|
|
|
|
|
async def check():
|
|
|
|
|
|
async with aiosqlite.connect(db_path) as db:
|
|
|
|
|
|
cur = await db.execute(
|
|
|
|
|
|
"SELECT name FROM sqlite_master WHERE type='table'"
|
|
|
|
|
|
)
|
|
|
|
|
|
return {r[0] for r in await cur.fetchall()}
|
|
|
|
|
|
|
|
|
|
|
|
tables = run(check())
|
|
|
|
|
|
assert "abgeordnetenwatch_polls" in tables
|
|
|
|
|
|
assert "abgeordnetenwatch_votes" in tables
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─── fallback_drucksache_by_date_title (#142 Phase 3) ────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
class TestFallbackDrucksacheByDateTitle:
|
|
|
|
|
|
"""Unit-Tests für den Datum+Titel-Fallback-Lookup gegen die Assessments-DB."""
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
|
|
|
|
|
def db_with_assessment(self, initialized_db):
|
|
|
|
|
|
"""DB mit einem vorbereiteten Assessment für den Fallback-Test."""
|
|
|
|
|
|
from app import database
|
|
|
|
|
|
run(database.upsert_assessment({
|
|
|
|
|
|
"drucksache": "7/1234",
|
|
|
|
|
|
"title": "Antrag zur Verbesserung des Nahverkehrs in MV",
|
|
|
|
|
|
"bundesland": "MV",
|
|
|
|
|
|
"datum": "2026-04-10",
|
|
|
|
|
|
"fraktionen": ["SPD"],
|
|
|
|
|
|
"gwoeScore": 7.0,
|
|
|
|
|
|
"gwoeBegründung": "Test",
|
|
|
|
|
|
"gwoeMatrix": [],
|
|
|
|
|
|
"gwoeSchwerpunkt": [],
|
|
|
|
|
|
"wahlprogrammScores": [],
|
|
|
|
|
|
"verbesserungen": [],
|
|
|
|
|
|
"stärken": [],
|
|
|
|
|
|
"schwächen": [],
|
|
|
|
|
|
"themen": [],
|
|
|
|
|
|
"antragZusammenfassung": "",
|
|
|
|
|
|
"antragKernpunkte": [],
|
|
|
|
|
|
"source": "batch",
|
|
|
|
|
|
"model": "test",
|
|
|
|
|
|
}))
|
|
|
|
|
|
return initialized_db
|
|
|
|
|
|
|
|
|
|
|
|
def test_fallback_finds_match_within_14_days(self, db_with_assessment):
|
|
|
|
|
|
from app.abgeordnetenwatch import fallback_drucksache_by_date_title
|
|
|
|
|
|
result = run(fallback_drucksache_by_date_title(
|
|
|
|
|
|
datum="2026-04-12", # 2 Tage nach Assessment-Datum
|
|
|
|
|
|
titel="Verbesserung des Nahverkehrs in MV",
|
|
|
|
|
|
bundesland="MV",
|
|
|
|
|
|
))
|
|
|
|
|
|
assert result == "7/1234"
|
|
|
|
|
|
|
|
|
|
|
|
def test_fallback_returns_none_outside_14_days(self, db_with_assessment):
|
|
|
|
|
|
from app.abgeordnetenwatch import fallback_drucksache_by_date_title
|
|
|
|
|
|
result = run(fallback_drucksache_by_date_title(
|
|
|
|
|
|
datum="2026-05-01", # 21 Tage nach Assessment-Datum
|
|
|
|
|
|
titel="Verbesserung des Nahverkehrs in MV",
|
|
|
|
|
|
bundesland="MV",
|
|
|
|
|
|
))
|
|
|
|
|
|
assert result is None
|
|
|
|
|
|
|
|
|
|
|
|
def test_fallback_returns_none_wrong_bundesland(self, db_with_assessment):
|
|
|
|
|
|
from app.abgeordnetenwatch import fallback_drucksache_by_date_title
|
|
|
|
|
|
result = run(fallback_drucksache_by_date_title(
|
|
|
|
|
|
datum="2026-04-10",
|
|
|
|
|
|
titel="Verbesserung des Nahverkehrs in MV",
|
|
|
|
|
|
bundesland="BY", # falsches BL
|
|
|
|
|
|
))
|
|
|
|
|
|
assert result is None
|
|
|
|
|
|
|
|
|
|
|
|
def test_fallback_returns_none_no_titel_match(self, db_with_assessment):
|
|
|
|
|
|
from app.abgeordnetenwatch import fallback_drucksache_by_date_title
|
|
|
|
|
|
result = run(fallback_drucksache_by_date_title(
|
|
|
|
|
|
datum="2026-04-10",
|
|
|
|
|
|
titel="Irgendwas voellig anderes ohne Treffer",
|
|
|
|
|
|
bundesland="MV",
|
|
|
|
|
|
))
|
|
|
|
|
|
assert result is None
|
|
|
|
|
|
|
|
|
|
|
|
def test_fallback_returns_none_for_missing_inputs(self, db_with_assessment):
|
|
|
|
|
|
from app.abgeordnetenwatch import fallback_drucksache_by_date_title
|
|
|
|
|
|
assert run(fallback_drucksache_by_date_title(None, "Titel", "MV")) is None
|
|
|
|
|
|
assert run(fallback_drucksache_by_date_title("2026-04-10", None, "MV")) is None
|