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
152 lines
6.1 KiB
Python
152 lines
6.1 KiB
Python
"""Тесты 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)])
|
||
|
||
|
||
def test_non_numeric_duration_degrades_instead_of_500(with_key, monkeypatch):
|
||
"""#2464: нечисловая длительность → OrsUnavailableError, а не ValueError.
|
||
|
||
Довод уже сформулирован в самом файле — у проверки длины ниже написано, что
|
||
ValueError «не OrsUnavailableError → 500». К самой конверсии принцип применён
|
||
не был: вызывающий (poi_score.py:348) ловит только доменную ошибку.
|
||
"""
|
||
|
||
def handler(_request):
|
||
return httpx.Response(200, json={"durations": [[0.0, "не число"]]})
|
||
|
||
_install_transport(monkeypatch, handler)
|
||
|
||
with pytest.raises(ors_client.OrsUnavailableError):
|
||
ors_client.matrix_durations_min(60.6, 56.8, [(60.61, 56.81), (60.62, 56.82)])
|
||
|
||
|
||
def test_list_instead_of_duration_also_degrades(with_key, monkeypatch):
|
||
"""TypeError тоже обязан стать доменной ошибкой, не только ValueError."""
|
||
|
||
def handler(_request):
|
||
return httpx.Response(200, json={"durations": [[0.0, [300]]]})
|
||
|
||
_install_transport(monkeypatch, handler)
|
||
|
||
with pytest.raises(ors_client.OrsUnavailableError):
|
||
ors_client.matrix_durations_min(60.6, 56.8, [(60.61, 56.81), (60.62, 56.82)])
|