Skip to main content
Glama
test_errors.py14.1 kB
"""Tests for utils.api.errors module.""" from collections.abc import Callable from typing import Any from unittest.mock import AsyncMock, MagicMock import httpx import pytest from utils.api.error_types import ( AuthenticationRequiredError, TraktRateLimitError, TraktResourceNotFoundError, TraktServerError, ) from utils.api.errors import ( InternalError, InvalidParamsError, handle_api_errors, handle_api_errors_func, ) @pytest.fixture def mock_async_func() -> AsyncMock: """Create a mock async function for testing.""" return AsyncMock() class TestHandleApiErrorsDecorator: """Test handle_api_errors decorator functionality.""" def test_decorator_preserves_function_metadata( self, mock_async_func: AsyncMock ) -> None: """Test decorator preserves original function metadata.""" mock_async_func.__name__ = "test_function" mock_async_func.__doc__ = "Test docstring" decorated_func = handle_api_errors_func(mock_async_func) assert decorated_func.__name__ == "test_function" assert decorated_func.__doc__ == "Test docstring" @pytest.mark.asyncio async def test_successful_function_call(self, mock_async_func: AsyncMock) -> None: """Test decorator passes through successful function calls.""" expected_result = {"success": True, "data": "test_data"} mock_async_func.return_value = expected_result decorated_func = handle_api_errors_func(mock_async_func) result = await decorated_func("arg1", kwarg1="value1") assert result == expected_result mock_async_func.assert_called_once_with("arg1", kwarg1="value1") @pytest.mark.asyncio async def test_http_401_unauthorized_error( self, mock_async_func: AsyncMock ) -> None: """Test handling of HTTP 401 Unauthorized error.""" mock_response = MagicMock() mock_response.status_code = 401 mock_response.text = "Unauthorized" http_error = httpx.HTTPStatusError( message="401 Unauthorized", request=MagicMock(), response=mock_response ) mock_async_func.side_effect = http_error decorated_func = handle_api_errors_func(mock_async_func) with pytest.raises(AuthenticationRequiredError) as exc_info: await decorated_func() assert exc_info.value.message.startswith("Authentication required") assert exc_info.value.data is not None assert exc_info.value.data["error_type"] == "auth_required" assert exc_info.value.data["action"] == "access resource" @pytest.mark.asyncio async def test_http_404_not_found_error(self, mock_async_func: AsyncMock) -> None: """Test handling of HTTP 404 Not Found error.""" mock_response = MagicMock() mock_response.status_code = 404 mock_response.text = "Not Found" http_error = httpx.HTTPStatusError( message="404 Not Found", request=MagicMock(), response=mock_response ) mock_async_func.side_effect = http_error decorated_func = handle_api_errors_func(mock_async_func) with pytest.raises(TraktResourceNotFoundError) as exc_info: await decorated_func() assert ( "The requested resource 'unknown' was not found" in exc_info.value.message ) assert exc_info.value.data is not None assert exc_info.value.data["http_status"] == 404 assert exc_info.value.data["resource_type"] == "resource" assert exc_info.value.data["resource_id"] == "unknown" @pytest.mark.asyncio async def test_http_429_rate_limit_error(self, mock_async_func: AsyncMock) -> None: """Test handling of HTTP 429 Rate Limit error.""" mock_response = MagicMock() mock_response.status_code = 429 mock_response.text = "Too Many Requests" mock_response.headers = {} http_error = httpx.HTTPStatusError( message="429 Too Many Requests", request=MagicMock(), response=mock_response ) mock_async_func.side_effect = http_error decorated_func = handle_api_errors_func(mock_async_func) with pytest.raises(TraktRateLimitError) as exc_info: await decorated_func() assert exc_info.value.message == "Rate limit exceeded. Please try again later." assert exc_info.value.data is not None assert exc_info.value.data["http_status"] == 429 assert "retry_after" not in exc_info.value.data @pytest.mark.asyncio async def test_http_unknown_status_error(self, mock_async_func: AsyncMock) -> None: """Test handling of unknown HTTP status error.""" mock_response = MagicMock() mock_response.status_code = 503 mock_response.text = "Service Unavailable" http_error = httpx.HTTPStatusError( message="503 Service Unavailable", request=MagicMock(), response=mock_response, ) mock_async_func.side_effect = http_error decorated_func = handle_api_errors_func(mock_async_func) with pytest.raises(TraktServerError) as exc_info: await decorated_func() assert ( exc_info.value.message == "Service unavailable. Please try again in 30 seconds." ) assert exc_info.value.data is not None assert exc_info.value.data["http_status"] == 503 assert exc_info.value.data["is_temporary"] is True @pytest.mark.asyncio async def test_request_error_handling(self, mock_async_func: AsyncMock) -> None: """Test handling of HTTP request errors.""" request_error = httpx.RequestError("Connection failed") mock_async_func.side_effect = request_error decorated_func = handle_api_errors_func(mock_async_func) with pytest.raises(InternalError) as exc_info: await decorated_func() assert ( exc_info.value.message == "Unable to connect to Trakt API. Please check your internet connection." ) assert exc_info.value.data is not None assert exc_info.value.data["error_type"] == "request_error" assert exc_info.value.data["details"] == "Connection failed" @pytest.mark.asyncio async def test_unexpected_error_handling(self, mock_async_func: AsyncMock) -> None: """Test handling of unexpected errors.""" # Use a custom exception that's not in the business logic list class UnexpectedException(Exception): pass unexpected_error = UnexpectedException("Unexpected error") mock_async_func.side_effect = unexpected_error decorated_func = handle_api_errors_func(mock_async_func) with pytest.raises(InternalError) as exc_info: await decorated_func() assert ( exc_info.value.message == "An unexpected error occurred: Unexpected error" ) assert exc_info.value.data is not None assert exc_info.value.data["error_type"] == "unexpected_error" @pytest.mark.asyncio async def test_decorator_with_args_and_kwargs( self, mock_async_func: AsyncMock ) -> None: """Test decorator properly passes args and kwargs.""" mock_async_func.return_value = "success" decorated_func = handle_api_errors_func(mock_async_func) result = await decorated_func("arg1", "arg2", key1="value1", key2="value2") assert result == "success" mock_async_func.assert_called_once_with( "arg1", "arg2", key1="value1", key2="value2" ) @pytest.mark.asyncio async def test_http_status_error_is_mapped_to_server_error( self, mock_async_func: AsyncMock ) -> None: """Decorator delegates HTTPStatusError to the handler and raises TraktServerError.""" mock_response = MagicMock() mock_response.status_code = 500 mock_response.text = "Internal Server Error" http_error = httpx.HTTPStatusError( message="500 Internal Server Error", request=MagicMock(), response=mock_response, ) mock_async_func.side_effect = http_error decorated_func = handle_api_errors_func(mock_async_func) with pytest.raises(TraktServerError): await decorated_func() # Logging assertions belong to TraktAPIErrorHandler tests, not this decorator. class TestDecoratorTypes: """Test decorator type annotations and type safety.""" def test_decorator_is_callable_and_available(self) -> None: """Test that the decorator function is properly imported and callable.""" # The decorator should work with both methods and functions assert handle_api_errors_func is not None assert callable(handle_api_errors_func) def test_decorator_type_hints(self) -> None: """Test decorator maintains proper type hints.""" @handle_api_errors_func async def test_func(x: int) -> str: return str(x) # Function should still be callable and maintain signature assert callable(test_func) class TestHandleApiErrorsMethodDecorator: """Test handle_api_errors decorator functionality for class methods.""" class MockService: """Mock service class for testing method decorators.""" @handle_api_errors async def test_method(self, x: int) -> str: """Test method that returns a string.""" return str(x) @handle_api_errors async def method_that_raises_http_error(self) -> str: """Test method that raises an HTTP error.""" mock_response = MagicMock() mock_response.status_code = 401 mock_response.text = "Unauthorized" raise httpx.HTTPStatusError( message="401 Unauthorized", request=MagicMock(), response=mock_response ) @handle_api_errors async def method_that_raises_request_error(self) -> str: """Test method that raises a request error.""" raise httpx.RequestError("Connection failed") @pytest.mark.asyncio async def test_method_decorator_successful_call(self) -> None: """Test decorator works correctly on class methods for successful calls.""" service = self.MockService() result = await service.test_method(42) assert result == "42" @pytest.mark.asyncio async def test_method_decorator_http_error_handling(self) -> None: """Test decorator handles HTTP errors correctly on class methods.""" service = self.MockService() with pytest.raises(AuthenticationRequiredError) as exc_info: await service.method_that_raises_http_error() assert ( exc_info.value.message == "Authentication required. Please check your Trakt API credentials." ) assert exc_info.value.data is not None assert exc_info.value.data["error_type"] == "auth_required" @pytest.mark.asyncio async def test_method_decorator_request_error_handling(self) -> None: """Test decorator handles request errors correctly on class methods.""" service = self.MockService() with pytest.raises(InternalError) as exc_info: await service.method_that_raises_request_error() assert ( exc_info.value.message == "Unable to connect to Trakt API. Please check your internet connection." ) assert exc_info.value.data is not None assert exc_info.value.data["error_type"] == "request_error" class TestDecoratorIntegration: """Test decorator integration and edge cases.""" @pytest.mark.asyncio async def test_nested_decorators_compatibility(self) -> None: """Test decorator works with other decorators.""" call_order: list[str] = [] def outer_decorator(func: Callable[..., Any]) -> Callable[..., Any]: async def wrapper(*args: Any, **kwargs: Any) -> Any: call_order.append("outer_start") result = await func(*args, **kwargs) call_order.append("outer_end") return result return wrapper @outer_decorator @handle_api_errors_func async def test_func() -> str: call_order.append("inner") return "success" result = await test_func() assert result == "success" assert call_order == ["outer_start", "inner", "outer_end"] @pytest.mark.asyncio async def test_decorator_with_none_return(self) -> None: """Test decorator handles functions that return None.""" @handle_api_errors_func async def test_func() -> None: return None result = await test_func() assert result is None @pytest.mark.asyncio async def test_decorator_preserves_exceptions_from_inner_decorators(self) -> None: """Test decorator preserves the proper error handling chain.""" def inner_decorator(func: Callable[..., Any]) -> Callable[..., Any]: async def wrapper(*args: Any, **kwargs: Any) -> Any: # This decorator raises its own HTTP error mock_response = MagicMock() mock_response.status_code = 400 mock_response.text = "Bad Request" raise httpx.HTTPStatusError( "400 Bad Request", request=MagicMock(), response=mock_response ) return wrapper @handle_api_errors_func @inner_decorator async def test_func() -> str: return "should not reach here" with pytest.raises(InvalidParamsError) as exc_info: await test_func() assert "bad request" in exc_info.value.message.lower() assert exc_info.value.data is not None assert exc_info.value.data["http_status"] == 400 assert exc_info.value.data["details"] == "Bad Request"

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/wwiens/trakt_mcpserver'

If you have feedback or need assistance with the MCP directory API, please join our Discord server