All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 11s
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 2m33s
Удалены мёртвые ops-скрипты Yandex Geocoder (уже недостижимы после #2593 частей 1-2): - scripts/_yandex_reverse.py, scripts/audit_address_mismatch.py, scripts/backfill_house_coords.py - их тесты + осиротевшая фикстура tests/fixtures/yandex_geocode_sample.json - осиротевшие SQL-хелперы scripts/audit_address_sample.sql, scripts/address_audit_report.sql (использовались только audit_address_mismatch.py) Обновлена документация (осиротевшие упоминания YANDEX_GEOCODER_API_KEY / удалённых скриптов): scripts/README.md, tradein-mvp/DEPLOY.md, docs/Secrets_Rotation_Policy.md. Добавлен tests/test_geocoder_nominatim_lookup.py — покрывает _nominatim_lookup (единственный живой внешний геокодер) на предмет реальной передачи city_hint в исходящий HTTP-запрос к Nominatim; закрывает дыру в coverage, оставленную удалёнными yandex-тестами. Refs #2593
91 lines
4.4 KiB
Python
91 lines
4.4 KiB
Python
"""Тесты `_nominatim_lookup` — city реально доходит до исходящего HTTP-запроса.
|
||
|
||
#2593 (часть 3): Yandex Geocoder полностью удалён из проекта, вместе с ним ушли
|
||
`_yandex_reverse.py` + `tests/test_audit_address_mismatch.py` +
|
||
`tests/test_backfill_house_coords.py` — они были единственной проверкой, что
|
||
`city`/`city_hint` реально передаётся во внешний геокодер, а не только влияет на
|
||
cache-ключ (см. `tests/test_geocoder_city_hint.py`, который мокает
|
||
`_nominatim_lookup`/`_nominatim_suggest` целиком и потому не видит их внутренности).
|
||
|
||
Nominatim теперь единственный живой внешний провайдер (`_nominatim_lookup`
|
||
docstring, `app/services/geocoder.py`) — этот файл закрывает получившуюся дыру:
|
||
мокает HTTP-транспорт (`httpx.MockTransport`, паттерн из `test_geocoder_bbox.py` /
|
||
`tests/services/test_dadata.py`) и проверяет параметр `q` реального исходящего
|
||
GET-запроса к `nominatim.openstreetmap.org/search`.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
|
||
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
|
||
|
||
from unittest.mock import patch
|
||
|
||
import httpx
|
||
|
||
from app.services.geocoder import _nominatim_lookup
|
||
|
||
# EKB-центр (Плотинка) — внутри tight EKB bbox, `_nominatim_query` его примет
|
||
# без похода во второй (typo-variant) тир.
|
||
_SAMPLE_ITEM = {
|
||
"lat": "56.838",
|
||
"lon": "60.605",
|
||
"class": "building",
|
||
"display_name": "ул. Малышева, 30, Екатеринбург",
|
||
"address": {"state": "Свердловская область"},
|
||
}
|
||
|
||
# Snapshot реального httpx.AsyncClient ДО patch'а — фабрика ниже использует именно
|
||
# его с подменённым transport (паттерн tests/services/test_dadata.py: избегает
|
||
# recursion, если бы `httpx.AsyncClient` патчился поверх самого себя).
|
||
_REAL_ASYNC_CLIENT = httpx.AsyncClient
|
||
|
||
|
||
def _async_client_factory(transport: httpx.MockTransport):
|
||
def factory(*_: object, **__: object) -> httpx.AsyncClient:
|
||
return _REAL_ASYNC_CLIENT(transport=transport)
|
||
|
||
return factory
|
||
|
||
|
||
def _capturing_transport(captured_q: list[str]) -> httpx.MockTransport:
|
||
def handler(request: httpx.Request) -> httpx.Response:
|
||
captured_q.append(request.url.params.get("q", ""))
|
||
return httpx.Response(200, json=[_SAMPLE_ITEM])
|
||
|
||
return httpx.MockTransport(handler)
|
||
|
||
|
||
async def test_nominatim_lookup_sends_city_hint_in_query_param() -> None:
|
||
"""city_hint="Нижний Тагил" должен попасть в q= реального GET-запроса.
|
||
|
||
Регрессия, о которой явно предупреждает docstring `_nominatim_lookup` (#2580 C):
|
||
city_hint обязан влиять на сам запрос к провайдеру, не только на cache-ключ.
|
||
"""
|
||
captured_q: list[str] = []
|
||
transport = _capturing_transport(captured_q)
|
||
|
||
with patch("app.services.geocoder.httpx.AsyncClient", _async_client_factory(transport)):
|
||
result = await _nominatim_lookup("Ленина, 1", city_hint="Нижний Тагил")
|
||
|
||
assert captured_q, "запрос к Nominatim не был отправлен"
|
||
assert captured_q[0] == "Нижний Тагил, Ленина, 1"
|
||
assert result is not None
|
||
assert result.provider == "nominatim"
|
||
|
||
|
||
async def test_nominatim_lookup_no_city_sends_bare_address() -> None:
|
||
"""Без city_hint и без маркера города в тексте — q= остаётся bare-адресом.
|
||
|
||
Guard против регрессии в молчаливый дефолт на конкретный город (#2576/#2593)
|
||
— до фикса #2576 сюда молча подставлялся "Екатеринбург".
|
||
"""
|
||
captured_q: list[str] = []
|
||
transport = _capturing_transport(captured_q)
|
||
|
||
with patch("app.services.geocoder.httpx.AsyncClient", _async_client_factory(transport)):
|
||
result = await _nominatim_lookup("Малышева, 30")
|
||
|
||
assert captured_q == ["Малышева, 30"]
|
||
assert result is not None
|