fix: fixes tenera interface
This commit is contained in:
parent
0ea7add4c3
commit
2e7aace21f
|
|
@ -89,18 +89,41 @@ class SuperTeneraInterface:
|
||||||
content_type: str | None = "application/json",
|
content_type: str | None = "application/json",
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""Выполняет GET запрос."""
|
"""
|
||||||
|
Выполняет GET запрос.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
SuperTeneraConnectionError: При ошибке запроса
|
||||||
|
"""
|
||||||
kwargs["ssl"] = self._ssl_context
|
kwargs["ssl"] = self._ssl_context
|
||||||
|
|
||||||
async with self._session.get(url, **kwargs) as response:
|
try:
|
||||||
if APP_CONFIG.app.debug:
|
async with self._session.get(url, **kwargs) as response:
|
||||||
self.logger.debug(
|
if APP_CONFIG.app.debug:
|
||||||
f"Response: {(await response.text(errors='ignore'))[:100]}"
|
self.logger.debug(
|
||||||
)
|
f"Response: {(await response.text(errors='ignore'))[:100]}"
|
||||||
return await response.json(encoding=encoding, content_type=content_type)
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return await response.json(encoding=encoding, content_type=content_type)
|
||||||
|
except aiohttp.ClientResponseError as e:
|
||||||
|
raise SuperTeneraConnectionError(
|
||||||
|
f"HTTP error {e.status}: {e.message} at {url}"
|
||||||
|
) from e
|
||||||
|
except (aiohttp.ClientError, TimeoutError) as e:
|
||||||
|
raise SuperTeneraConnectionError(
|
||||||
|
f"Connection error to SuperTenera API: {e}"
|
||||||
|
) from e
|
||||||
|
|
||||||
async def get_quotes_data(self) -> MainData:
|
async def get_quotes_data(self) -> MainData:
|
||||||
"""Получить данные котировок от SuperTenera."""
|
"""
|
||||||
|
Получить данные котировок от SuperTenera.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
MainData с котировками из всех источников
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
SuperTeneraConnectionError: При ошибке подключения или HTTP ошибке
|
||||||
|
"""
|
||||||
data = await self._get_request(APP_CONFIG.supertenera.quotes_endpoint)
|
data = await self._get_request(APP_CONFIG.supertenera.quotes_endpoint)
|
||||||
return MainData.model_validate(data)
|
return MainData.model_validate(data)
|
||||||
|
|
||||||
|
|
@ -108,7 +131,11 @@ class SuperTeneraInterface:
|
||||||
"""
|
"""
|
||||||
Быстрая проверка доступности SuperTenera API.
|
Быстрая проверка доступности SuperTenera API.
|
||||||
|
|
||||||
True - если ответ < 400, иначе SuperTeneraConnectionError.
|
Returns:
|
||||||
|
True - если сервис доступен
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
SuperTeneraConnectionError: При любой ошибке подключения или HTTP ошибке
|
||||||
"""
|
"""
|
||||||
kwargs["ssl"] = self._ssl_context
|
kwargs["ssl"] = self._ssl_context
|
||||||
try:
|
try:
|
||||||
|
|
@ -121,11 +148,15 @@ class SuperTeneraInterface:
|
||||||
return True
|
return True
|
||||||
except aiohttp.ClientResponseError as e:
|
except aiohttp.ClientResponseError as e:
|
||||||
raise SuperTeneraConnectionError(
|
raise SuperTeneraConnectionError(
|
||||||
f"Ошибка подключения к SuperTenera API при проверке системы - {e.status}."
|
f"HTTP error {e.status} при проверке доступности SuperTenera API"
|
||||||
) from e
|
) from e
|
||||||
except TimeoutError as e:
|
except TimeoutError as e:
|
||||||
raise SuperTeneraConnectionError(
|
raise SuperTeneraConnectionError(
|
||||||
"Ошибка Timeout подключения к SuperTenera API при проверке системы."
|
f"Timeout ({APP_CONFIG.supertenera.timeout}s) при проверке доступности SuperTenera API"
|
||||||
|
) from e
|
||||||
|
except aiohttp.ClientError as e:
|
||||||
|
raise SuperTeneraConnectionError(
|
||||||
|
f"Connection error при проверке доступности SuperTenera API: {e}"
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,346 @@
|
||||||
|
"""Unit тесты для обработки ошибок в интерфейсах."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import httpx
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from dataloader.interfaces.gmap2_brief.interface import (
|
||||||
|
Gmap2BriefConnectionError,
|
||||||
|
Gmap2BriefInterface,
|
||||||
|
)
|
||||||
|
from dataloader.interfaces.tenera.interface import (
|
||||||
|
SuperTeneraConnectionError,
|
||||||
|
SuperTeneraInterface,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
class TestGmap2BriefErrorHandling:
|
||||||
|
"""Тесты обработки ошибок в Gmap2BriefInterface."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_start_export_handles_http_status_error(self):
|
||||||
|
"""Тест обработки HTTP статус ошибки в start_export."""
|
||||||
|
mock_logger = MagicMock()
|
||||||
|
|
||||||
|
with patch("dataloader.interfaces.gmap2_brief.interface.APP_CONFIG") as mock_config:
|
||||||
|
mock_config.gmap2brief.base_url = "http://test.com"
|
||||||
|
mock_config.gmap2brief.start_endpoint = "/start"
|
||||||
|
mock_config.gmap2brief.timeout = 30
|
||||||
|
mock_config.app.local = False
|
||||||
|
|
||||||
|
interface = Gmap2BriefInterface(mock_logger)
|
||||||
|
|
||||||
|
with patch("dataloader.interfaces.gmap2_brief.interface.httpx.AsyncClient") as mock_client_cls:
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 500
|
||||||
|
mock_response.text = "Internal Server Error"
|
||||||
|
mock_response.raise_for_status.side_effect = httpx.HTTPStatusError(
|
||||||
|
"Server error", request=MagicMock(), response=mock_response
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_client = AsyncMock()
|
||||||
|
mock_client.post.return_value = mock_response
|
||||||
|
mock_client.__aenter__.return_value = mock_client
|
||||||
|
mock_client.__aexit__.return_value = None
|
||||||
|
mock_client_cls.return_value = mock_client
|
||||||
|
|
||||||
|
with pytest.raises(Gmap2BriefConnectionError) as exc_info:
|
||||||
|
await interface.start_export()
|
||||||
|
|
||||||
|
assert "Failed to start export" in str(exc_info.value)
|
||||||
|
assert "500" in str(exc_info.value)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_start_export_handles_request_error(self):
|
||||||
|
"""Тест обработки сетевой ошибки в start_export."""
|
||||||
|
mock_logger = MagicMock()
|
||||||
|
|
||||||
|
with patch("dataloader.interfaces.gmap2_brief.interface.APP_CONFIG") as mock_config:
|
||||||
|
mock_config.gmap2brief.base_url = "http://test.com"
|
||||||
|
mock_config.gmap2brief.start_endpoint = "/start"
|
||||||
|
mock_config.gmap2brief.timeout = 30
|
||||||
|
mock_config.app.local = False
|
||||||
|
|
||||||
|
interface = Gmap2BriefInterface(mock_logger)
|
||||||
|
|
||||||
|
with patch("dataloader.interfaces.gmap2_brief.interface.httpx.AsyncClient") as mock_client_cls:
|
||||||
|
mock_client = AsyncMock()
|
||||||
|
mock_client.post.side_effect = httpx.ConnectError("Connection refused")
|
||||||
|
mock_client.__aenter__.return_value = mock_client
|
||||||
|
mock_client.__aexit__.return_value = None
|
||||||
|
mock_client_cls.return_value = mock_client
|
||||||
|
|
||||||
|
with pytest.raises(Gmap2BriefConnectionError) as exc_info:
|
||||||
|
await interface.start_export()
|
||||||
|
|
||||||
|
assert "Request error" in str(exc_info.value)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_status_handles_http_error(self):
|
||||||
|
"""Тест обработки HTTP ошибки в get_status."""
|
||||||
|
mock_logger = MagicMock()
|
||||||
|
|
||||||
|
with patch("dataloader.interfaces.gmap2_brief.interface.APP_CONFIG") as mock_config:
|
||||||
|
mock_config.gmap2brief.base_url = "http://test.com"
|
||||||
|
mock_config.gmap2brief.status_endpoint = "/status/{job_id}"
|
||||||
|
mock_config.gmap2brief.timeout = 30
|
||||||
|
mock_config.app.local = False
|
||||||
|
|
||||||
|
interface = Gmap2BriefInterface(mock_logger)
|
||||||
|
|
||||||
|
with patch("dataloader.interfaces.gmap2_brief.interface.httpx.AsyncClient") as mock_client_cls:
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 404
|
||||||
|
mock_response.text = "Not Found"
|
||||||
|
mock_response.raise_for_status.side_effect = httpx.HTTPStatusError(
|
||||||
|
"Not found", request=MagicMock(), response=mock_response
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_client = AsyncMock()
|
||||||
|
mock_client.get.return_value = mock_response
|
||||||
|
mock_client.__aenter__.return_value = mock_client
|
||||||
|
mock_client.__aexit__.return_value = None
|
||||||
|
mock_client_cls.return_value = mock_client
|
||||||
|
|
||||||
|
with pytest.raises(Gmap2BriefConnectionError) as exc_info:
|
||||||
|
await interface.get_status("job123")
|
||||||
|
|
||||||
|
assert "Failed to get status" in str(exc_info.value)
|
||||||
|
assert "404" in str(exc_info.value)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_download_export_handles_error(self):
|
||||||
|
"""Тест обработки ошибки в download_export."""
|
||||||
|
mock_logger = MagicMock()
|
||||||
|
|
||||||
|
with patch("dataloader.interfaces.gmap2_brief.interface.APP_CONFIG") as mock_config:
|
||||||
|
mock_config.gmap2brief.base_url = "http://test.com"
|
||||||
|
mock_config.gmap2brief.download_endpoint = "/download/{job_id}"
|
||||||
|
mock_config.gmap2brief.timeout = 30
|
||||||
|
mock_config.app.local = False
|
||||||
|
|
||||||
|
interface = Gmap2BriefInterface(mock_logger)
|
||||||
|
|
||||||
|
with patch("dataloader.interfaces.gmap2_brief.interface.httpx.AsyncClient") as mock_client_cls:
|
||||||
|
# Create mock for stream context manager
|
||||||
|
mock_stream_ctx = MagicMock()
|
||||||
|
mock_stream_ctx.__aenter__ = AsyncMock(side_effect=httpx.TimeoutException("Timeout"))
|
||||||
|
mock_stream_ctx.__aexit__ = AsyncMock(return_value=None)
|
||||||
|
|
||||||
|
mock_client = MagicMock()
|
||||||
|
mock_client.stream = MagicMock(return_value=mock_stream_ctx)
|
||||||
|
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
||||||
|
mock_client.__aexit__ = AsyncMock(return_value=None)
|
||||||
|
mock_client_cls.return_value = mock_client
|
||||||
|
|
||||||
|
with pytest.raises(Gmap2BriefConnectionError) as exc_info:
|
||||||
|
from pathlib import Path
|
||||||
|
await interface.download_export("job123", Path("/tmp/test.zst"))
|
||||||
|
|
||||||
|
assert "Request error" in str(exc_info.value)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
class TestSuperTeneraErrorHandling:
|
||||||
|
"""Тесты обработки ошибок в SuperTeneraInterface."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_request_handles_http_status_error(self):
|
||||||
|
"""Тест обработки HTTP статус ошибки в _get_request."""
|
||||||
|
mock_logger = MagicMock()
|
||||||
|
|
||||||
|
with patch("dataloader.interfaces.tenera.interface.APP_CONFIG") as mock_config:
|
||||||
|
mock_config.app.local = False
|
||||||
|
mock_config.app.debug = False
|
||||||
|
|
||||||
|
interface = SuperTeneraInterface(mock_logger, "http://test.com")
|
||||||
|
|
||||||
|
mock_session = MagicMock()
|
||||||
|
interface._session = mock_session
|
||||||
|
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status = 503
|
||||||
|
mock_response.raise_for_status = MagicMock(side_effect=aiohttp.ClientResponseError(
|
||||||
|
request_info=MagicMock(),
|
||||||
|
history=(),
|
||||||
|
status=503,
|
||||||
|
message="Service Unavailable",
|
||||||
|
))
|
||||||
|
mock_response.json = AsyncMock(return_value={})
|
||||||
|
|
||||||
|
# Create context manager mock
|
||||||
|
mock_ctx = MagicMock()
|
||||||
|
mock_ctx.__aenter__ = AsyncMock(return_value=mock_response)
|
||||||
|
mock_ctx.__aexit__ = AsyncMock(return_value=None)
|
||||||
|
mock_session.get = MagicMock(return_value=mock_ctx)
|
||||||
|
|
||||||
|
with pytest.raises(SuperTeneraConnectionError) as exc_info:
|
||||||
|
await interface._get_request("/test")
|
||||||
|
|
||||||
|
assert "HTTP error 503" in str(exc_info.value)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_request_handles_connection_error(self):
|
||||||
|
"""Тест обработки ошибки подключения в _get_request."""
|
||||||
|
mock_logger = MagicMock()
|
||||||
|
|
||||||
|
with patch("dataloader.interfaces.tenera.interface.APP_CONFIG") as mock_config:
|
||||||
|
mock_config.app.local = False
|
||||||
|
mock_config.app.debug = False
|
||||||
|
|
||||||
|
interface = SuperTeneraInterface(mock_logger, "http://test.com")
|
||||||
|
|
||||||
|
mock_session = MagicMock()
|
||||||
|
interface._session = mock_session
|
||||||
|
|
||||||
|
# Create context manager mock that raises on __aenter__
|
||||||
|
mock_ctx = MagicMock()
|
||||||
|
mock_ctx.__aenter__ = AsyncMock(side_effect=aiohttp.ClientConnectionError("Connection failed"))
|
||||||
|
mock_ctx.__aexit__ = AsyncMock(return_value=None)
|
||||||
|
mock_session.get = MagicMock(return_value=mock_ctx)
|
||||||
|
|
||||||
|
with pytest.raises(SuperTeneraConnectionError) as exc_info:
|
||||||
|
await interface._get_request("/test")
|
||||||
|
|
||||||
|
assert "Connection error" in str(exc_info.value)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_request_handles_timeout(self):
|
||||||
|
"""Тест обработки таймаута в _get_request."""
|
||||||
|
mock_logger = MagicMock()
|
||||||
|
|
||||||
|
with patch("dataloader.interfaces.tenera.interface.APP_CONFIG") as mock_config:
|
||||||
|
mock_config.app.local = False
|
||||||
|
mock_config.app.debug = False
|
||||||
|
|
||||||
|
interface = SuperTeneraInterface(mock_logger, "http://test.com")
|
||||||
|
|
||||||
|
mock_session = MagicMock()
|
||||||
|
interface._session = mock_session
|
||||||
|
|
||||||
|
# Create context manager mock that raises on __aenter__
|
||||||
|
mock_ctx = MagicMock()
|
||||||
|
mock_ctx.__aenter__ = AsyncMock(side_effect=TimeoutError("Request timeout"))
|
||||||
|
mock_ctx.__aexit__ = AsyncMock(return_value=None)
|
||||||
|
mock_session.get = MagicMock(return_value=mock_ctx)
|
||||||
|
|
||||||
|
with pytest.raises(SuperTeneraConnectionError) as exc_info:
|
||||||
|
await interface._get_request("/test")
|
||||||
|
|
||||||
|
assert "Connection error" in str(exc_info.value)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_quotes_data_propagates_error(self):
|
||||||
|
"""Тест что get_quotes_data пробрасывает ошибки из _get_request."""
|
||||||
|
mock_logger = MagicMock()
|
||||||
|
|
||||||
|
with patch("dataloader.interfaces.tenera.interface.APP_CONFIG") as mock_config:
|
||||||
|
mock_config.app.local = False
|
||||||
|
mock_config.app.debug = False
|
||||||
|
mock_config.supertenera.quotes_endpoint = "/quotes"
|
||||||
|
|
||||||
|
interface = SuperTeneraInterface(mock_logger, "http://test.com")
|
||||||
|
|
||||||
|
mock_session = MagicMock()
|
||||||
|
interface._session = mock_session
|
||||||
|
|
||||||
|
# Create context manager mock that raises on __aenter__
|
||||||
|
mock_ctx = MagicMock()
|
||||||
|
mock_ctx.__aenter__ = AsyncMock(side_effect=aiohttp.ServerTimeoutError("Server timeout"))
|
||||||
|
mock_ctx.__aexit__ = AsyncMock(return_value=None)
|
||||||
|
mock_session.get = MagicMock(return_value=mock_ctx)
|
||||||
|
|
||||||
|
with pytest.raises(SuperTeneraConnectionError):
|
||||||
|
await interface.get_quotes_data()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_ping_handles_client_response_error(self):
|
||||||
|
"""Тест обработки ClientResponseError в ping."""
|
||||||
|
mock_logger = MagicMock()
|
||||||
|
|
||||||
|
with patch("dataloader.interfaces.tenera.interface.APP_CONFIG") as mock_config:
|
||||||
|
mock_config.app.local = False
|
||||||
|
mock_config.supertenera.quotes_endpoint = "/quotes"
|
||||||
|
mock_config.supertenera.timeout = 10
|
||||||
|
|
||||||
|
interface = SuperTeneraInterface(mock_logger, "http://test.com")
|
||||||
|
|
||||||
|
mock_session = MagicMock()
|
||||||
|
interface._session = mock_session
|
||||||
|
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.raise_for_status = MagicMock(side_effect=aiohttp.ClientResponseError(
|
||||||
|
request_info=MagicMock(),
|
||||||
|
history=(),
|
||||||
|
status=500,
|
||||||
|
message="Internal Server Error",
|
||||||
|
))
|
||||||
|
|
||||||
|
# Create context manager mock
|
||||||
|
mock_ctx = MagicMock()
|
||||||
|
mock_ctx.__aenter__ = AsyncMock(return_value=mock_response)
|
||||||
|
mock_ctx.__aexit__ = AsyncMock(return_value=None)
|
||||||
|
mock_session.get = MagicMock(return_value=mock_ctx)
|
||||||
|
|
||||||
|
with pytest.raises(SuperTeneraConnectionError) as exc_info:
|
||||||
|
await interface.ping()
|
||||||
|
|
||||||
|
assert "HTTP error 500" in str(exc_info.value)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_ping_handles_timeout_error(self):
|
||||||
|
"""Тест обработки TimeoutError в ping."""
|
||||||
|
mock_logger = MagicMock()
|
||||||
|
|
||||||
|
with patch("dataloader.interfaces.tenera.interface.APP_CONFIG") as mock_config:
|
||||||
|
mock_config.app.local = False
|
||||||
|
mock_config.supertenera.quotes_endpoint = "/quotes"
|
||||||
|
mock_config.supertenera.timeout = 10
|
||||||
|
|
||||||
|
interface = SuperTeneraInterface(mock_logger, "http://test.com")
|
||||||
|
|
||||||
|
mock_session = MagicMock()
|
||||||
|
interface._session = mock_session
|
||||||
|
|
||||||
|
# Create context manager mock that raises on __aenter__
|
||||||
|
mock_ctx = MagicMock()
|
||||||
|
mock_ctx.__aenter__ = AsyncMock(side_effect=TimeoutError())
|
||||||
|
mock_ctx.__aexit__ = AsyncMock(return_value=None)
|
||||||
|
mock_session.get = MagicMock(return_value=mock_ctx)
|
||||||
|
|
||||||
|
with pytest.raises(SuperTeneraConnectionError) as exc_info:
|
||||||
|
await interface.ping()
|
||||||
|
|
||||||
|
assert "Timeout" in str(exc_info.value)
|
||||||
|
assert "10s" in str(exc_info.value)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_ping_handles_client_error(self):
|
||||||
|
"""Тест обработки общей ClientError в ping."""
|
||||||
|
mock_logger = MagicMock()
|
||||||
|
|
||||||
|
with patch("dataloader.interfaces.tenera.interface.APP_CONFIG") as mock_config:
|
||||||
|
mock_config.app.local = False
|
||||||
|
mock_config.supertenera.quotes_endpoint = "/quotes"
|
||||||
|
mock_config.supertenera.timeout = 10
|
||||||
|
|
||||||
|
interface = SuperTeneraInterface(mock_logger, "http://test.com")
|
||||||
|
|
||||||
|
mock_session = MagicMock()
|
||||||
|
interface._session = mock_session
|
||||||
|
|
||||||
|
# Create context manager mock that raises on __aenter__
|
||||||
|
mock_ctx = MagicMock()
|
||||||
|
mock_ctx.__aenter__ = AsyncMock(side_effect=aiohttp.ClientError("Generic client error"))
|
||||||
|
mock_ctx.__aexit__ = AsyncMock(return_value=None)
|
||||||
|
mock_session.get = MagicMock(return_value=mock_ctx)
|
||||||
|
|
||||||
|
with pytest.raises(SuperTeneraConnectionError) as exc_info:
|
||||||
|
await interface.ping()
|
||||||
|
|
||||||
|
assert "Connection error" in str(exc_info.value)
|
||||||
Loading…
Reference in New Issue