"""Tests für den Tab „Eigentümer“ – Flurstückssuche, Eigentümersuche, Anzeige und Korrektur (#56). Alle Namen und Anschriften sind erfunden. """ import os import sys import tempfile import types import unittest from datetime import datetime from unittest.mock import MagicMock, patch if "qgis" not in sys.modules: qgis_mod = types.ModuleType("qgis") qgis_utils = types.ModuleType("qgis.utils") qgis_utils.iface = types.SimpleNamespace() qgis_utils.plugins = {} qgis_mod.utils = qgis_utils sys.modules["qgis"] = qgis_mod sys.modules["qgis.utils"] = qgis_utils from sn_verfahrensgebiet.ui.tabs import eigentuemer_tab as et from sn_verfahrensgebiet.ui.tabs.eigentuemer_tab import EigentuemerTab from sn_verfahrensgebiet.functions.flurstueck_suche import ( FlurstueckSucheFehler, KeinFlurstueckslayer, SuchErgebnis, ) from sn_verfahrensgebiet.functions.eigentuemer_suche import Eigentuemer, EigentuemerSucheFehler from sn_verfahrensgebiet.ui import eigentuemer_treffer as treffer_modul from sn_verfahrensgebiet.ui.eigentuemer_treffer import EigentuemerTrefferFenster, trefferzeile def _tab(gemarkung="0268", flurstueck="562"): tab = EigentuemerTab.__new__(EigentuemerTab) tab.gemarkung_edit = types.SimpleNamespace(text=lambda: gemarkung) tab.flurstueck_edit = types.SimpleNamespace(text=lambda: flurstueck) return tab @patch.object(et, "success") @patch.object(et, "info") @patch.object(et, "warning") @patch.object(et, "show_info_dialog") class TestFlurstueckSuchen(unittest.TestCase): def test_ohne_flurstueckslayer_erscheint_hinweisdialog(self, dialog, warnung, hinweis, erfolg): """Prüft, dass ohne Layer „Flurstücke“ ein Dialog zum Laden von Verfahrensgebiet und Flurstücken erscheint.""" with patch.object(et, "flurstueck_suchen", side_effect=KeinFlurstueckslayer()): _tab()._on_flurstueck_suchen() dialog.assert_called_once() self.assertIn("Flurstücke aus ALKIS laden", dialog.call_args[0][1]) warnung.assert_not_called() erfolg.assert_not_called() def test_eingabefehler_als_warnung(self, dialog, warnung, hinweis, erfolg): """Prüft, dass eine ungültige Eingabe als Warnung in der Meldungsleiste erscheint, nicht als Dialog.""" with patch.object(et, "flurstueck_suchen", side_effect=FlurstueckSucheFehler("falsch")): _tab()._on_flurstueck_suchen() warnung.assert_called_once_with("Flurstückssuche", "falsch") dialog.assert_not_called() def test_kein_treffer(self, dialog, warnung, hinweis, erfolg): """Prüft, dass ohne Treffer ein Hinweis erscheint und keine Auswahl gesetzt wird.""" layer = MagicMock() with patch.object(et, "flurstueck_suchen", return_value=SuchErgebnis(layer, [])): _tab(flurstueck="999")._on_flurstueck_suchen() hinweis.assert_called_once() self.assertIn("999", hinweis.call_args[0][1]) layer.selectByIds.assert_not_called() def test_treffer_werden_ausgewaehlt_und_angezoomt(self, dialog, warnung, hinweis, erfolg): """Prüft, dass Treffer im Flurstückslayer ausgewählt, angezoomt und gemeldet werden.""" layer = MagicMock() layer.name.return_value = "Flurstücke" canvas = MagicMock() with patch.object(et, "flurstueck_suchen", return_value=SuchErgebnis(layer, [1, 2])) as suche, \ patch.object(et, "iface", types.SimpleNamespace(mapCanvas=lambda: canvas)): _tab(gemarkung=" Beispielhausen ", flurstueck=" 562 ")._on_flurstueck_suchen() suche.assert_called_once_with("Beispielhausen", "562") layer.selectByIds.assert_called_once_with([1, 2]) canvas.zoomToSelected.assert_called_once_with(layer) canvas.flashFeatureIds.assert_called_once_with(layer, [1, 2]) erfolg.assert_called_once() self.assertIn("2 Flurstücke", erfolg.call_args[0][1]) def test_ohne_kartenansicht_bleibt_auswahl(self, dialog, warnung, hinweis, erfolg): """Prüft, dass die Auswahl auch ohne Kartenansicht gesetzt und gemeldet wird.""" layer = MagicMock() with patch.object(et, "flurstueck_suchen", return_value=SuchErgebnis(layer, [7])), \ patch.object(et, "iface", types.SimpleNamespace()): _tab()._on_flurstueck_suchen() layer.selectByIds.assert_called_once_with([7]) self.assertIn("1 Flurstück", erfolg.call_args[0][1]) def _eig_tab(nachname="Beispiel", vorname="", layer_id="csv1"): tab = EigentuemerTab.__new__(EigentuemerTab) tab.nachname_edit = types.SimpleNamespace(text=lambda: nachname) tab.vorname_edit = types.SimpleNamespace(text=lambda: vorname) tab.eigentuemer_layer_combo = MagicMock() tab.eigentuemer_layer_combo.currentData.return_value = layer_id tab._trefferfenster = None return tab _ANNA = Eigentuemer(vorname="Anna", nachname="Beispiel", strasse="Hauptstraße 1", plz="01234", wohnort="Musterstadt", emails=("anna@example.org",), verstorben=True, rechtsperson=("0",)) @patch.object(et, "info") @patch.object(et, "warning") class TestEigentuemerSuchen(unittest.TestCase): def test_ohne_eigentuemerdatei_warnung(self, warnung, hinweis): """Prüft, dass ohne gewählte Eigentümerdatei die Liste aufgefrischt und dann auf das Laden der CSV hingewiesen wird.""" tab = _eig_tab(layer_id=None) with patch.object(et, "hole_layer", return_value=None), \ patch.object(et, "suche_eigentuemer") as suche: tab._on_eigentuemer_suchen() tab.eigentuemer_layer_combo.aktualisieren.assert_called_once() warnung.assert_called_once() self.assertIn("CSV", warnung.call_args[0][1]) suche.assert_not_called() def test_suchfehler_als_warnung(self, warnung, hinweis): """Prüft, dass ein Fehler der Suche (z. B. leere Eingabe) als Warnung erscheint.""" with patch.object(et, "hole_layer", return_value=object()), \ patch.object(et, "suche_eigentuemer", side_effect=EigentuemerSucheFehler("Bitte eingeben")): _eig_tab()._on_eigentuemer_suchen() warnung.assert_called_once_with("Eigentümersuche", "Bitte eingeben") def test_kein_treffer_ohne_namen_in_meldung(self, warnung, hinweis): """Prüft, dass ohne Treffer ein Hinweis erscheint, der den gesuchten Namen nicht wiederholt (Datenschutz).""" with patch.object(et, "hole_layer", return_value=object()), \ patch.object(et, "suche_eigentuemer", return_value=[]), \ patch.object(et, "EigentuemerTrefferFenster") as fenster: _eig_tab(nachname="Beispiel")._on_eigentuemer_suchen() hinweis.assert_called_once() self.assertNotIn("Beispiel", hinweis.call_args[0][1]) fenster.assert_not_called() def test_treffer_oeffnen_trefferliste_und_ersetzen_die_alte(self, warnung, hinweis): """Prüft, dass Treffer in einer neuen Trefferliste mit Anzeige-Rückruf erscheinen, eine offene alte Liste geschlossen wird und die Eingaben an die Suche gehen.""" layer = MagicMock() layer.id.return_value = "csv1" tab = _eig_tab(nachname="Beisp", vorname="An") altes_fenster = MagicMock() tab._trefferfenster = altes_fenster with patch.object(et, "hole_layer", return_value=layer), \ patch.object(et, "suche_eigentuemer", return_value=[_ANNA]) as suche, patch.object(et, "EigentuemerTrefferFenster") as fenster: tab._on_eigentuemer_suchen() suche.assert_called_once_with(layer, "Beisp", "An") altes_fenster.close.assert_called_once() fenster.assert_called_once_with([_ANNA], tab, beim_anzeigen=tab._daten_anzeigen) fenster.return_value.show.assert_called_once() self.assertIs(tab._trefferfenster, fenster.return_value) self.assertEqual(tab._treffer_layer_id, "csv1") class TestTrefferliste(unittest.TestCase): def test_trefferzeile_mit_hinweis(self): """Prüft, dass die Trefferliste Name, Anschrift, E-Mail, den Rohwert von PER_RECHTSPERSON und das Kennzeichen „verstorben“ zeigt.""" self.assertEqual( trefferzeile(_ANNA), ("Beispiel, Anna", "Hauptstraße 1, 01234 Musterstadt", "anna@example.org", "0", "verstorben"), ) def test_ausgewaehlte_eigentuemer(self): """Prüft, dass das Trefferfenster die in der Tabelle gewählten Einträge zurückgibt.""" bernd = Eigentuemer(vorname="Bernd", nachname="Muster") fenster = EigentuemerTrefferFenster.__new__(EigentuemerTrefferFenster) fenster.treffer = [_ANNA, bernd] fenster.tabelle = object() with patch.object(treffer_modul, "ausgewaehlte_indizes", return_value=[1]) as indizes: self.assertEqual(fenster.ausgewaehlte_eigentuemer(), [bernd]) indizes.assert_called_once_with(fenster.tabelle) def test_daten_anzeigen_gibt_auswahl_weiter(self): """Prüft, dass „Daten anzeigen“ die Auswahl der Trefferliste an den Tab übergibt.""" fenster = EigentuemerTrefferFenster.__new__(EigentuemerTrefferFenster) fenster.treffer = [_ANNA] fenster.tabelle = object() fenster.beim_anzeigen = MagicMock() with patch.object(treffer_modul, "ausgewaehlte_indizes", return_value=[0]): fenster._on_anzeigen() fenster.beim_anzeigen.assert_called_once_with([_ANNA]) def _anz_tab(angezeigt=None, anzeige=None, layer_id="csv1"): tab = EigentuemerTab.__new__(EigentuemerTab) for name in ("btn_sitzung", "btn_anzeige_leeren", "btn_korrigieren", "sitzung_label", "bildschirm_combo"): setattr(tab, name, MagicMock()) tab._anzeige = anzeige tab._sitzung_seit = datetime(2026, 10, 6, 9, 5) if anzeige is not None else None tab._angezeigt = list(angezeigt or []) tab._treffer_layer_id = layer_id tab._anzeige_layer_id = layer_id tab._korrektur_pfade = {} return tab _BERND = Eigentuemer(vorname="Bernd", nachname="Muster", konten=("300",), per_ids=("8",)) @patch.object(et, "zeige_auf_bildschirm") @patch.object(et, "qgis_bildschirm") @patch.object(et, "EigentuemerAnzeige") class TestSitzung(unittest.TestCase): def test_sitzung_beginnen_im_vollbild_auf_zweitem_bildschirm(self, anzeige_klasse, qgis, zeige): """Prüft, dass „Sitzung beginnen“ ein leeres Anzeigefenster auf dem gewählten Bildschirm öffnet, im Vollbild, wenn QGIS woanders liegt.""" tab = _anz_tab() zweiter = object() tab.bildschirm_combo.gewaehlter_bildschirm.return_value = zweiter qgis.return_value = object() tab._on_sitzung_umschalten() anzeige_klasse.assert_called_once_with(tab) zeige.assert_called_once_with(anzeige_klasse.return_value, zweiter, True) self.assertIs(tab._anzeige, anzeige_klasse.return_value) self.assertEqual(tab._angezeigt, []) tab.btn_sitzung.setText.assert_called_with("Sitzung beenden") self.assertIn("leer", tab.sitzung_label.setText.call_args[0][0]) def test_auf_qgis_bildschirm_kein_vollbild(self, anzeige_klasse, qgis, zeige): """Prüft, dass das Anzeigefenster auf dem QGIS-Bildschirm als normales Fenster öffnet.""" tab = _anz_tab() einziger = object() tab.bildschirm_combo.gewaehlter_bildschirm.return_value = einziger qgis.return_value = einziger tab._sitzung_beginnen() self.assertFalse(zeige.call_args[0][2]) def test_daten_anzeigen_beginnt_sitzung_bei_bedarf(self, anzeige_klasse, qgis, zeige): """Prüft, dass „Daten anzeigen“ ohne laufende Sitzung diese beginnt und die Einträge zeigt; die Statuszeile nennt nur die Anzahl.""" tab = _anz_tab() tab._daten_anzeigen([_ANNA, _BERND]) anzeige_klasse.return_value.zeige.assert_called_once_with([_ANNA, _BERND]) self.assertEqual(tab._angezeigt, [_ANNA, _BERND]) status = tab.sitzung_label.setText.call_args[0][0] self.assertIn("2 Einträge", status) self.assertNotIn("Beispiel", status) def test_daten_anzeigen_ohne_auswahl(self, anzeige_klasse, qgis, zeige): """Prüft, dass „Daten anzeigen“ ohne Auswahl nur einen Hinweis gibt und keine Sitzung beginnt.""" tab = _anz_tab() with patch.object(et, "warning") as warnung: tab._daten_anzeigen([]) warnung.assert_called_once() anzeige_klasse.assert_not_called() def test_anzeige_leeren_laesst_fenster_offen(self, anzeige_klasse, qgis, zeige): """Prüft, dass „Anzeige leeren“ die Daten entfernt, das Fenster aber nicht schließt.""" anzeige = MagicMock() tab = _anz_tab(angezeigt=[_ANNA], anzeige=anzeige) tab._on_anzeige_leeren() anzeige.leeren.assert_called_once() anzeige.close.assert_not_called() self.assertEqual(tab._angezeigt, []) self.assertIs(tab._anzeige, anzeige) tab.btn_anzeige_leeren.setEnabled.assert_called_with(False) def test_sitzung_beenden_schliesst_fenster(self, anzeige_klasse, qgis, zeige): """Prüft, dass „Sitzung beenden“ das Anzeigefenster schließt und die angezeigten Daten vergisst.""" anzeige = MagicMock() tab = _anz_tab(angezeigt=[_ANNA], anzeige=anzeige) tab._on_sitzung_umschalten() anzeige.close.assert_called_once() self.assertIsNone(anzeige.beim_schliessen) self.assertIsNone(tab._anzeige) self.assertEqual(tab._angezeigt, []) tab.btn_sitzung.setText.assert_called_with("Sitzung beginnen") def test_von_aussen_geschlossen_beendet_sitzung(self, anzeige_klasse, qgis, zeige): """Prüft, dass ein von außen geschlossenes Anzeigefenster (z. B. Alt+F4) die Sitzung im Tab beendet.""" tab = _anz_tab() tab._sitzung_beginnen() rueckruf = anzeige_klasse.return_value.beim_schliessen rueckruf() self.assertIsNone(tab._anzeige) @patch.object(et, "success") @patch.object(et, "info") @patch.object(et, "warning") class TestKorrektur(unittest.TestCase): def test_korrektur_wird_festgehalten_und_angezeigt(self, warnung, hinweis, erfolg): """Prüft, dass eine Korrektur in die Korrektur-CSV geschrieben, sofort angezeigt und ohne Namen gemeldet wird.""" anzeige = MagicMock() tab = _anz_tab(angezeigt=[_ANNA, _BERND], anzeige=anzeige) tab._korrektur_pfade["csv1"] = "D:/Verfahren/export_korrekturen.csv" with patch.object(et, "frage_korrektur", return_value=(1, {"wohnort": "Neustadt"})), \ patch.object(et, "schreibe_korrekturen") as schreiben: tab._on_korrigieren() schreiben.assert_called_once_with("D:/Verfahren/export_korrekturen.csv", _BERND, [("wohnort", "", "Neustadt")]) self.assertEqual(tab._angezeigt[1].wohnort, "Neustadt") self.assertIs(tab._angezeigt[0], _ANNA) anzeige.zeige.assert_called_once_with(tab._angezeigt) meldung = erfolg.call_args[0][1] self.assertIn("export_korrekturen.csv", meldung) self.assertNotIn("Muster", meldung) def test_ohne_aenderung_nichts_schreiben(self, warnung, hinweis, erfolg): """Prüft, dass ein unverändert gespeicherter Dialog nichts schreibt.""" tab = _anz_tab(angezeigt=[_ANNA], anzeige=MagicMock()) with patch.object(et, "frage_korrektur", return_value=(0, {"nachname": "Beispiel"})), \ patch.object(et, "schreibe_korrekturen") as schreiben: tab._on_korrigieren() schreiben.assert_not_called() hinweis.assert_called_once() def test_abbruch_nichts_schreiben(self, warnung, hinweis, erfolg): """Prüft, dass ein abgebrochener Korrekturdialog nichts schreibt.""" tab = _anz_tab(angezeigt=[_ANNA], anzeige=MagicMock()) with patch.object(et, "frage_korrektur", return_value=None), \ patch.object(et, "schreibe_korrekturen") as schreiben: tab._on_korrigieren() schreiben.assert_not_called() def test_schreibfehler_warnung_und_anzeige_unveraendert(self, warnung, hinweis, erfolg): """Prüft, dass bei einem Schreibfehler gewarnt, die Anzeige nicht geändert und der Pfad beim nächsten Mal neu bestimmt wird.""" anzeige = MagicMock() tab = _anz_tab(angezeigt=[_ANNA], anzeige=anzeige) tab._korrektur_pfade["csv1"] = "X:/gesperrt.csv" with patch.object(et, "frage_korrektur", return_value=(0, {"wohnort": "Neustadt"})), \ patch.object(et, "schreibe_korrekturen", side_effect=PermissionError(13, "Zugriff verweigert")): tab._on_korrigieren() warnung.assert_called_once() anzeige.zeige.assert_not_called() self.assertIs(tab._angezeigt[0], _ANNA) self.assertNotIn("csv1", tab._korrektur_pfade) def test_ohne_anzeige_keine_korrektur(self, warnung, hinweis, erfolg): """Prüft, dass ohne angezeigte Daten kein Korrekturdialog erscheint.""" tab = _anz_tab() with patch.object(et, "frage_korrektur") as dialog: tab._on_korrigieren() dialog.assert_not_called() warnung.assert_called_once() class TestKorrekturDatei(unittest.TestCase): def test_vorschlag_neben_der_eigentuemerdatei(self): """Prüft, dass die Korrektur-CSV ohne Rückfrage neben der Eigentümerdatei liegt, wenn der Ordner beschreibbar ist, und gemerkt wird.""" with tempfile.TemporaryDirectory() as ordner: quelle = "file:///" + os.path.join(ordner, "export.csv").replace(os.sep, "/") + "?type=csv" layer = types.SimpleNamespace(source=lambda: quelle) tab = _anz_tab() with patch.object(et, "hole_layer", return_value=layer), \ patch.object(et.QFileDialog, "getSaveFileName") as dialog: pfad = tab._korrektur_datei() dialog.assert_not_called() self.assertEqual(os.path.normcase(os.path.normpath(pfad)), os.path.normcase(os.path.join(ordner, "export_korrekturen.csv"))) self.assertEqual(tab._korrektur_pfade["csv1"], pfad) def test_ohne_csv_quelle_wird_gefragt(self): """Prüft, dass ohne verwertbare CSV-Quelle nach dem Speicherort gefragt wird; Abbrechen liefert keinen Pfad.""" tab = _anz_tab() layer = types.SimpleNamespace(source=lambda: "memory?geometry=None") with patch.object(et, "hole_layer", return_value=layer), \ patch.object(et.QFileDialog, "getSaveFileName", return_value=("", "")) as dialog: self.assertIsNone(tab._korrektur_datei()) dialog.assert_called_once() self.assertEqual(tab._korrektur_pfade, {}) with patch.object(et, "hole_layer", return_value=layer), \ patch.object(et.QFileDialog, "getSaveFileName", return_value=("D:/k.csv", "")): self.assertEqual(tab._korrektur_datei(), "D:/k.csv") class TestDockWidgetTabs(unittest.TestCase): def test_tab_ist_im_dockwidget_eingetragen(self): """Prüft, dass der Tab „Eigentümer“ im DockWidget direkt nach dem Bearbeitungs-Tab steht.""" from sn_verfahrensgebiet.ui.dockwidget import DockWidget from sn_verfahrensgebiet.ui.tabs.working_tab import WorkingTab self.assertEqual(DockWidget.tabs[:2], [WorkingTab, EigentuemerTab]) self.assertEqual(EigentuemerTab.tab_title, "Eigentümer") if __name__ == "__main__": unittest.main()