gendesign/backend/tests/services/test_dadata_client.py
bot-backend 55b13e2b13
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m54s
Deploy / build-worker (push) Successful in 2m56s
Deploy / deploy (push) Successful in 1m33s
fix(dadata): фолбэк на suggest/address при отключённой фиче CLEAN (#2250)
2026-07-03 03:42:40 +00:00

144 lines
5.5 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.

"""Тесты для dadata_client.clean_address (#2177 geo-pass).
httpx.Client замокан — реальная сеть/DaData не дёргается. settings-credentials
патчатся через monkeypatch (литерала ключа нигде нет).
"""
from __future__ import annotations
from typing import Any
from unittest.mock import MagicMock, patch
import httpx
import pytest
from app.services import dadata_client
from app.services.dadata_client import clean_address
@pytest.fixture(autouse=True)
def _creds(monkeypatch: pytest.MonkeyPatch) -> None:
"""Задаём фейковые credentials, чтобы clean_address не отваливался на guard."""
monkeypatch.setattr(dadata_client.settings, "dadata_api_token", "test-token")
monkeypatch.setattr(dadata_client.settings, "dadata_api_secret", "test-secret")
def _mock_client(response: MagicMock) -> MagicMock:
"""httpx.Client() context-manager, post() → response."""
client = MagicMock()
client.__enter__ = MagicMock(return_value=client)
client.__exit__ = MagicMock(return_value=False)
client.post.return_value = response
return client
def _resp(status: int, json_body: Any) -> MagicMock:
resp = MagicMock()
resp.status_code = status
resp.json.return_value = json_body
resp.text = ""
return resp
def test_clean_address_returns_coords() -> None:
"""Успешный ответ с geo_lat/geo_lon → (lat, lon) float-пара."""
resp = _resp(200, [{"geo_lat": "56.8389", "geo_lon": "60.6057", "qc_geo": 0}])
with patch("httpx.Client", return_value=_mock_client(resp)):
result = clean_address("г Екатеринбург, ул Малышева, 125")
assert result == (56.8389, 60.6057)
def test_clean_address_no_coords_returns_none() -> None:
"""Ответ без geo_lat/geo_lon (адрес не геокодирован) → None."""
resp = _resp(200, [{"qc_geo": 5, "result": None}])
with patch("httpx.Client", return_value=_mock_client(resp)):
assert clean_address("нераспознаваемо") is None
def test_clean_address_empty_array_returns_none() -> None:
"""Пустой массив → None."""
resp = _resp(200, [])
with patch("httpx.Client", return_value=_mock_client(resp)):
assert clean_address("г Екатеринбург, ул X, 1") is None
def test_clean_address_short_input_returns_none() -> None:
"""Короткий/пустой адрес → None без сетевого вызова."""
assert clean_address("") is None
assert clean_address("ab") is None
def test_clean_address_no_credentials_returns_none(monkeypatch: pytest.MonkeyPatch) -> None:
"""Нет credentials → graceful None (geo-pass reject'ит, не падает)."""
monkeypatch.setattr(dadata_client.settings, "dadata_api_token", None)
monkeypatch.setattr(dadata_client.settings, "dadata_api_secret", None)
assert clean_address("г Екатеринбург, ул Малышева, 125") is None
def test_clean_address_429_returns_none() -> None:
"""HTTP 429 quota → None."""
resp = _resp(429, {})
with patch("httpx.Client", return_value=_mock_client(resp)):
assert clean_address("г Екатеринбург, ул X, 1") is None
def test_clean_address_timeout_returns_none() -> None:
"""Сетевой timeout → None, не пробрасываем исключение."""
client = MagicMock()
client.__enter__ = MagicMock(return_value=client)
client.__exit__ = MagicMock(return_value=False)
client.post.side_effect = httpx.TimeoutException("timed out")
with patch("httpx.Client", return_value=client):
assert clean_address("г Екатеринбург, ул X, 1") is None
def test_clean_address_403_falls_back_to_suggest(monkeypatch) -> None:
"""Фича CLEAN отключена (403) → фолбэк на suggest/address отдаёт координаты.
Прод-инцидент 2026-07-03: «Feature 'CLEAN' disabled for token» при живом
бесплатном suggestions API.
"""
from app.services import dadata_client as dc
monkeypatch.setattr(dc.settings, "dadata_api_token", "test-token")
monkeypatch.setattr(dc.settings, "dadata_api_secret", "test-secret")
class _Resp:
def __init__(self, status_code, payload):
self.status_code = status_code
self._payload = payload
def json(self):
return self._payload
class _Client:
def __init__(self, *a, **k):
pass
def __enter__(self):
return self
def __exit__(self, *a):
return False
def post(self, url, **kwargs):
if "cleaner" in url:
return _Resp(403, {"message": "Feature 'CLEAN' disabled"})
assert "suggestions" in url
assert "X-Secret" not in kwargs.get("headers", {})
return _Resp(
200,
{
"suggestions": [
{
"value": "г Екатеринбург, ул Мира, д 19",
"data": {"geo_lat": "56.8447", "geo_lon": "60.6547"},
}
]
},
)
monkeypatch.setattr(dc.httpx, "Client", _Client)
coords = dc.clean_address("г Екатеринбург, ул Мира, 19")
assert coords is not None
assert abs(coords[0] - 56.8447) < 1e-6 and abs(coords[1] - 60.6547) < 1e-6