474 lines
18 KiB
Python
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()
|