gendesign/tradein-mvp/backend/tests/test_geocode_reverse_api.py
bot-backend 3d075632a9
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 2m28s
chore(tradein/geocoder): удалить Яндекс-геокодер (#2593)
Убирает ядро Yandex Geocoder (forward/reverse/suggest lookups + region-check
+ bias-хелперы + EKB_BBOX dict) из app/services/geocoder.py — Yandex demo-key
исчерпан, Nominatim/DaData/локальные ЕКБ-тиры (geoportal/cadastral) остаются
единственными живыми провайдерами. Цепочка тиров после удаления: кэш →
геопортал ЕКБ → кадастр (house-match) → кадастр (raw) → Nominatim; в
подсказках дополнительно DaData.

НЕ затронуто (намеренно): Yandex.Недвижимость как источник объявлений
(source='yandex', yandex_city_sweep*, providers/yandex/serp.py:geocoderAddress),
Avito geocoder (providers/avito/imv.py:_geocode), EKB_BBOX_TIGHT/WIDE,
_nominatim_region_ok, scripts/*_yandex_reverse.py и их тесты, tests/fixtures/
yandex_geocode_sample.json (всё ещё используется test_audit_address_mismatch.py).

_SNAP_PRECISIONS оставлен с "exact" (недостижимо без Yandex-tier, но дёшево
хранить — parity с frontend MapPicker.tsx SNAP_PRECISIONS и не ломает
test_snap_precision_useful_exact_and_number).
2026-07-31 21:22:09 +03:00

129 lines
4.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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