gendesign/backend/tests/test_ors_client.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

152 lines
6.1 KiB
Python
Raw Permalink 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.

"""Тесты 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)])