"""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 SampleModel(BaseModel): """Sample Pydantic model for testing serialization/deserialization.""" 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 = SampleModel(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, SampleModel) assert isinstance(result, SampleModel) 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, SampleModel) @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, SampleModel) assert isinstance(result, SampleModel) 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=SampleModel) assert isinstance(result, SampleModel) 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 = SampleModel(name="new", value=50) result = await interface.post("/users", body=body, response_model=SampleModel) assert isinstance(result, SampleModel) 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 = SampleModel(name="updated", value=75) result = await interface.put("/users/1", body=body, response_model=SampleModel) assert isinstance(result, SampleModel) 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 = SampleModel(name="test", value=1) with pytest.raises(httpx.HTTPStatusError): await interface.post("/users", body=body)