"""sn_verfahrensgebiet/tests/test_flurstueck_alkis.py Unit-Tests für das flurstueck_alkis-Modul. Getestet werden: - Nutzerabbruch bei ask_text → (None, LoadStatus.NONE) - Ungültiger WFS-Layer → (None, LoadStatus.NONE) - Erfolgreicher Ladevorgang → gültiger Layer + LoadStatus.FIRST - Bestehender Layer, Drei-Knopf-Dialog: "Überschreiben" → LoadStatus.RELOAD, "Abbrechen" → (None, LoadStatus.NONE), "Anhängen" siehe TestFlurstueckAlkisAnhaengenDedup - vorgabe_schluessel/vorgabe_namen füllen den Eingabedialog vor - ohne_schluessel_abfrage=True überspringt die ask_text-Abfrage """ import sys import types import unittest from unittest.mock import patch, MagicMock # --------------------------------------------------------------------------- # Minimales QGIS-Mock-Setup # --------------------------------------------------------------------------- if "qgis" not in sys.modules: qgis_mod = types.ModuleType("qgis") qgis_core = types.ModuleType("qgis.core") qgis_utils = types.ModuleType("qgis.utils") qgis_utils.iface = types.SimpleNamespace() qgis_utils.plugins = {} qgis_mod.utils = qgis_utils qgis_mod.core = qgis_core sys.modules["qgis"] = qgis_mod sys.modules["qgis.core"] = qgis_core sys.modules["qgis.utils"] = qgis_utils from sn_verfahrensgebiet.functions.flurstueck_alkis import ( flurstueck_alkis, lade_flurstuecke_per_gemarkung, FLST_LAYER_NAME, FELD_FLURSTUECKSKENNZEICHEN, ) from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import LoadStatus # --------------------------------------------------------------------------- # Dummies # --------------------------------------------------------------------------- class _DummyLayer: def __init__(self, valid: bool, layer_id="L1"): self._valid = valid self._id = layer_id def isValid(self): return self._valid def id(self): return self._id def featureCount(self): return 1 class _DummyProject: def __init__(self, named_layers=None): self._named_layers = named_layers or [] self.removed = [] def mapLayersByName(self, name): return self._named_layers def removeMapLayer(self, lid): self.removed.append(lid) # --------------------------------------------------------------------------- # Tests # --------------------------------------------------------------------------- class TestFlurstueckAlkis(unittest.TestCase): # ------------------------------------------------------------------ # # 1. Nutzerabbruch (ask_text liefert ok=False / None) # # ------------------------------------------------------------------ # @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QMessageBox") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text") def test_user_cancel_returns_none(self, mock_ask_text, mock_project, mock_msgbox): """Prüft, dass ein Abbruch im Eingabedialog (ok=False) (None, LoadStatus.NONE) liefert.""" mock_ask_text.return_value = ("", False) mock_project.return_value = _DummyProject() layer, status = flurstueck_alkis(None) self.assertIsNone(layer) self.assertEqual(status, LoadStatus.NONE) @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QMessageBox") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text") def test_empty_text_after_confirmation_returns_none(self, mock_ask_text, mock_project, mock_msgbox): """Prüft, dass ein bestätigter, aber leerer Eingabetext ebenfalls (None, LoadStatus.NONE) liefert.""" mock_ask_text.return_value = (" ", True) mock_project.return_value = _DummyProject() layer, status = flurstueck_alkis(None) self.assertIsNone(layer) self.assertEqual(status, LoadStatus.NONE) # ------------------------------------------------------------------ # # 2. Ungültiger WFS-Layer # # ------------------------------------------------------------------ # @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QMessageBox") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text") def test_invalid_wfs_layer_returns_none( self, mock_ask_text, mock_project, mock_wfs_layer_cls, mock_msgbox ): """Prüft, dass ein ungültiger WFS-Layer (None, LoadStatus.NONE) liefert.""" mock_ask_text.return_value = ("1234", True) mock_project.return_value = _DummyProject() mock_wfs_layer_cls.return_value = _DummyLayer(valid=False) layer, status = flurstueck_alkis(None) self.assertIsNone(layer) self.assertEqual(status, LoadStatus.NONE) # ------------------------------------------------------------------ # # 3. Erfolgreicher Erstladevorgang # # ------------------------------------------------------------------ # @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text") def test_success_returns_layer_and_first_status( self, mock_ask_text, mock_project, mock_lade ): """Prüft, dass ohne bestehenden Layer der von lade_flurstuecke_per_gemarkung gelieferte Layer mit LoadStatus.FIRST zurückgegeben wird.""" mock_ask_text.return_value = ("1234, 5678", True) mock_project.return_value = _DummyProject() valid_layer = _DummyLayer(valid=True, layer_id="wfs_ok") mock_lade.return_value = valid_layer layer, status = flurstueck_alkis(None) self.assertIs(layer, valid_layer) self.assertEqual(status, LoadStatus.FIRST) mock_lade.assert_called_once_with(["1234", "5678"]) @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer") def test_sql_filter_contains_all_schluessel(self, mock_wfs_layer_cls): """Prüft, dass lade_flurstuecke_per_gemarkung für jeden Gemarkungsschlüssel eine eigene WFS-Anfrage mit dessen Schlüssel stellt.""" # Ungültig genügt: nur die aufgebaute URI je Aufruf wird geprüft, das # tatsächliche Zusammenführen der Features ist nicht Teil dieses Tests. mock_wfs_layer_cls.return_value = _DummyLayer(valid=False) lade_flurstuecke_per_gemarkung(["1111", "2222", "3333"]) uris = [call.args[0] for call in mock_wfs_layer_cls.call_args_list] self.assertEqual(len(uris), 3) self.assertIn("1111", uris[0]) self.assertIn("2222", uris[1]) self.assertIn("3333", uris[2]) # ------------------------------------------------------------------ # # 4. Bestehender Layer – Nutzer klickt "Überschreiben" # # ------------------------------------------------------------------ # @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QMessageBox") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text") def test_existing_layer_reload_yes( self, mock_ask_text, mock_project, mock_msgbox_cls, mock_lade ): """Prüft, dass bei bestehendem Layer und Klick auf 'Überschreiben' der alte Layer entfernt und LoadStatus.RELOAD geliefert wird.""" existing = _DummyLayer(valid=True, layer_id="old_flst") project = _DummyProject(named_layers=[existing]) mock_ask_text.return_value = ("1234", True) mock_project.return_value = project # Drei-Knopf-Dialog: addButton() liefert je Aufruf einen eigenen # Sentinel, clickedButton() simuliert den Klick auf "Überschreiben". btn_ueberschreiben, btn_anhaengen, btn_abbrechen = object(), object(), object() msg_instance = mock_msgbox_cls.return_value msg_instance.addButton.side_effect = [btn_ueberschreiben, btn_anhaengen, btn_abbrechen] msg_instance.clickedButton.return_value = btn_ueberschreiben new_layer = _DummyLayer(valid=True, layer_id="new_flst") mock_lade.return_value = new_layer layer, status = flurstueck_alkis(None) self.assertIn(existing.id(), project.removed) self.assertIs(layer, new_layer) self.assertEqual(status, LoadStatus.RELOAD) # ------------------------------------------------------------------ # # 5. Bestehender Layer – Nutzer klickt "Abbrechen" # # ------------------------------------------------------------------ # @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QMessageBox") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text") def test_existing_layer_abbrechen_returns_none( self, mock_ask_text, mock_project, mock_msgbox_cls ): """Prüft, dass bei bestehendem Layer und Klick auf 'Abbrechen' nichts verändert wird und (None, LoadStatus.NONE) geliefert wird.""" existing = _DummyLayer(valid=True, layer_id="keep_flst") project = _DummyProject(named_layers=[existing]) mock_ask_text.return_value = ("1234", True) mock_project.return_value = project btn_ueberschreiben, btn_anhaengen, btn_abbrechen = object(), object(), object() msg_instance = mock_msgbox_cls.return_value msg_instance.addButton.side_effect = [btn_ueberschreiben, btn_anhaengen, btn_abbrechen] msg_instance.clickedButton.return_value = btn_abbrechen layer, status = flurstueck_alkis(None) self.assertIsNone(layer) self.assertEqual(status, LoadStatus.NONE) self.assertEqual(project.removed, []) # nichts gelöscht mock_ask_text.assert_not_called() # Abbruch schon vor der Schlüsselabfrage # ------------------------------------------------------------------ # # 6. vorgabe_schluessel füllt ask_text-Default vor # # ------------------------------------------------------------------ # @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text") def test_vorgabe_schluessel_prefills_default_text( self, mock_ask_text, mock_project, mock_lade ): """Prüft, dass vorgabe_schluessel als vorbelegter default_text im Eingabedialog erscheint.""" mock_ask_text.return_value = ("1234, 5678", True) mock_project.return_value = _DummyProject() mock_lade.return_value = _DummyLayer(valid=True) flurstueck_alkis(None, vorgabe_schluessel=["1234", "5678"]) args, kwargs = mock_ask_text.call_args # ask_text("title", label, default_text=..., parent=...) # default_text wird als Keyword-Arg übergeben default_text_arg = kwargs.get("default_text", args[2] if len(args) > 2 else "") self.assertIn("1234", default_text_arg) self.assertIn("5678", default_text_arg) # ------------------------------------------------------------------ # # 7. vorgabe_namen erscheint im Label-Text # # ------------------------------------------------------------------ # @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text") def test_vorgabe_namen_appears_in_label( self, mock_ask_text, mock_project, mock_lade ): """Prüft, dass vorgabe_namen im Beschriftungstext des Eingabedialogs erscheint.""" mock_ask_text.return_value = ("9999", True) mock_project.return_value = _DummyProject() mock_lade.return_value = _DummyLayer(valid=True) flurstueck_alkis(None, vorgabe_namen=["Musterstadt"]) args, kwargs = mock_ask_text.call_args # label ist das 2. Positional-Argument label_arg = args[1] if len(args) > 1 else kwargs.get("label", "") self.assertIn("Musterstadt", label_arg) # ------------------------------------------------------------------ # # 8. Rückwärtskompatibilität: alle neuen Params optional # # ------------------------------------------------------------------ # @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text") def test_backwards_compatible_no_new_params( self, mock_ask_text, mock_project, mock_lade ): """Prüft, dass flurstueck_alkis() ohne die neuen optionalen Parameter weiterhin fehlerfrei einen Layer mit LoadStatus.FIRST liefert.""" mock_ask_text.return_value = ("0000", True) mock_project.return_value = _DummyProject() mock_lade.return_value = _DummyLayer(valid=True) layer, status = flurstueck_alkis(None) self.assertIsNotNone(layer) self.assertEqual(status, LoadStatus.FIRST) # ------------------------------------------------------------------ # # 9. ohne_schluessel_abfrage: vorgabe_schluessel direkt, kein ask_text (#47) # ------------------------------------------------------------------ # @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text") def test_ohne_schluessel_abfrage_ueberspringt_ask_text( self, mock_ask_text, mock_project, mock_lade ): """Prüft, dass ohne_schluessel_abfrage=True vorgabe_schluessel direkt verwendet und ask_text nicht aufgerufen wird.""" mock_project.return_value = _DummyProject() geladen = _DummyFlstLayer([_DummyFlstFeature("110001_0001__00001")]) mock_lade.return_value = geladen layer, status = flurstueck_alkis( None, vorgabe_schluessel=["1234", "5678"], ohne_schluessel_abfrage=True ) mock_ask_text.assert_not_called() self.assertIs(layer, geladen) self.assertEqual(status, LoadStatus.FIRST) mock_lade.assert_called_once_with(["1234", "5678"]) # --------------------------------------------------------------------------- # Dummies für den Anhängen-Deduplizierungstest (#28) # --------------------------------------------------------------------------- class _DummyFlstFeature: def __init__(self, kennzeichen): self._kennzeichen = kennzeichen def __getitem__(self, key): if key == FELD_FLURSTUECKSKENNZEICHEN: return self._kennzeichen return None class _DummyFlstFields: def __init__(self, hat_kennzeichen_feld: bool): self._hat_feld = hat_kennzeichen_feld def indexFromName(self, name): if name == FELD_FLURSTUECKSKENNZEICHEN and self._hat_feld: return 0 return -1 class _DummyFlstLayer: """Flurstück-Layer-Dummy mit Feature-Liste und aufzeichnendem ``dataProvider``.""" def __init__(self, features, hat_kennzeichen_feld: bool = True): self._features = list(features) self._hat_feld = hat_kennzeichen_feld self.added = None def getFeatures(self): return list(self._features) def fields(self): return _DummyFlstFields(self._hat_feld) def dataProvider(self): layer = self class _Provider: def addFeatures(self, feats): layer.added = list(feats) layer._features.extend(feats) return True return _Provider() def updateExtents(self): pass def triggerRepaint(self): pass def isValid(self): return True def featureCount(self): return len(self._features) class TestFlurstueckAlkisAnhaengenDedup(unittest.TestCase): """Anhängen an einen bestehenden Flurstück-Layer (Issue #28).""" @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis._check_existing_flurstueck_layer") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text") def test_append_ueberspringt_bereits_vorhandene_flurstuecke( self, mock_ask_text, mock_check, mock_lade ): """Prüft, dass beim Anhängen bereits vorhandene flurstueckskennzeichen nicht erneut hinzugefügt werden.""" mock_ask_text.return_value = ("0001, 0002", True) bestehend = _DummyFlstLayer([_DummyFlstFeature("110001_0001__00001")]) neu_geladen = _DummyFlstLayer([ _DummyFlstFeature("110001_0001__00001"), # bereits im Layer vorhanden _DummyFlstFeature("110001_0002__00007"), # noch nicht vorhanden ]) mock_check.return_value = (bestehend, LoadStatus.APPEND) mock_lade.return_value = neu_geladen layer, status = flurstueck_alkis(None) self.assertEqual(status, LoadStatus.APPEND) self.assertIs(layer, bestehend) self.assertEqual(len(bestehend.added), 1) self.assertEqual(bestehend.added[0][FELD_FLURSTUECKSKENNZEICHEN], "110001_0002__00007") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis._check_existing_flurstueck_layer") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text") def test_append_ohne_kennzeichen_feld_haengt_alles_an( self, mock_ask_text, mock_check, mock_lade ): """Prüft, dass ohne flurstueckskennzeichen-Feld weiterhin alle Features angehängt werden (Rückfallverhalten).""" mock_ask_text.return_value = ("0001", True) bestehend = _DummyFlstLayer([_DummyFlstFeature("110001_0001__00001")], hat_kennzeichen_feld=False) neu_geladen = _DummyFlstLayer( [_DummyFlstFeature("110001_0001__00001"), _DummyFlstFeature("110001_0002__00007")], hat_kennzeichen_feld=False, ) mock_check.return_value = (bestehend, LoadStatus.APPEND) mock_lade.return_value = neu_geladen layer, status = flurstueck_alkis(None) self.assertEqual(status, LoadStatus.APPEND) self.assertEqual(len(bestehend.added), 2) @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis._check_existing_flurstueck_layer") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text") def test_ohne_schluessel_abfrage_append_dedup(self, mock_ask_text, mock_check, mock_lade): """ohne_schluessel_abfrage=True: Anhängen dedupliziert trotzdem, ohne nach Schlüsseln zu fragen (#47).""" bestehend = _DummyFlstLayer([_DummyFlstFeature("110001_0001__00001")]) neu_geladen = _DummyFlstLayer([ _DummyFlstFeature("110001_0001__00001"), # bereits vorhanden _DummyFlstFeature("110001_0002__00007"), # neu ]) mock_check.return_value = (bestehend, LoadStatus.APPEND) mock_lade.return_value = neu_geladen layer, status = flurstueck_alkis( None, vorgabe_schluessel=["0001", "0002"], ohne_schluessel_abfrage=True ) mock_ask_text.assert_not_called() self.assertEqual(status, LoadStatus.APPEND) self.assertEqual(len(bestehend.added), 1) self.assertEqual(bestehend.added[0][FELD_FLURSTUECKSKENNZEICHEN], "110001_0002__00007") if __name__ == "__main__": unittest.main()