gendesign/tradein-mvp/backend/tests/services/test_cadastral_reverse.py
lekss361 0efd2dca1d
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
feat(tradein): cadastral reverse geocode via main backend cad_buildings (#492)
2026-05-24 07:31:12 +00:00

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