"""Tests für app.parteien — den zentralen Parteinamen-Mapper. Issue #55 + Roadmap #59 Phase B. Drei Schichten: 1. ``normalize_partei`` — single-string lookup (für Wahlprogramm-/ Embedding-Lookups, wo wir nur einen Namen haben) 2. ``extract_fraktionen`` — komma-separierte Listen, MdL-mit-Klammerpartei, HTML-Reste; ersetzt die vier ``_normalize_fraktion()``-Methoden 3. ``display_name`` — Anzeigeform (kurz/lang) Plus Roundtrip-Tests gegen die historisch in den Adaptern erkannten Patterns (F.D.P., LL/PDS, BÜNDNIS 90, MINISTERIUM, etc.) damit der Refactor garantiert nichts kaputt macht. """ from __future__ import annotations import pytest from app.parteien import ( GOVERNMENT_KEY, PARTEIEN, all_canonical_keys, display_name, extract_fraktionen, normalize_partei, ) # ───────────────────────────────────────────────────────────────────────────── # normalize_partei — single-string lookup # ───────────────────────────────────────────────────────────────────────────── class TestNormalizePartei: @pytest.mark.parametrize( ("raw", "expected"), [ ("CDU", "CDU"), ("SPD", "SPD"), ("FDP", "FDP"), ("F.D.P.", "FDP"), ("F. D. P.", "FDP"), ("AfD", "AfD"), ("AFD", "AfD"), ("Alternative für Deutschland", "AfD"), ("GRÜNE", "GRÜNE"), ("Grüne", "GRÜNE"), ("GRUENE", "GRÜNE"), ("Bündnis 90/Die Grünen", "GRÜNE"), ("LINKE", "LINKE"), ("Die Linke", "LINKE"), ("DIE LINKE", "LINKE"), ("LL/PDS", "LINKE"), ("BSW", "BSW"), ], ) def test_canonical_keys_for_known_aliases(self, raw, expected): assert normalize_partei(raw) == expected def test_unknown_returns_none(self): assert normalize_partei("Pirateneinhornpartei XYZ") is None def test_empty_returns_none(self): assert normalize_partei("") is None assert normalize_partei(None) is None # type: ignore[arg-type] def test_government_marker_takes_precedence(self): # "Antrag der Landesregierung" → Government, nicht eine zufällig # erkannte Partei aus dem Rest des Strings. assert normalize_partei("Antrag der Landesregierung") == GOVERNMENT_KEY assert normalize_partei("Ministerium der Finanzen") == GOVERNMENT_KEY assert normalize_partei("Senat von Berlin") == GOVERNMENT_KEY def test_ssw_only_in_sh(self): # SSW ist BL-spezifisch — bei explizit SH klappt es, ohne BL # akzeptieren wir es auch (tolerant) — die strenge BL-Constraint # gilt nur bei extract_fraktionen. assert normalize_partei("SSW", bundesland="SH") == "SSW" def test_csu_only_in_by(self): assert normalize_partei("CSU", bundesland="BY") == "CSU" # ───────────────────────────────────────────────────────────────────────────── # Freie-Wähler-Disambiguierung — der eigentliche Mehrwert des Mappers # ───────────────────────────────────────────────────────────────────────────── class TestFreieWaehlerDisambiguation: def test_freie_waehler_in_brandenburg_resolves_to_bvb(self): assert normalize_partei("FREIE WÄHLER", bundesland="BB") == "BVB-FW" def test_freie_waehler_in_bayern_resolves_to_fw_bayern(self): assert normalize_partei("FREIE WÄHLER", bundesland="BY") == "FW-BAYERN" def test_freie_waehler_in_saarland_resolves_to_fw_sl(self): assert normalize_partei("FREIE WÄHLER", bundesland="SL") == "FW-SL" def test_freie_waehler_in_rlp_resolves_to_generic(self): # In RP gehört der Landesverband zur Bundesvereinigung — der ist # der generische "FREIE WÄHLER"-Eintrag (kein Scope). assert normalize_partei("FREIE WÄHLER", bundesland="RP") == "FREIE WÄHLER" def test_fw_kuerzel_disambiguates(self): assert normalize_partei("FW", bundesland="BY") == "FW-BAYERN" assert normalize_partei("FW", bundesland="BB") == "BVB-FW" def test_freie_waehler_without_bundesland_falls_back_to_generic(self): # Kein BL → wir können nicht disambiguieren, der generische # Eintrag bleibt der Default assert normalize_partei("FREIE WÄHLER") == "FREIE WÄHLER" # ───────────────────────────────────────────────────────────────────────────── # extract_fraktionen — der Adapter-Funnel # ───────────────────────────────────────────────────────────────────────────── class TestExtractFraktionen: def test_single_simple(self): assert extract_fraktionen("CDU") == ["CDU"] def test_comma_separated(self): assert set(extract_fraktionen("CDU, SPD, FDP")) == {"CDU", "SPD", "FDP"} def test_dedupe(self): # CDU zweimal im Text → nur einmal im Output out = extract_fraktionen("CDU und CDU") assert out == ["CDU"] def test_empty(self): assert extract_fraktionen("") == [] assert extract_fraktionen(None) == [] # type: ignore[arg-type] def test_mdl_with_paren_party(self): # ParlDok-Stil: "Felix Herkens (GRÜNE), Saskia Frank (GRÜNE)" out = extract_fraktionen("Felix Herkens (GRÜNE), Saskia Frank (GRÜNE)") assert out == ["GRÜNE"] def test_mixed_mdl_and_government(self): out = extract_fraktionen("Antrag der Fraktion CDU und der Landesregierung") assert "CDU" in out assert GOVERNMENT_KEY in out def test_fdp_with_dots(self): assert extract_fraktionen("F.D.P.") == ["FDP"] assert extract_fraktionen("Antrag der F.D.P.-Fraktion") == ["FDP"] def test_bsw(self): assert extract_fraktionen("BSW") == ["BSW"] def test_linke_via_ll_pds_legacy(self): # Historische ParlDok-Schreibweise assert extract_fraktionen("LL/PDS") == ["LINKE"] def test_gruene_via_buendnis_90(self): assert extract_fraktionen("BÜNDNIS 90/DIE GRÜNEN") == ["GRÜNE"] def test_ministerium_marker(self): assert extract_fraktionen("Ministerium der Finanzen") == [GOVERNMENT_KEY] def test_real_th_urheber(self): # TH liefert oft "Antrag der Fraktion der CDU" assert extract_fraktionen("Antrag der Fraktion der CDU") == ["CDU"] def test_ssw_only_when_in_sh_context(self): # SH-spezifisch — in einem SH-Antrag taucht SSW auf assert extract_fraktionen("Christian Dirschauer (SSW)", bundesland="SH") == ["SSW"] def test_ssw_filtered_in_other_bl(self): # In einem MV-Antrag würde "SSW" nichts ergeben — BL-Filter greift assert extract_fraktionen("SSW Beispiel", bundesland="MV") == [] def test_freie_waehler_bb_resolves_to_bvb_in_extract(self): # BB-Drucksache der "FREIE WÄHLER"-Fraktion → BVB-FW out = extract_fraktionen("Antrag der Fraktion FREIE WÄHLER", bundesland="BB") assert out == ["BVB-FW"] def test_freie_waehler_rp_resolves_to_generic(self): out = extract_fraktionen("Antrag der Fraktion FREIE WÄHLER", bundesland="RP") assert out == ["FREIE WÄHLER"] def test_freie_waehler_by(self): out = extract_fraktionen("Antrag der Fraktion FREIE WÄHLER", bundesland="BY") assert out == ["FW-BAYERN"] # ───────────────────────────────────────────────────────────────────────────── # display_name # ───────────────────────────────────────────────────────────────────────────── class TestDisplayName: def test_short_default(self): assert display_name("CDU") == "CDU" assert display_name("GRÜNE") == "GRÜNE" def test_long_form(self): assert display_name("GRÜNE", long=True) == "BÜNDNIS 90/DIE GRÜNEN" assert display_name("LINKE", long=True) == "DIE LINKE" assert display_name("BiW", long=True) == "BÜRGER IN WUT" def test_government(self): assert display_name(GOVERNMENT_KEY) == "Landesregierung" def test_unknown_passthrough(self): # Unbekannt — kein Fehler, einfach durchreichen assert display_name("UnknownPartyXYZ") == "UnknownPartyXYZ" # ───────────────────────────────────────────────────────────────────────────── # Tabellen-Konsistenz # ───────────────────────────────────────────────────────────────────────────── class TestTableConsistency: def test_no_duplicate_canonical_keys(self): keys = [p.canonical for p in PARTEIEN] assert len(keys) == len(set(keys)), f"Duplikate: {[k for k in keys if keys.count(k) > 1]}" def test_all_canonical_keys_resolvable_by_themselves(self): # Jeder kanonische Key muss als Roh-Eingabe wieder auf sich selbst # auflösen — das ist die Identitäts-Invariante des Mappers for p in PARTEIEN: if p.bundesland_scope: # Disambiguierungs-Einträge brauchen ihren Scope mitgegeben assert normalize_partei(p.canonical, bundesland=p.bundesland_scope) in { p.canonical, # FW-BAYERN/FW-SL/BVB-FW haben ihren canonical-Key nicht # als alias drin — er ist nur ein interner Schlüssel. # Sie matchen über die FW-Aliase. "FREIE WÄHLER", "FW-BAYERN", "FW-SL", "BVB-FW", } else: assert normalize_partei(p.canonical) == p.canonical def test_all_canonical_keys_helper_includes_government(self): keys = all_canonical_keys() assert GOVERNMENT_KEY in keys assert "CDU" in keys assert "GRÜNE" in keys