380 lines
15 KiB
Python
380 lines
15 KiB
Python
"""Tests for TgBackendInterface base class."""
|
|
|
|
import pytest
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
import httpx
|
|
from pydantic import BaseModel, ValidationError
|
|
from app.interfaces.base import TgBackendInterface
|
|
|
|
|
|
class TestModel(BaseModel):
|
|
"""Test Pydantic model for testing."""
|
|
name: str
|
|
value: int
|
|
|
|
|
|
class TestTgBackendInterface:
|
|
"""Tests for TgBackendInterface base class."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_init(self):
|
|
"""Test initialization with default parameters."""
|
|
with patch('app.interfaces.base.httpx.AsyncClient') as MockClient:
|
|
interface = TgBackendInterface(api_prefix="http://api.example.com/v1")
|
|
|
|
assert interface.api_prefix == "http://api.example.com/v1"
|
|
MockClient.assert_called_once()
|
|
|
|
|
|
call_kwargs = MockClient.call_args[1]
|
|
assert call_kwargs['follow_redirects'] is True
|
|
assert isinstance(call_kwargs['timeout'], httpx.Timeout)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_init_strips_trailing_slash(self):
|
|
"""Test that trailing slash is stripped from api_prefix."""
|
|
with patch('app.interfaces.base.httpx.AsyncClient'):
|
|
interface = TgBackendInterface(api_prefix="http://api.example.com/v1/")
|
|
assert interface.api_prefix == "http://api.example.com/v1"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_init_custom_params(self):
|
|
"""Test initialization with custom timeout and retries."""
|
|
with patch('app.interfaces.base.httpx.AsyncClient') as MockClient:
|
|
interface = TgBackendInterface(
|
|
api_prefix="http://api.example.com",
|
|
timeout=60.0,
|
|
max_retries=5
|
|
)
|
|
|
|
call_kwargs = MockClient.call_args[1]
|
|
|
|
assert isinstance(call_kwargs['timeout'], httpx.Timeout)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_close(self):
|
|
"""Test closing the HTTP client."""
|
|
mock_client = AsyncMock()
|
|
with patch('app.interfaces.base.httpx.AsyncClient', return_value=mock_client):
|
|
interface = TgBackendInterface(api_prefix="http://api.example.com")
|
|
await interface.close()
|
|
|
|
mock_client.aclose.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_context_manager(self):
|
|
"""Test using interface as async context manager."""
|
|
mock_client = AsyncMock()
|
|
with patch('app.interfaces.base.httpx.AsyncClient', return_value=mock_client):
|
|
async with TgBackendInterface(api_prefix="http://api.example.com") as interface:
|
|
assert interface is not None
|
|
|
|
|
|
mock_client.aclose.assert_called_once()
|
|
|
|
def test_build_url_with_leading_slash(self):
|
|
"""Test building URL with path that has leading slash."""
|
|
with patch('app.interfaces.base.httpx.AsyncClient'):
|
|
interface = TgBackendInterface(api_prefix="http://api.example.com/v1")
|
|
url = interface._build_url("/users/123")
|
|
|
|
assert url == "http://api.example.com/v1/users/123"
|
|
|
|
def test_build_url_without_leading_slash(self):
|
|
"""Test building URL with path without leading slash."""
|
|
with patch('app.interfaces.base.httpx.AsyncClient'):
|
|
interface = TgBackendInterface(api_prefix="http://api.example.com/v1")
|
|
url = interface._build_url("users/123")
|
|
|
|
assert url == "http://api.example.com/v1/users/123"
|
|
|
|
def test_serialize_body_with_model(self):
|
|
"""Test serializing Pydantic model to dict."""
|
|
with patch('app.interfaces.base.httpx.AsyncClient'):
|
|
interface = TgBackendInterface(api_prefix="http://api.example.com")
|
|
model = TestModel(name="test", value=42)
|
|
|
|
result = interface._serialize_body(model)
|
|
|
|
assert result == {"name": "test", "value": 42}
|
|
|
|
def test_serialize_body_with_none(self):
|
|
"""Test serializing None body."""
|
|
with patch('app.interfaces.base.httpx.AsyncClient'):
|
|
interface = TgBackendInterface(api_prefix="http://api.example.com")
|
|
|
|
result = interface._serialize_body(None)
|
|
|
|
assert result is None
|
|
|
|
def test_deserialize_response_with_dict(self):
|
|
"""Test deserializing dict response to Pydantic model."""
|
|
with patch('app.interfaces.base.httpx.AsyncClient'):
|
|
interface = TgBackendInterface(api_prefix="http://api.example.com")
|
|
data = {"name": "test", "value": 42}
|
|
|
|
result = interface._deserialize_response(data, TestModel)
|
|
|
|
assert isinstance(result, TestModel)
|
|
assert result.name == "test"
|
|
assert result.value == 42
|
|
|
|
def test_deserialize_response_no_model(self):
|
|
"""Test deserializing response without model returns raw data."""
|
|
with patch('app.interfaces.base.httpx.AsyncClient'):
|
|
interface = TgBackendInterface(api_prefix="http://api.example.com")
|
|
data = {"name": "test", "value": 42}
|
|
|
|
result = interface._deserialize_response(data, None)
|
|
|
|
assert result == data
|
|
|
|
def test_deserialize_response_validation_error(self):
|
|
"""Test deserialization with validation error."""
|
|
with patch('app.interfaces.base.httpx.AsyncClient'):
|
|
interface = TgBackendInterface(api_prefix="http://api.example.com")
|
|
|
|
data = {"name": "test"}
|
|
|
|
with pytest.raises(ValidationError):
|
|
interface._deserialize_response(data, TestModel)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_response_success(self):
|
|
"""Test handling successful HTTP response."""
|
|
with patch('app.interfaces.base.httpx.AsyncClient'):
|
|
interface = TgBackendInterface(api_prefix="http://api.example.com")
|
|
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.content = b'{"name": "test", "value": 42}'
|
|
mock_response.json.return_value = {"name": "test", "value": 42}
|
|
mock_response.raise_for_status = MagicMock()
|
|
|
|
result = await interface._handle_response(mock_response, TestModel)
|
|
|
|
assert isinstance(result, TestModel)
|
|
assert result.name == "test"
|
|
mock_response.raise_for_status.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_response_204_no_content(self):
|
|
"""Test handling 204 No Content response."""
|
|
with patch('app.interfaces.base.httpx.AsyncClient'):
|
|
interface = TgBackendInterface(api_prefix="http://api.example.com")
|
|
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 204
|
|
mock_response.content = b''
|
|
mock_response.raise_for_status = MagicMock()
|
|
|
|
result = await interface._handle_response(mock_response)
|
|
|
|
assert result == {}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_response_empty_content(self):
|
|
"""Test handling response with empty content."""
|
|
with patch('app.interfaces.base.httpx.AsyncClient'):
|
|
interface = TgBackendInterface(api_prefix="http://api.example.com")
|
|
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.content = b''
|
|
mock_response.raise_for_status = MagicMock()
|
|
|
|
result = await interface._handle_response(mock_response)
|
|
|
|
assert result == {}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_response_http_error(self):
|
|
"""Test handling HTTP error response."""
|
|
with patch('app.interfaces.base.httpx.AsyncClient'):
|
|
interface = TgBackendInterface(api_prefix="http://api.example.com")
|
|
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 404
|
|
mock_response.text = "Not Found"
|
|
|
|
error = httpx.HTTPStatusError(
|
|
"Not Found",
|
|
request=MagicMock(),
|
|
response=mock_response
|
|
)
|
|
mock_response.raise_for_status = MagicMock(side_effect=error)
|
|
|
|
with pytest.raises(httpx.HTTPStatusError):
|
|
await interface._handle_response(mock_response)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_response_invalid_json(self):
|
|
"""Test handling response with invalid JSON."""
|
|
with patch('app.interfaces.base.httpx.AsyncClient'):
|
|
interface = TgBackendInterface(api_prefix="http://api.example.com")
|
|
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.content = b'not empty'
|
|
mock_response.text = "Invalid JSON"
|
|
mock_response.json.side_effect = ValueError("Invalid JSON")
|
|
mock_response.raise_for_status = MagicMock()
|
|
|
|
with pytest.raises(ValueError):
|
|
await interface._handle_response(mock_response)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_success(self):
|
|
"""Test successful GET request."""
|
|
mock_client = AsyncMock()
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.content = b'{"name": "test", "value": 42}'
|
|
mock_response.json.return_value = {"name": "test", "value": 42}
|
|
mock_response.raise_for_status = MagicMock()
|
|
mock_client.get.return_value = mock_response
|
|
|
|
with patch('app.interfaces.base.httpx.AsyncClient', return_value=mock_client):
|
|
interface = TgBackendInterface(api_prefix="http://api.example.com")
|
|
|
|
result = await interface.get("/users", params={"id": 123}, response_model=TestModel)
|
|
|
|
assert isinstance(result, TestModel)
|
|
assert result.name == "test"
|
|
mock_client.get.assert_called_once()
|
|
call_args = mock_client.get.call_args
|
|
assert call_args[0][0] == "http://api.example.com/users"
|
|
assert call_args[1]['params'] == {"id": 123}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_post_success(self):
|
|
"""Test successful POST request."""
|
|
mock_client = AsyncMock()
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 201
|
|
mock_response.content = b'{"name": "created", "value": 100}'
|
|
mock_response.json.return_value = {"name": "created", "value": 100}
|
|
mock_response.raise_for_status = MagicMock()
|
|
mock_client.post.return_value = mock_response
|
|
|
|
with patch('app.interfaces.base.httpx.AsyncClient', return_value=mock_client):
|
|
interface = TgBackendInterface(api_prefix="http://api.example.com")
|
|
body = TestModel(name="new", value=50)
|
|
|
|
result = await interface.post("/users", body=body, response_model=TestModel)
|
|
|
|
assert isinstance(result, TestModel)
|
|
assert result.name == "created"
|
|
mock_client.post.assert_called_once()
|
|
call_args = mock_client.post.call_args
|
|
assert call_args[0][0] == "http://api.example.com/users"
|
|
assert call_args[1]['json'] == {"name": "new", "value": 50}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_post_without_body(self):
|
|
"""Test POST request without body."""
|
|
mock_client = AsyncMock()
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.content = b'{"result": "ok"}'
|
|
mock_response.json.return_value = {"result": "ok"}
|
|
mock_response.raise_for_status = MagicMock()
|
|
mock_client.post.return_value = mock_response
|
|
|
|
with patch('app.interfaces.base.httpx.AsyncClient', return_value=mock_client):
|
|
interface = TgBackendInterface(api_prefix="http://api.example.com")
|
|
|
|
result = await interface.post("/action")
|
|
|
|
assert result == {"result": "ok"}
|
|
call_args = mock_client.post.call_args
|
|
assert call_args[1]['json'] is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_put_success(self):
|
|
"""Test successful PUT request."""
|
|
mock_client = AsyncMock()
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.content = b'{"name": "updated", "value": 75}'
|
|
mock_response.json.return_value = {"name": "updated", "value": 75}
|
|
mock_response.raise_for_status = MagicMock()
|
|
mock_client.put.return_value = mock_response
|
|
|
|
with patch('app.interfaces.base.httpx.AsyncClient', return_value=mock_client):
|
|
interface = TgBackendInterface(api_prefix="http://api.example.com")
|
|
body = TestModel(name="updated", value=75)
|
|
|
|
result = await interface.put("/users/1", body=body, response_model=TestModel)
|
|
|
|
assert isinstance(result, TestModel)
|
|
assert result.name == "updated"
|
|
mock_client.put.assert_called_once()
|
|
call_args = mock_client.put.call_args
|
|
assert call_args[0][0] == "http://api.example.com/users/1"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_success(self):
|
|
"""Test successful DELETE request."""
|
|
mock_client = AsyncMock()
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 204
|
|
mock_response.content = b''
|
|
mock_response.raise_for_status = MagicMock()
|
|
mock_client.delete.return_value = mock_response
|
|
|
|
with patch('app.interfaces.base.httpx.AsyncClient', return_value=mock_client):
|
|
interface = TgBackendInterface(api_prefix="http://api.example.com")
|
|
|
|
result = await interface.delete("/users/1")
|
|
|
|
assert result == {}
|
|
mock_client.delete.assert_called_once()
|
|
call_args = mock_client.delete.call_args
|
|
assert call_args[0][0] == "http://api.example.com/users/1"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_http_error(self):
|
|
"""Test GET request with HTTP error."""
|
|
mock_client = AsyncMock()
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 500
|
|
mock_response.text = "Internal Server Error"
|
|
|
|
error = httpx.HTTPStatusError(
|
|
"Internal Server Error",
|
|
request=MagicMock(),
|
|
response=mock_response
|
|
)
|
|
mock_response.raise_for_status = MagicMock(side_effect=error)
|
|
mock_client.get.return_value = mock_response
|
|
|
|
with patch('app.interfaces.base.httpx.AsyncClient', return_value=mock_client):
|
|
interface = TgBackendInterface(api_prefix="http://api.example.com")
|
|
|
|
with pytest.raises(httpx.HTTPStatusError):
|
|
await interface.get("/users")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_post_http_error(self):
|
|
"""Test POST request with HTTP error."""
|
|
mock_client = AsyncMock()
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 400
|
|
mock_response.text = "Bad Request"
|
|
|
|
error = httpx.HTTPStatusError(
|
|
"Bad Request",
|
|
request=MagicMock(),
|
|
response=mock_response
|
|
)
|
|
mock_response.raise_for_status = MagicMock(side_effect=error)
|
|
mock_client.post.return_value = mock_response
|
|
|
|
with patch('app.interfaces.base.httpx.AsyncClient', return_value=mock_client):
|
|
interface = TgBackendInterface(api_prefix="http://api.example.com")
|
|
body = TestModel(name="test", value=1)
|
|
|
|
with pytest.raises(httpx.HTTPStatusError):
|
|
await interface.post("/users", body=body)
|