"""Tests für sn_basis/modules/Datenabruf.py (Datenabruf-Kernpipeline, Issue #63).""" import sys import unittest from typing import Any, cast from unittest.mock import patch from sn_basis.modules.Datenabruf import Datenabruf from sn_basis.modules.pruef_ergebnis import pruef_ergebnis class _DummyPruefmanager: def verarbeite(self, ergebnis: Any): return ergebnis class _FakeCrs: def __init__(self, authid: str) -> None: self._authid = authid def authid(self) -> str: return self._authid class _FakeLayer: def __init__(self, authid: str = "EPSG:25833") -> None: self._crs = _FakeCrs(authid) def crs(self) -> _FakeCrs: return self._crs def _make_abruf() -> Datenabruf: return Datenabruf(pruefmanager=cast(Any, _DummyPruefmanager())) class DetermineSpatialFilterTests(unittest.TestCase): def test_raumfilter_ohne_liefert_none(self): """Prüft, dass Raumfilter 'ohne' unabhängig vom Layer immer None liefert.""" abruf = _make_abruf() self.assertIsNone(abruf._determine_spatial_filter("ohne", _FakeLayer())) def test_fehlender_layer_liefert_none(self): """Prüft, dass ohne Verfahrensgebiet-Layer kein Raumfilter gebaut wird.""" abruf = _make_abruf() self.assertIsNone(abruf._determine_spatial_filter("Verfahrensgebiet", None)) def test_unbekannter_raumfilter_liefert_none(self): """Prüft, dass ein unbekannter Raumfilter-Wert None liefert, statt einen Fehler zu werfen.""" abruf = _make_abruf() self.assertIsNone(abruf._determine_spatial_filter("Sonstwas", _FakeLayer())) @patch("sn_basis.modules.Datenabruf.qgiscore.get_layer_extent") def test_verfahrensgebiet_liefert_extent_und_crs(self, mock_extent): """Prüft, dass Raumfilter 'Verfahrensgebiet' Extent und CRS des Verfahrensgebiet-Layers liefert.""" mock_extent.return_value = "FAKE_EXTENT" abruf = _make_abruf() ergebnis = abruf._determine_spatial_filter("Verfahrensgebiet", _FakeLayer("EPSG:25833")) self.assertEqual(ergebnis, {"extent": "FAKE_EXTENT", "crs_authid": "EPSG:25833"}) @patch("sn_basis.modules.Datenabruf.qgiscore.get_layer_extent") def test_verfahrensgebiet_ohne_extent_liefert_none(self, mock_extent): """Prüft, dass ein Verfahrensgebiet-Layer ohne ermittelbaren Extent None liefert.""" mock_extent.return_value = None abruf = _make_abruf() self.assertIsNone(abruf._determine_spatial_filter("Verfahrensgebiet", _FakeLayer())) @patch("sn_basis.modules.Datenabruf.qgiscore.get_layer_extent") @patch("sn_basis.modules.Datenabruf.qgiscore.create_buffer_layer") def test_pufferlayer_liefert_extent_des_pufferlayers(self, mock_buffer, mock_extent): """Prüft, dass Raumfilter 'Pufferlayer' Extent und CRS des erzeugten Pufferlayers liefert.""" mock_buffer.return_value = _FakeLayer("EPSG:25833") mock_extent.return_value = "FAKE_PUFFER_EXTENT" abruf = _make_abruf() ergebnis = abruf._determine_spatial_filter("Pufferlayer", _FakeLayer("EPSG:4326")) mock_buffer.assert_called_once() self.assertEqual(ergebnis, {"extent": "FAKE_PUFFER_EXTENT", "crs_authid": "EPSG:25833"}) @patch("sn_basis.modules.Datenabruf.qgiscore.create_buffer_layer") def test_pufferlayer_ohne_puffer_liefert_none(self, mock_buffer): """Prüft, dass 'Pufferlayer' None liefert, wenn kein Pufferlayer erzeugt werden konnte.""" mock_buffer.return_value = None abruf = _make_abruf() self.assertIsNone(abruf._determine_spatial_filter("Pufferlayer", _FakeLayer())) class BuildProviderUrlTests(unittest.TestCase): def test_wms_liefert_link_unveraendert(self): """Prüft, dass bei Provider WMS der Link unverändert durchgereicht wird, auch mit Raumfilter.""" abruf = _make_abruf() bbox_geom = {"extent": (1.0, 2.0, 3.0, 4.0), "crs_authid": "EPSG:25833"} url = abruf._build_provider_url(link="http://server/wms?SERVICE=WMS", provider="WMS", bbox_geom=bbox_geom) self.assertEqual(url, "http://server/wms?SERVICE=WMS") def test_url_praefix_wird_entfernt(self): """Prüft, dass ein vorangestelltes 'url=' aus dem Link entfernt wird.""" abruf = _make_abruf() url = abruf._build_provider_url(link="url=http://server/dienst", provider="OGR", bbox_geom=None) self.assertTrue(url.startswith("http://server/dienst")) def test_wfs_baut_standardparameter_und_bbox_mit_crs(self): """Prüft, dass WFS-Abrufe Standardparameter, umbenanntes TYPENAMES und BBOX mit CRS-URN erhalten.""" abruf = _make_abruf() bbox_geom = {"extent": (1.0, 2.0, 3.0, 4.0), "crs_authid": "EPSG:25833"} url = abruf._build_provider_url( link="http://server/wfs?typename=Flurstueck", provider="WFS", bbox_geom=bbox_geom ) self.assertIn("service=WFS", url) self.assertIn("request=GetFeature", url) self.assertIn("VERSION=2.0.0", url) self.assertIn("resultType=hits", url) self.assertIn("TYPENAMES=Flurstueck", url) self.assertNotIn("typename=Flurstueck", url) self.assertIn("1.0%2C2.0%2C3.0%2C4.0", url) self.assertIn("urn:ogc:def:crs:EPSG::25833", url) def test_wfs_repariert_doppeltes_fragezeichen(self): """Prüft, dass ein zweites '?' im WFS-Link zu '&' korrigiert wird, statt eine ungültige URL zu erzeugen.""" abruf = _make_abruf() url = abruf._build_provider_url( link="http://server/wfs?service=WFS?typename=Flurstueck", provider="WFS", bbox_geom=None ) self.assertEqual(url.count("?"), 1) self.assertIn("typename=Flurstueck", url.replace("TYPENAMES", "typename")) def test_wfs_ohne_raumfilter_setzt_keine_bbox(self): """Prüft, dass WFS ohne Raumfilter (bbox_geom=None) keinen BBOX-Parameter setzt.""" abruf = _make_abruf() url = abruf._build_provider_url(link="http://server/wfs?typename=Flurstueck", provider="WFS", bbox_geom=None) self.assertNotIn("BBOX", url) def test_rest_provider_haengt_query_endpunkt_an(self): """Prüft, dass REST/ArcGIS-Links ohne '/query'-Endpunkt automatisch ergänzt werden.""" abruf = _make_abruf() url = abruf._build_provider_url(link="http://server/FeatureServer/0", provider="REST", bbox_geom=None) self.assertTrue(url.startswith("http://server/FeatureServer/0/query?")) self.assertIn("where=1%3D1", url) self.assertIn("outFields=%2A", url) self.assertIn("returnGeometry=true", url) self.assertIn("f=json", url) def test_rest_provider_mit_bbox_baut_geometry_envelope_und_srid(self): """Prüft, dass REST-Abrufe mit Raumfilter eine Geometry-Envelope und inSR/outSR aus dem CRS setzen.""" abruf = _make_abruf() bbox_geom = {"extent": (1.0, 2.0, 3.0, 4.0), "crs_authid": "EPSG:25833"} url = abruf._build_provider_url(link="http://server/FeatureServer/0/query", provider="REST", bbox_geom=bbox_geom) self.assertIn("geometryType=esriGeometryEnvelope", url) self.assertIn("spatialRel=esriSpatialRelIntersects", url) self.assertIn("inSR=25833", url) self.assertIn("outSR=25833", url) def test_default_provider_haengt_generischen_bbox_parameter_an(self): """Prüft, dass ein unbekannter Provider mit Raumfilter einen generischen bbox-Parameter erhält.""" abruf = _make_abruf() bbox_geom = {"extent": (1.0, 2.0, 3.0, 4.0), "crs_authid": None} url = abruf._build_provider_url(link="http://server/ogr-quelle", provider="OGR", bbox_geom=bbox_geom) self.assertIn("bbox=", url) def test_default_provider_ohne_raumfilter_laesst_query_unveraendert(self): """Prüft, dass ein unbekannter Provider ohne Raumfilter keinen bbox-Parameter ergänzt.""" abruf = _make_abruf() url = abruf._build_provider_url(link="http://server/ogr-quelle?feld=wert", provider="OGR", bbox_geom=None) self.assertNotIn("bbox=", url) self.assertIn("feld=wert", url) class _FakeResponse: def __init__(self, text: str) -> None: self.text = text def raise_for_status(self) -> None: return None class FetchFeaturesTests(unittest.TestCase): def setUp(self): self._alte_module = sys.modules.get("requests") self.addCleanup(self._requests_wiederherstellen) def _requests_wiederherstellen(self): if self._alte_module is None: sys.modules.pop("requests", None) else: sys.modules["requests"] = self._alte_module def _fake_requests(self, get): fake = type(sys)("requests") fake.get = get sys.modules["requests"] = fake def test_wms_liefert_keine_features_ohne_fehler(self): """Prüft, dass bei Provider WMS kein Featureabruf stattfindet und kein Fehler gemeldet wird.""" abruf = _make_abruf() features, fehler = abruf._fetch_features(url="http://server/wms", provider="WMS") self.assertEqual(features, []) self.assertIsNone(fehler) def test_abbruch_vor_abruf_liefert_fehlermeldung(self): """Prüft, dass ein bereits gesetzter Abbruch vor dem eigentlichen Abruf erkannt wird.""" abruf = _make_abruf() features, fehler = abruf._fetch_features(url="http://server/wfs", provider="WFS", cancel_callback=lambda: True) self.assertEqual(features, []) self.assertEqual(fehler, "Abbruch durch Benutzer") def test_json_featurecollection_wird_geparst(self): """Prüft, dass eine JSON-FeatureCollection-Antwort in eine Feature-Liste umgewandelt wird.""" self._fake_requests(lambda url, timeout: _FakeResponse( '{"type": "FeatureCollection", "features": [{"id": 1}, {"id": 2}]}' )) abruf = _make_abruf() features, fehler = abruf._fetch_features(url="http://server/rest/query", provider="REST") self.assertIsNone(fehler) self.assertEqual(features, [{"id": 1}, {"id": 2}]) def test_netzwerkfehler_wird_als_fehlermeldung_gemeldet(self): """Prüft, dass ein fehlgeschlagener HTTP-Abruf eine Fehlermeldung statt einer Ausnahme liefert.""" def _get(url, timeout): raise ConnectionError("Server nicht erreichbar") self._fake_requests(_get) abruf = _make_abruf() features, fehler = abruf._fetch_features(url="http://server/rest/query", provider="REST") self.assertEqual(features, []) self.assertIsNotNone(fehler) self.assertIn("Server nicht erreichbar", fehler) def test_wfs_xml_liefert_anzahl_aus_numberofmatched(self): """Prüft, dass eine WFS-XML-Antwort ohne JSON die Trefferzahl aus numberOfMatched übernimmt.""" self._fake_requests(lambda url, timeout: _FakeResponse( '' )) abruf = _make_abruf() features, fehler = abruf._fetch_features(url="http://server/wfs", provider="WFS") self.assertIsNone(fehler) self.assertEqual(len(features), 3) class DatenabrufOrchestrierungTests(unittest.TestCase): def test_fehlende_pflichtfelder_werden_als_ungueltig_gemeldet(self): """Prüft, dass eine Zeile ohne Link/Provider mit Aktion 'pflichtfelder_fehlen' gemeldet und übersprungen wird.""" abruf = _make_abruf() result_dict = {"rows": [{"ident": "1", "Inhalt": "Ohne Link"}]} _daten, ergebnisse = abruf.datenabruf( result_dict=result_dict, raumfilter="ohne", verfahrensgebiet_layer=None, speicherort="C:/tmp/ziel.gpkg" ) aktionen = [e.aktion for e in cast(list, ergebnisse) if isinstance(e, pruef_ergebnis)] self.assertIn("pflichtfelder_fehlen", aktionen) def test_wms_zeile_ohne_features_landet_in_ausserhalb(self): """Prüft, dass eine erfolgreich abgerufene WMS-Zeile mit 0 Features als 'ausserhalb' gezählt wird.""" abruf = _make_abruf() result_dict = {"rows": [{"ident": "1", "Link": "http://server/wms", "Provider": "WMS", "Inhalt": "Kartendienst"}]} daten_dict, ergebnisse = abruf.datenabruf( result_dict=result_dict, raumfilter="ohne", verfahrensgebiet_layer=None, speicherort="C:/tmp/ziel.gpkg" ) self.assertEqual(daten_dict["daten"]["1"], []) summary = next(e for e in cast(list, ergebnisse) if getattr(e, "aktion", None) == "datenabruf" and "Dienste" in e.meldung) self.assertEqual(summary.kontext["ausserhalb"], {"Kartendienst": 0}) def test_abbruch_ueber_progress_stoppt_weitere_zeilen(self): """Prüft, dass ein Abbruch über das Progress-Objekt die Verarbeitung weiterer Zeilen stoppt.""" abruf = _make_abruf() result_dict = { "rows": [ {"ident": "1", "Link": "http://server/wms", "Provider": "WMS"}, {"ident": "2", "Link": "http://server/wms", "Provider": "WMS"}, ] } class _AbbruchProgress: def set_label(self, _text): return None def is_canceled(self): return True daten_dict, ergebnisse = abruf.datenabruf( result_dict=result_dict, raumfilter="ohne", verfahrensgebiet_layer=None, speicherort="C:/tmp/ziel.gpkg", progress=_AbbruchProgress(), ) self.assertEqual(daten_dict["daten"], {}) aktionen = [getattr(e, "aktion", None) for e in cast(list, ergebnisse)] self.assertIn("abbruch", aktionen) if __name__ == "__main__": unittest.main()