Files
Plugin_SN_Plan41/tests/test_vln_api_logic.py
T
2026-06-19 11:47:15 +02:00

474 lines
18 KiB
Python

"""
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()