"""Unit tests for cadastral reverse geocode and modified reverse_geocode.""" from __future__ import annotations import os import sys from unittest.mock import AsyncMock, MagicMock, patch # DATABASE_URL required by config before any app import. os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test") os.environ.setdefault("MAIN_BACKEND_URL", "http://gendesign-backend:8000") # WeasyPrint stub — not installed in CI without GTK. _wp_mock = MagicMock() sys.modules.setdefault("weasyprint", _wp_mock) import pytest from app.services.geocoder import _cadastral_reverse, reverse_geocode # ── _cadastral_reverse ──────────────────────────────────────────────────────── async def test_cadastral_reverse_returns_address_on_200() -> None: """Main backend returns 200 → address string returned.""" mock_response = MagicMock() mock_response.status_code = 200 mock_response.raise_for_status = MagicMock() mock_response.json.return_value = { "address": "ул. Малышева, 30, Екатеринбург", "lat": 56.838, "lon": 60.605, "distance_m": 12.5, } mock_client = AsyncMock() mock_client.get = AsyncMock(return_value=mock_response) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=False) with patch("app.services.geocoder.httpx.AsyncClient", return_value=mock_client): result = await _cadastral_reverse(56.838, 60.605) assert result == "ул. Малышева, 30, Екатеринбург" async def test_cadastral_reverse_returns_none_on_404() -> None: """Main backend returns 404 (no building in radius) → None, no exception.""" mock_response = MagicMock() mock_response.status_code = 404 mock_client = AsyncMock() mock_client.get = AsyncMock(return_value=mock_response) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=False) with patch("app.services.geocoder.httpx.AsyncClient", return_value=mock_client): result = await _cadastral_reverse(56.838, 60.605) assert result is None async def test_cadastral_reverse_returns_none_on_timeout() -> None: """Network timeout → None returned, no exception bubbles out.""" import httpx mock_client = AsyncMock() mock_client.get = AsyncMock(side_effect=httpx.TimeoutException("timeout")) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=False) with patch("app.services.geocoder.httpx.AsyncClient", return_value=mock_client): result = await _cadastral_reverse(56.838, 60.605) assert result is None async def test_cadastral_reverse_returns_none_when_not_configured() -> None: """main_backend_url is None → immediately returns None without HTTP call.""" with patch("app.services.geocoder.settings") as mock_settings: mock_settings.main_backend_url = None with patch("app.services.geocoder.httpx.AsyncClient") as mock_client_cls: result = await _cadastral_reverse(56.838, 60.605) mock_client_cls.assert_not_called() assert result is None # ── reverse_geocode ─────────────────────────────────────────────────────────── async def test_reverse_geocode_returns_cadastral_when_available() -> None: """Cadastral returns address → Nominatim is never called.""" with ( patch( "app.services.geocoder._cadastral_reverse", new_callable=AsyncMock, return_value="ул. Малышева, 30, Екатеринбург", ) as mock_cadastral, patch( "app.services.geocoder._nominatim_reverse", new_callable=AsyncMock, ) as mock_nominatim, ): result = await reverse_geocode(56.838, 60.605) assert result == "ул. Малышева, 30, Екатеринбург" mock_cadastral.assert_called_once_with(56.838, 60.605) mock_nominatim.assert_not_called() async def test_reverse_geocode_falls_back_to_nominatim_when_cadastral_none() -> None: """Cadastral returns None → Nominatim is called and its result returned.""" with ( patch( "app.services.geocoder._cadastral_reverse", new_callable=AsyncMock, return_value=None, ), patch( "app.services.geocoder._nominatim_reverse", new_callable=AsyncMock, return_value="Трамвайный переулок, 2, Екатеринбург", ) as mock_nominatim, ): result = await reverse_geocode(56.838, 60.605) assert result == "Трамвайный переулок, 2, Екатеринбург" mock_nominatim.assert_called_once_with(56.838, 60.605) async def test_reverse_geocode_returns_none_when_both_fail() -> None: """Both cadastral and Nominatim fail → returns None, no exception raised.""" with ( patch( "app.services.geocoder._cadastral_reverse", new_callable=AsyncMock, return_value=None, ), patch( "app.services.geocoder._nominatim_reverse", new_callable=AsyncMock, side_effect=RuntimeError("Nominatim 403 Forbidden — IP banned"), ), ): result = await reverse_geocode(56.838, 60.605) # Must return None, not raise assert result is None