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
Убирает ядро 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).
129 lines
4.7 KiB
Python
129 lines
4.7 KiB
Python
"""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
|