""" Tests für sn_plan41/modules/vln_api_logic.py Alle externen Abhängigkeiten (vln_karten, QSettings, QgsProject, Netzwerk) werden mit unittest.mock gemockt, sodass keine QGIS-Laufzeitumgebung nötig ist. """ import unittest from typing import Any, Optional from unittest.mock import MagicMock, patch, call # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _make_logic(api_key: Optional[str] = None, mail: Optional[str] = None): """Erzeugt eine VlnApiLogic-Instanz mit gemockten QSettings.""" from sn_plan41.modules.vln_api_logic import VlnApiLogic with patch("sn_plan41.modules.vln_api_logic.QSettings") as mock_qs_cls: mock_qs = MagicMock() mock_qs.value.side_effect = lambda key, default="": ( api_key if key == "api_key" else (mail if key == "mail" else default) ) mock_qs_cls.return_value = mock_qs if api_key and VlnApiLogic.is_available(): with patch( "sn_plan41.modules.vln_api_logic.KartenApiClient" ) as mock_client_cls: mock_client = MagicMock() mock_client.is_authenticated = True mock_client.api_key = api_key mock_client.mail = mail mock_client_cls.return_value = mock_client logic = VlnApiLogic() else: logic = VlnApiLogic() return logic # --------------------------------------------------------------------------- # Tests: Verfügbarkeit # --------------------------------------------------------------------------- class TestVlnApiLogicAvailability(unittest.TestCase): def test_is_available_reflects_import_state(self): from sn_plan41.modules.vln_api_logic import VlnApiLogic result = VlnApiLogic.is_available() # Ergebnis hängt davon ab, ob vln_karten installiert ist — # wir prüfen nur, dass ein bool zurückkommt self.assertIsInstance(result, bool) def test_is_available_false_when_vln_karten_missing(self): """Wenn VLN_KARTEN_AVAILABLE=False, liefert is_available() False.""" import sn_plan41.modules.vln_api_logic as module original = module.VLN_KARTEN_AVAILABLE try: module.VLN_KARTEN_AVAILABLE = False from sn_plan41.modules.vln_api_logic import VlnApiLogic with patch.object(module, "VLN_KARTEN_AVAILABLE", False): self.assertFalse(VlnApiLogic.is_available()) finally: module.VLN_KARTEN_AVAILABLE = original # --------------------------------------------------------------------------- # Tests: Credentials-Persistenz # --------------------------------------------------------------------------- class TestVlnApiLogicCredentials(unittest.TestCase): def _make(self): from sn_plan41.modules.vln_api_logic import VlnApiLogic with patch("sn_plan41.modules.vln_api_logic.QSettings"): with patch("sn_plan41.modules.vln_api_logic.KartenApiClient"): return VlnApiLogic() def test_save_api_key_calls_qsettings(self): logic = self._make() with patch("sn_plan41.modules.vln_api_logic.QSettings") as mock_qs_cls: mock_qs = MagicMock() mock_qs_cls.return_value = mock_qs logic.save_api_key("token123", "user@test.de") mock_qs.setValue.assert_any_call("api_key", "token123") mock_qs.setValue.assert_any_call("mail", "user@test.de") mock_qs.beginGroup.assert_called_with("vln_karten") def test_clear_api_key_calls_remove(self): logic = self._make() with patch("sn_plan41.modules.vln_api_logic.QSettings") as mock_qs_cls: mock_qs = MagicMock() mock_qs_cls.return_value = mock_qs logic.clear_api_key() mock_qs.remove.assert_called_with("api_key") def test_load_stored_credentials_returns_api_key_and_mail(self): logic = self._make() with patch("sn_plan41.modules.vln_api_logic.QSettings") as mock_qs_cls: mock_qs = MagicMock() mock_qs.value.side_effect = lambda key, default="": { "api_key": "mykey", "mail": "user@test.de", }.get(key, default) mock_qs_cls.return_value = mock_qs api_key, mail = logic.load_stored_credentials() self.assertEqual(api_key, "mykey") self.assertEqual(mail, "user@test.de") def test_load_stored_credentials_returns_none_for_empty(self): logic = self._make() with patch("sn_plan41.modules.vln_api_logic.QSettings") as mock_qs_cls: mock_qs = MagicMock() mock_qs.value.return_value = "" mock_qs_cls.return_value = mock_qs api_key, mail = logic.load_stored_credentials() self.assertIsNone(api_key) self.assertIsNone(mail) # --------------------------------------------------------------------------- # Tests: Login / Logout # --------------------------------------------------------------------------- class TestVlnApiLogicLogin(unittest.TestCase): def _make(self): from sn_plan41.modules.vln_api_logic import VlnApiLogic with patch("sn_plan41.modules.vln_api_logic.QSettings"): with patch("sn_plan41.modules.vln_api_logic.KartenApiClient"): return VlnApiLogic() def test_login_sets_client_and_saves_key(self): import sn_plan41.modules.vln_api_logic as module if not module.VLN_KARTEN_AVAILABLE: self.skipTest("vln_karten nicht verfügbar") logic = self._make() mock_client = MagicMock() mock_client.api_key = "newtoken" with patch("sn_plan41.modules.vln_api_logic.KartenApiClient", return_value=mock_client): with patch.object(logic, "save_api_key") as mock_save: logic.login("user@test.de", "secret") mock_client.login.assert_called_once_with("user@test.de", "secret") mock_save.assert_called_once_with("newtoken", "user@test.de") def test_login_raises_runtime_error_without_qgis_modules(self): import sn_plan41.modules.vln_api_logic as module logic = self._make() with patch.object(module, "VLN_KARTEN_AVAILABLE", False): with self.assertRaises(RuntimeError): logic.login("user@test.de", "secret") def test_login_propagates_api_error(self): import sn_plan41.modules.vln_api_logic as module if not module.VLN_KARTEN_AVAILABLE: self.skipTest("vln_karten nicht verfügbar") logic = self._make() mock_client = MagicMock() mock_client.login.side_effect = module.ApiError("Ungültige Zugangsdaten") with patch("sn_plan41.modules.vln_api_logic.KartenApiClient", return_value=mock_client): with self.assertRaises(Exception): logic.login("user@test.de", "wrong") def test_logout_clears_client(self): logic = self._make() logic._client = MagicMock() logic.logout() self.assertIsNone(logic._client) def test_is_authenticated_false_after_logout(self): logic = self._make() logic._client = None self.assertFalse(logic.is_authenticated) def test_handle_session_expired_clears_key_and_client(self): logic = self._make() with patch.object(logic, "clear_api_key") as mock_clear: with patch.object(logic, "logout") as mock_logout: logic.handle_session_expired() mock_clear.assert_called_once() mock_logout.assert_called_once() # --------------------------------------------------------------------------- # Tests: Verfahrensliste # --------------------------------------------------------------------------- class TestVlnApiLogicVerfahren(unittest.TestCase): def _make_authenticated(self): from sn_plan41.modules.vln_api_logic import VlnApiLogic with patch("sn_plan41.modules.vln_api_logic.QSettings"): with patch("sn_plan41.modules.vln_api_logic.KartenApiClient"): logic = VlnApiLogic() mock_client = MagicMock() mock_client.is_authenticated = True logic._client = mock_client return logic, mock_client def test_get_verfahren_raises_when_not_authenticated(self): from sn_plan41.modules.vln_api_logic import VlnApiLogic with patch("sn_plan41.modules.vln_api_logic.QSettings"): with patch("sn_plan41.modules.vln_api_logic.KartenApiClient"): logic = VlnApiLogic() logic._client = None with self.assertRaises(Exception): logic.get_verfahren() def test_get_verfahren_delegates_to_client(self): logic, mock_client = self._make_authenticated() mock_client.get_verfahren.return_value = [ {"vkz": "27010", "name": "Alpha"}, {"vkz": "27001", "name": "Beta"}, ] result = logic.get_verfahren() mock_client.get_verfahren.assert_called_once() self.assertEqual(len(result), 2) # --------------------------------------------------------------------------- # Tests: VKZ-Persistenz # --------------------------------------------------------------------------- class TestVlnApiLogicVkz(unittest.TestCase): def _make(self): from sn_plan41.modules.vln_api_logic import VlnApiLogic with patch("sn_plan41.modules.vln_api_logic.QSettings"): with patch("sn_plan41.modules.vln_api_logic.KartenApiClient"): return VlnApiLogic() def test_load_stored_vkz_reads_from_project(self): logic = self._make() mock_project = MagicMock() mock_project.readEntry.return_value = ("27010", True) with patch("sn_plan41.modules.vln_api_logic.QgsProject") as mock_qgsproj_cls: mock_qgsproj_cls.instance.return_value = mock_project vkz = logic.load_stored_vkz() self.assertEqual(vkz, "27010") mock_project.readEntry.assert_called_once_with("vln_karten", "/vkz", "") def test_load_stored_vkz_returns_none_for_empty(self): logic = self._make() mock_project = MagicMock() mock_project.readEntry.return_value = ("", True) with patch("sn_plan41.modules.vln_api_logic.QgsProject") as mock_qgsproj_cls: mock_qgsproj_cls.instance.return_value = mock_project vkz = logic.load_stored_vkz() self.assertIsNone(vkz) def test_save_vkz_writes_to_project(self): logic = self._make() mock_project = MagicMock() with patch("sn_plan41.modules.vln_api_logic.QgsProject") as mock_qgsproj_cls: mock_qgsproj_cls.instance.return_value = mock_project logic.save_vkz("27010") mock_project.writeEntry.assert_called_once_with("vln_karten", "/vkz", "27010") def test_save_vkz_does_nothing_for_none(self): logic = self._make() mock_project = MagicMock() with patch("sn_plan41.modules.vln_api_logic.QgsProject") as mock_qgsproj_cls: mock_qgsproj_cls.instance.return_value = mock_project logic.save_vkz(None) mock_project.writeEntry.assert_not_called() # --------------------------------------------------------------------------- # Tests: Plan 41 laden (Stil-Anwendung) # --------------------------------------------------------------------------- class TestVlnApiLogicLoadP41(unittest.TestCase): def _make(self): from sn_plan41.modules.vln_api_logic import VlnApiLogic with patch("sn_plan41.modules.vln_api_logic.QSettings"): with patch("sn_plan41.modules.vln_api_logic.KartenApiClient"): logic = VlnApiLogic() mock_client = MagicMock() mock_client.is_authenticated = True mock_client.load_layer_complete.return_value = { "type": "FeatureCollection", "features": [], } logic._client = mock_client return logic def test_load_p41_raises_without_vln_karten(self): import sn_plan41.modules.vln_api_logic as module logic = self._make() with patch.object(module, "VLN_KARTEN_AVAILABLE", False): with self.assertRaises(RuntimeError): logic.load_p41("27010") def test_load_p41_applies_styles_to_polygon_and_line_layers(self): import sn_plan41.modules.vln_api_logic as module if not module.VLN_KARTEN_AVAILABLE: self.skipTest("vln_karten nicht verfügbar") logic = self._make() # Zwei Mock-Layer: einer Polygon (2), einer Linie (1) mock_layer_polygon = MagicMock() mock_layer_polygon.geometryType.return_value = module._GEOM_POLYGON mock_layer_line = MagicMock() mock_layer_line.geometryType.return_value = module._GEOM_LINE with patch( "sn_plan41.modules.vln_api_logic.feature_collection_to_layers", return_value=[mock_layer_polygon, mock_layer_line], ): with patch( "sn_plan41.modules.vln_api_logic.apply_style_from_path" ) as mock_style: with patch("sn_plan41.modules.vln_api_logic.join_path", side_effect=lambda *p: "/".join(p)): with patch("sn_plan41.modules.vln_api_logic.get_plugin_root", return_value="/plugins"): logic.load_p41("27010") # Prüfen, dass der richtige Stil für jeden Geometrietyp verwendet wurde style_calls = [str(c) for c in mock_style.call_args_list] polygon_style_used = any( module.STYLE_FLAECHE in c for c in style_calls ) line_style_used = any( module.STYLE_LINIE in c for c in style_calls ) self.assertTrue(polygon_style_used, "Flächen-Stil wurde nicht angewendet") self.assertTrue(line_style_used, "Linien-Stil wurde nicht angewendet") def test_load_p41_no_style_for_point_layer(self): import sn_plan41.modules.vln_api_logic as module if not module.VLN_KARTEN_AVAILABLE: self.skipTest("vln_karten nicht verfügbar") logic = self._make() mock_layer_point = MagicMock() mock_layer_point.geometryType.return_value = 0 # Point with patch( "sn_plan41.modules.vln_api_logic.feature_collection_to_layers", return_value=[mock_layer_point], ): with patch( "sn_plan41.modules.vln_api_logic.apply_style_from_path" ) as mock_style: with patch("sn_plan41.modules.vln_api_logic.join_path", side_effect=lambda *p: "/".join(p)): with patch("sn_plan41.modules.vln_api_logic.get_plugin_root", return_value="/plugins"): logic.load_p41("27010") mock_style.assert_not_called() # --------------------------------------------------------------------------- # Tests: Upload # --------------------------------------------------------------------------- class TestVlnApiLogicUpload(unittest.TestCase): def _make(self): from sn_plan41.modules.vln_api_logic import VlnApiLogic with patch("sn_plan41.modules.vln_api_logic.QSettings"): with patch("sn_plan41.modules.vln_api_logic.KartenApiClient"): logic = VlnApiLogic() mock_client = MagicMock() mock_client.is_authenticated = True logic._client = mock_client return logic def test_upload_returns_error_for_layer_without_api_path(self): import sn_plan41.modules.vln_api_logic as module if not module.VLN_KARTEN_AVAILABLE: self.skipTest("vln_karten nicht verfügbar") logic = self._make() mock_layer = MagicMock() with patch( "sn_plan41.modules.vln_api_logic.layer_api_path", return_value=None ): success, message = logic.upload_active_layer(mock_layer) self.assertFalse(success) self.assertIn("nicht", message.lower()) def test_upload_returns_error_for_none_layer(self): import sn_plan41.modules.vln_api_logic as module if not module.VLN_KARTEN_AVAILABLE: self.skipTest("vln_karten nicht verfügbar") logic = self._make() with patch( "sn_plan41.modules.vln_api_logic.layer_api_path", return_value=None ): success, message = logic.upload_active_layer(None) self.assertFalse(success) def test_upload_returns_false_without_qgis_modules(self): import sn_plan41.modules.vln_api_logic as module logic = self._make() with patch.object(module, "VLN_KARTEN_AVAILABLE", False): success, message = logic.upload_active_layer(MagicMock()) self.assertFalse(success) def test_upload_calls_save_feature_collection(self): import sn_plan41.modules.vln_api_logic as module if not module.VLN_KARTEN_AVAILABLE: self.skipTest("vln_karten nicht verfügbar") logic = self._make() mock_layer = MagicMock() mock_layer.isEditable.return_value = False mock_layer.featureCount.return_value = 5 fc = {"type": "FeatureCollection", "features": []} with patch("sn_plan41.modules.vln_api_logic.layer_api_path", return_value="/maps/p41/27010"): with patch("sn_plan41.modules.vln_api_logic.plugin_layers", return_value=[mock_layer]): with patch("sn_plan41.modules.vln_api_logic.layers_to_feature_collection", return_value=fc): success, message = logic.upload_active_layer(mock_layer) logic._client.save_feature_collection.assert_called_once_with("/maps/p41/27010", fc) self.assertTrue(success) if __name__ == "__main__": unittest.main()