"""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", "korrektur_label", "eigentuemer_layer_combo"): setattr(tab, name, MagicMock()) tab.eigentuemer_layer_combo.currentData.return_value = layer_id tab._anzeige = anzeige tab._anzeige_geometrie = None 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, "hole_layer", return_value=None) @patch.object(et, "zeige_anzeigefenster") @patch.object(et, "EigentuemerAnzeige") class TestSitzung(unittest.TestCase): def test_sitzung_beginnen_oeffnet_leeres_fenster_mit_rahmen(self, anzeige_klasse, zeige, _layer): """Prüft, dass „Sitzung beginnen“ ein leeres Anzeigefenster als normales Fenster öffnet (ohne gemerkte Lage beim ersten Mal).""" tab = _anz_tab() tab._on_sitzung_umschalten() anzeige_klasse.assert_called_once_with(tab) zeige.assert_called_once_with(anzeige_klasse.return_value, None) 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_lage_gilt_fuer_naechste_sitzung(self, anzeige_klasse, zeige, _layer): """Prüft, dass Größe und Lage des Fensters beim Beenden gemerkt und in der nächsten Sitzung wiederverwendet werden.""" tab = _anz_tab() tab._sitzung_beginnen() anzeige_klasse.return_value.saveGeometry.return_value = b"zweiter-bildschirm" tab._sitzung_beenden() tab._sitzung_beginnen() self.assertEqual(zeige.call_args_list[-1][0][1], b"zweiter-bildschirm") def test_daten_anzeigen_beginnt_sitzung_bei_bedarf(self, anzeige_klasse, zeige, _layer): """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, zeige, _layer): """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, zeige, _layer): """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, zeige, _layer): """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, zeige, _layer): """Prüft, dass ein über den Fensterrahmen geschlossenes Anzeigefenster die Sitzung beendet und seine Lage gemerkt wird.""" tab = _anz_tab() tab._sitzung_beginnen() anzeige_klasse.return_value.saveGeometry.return_value = b"lage" anzeige_klasse.return_value.beim_schliessen() self.assertIsNone(tab._anzeige) self.assertEqual(tab._anzeige_geometrie, b"lage") class TestKorrekturLabel(unittest.TestCase): def _text(self, tab): return tab.korrektur_label.setText.call_args[0][0] def test_ohne_eigentuemerdatei(self): """Prüft, dass ohne Eigentümerdatei unter den Knöpfen steht, dass keine gewählt ist.""" tab = _anz_tab(layer_id=None) tab._aktualisiere_korrektur_label() self.assertIn("keine Eigentümerdatei", self._text(tab)) def test_pfad_neben_der_eigentuemerdatei(self): """Prüft, dass der Pfad der Korrektur-CSV angezeigt wird, mit Hinweis, solange sie noch nicht angelegt ist.""" with tempfile.TemporaryDirectory() as ordner: quelle = "file:///" + os.path.join(ordner, "export.csv").replace(os.sep, "/") + "?type=csv" erwartet = os.path.normpath(os.path.join(ordner, "export_korrekturen.csv")) tab = _anz_tab() with patch.object(et, "hole_layer", return_value=types.SimpleNamespace(source=lambda: quelle)): tab._aktualisiere_korrektur_label() self.assertEqual(os.path.normcase(self._text(tab)), os.path.normcase(f"Korrektur-Datei: {erwartet} (wird beim ersten Speichern angelegt)")) open(erwartet, "w").close() tab._aktualisiere_korrektur_label() self.assertEqual(os.path.normcase(self._text(tab)), os.path.normcase(f"Korrektur-Datei: {erwartet}")) def test_ohne_csv_quelle_wird_abgefragt(self): """Prüft, dass bei einer Quelle ohne CSV angekündigt wird, dass der Speicherort beim ersten Speichern abgefragt wird.""" tab = _anz_tab() with patch.object(et, "hole_layer", return_value=types.SimpleNamespace(source=lambda: "memory?geometry=None")): tab._aktualisiere_korrektur_label() self.assertIn("abgefragt", self._text(tab)) def test_gewaehlter_pfad_wird_angezeigt(self): """Prüft, dass ein einmal gewählter Speicherort angezeigt wird.""" tab = _anz_tab() tab._korrektur_pfade["csv1"] = "D:/Termine/korrekturen.csv" with patch.object(et, "hole_layer", return_value=None): tab._aktualisiere_korrektur_label() self.assertIn(os.path.normpath("D:/Termine/korrekturen.csv"), self._text(tab)) @patch.object(et, "hole_layer", return_value=None) @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, _layer): """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, _layer): """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, _layer): """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, _layer): """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, _layer): """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()