"""Tests for /api/v1/geocode/reverse — snapped coords + precision in response. Bug context (issue #582 Phase 5): До этого PR'а endpoint echo'ил input lat/lon в ответе. После reverse-геокодинга адрес resolved'ился до canonical здания (ул. Малышева, 125), но marker на фронте оставался где user кликнул — иногда в проезде / дворе. Fix: backend теперь возвращает snapped_lat/snapped_lon (центр matched здания от Nominatim/cadastral) + precision, фронт двигает marker если precision in (exact, number, cadastral). """ 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") # WeasyPrint stub — not installed in CI without GTK. _wp_mock = MagicMock() sys.modules.setdefault("weasyprint", _wp_mock) import pytest # noqa: E402 from fastapi import FastAPI # noqa: E402 from fastapi.testclient import TestClient # noqa: E402 from app.api.v1 import geocode as geocode_module # noqa: E402 from app.core.db import get_db # noqa: E402 from app.services.geocoder import ( # noqa: E402 ReverseGeocodeResult, snap_precision_useful, ) @pytest.fixture def app() -> FastAPI: application = FastAPI() application.include_router(geocode_module.router, prefix="/api/v1/geocode") application.dependency_overrides[get_db] = lambda: MagicMock() return application # ── Endpoint response shape ────────────────────────────────────────────────── def test_reverse_endpoint_returns_snapped_fields(app: FastAPI) -> None: """Endpoint should include address + lat/lon (echo) + snapped_lat/snapped_lon + precision.""" client = TestClient(app) fake = ReverseGeocodeResult( address="улица Малышева, 51, Екатеринбург", snapped_lat=56.838004, snapped_lon=60.586155, precision="exact", provider="cadastral", ) with patch( "app.api.v1.geocode.reverse_geocode", new_callable=AsyncMock, return_value=fake, ): r = client.get("/api/v1/geocode/reverse?lat=56.8381&lon=60.5860") assert r.status_code == 200 body = r.json() # Echo input assert body["lat"] == 56.8381 assert body["lon"] == 60.5860 # Snap from provider assert body["address"] == "улица Малышева, 51, Екатеринбург" assert body["snapped_lat"] == 56.838004 assert body["snapped_lon"] == 60.586155 assert body["precision"] == "exact" assert body["provider"] == "cadastral" def test_reverse_endpoint_404_when_no_address(app: FastAPI) -> None: """Provider returned None → endpoint должен дать 404, не 500.""" client = TestClient(app) with patch( "app.api.v1.geocode.reverse_geocode", new_callable=AsyncMock, return_value=None, ): r = client.get("/api/v1/geocode/reverse?lat=56.0&lon=60.0") assert r.status_code == 404 def test_reverse_endpoint_street_precision_does_not_lose_snap_fields(app: FastAPI) -> None: """Для precision=street snapped_lat/lon всё равно присутствуют (могут == input).""" client = TestClient(app) fake = ReverseGeocodeResult( address="улица Малышева, Екатеринбург", snapped_lat=56.84, snapped_lon=60.61, precision="street", provider="nominatim", ) with patch( "app.api.v1.geocode.reverse_geocode", new_callable=AsyncMock, return_value=fake, ): r = client.get("/api/v1/geocode/reverse?lat=56.84&lon=60.61") assert r.status_code == 200 body = r.json() assert "snapped_lat" in body assert "snapped_lon" in body assert body["precision"] == "street" # ── snap_precision_useful helper ───────────────────────────────────────────── def test_snap_precision_useful_exact_and_number() -> None: assert snap_precision_useful("exact") is True assert snap_precision_useful("number") is True def test_snap_precision_useful_rejects_street_and_other() -> None: assert snap_precision_useful("street") is False assert snap_precision_useful("range") is False assert snap_precision_useful("near") is False assert snap_precision_useful("locality") is False assert snap_precision_useful("other") is False assert snap_precision_useful("") is False