All checks were successful
Deploy Trade-In / changes (push) Successful in 5s
Deploy / changes (push) Successful in 5s
Deploy Trade-In / build-frontend (push) Successful in 25s
Deploy Trade-In / build-backend (push) Successful in 47s
Deploy / build-frontend (push) Successful in 29s
Deploy / build-backend (push) Successful in 1m25s
Deploy Trade-In / deploy (push) Successful in 37s
Deploy / build-worker (push) Successful in 2m29s
Deploy / deploy (push) Successful in 1m2s
150 lines
5.6 KiB
Python
150 lines
5.6 KiB
Python
"""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
|