"""Тесты ORS-клиента (#41) — /matrix durations. Сеть НЕ дёргается (ORS free tier 2000/день): подменяем httpx.Client на MockTransport. Покрывает: парсинг durations, усечение до лимита, 429 → OrsUnavailableError, нет ключа → OrsUnavailableError, None-маршруты. """ from __future__ import annotations import httpx import pytest from app.core.config import settings from app.services.site_finder import ors_client @pytest.fixture def with_key(monkeypatch: pytest.MonkeyPatch): """Задать фиктивный ORS-ключ на время теста.""" monkeypatch.setattr(settings, "openrouteservice_api_key", "test-key-123") yield def _install_transport(monkeypatch: pytest.MonkeyPatch, handler) -> dict: """Подменить httpx.Client в модуле ors_client на MockTransport(handler). Захватываем реальный httpx.Client ДО патча — иначе factory рекурсивно зовёт сама себя (ors_client.httpx — общий модуль httpx). """ captured: dict = {} real_client = httpx.Client def client_factory(*_args, **kwargs): kwargs.pop("transport", None) return real_client(transport=httpx.MockTransport(handler), **kwargs) monkeypatch.setattr(ors_client.httpx, "Client", client_factory) return captured def test_matrix_durations_parses_minutes(with_key, monkeypatch): """durations (секунды) → минуты, длина = числу destinations.""" def handler(request: httpx.Request) -> httpx.Response: assert request.url.path.endswith("/foot-walking") return httpx.Response(200, json={"durations": [[0.0, 300.0, 600.0]]}) _install_transport(monkeypatch, handler) out = ors_client.matrix_durations_min( 60.6, 56.8, [(60.61, 56.81), (60.62, 56.82), (60.63, 56.83)] ) assert out == [0.0, 5.0, 10.0] def test_matrix_none_route_preserved(with_key, monkeypatch): """ORS вернул null (нет маршрута) → None в результате.""" def handler(_request): return httpx.Response(200, json={"durations": [[120.0, None]]}) _install_transport(monkeypatch, handler) out = ors_client.matrix_durations_min(60.6, 56.8, [(60.61, 56.81), (60.62, 56.82)]) assert out == [2.0, None] def test_matrix_no_key_raises(): """Нет API-ключа → OrsUnavailableError (caller сделает straight-line fallback).""" # settings по умолчанию пустой ключ (testing). Явно убедимся. if settings.openrouteservice_api_key: pytest.skip("ключ задан в окружении") with pytest.raises(ors_client.OrsUnavailableError): ors_client.matrix_durations_min(60.6, 56.8, [(60.61, 56.81)]) def test_matrix_429_raises_unavailable(with_key, monkeypatch): """429 (rate-limit) → OrsUnavailableError, не пробрасываем HTTPStatusError.""" def handler(_request): return httpx.Response(429, json={"error": "rate limit"}) _install_transport(monkeypatch, handler) with pytest.raises(ors_client.OrsUnavailableError, match="daily limit"): ors_client.matrix_durations_min(60.6, 56.8, [(60.61, 56.81)]) def test_matrix_empty_destinations_returns_empty(with_key): assert ors_client.matrix_durations_min(60.6, 56.8, []) == [] def test_matrix_invalid_profile_raises(with_key): with pytest.raises(ors_client.OrsUnavailableError, match="profile"): ors_client.matrix_durations_min(60.6, 56.8, [(60.61, 56.81)], profile="teleport") def test_matrix_truncates_to_limit(with_key, monkeypatch): """>MAX_MATRIX_DESTINATIONS усекается; body.locations = 1 source + лимит.""" seen: dict = {} def handler(request: httpx.Request) -> httpx.Response: import json body = json.loads(request.content) seen["n_locations"] = len(body["locations"]) n_dest = len(body["destinations"]) return httpx.Response(200, json={"durations": [[60.0] * n_dest]}) _install_transport(monkeypatch, handler) dests = [(60.0 + i * 0.001, 56.0) for i in range(ors_client.MAX_MATRIX_DESTINATIONS + 50)] out = ors_client.matrix_durations_min(60.6, 56.8, dests) # 1 source + MAX destinations assert seen["n_locations"] == ors_client.MAX_MATRIX_DESTINATIONS + 1 assert len(out) == ors_client.MAX_MATRIX_DESTINATIONS def test_matrix_bad_response_raises(with_key, monkeypatch): """Битый ответ без durations → OrsUnavailableError.""" def handler(_request): return httpx.Response(200, json={"unexpected": True}) _install_transport(monkeypatch, handler) with pytest.raises(ors_client.OrsUnavailableError): ors_client.matrix_durations_min(60.6, 56.8, [(60.61, 56.81)])