gendesign/backend/tests/test_osrm_client_local.py
bot-backend 56868f2bde
All checks were successful
Deploy / changes (push) Successful in 9s
Deploy / build-frontend (push) Has been skipped
Deploy / deploy-caddy (push) Has been skipped
Deploy / build-backend (push) Successful in 2m16s
Deploy / build-worker (push) Successful in 6m44s
Deploy / deploy (push) Successful in 1m31s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Successful in 9s
fix(ptica): нечисловое значение от OSRM/ORS больше не даёт 500 вместо деградации (#2464) (#2944)
2026-08-19 17:20:28 +00:00

209 lines
8.8 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.

"""Тесты локального OSRM-клиента (#39 A2) — /table annotations=distance.
Сеть НЕ дёргается: подменяем httpx.Client на MockTransport. Покрывает:
- корректный URL (sources=0, annotations=distance, координаты в path);
- парсинг distances в МЕТРАХ + дроп self-index (row[0]);
- None для недостижимого POI;
- OsrmLocalUnavailableError на 500 / timeout / code!=Ok / битой форме;
- пустой destinations → [].
"""
from __future__ import annotations
import httpx
import pytest
from app.services.site_finder import osrm_client_local as osrm
def _install_transport(monkeypatch: pytest.MonkeyPatch, handler) -> None:
"""Подменить httpx.Client в модуле osrm_client_local на MockTransport(handler).
Захватываем реальный httpx.Client ДО патча — иначе factory рекурсивно зовёт сама себя.
"""
real_client = httpx.Client
def client_factory(*_args, **kwargs):
kwargs.pop("transport", None)
return real_client(transport=httpx.MockTransport(handler), **kwargs)
monkeypatch.setattr(osrm.httpx, "Client", client_factory)
def test_builds_correct_url_and_parses_meters(monkeypatch):
"""URL: /table/v1/driving/{coords}?sources=0&annotations=distance; distances в метрах."""
seen: dict = {}
def handler(request: httpx.Request) -> httpx.Response:
seen["path"] = request.url.path
seen["params"] = dict(request.url.params)
# distances[0] = origin→[origin, d1, d2] — метры; self = 0.
return httpx.Response(200, json={"code": "Ok", "distances": [[0.0, 1234.5, 6789.0]]})
_install_transport(monkeypatch, handler)
out = osrm.get_road_distances_m(60.6, 56.8, [(60.61, 56.81), (60.62, 56.82)])
# Дорожные расстояния (метры), self-index сброшен.
assert out == [1234.5, 6789.0]
# URL: профиль driving, координаты origin первой (lon,lat) и POI.
assert seen["path"] == "/table/v1/driving/60.6,56.8;60.61,56.81;60.62,56.82"
# Параметры sources=0, annotations=distance.
assert seen["params"]["sources"] == "0"
assert seen["params"]["annotations"] == "distance"
def test_base_url_override_hits_foot_url(monkeypatch):
"""base_url override (#39 A3) → URL строится из переданного base + profile=foot."""
seen: dict = {}
def handler(request: httpx.Request) -> httpx.Response:
seen["host"] = request.url.host
seen["path"] = request.url.path
return httpx.Response(200, json={"code": "Ok", "distances": [[0.0, 321.0]]})
_install_transport(monkeypatch, handler)
out = osrm.get_road_distances_m(
60.6,
56.8,
[(60.61, 56.81)],
profile="foot",
base_url="http://osrm-walk:5000/", # trailing slash → должен быть срезан
)
assert out == [321.0]
# Хост — foot-сервис, не дефолтный osrm; trailing slash снят (нет //table).
assert seen["host"] == "osrm-walk"
assert seen["path"] == "/table/v1/foot/60.6,56.8;60.61,56.81"
def test_base_url_none_uses_default_driving(monkeypatch):
"""base_url=None (дефолт) → settings.osrm_local_url + profile=driving (старое поведение)."""
seen: dict = {}
def handler(request: httpx.Request) -> httpx.Response:
seen["path"] = request.url.path
return httpx.Response(200, json={"code": "Ok", "distances": [[0.0, 100.0]]})
_install_transport(monkeypatch, handler)
out = osrm.get_road_distances_m(60.6, 56.8, [(60.61, 56.81)])
assert out == [100.0]
# Дефолтный профиль driving, дефолтный base (settings.osrm_local_url).
assert seen["path"] == "/table/v1/driving/60.6,56.8;60.61,56.81"
def test_unreachable_poi_returns_none(monkeypatch):
"""OSRM вернул null (нет маршрута) → None в результате (caller оставит straight-line)."""
def handler(_request):
return httpx.Response(200, json={"code": "Ok", "distances": [[0.0, 500.0, None]]})
_install_transport(monkeypatch, handler)
out = osrm.get_road_distances_m(60.6, 56.8, [(60.61, 56.81), (60.62, 56.82)])
assert out == [500.0, None]
def test_empty_destinations_returns_empty():
"""Пустой destinations → [] без HTTP-вызова."""
assert osrm.get_road_distances_m(60.6, 56.8, []) == []
def test_http_500_raises_unavailable(monkeypatch):
"""HTTP 500 → OsrmLocalUnavailableError (не пробрасываем HTTPStatusError)."""
def handler(_request):
return httpx.Response(500, text="boom")
_install_transport(monkeypatch, handler)
with pytest.raises(osrm.OsrmLocalUnavailableError, match="HTTP 500"):
osrm.get_road_distances_m(60.6, 56.8, [(60.61, 56.81)])
def test_timeout_raises_unavailable(monkeypatch):
"""Timeout/сетевой сбой → OsrmLocalUnavailableError."""
def handler(_request):
raise httpx.ConnectTimeout("timed out")
_install_transport(monkeypatch, handler)
with pytest.raises(osrm.OsrmLocalUnavailableError, match="request failed"):
osrm.get_road_distances_m(60.6, 56.8, [(60.61, 56.81)])
def test_code_not_ok_raises_unavailable(monkeypatch):
"""code != Ok (например NoRoute) → OsrmLocalUnavailableError."""
def handler(_request):
return httpx.Response(200, json={"code": "NoRoute", "distances": None})
_install_transport(monkeypatch, handler)
with pytest.raises(osrm.OsrmLocalUnavailableError, match="code != Ok"):
osrm.get_road_distances_m(60.6, 56.8, [(60.61, 56.81)])
def test_bad_shape_raises_unavailable(monkeypatch):
"""Ответ без distances → OsrmLocalUnavailableError."""
def handler(_request):
return httpx.Response(200, json={"code": "Ok", "unexpected": True})
_install_transport(monkeypatch, handler)
with pytest.raises(osrm.OsrmLocalUnavailableError):
osrm.get_road_distances_m(60.6, 56.8, [(60.61, 56.81)])
def test_length_mismatch_raises_unavailable(monkeypatch):
"""Число расстояний != числу destinations → OsrmLocalUnavailableError (рассинхрон)."""
def handler(_request):
# 2 destinations, но distances содержит только self + 1.
return httpx.Response(200, json={"code": "Ok", "distances": [[0.0, 500.0]]})
_install_transport(monkeypatch, handler)
with pytest.raises(osrm.OsrmLocalUnavailableError, match="!= destinations"):
osrm.get_road_distances_m(60.6, 56.8, [(60.61, 56.81), (60.62, 56.82)])
def test_non_numeric_distance_degrades_instead_of_500(monkeypatch):
"""#2464: нечисловое значение в distances → доменная ошибка, а не ValueError.
Весь файл переводит любую кривизну ответа в OsrmLocalUnavailableError, потому что
вызывающий (parcels.py:399) ловит ТОЛЬКО её и уходит на прямолинейный fallback.
Голый `float(d)` был исключением из этого правила: ValueError/TypeError пролетели
бы мимо обработчика и дали 500 на /analyze вместо деградации.
"""
def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(200, json={"code": "Ok", "distances": [[0.0, "не число", 10.0]]})
_install_transport(monkeypatch, handler)
with pytest.raises(osrm.OsrmLocalUnavailableError):
osrm.get_road_distances_m(60.6, 56.8, [(60.61, 56.81), (60.62, 56.82)])
def test_dict_instead_of_distance_also_degrades(monkeypatch):
"""TypeError (не только ValueError) тоже обязан стать доменной ошибкой."""
def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(200, json={"code": "Ok", "distances": [[0.0, {"m": 5}, 10.0]]})
_install_transport(monkeypatch, handler)
with pytest.raises(osrm.OsrmLocalUnavailableError):
osrm.get_road_distances_m(60.6, 56.8, [(60.61, 56.81), (60.62, 56.82)])
def test_null_distance_is_still_a_legitimate_none(monkeypatch):
"""Контроль: null — это «маршрут не построен», а не поломка ответа.
Зелёный с обеих сторон: правка не должна превращать законный None в ошибку.
"""
def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(200, json={"code": "Ok", "distances": [[0.0, None, 10.0]]})
_install_transport(monkeypatch, handler)
out = osrm.get_road_distances_m(60.6, 56.8, [(60.61, 56.81), (60.62, 56.82)])
assert out == [None, 10.0]