"""Тесты локального 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)])