gendesign/backend/tests/test_ors_client.py
Light1YT 50555821fb
Some checks failed
CI / changes (push) Successful in 7s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 7s
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 7m42s
CI / backend-tests (pull_request) Successful in 7m40s
Deploy / changes (push) Has been cancelled
Deploy / build-backend (push) Has been cancelled
Deploy / build-worker (push) Has been cancelled
Deploy / build-frontend (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
feat(site_finder): routing POI decay (#41) + OSM utility distances (#44) + ЗОУИТ geom (#255bk)
#41: per-category routing-radius decay через reusable ORS /matrix client (foot-walk
время + per-category пороги + decay-curves), opt-in ?decay=routing, graceful straight-line
fallback (analyze не 500 при ORS down) + length-guard durations==destinations.
#44: +5 OSM-категорий (transformer/gas/water/sewerage/heat) via Overpass nwr;
nearest_{water_main,substation,gas,heat}_m в analyze.utilities.
#255bk: ST_AsGeoJSON(CAST(geom AS geometry)) в _get_cad_zouit_overlaps → geom_geojson.

Closes #41
Closes #44
2026-06-13 22:31:56 +05:00

123 lines
4.8 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)])