Skip to main content
Glama
test_error_handler.py22.6 kB
"""Tests for utils.api.error_handler module.""" import uuid from typing import Any from unittest.mock import MagicMock, patch import httpx import pytest from utils.api.error_handler import TraktAPIErrorHandler, create_correlation_id from utils.api.error_types import ( AuthenticationRequiredError, AuthorizationPendingError, TraktRateLimitError, TraktResourceNotFoundError, TraktServerError, TraktValidationError, ) from utils.api.errors import InternalError, InvalidParamsError, InvalidRequestError class TestTraktAPIErrorHandler: """Test TraktAPIErrorHandler functionality.""" @pytest.fixture def mock_http_error(self) -> httpx.HTTPStatusError: """Create a mock HTTP error for testing.""" mock_response = MagicMock() mock_response.status_code = 404 mock_response.text = "Not Found" mock_response.headers = {} return httpx.HTTPStatusError( message="404 Not Found", request=MagicMock(), response=mock_response ) @patch("utils.api.error_handler.logger") def test_handle_http_error_basic( self, mock_logger: Any, mock_http_error: httpx.HTTPStatusError ) -> None: """Test basic HTTP error handling.""" result = TraktAPIErrorHandler.handle_http_error(mock_http_error) assert isinstance(result, TraktResourceNotFoundError) assert result.data is not None assert result.data["http_status"] == 404 assert "correlation_id" in result.data mock_logger.error.assert_called_once() @patch("utils.api.error_handler.logger") def test_handle_http_error_with_context( self, mock_logger: Any, mock_http_error: httpx.HTTPStatusError ) -> None: """Test HTTP error handling with context.""" result = TraktAPIErrorHandler.handle_http_error( mock_http_error, endpoint="/shows/test", resource_type="show", resource_id="test-show", correlation_id="test-123", ) assert result.data is not None assert result.data["endpoint"] == "/shows/test" assert result.data["resource_type"] == "show" assert result.data["resource_id"] == "test-show" assert result.data["correlation_id"] == "test-123" @patch("utils.api.error_handler.logger") def test_handle_http_error_generates_correlation_id( self, mock_logger: Any, mock_http_error: httpx.HTTPStatusError ) -> None: """Test correlation ID is generated when not provided.""" result = TraktAPIErrorHandler.handle_http_error(mock_http_error) assert result.data is not None correlation_id = result.data.get("correlation_id") assert correlation_id is not None # Should be a valid UUID string uuid.UUID(correlation_id) @patch("utils.api.error_handler.logger") def test_handle_http_error_response_text_error(self, mock_logger: Any) -> None: """Test handling when response.text raises an exception.""" mock_response = MagicMock() mock_response.status_code = 500 # Configure the text property to raise an exception when accessed def _raise_text_error(self: Any) -> None: raise Exception("Text error") type(mock_response).text = property(_raise_text_error) http_error = httpx.HTTPStatusError( message="500 Error", request=MagicMock(), response=mock_response ) result = TraktAPIErrorHandler.handle_http_error(http_error) assert isinstance(result, TraktServerError) # Should handle the exception gracefully class TestStatusCodeHandlers: """Test individual status code handlers.""" def test_handle_bad_request_generic(self) -> None: """Test generic 400 Bad Request handling.""" result = TraktAPIErrorHandler.handle_bad_request( response_text="Generic bad request", endpoint="/test", correlation_id="test-123", ) assert isinstance(result, InvalidParamsError) assert result.data is not None assert result.data["http_status"] == 400 assert result.data["details"] == "Generic bad request" assert result.data["endpoint"] == "/test" assert result.data["correlation_id"] == "test-123" def test_handle_bad_request_authorization_pending(self) -> None: """Test 400 Bad Request with authorization_pending.""" result = TraktAPIErrorHandler.handle_bad_request( response_text="authorization_pending - user has not approved", resource_id="device123", ) assert isinstance(result, AuthorizationPendingError) assert result.data is not None assert result.data["device_code"] == "device123" def test_handle_bad_request_validation_error(self) -> None: """Test 400 Bad Request with validation keywords.""" result = TraktAPIErrorHandler.handle_bad_request( response_text="invalid year parameter" ) assert isinstance(result, TraktValidationError) assert result.data is not None assert ( "invalid year parameter" in result.data["validation_details"]["api_response"] ) def test_handle_unauthorized(self) -> None: """Test 401 Unauthorized handling.""" result = TraktAPIErrorHandler.handle_unauthorized(resource_type="show") assert isinstance(result, AuthenticationRequiredError) assert result.data is not None assert result.data["action"] == "access show" def test_handle_forbidden(self) -> None: """Test 403 Forbidden handling.""" result = TraktAPIErrorHandler.handle_forbidden( endpoint="/api/test", correlation_id="test-456", response_text="Forbidden access", ) assert isinstance(result, InvalidRequestError) assert result.data is not None assert result.data["http_status"] == 403 assert result.data["endpoint"] == "/api/test" assert result.data["correlation_id"] == "test-456" assert result.data["details"] == "Forbidden access" def test_handle_not_found(self) -> None: """Test 404 Not Found handling.""" result = TraktAPIErrorHandler.handle_not_found( resource_type="movie", resource_id="inception", endpoint="/movies/inception", correlation_id="test-789", ) assert isinstance(result, TraktResourceNotFoundError) assert result.data is not None assert result.data["resource_type"] == "movie" assert result.data["resource_id"] == "inception" assert result.data["endpoint"] == "/movies/inception" assert result.data["correlation_id"] == "test-789" def test_handle_not_found_defaults(self) -> None: """Test 404 handling with default values.""" result = TraktAPIErrorHandler.handle_not_found() assert isinstance(result, TraktResourceNotFoundError) assert result.data is not None assert result.data["resource_type"] == "resource" assert result.data["resource_id"] == "unknown" def test_handle_conflict(self) -> None: """Test 409 Conflict handling.""" result = TraktAPIErrorHandler.handle_conflict( endpoint="/checkins", resource_type="checkin", resource_id="show-123", correlation_id="test-conflict", response_text="Already checked in", ) assert isinstance(result, InvalidRequestError) assert result.data is not None assert result.data["http_status"] == 409 assert result.data["endpoint"] == "/checkins" assert result.data["resource_type"] == "checkin" assert result.data["resource_id"] == "show-123" assert result.data["correlation_id"] == "test-conflict" assert result.data["details"] == "Already checked in" def test_handle_validation_error(self) -> None: """Test 422 Unprocessable Entity handling.""" result = TraktAPIErrorHandler.handle_validation_error( response_text="Validation failed: invalid year", endpoint="/shows", correlation_id="test-validation", ) assert isinstance(result, TraktValidationError) assert result.data is not None assert ( result.data["validation_details"]["api_response"] == "Validation failed: invalid year" ) assert result.data["validation_details"]["endpoint"] == "/shows" assert result.data["validation_details"]["correlation_id"] == "test-validation" def test_handle_rate_limit_without_retry_after(self) -> None: """Test 429 Rate Limit handling without retry-after header.""" result = TraktAPIErrorHandler.handle_rate_limit( endpoint="/shows/trending", correlation_id="test-rate-limit" ) assert isinstance(result, TraktRateLimitError) assert result.data is not None assert "retry_after" not in result.data assert result.data["endpoint"] == "/shows/trending" assert result.data["correlation_id"] == "test-rate-limit" def test_handle_rate_limit_with_retry_after(self) -> None: """Test 429 Rate Limit handling with retry-after header.""" mock_response = MagicMock() mock_response.headers = {"retry-after": "60"} mock_error = MagicMock() mock_error.response = mock_response result = TraktAPIErrorHandler.handle_rate_limit( error=mock_error, endpoint="/movies/popular" ) assert isinstance(result, TraktRateLimitError) assert result.data is not None assert result.data["retry_after"] == 60 assert result.data["endpoint"] == "/movies/popular" def test_handle_rate_limit_invalid_retry_after(self) -> None: """Test 429 Rate Limit handling with invalid retry-after header.""" mock_response = MagicMock() mock_response.headers = {"retry-after": "invalid"} mock_error = MagicMock() mock_error.response = mock_response result = TraktAPIErrorHandler.handle_rate_limit(error=mock_error) assert isinstance(result, TraktRateLimitError) assert result.data is not None assert "retry_after" not in result.data def test_handle_server_error(self) -> None: """Test 500 Internal Server Error handling.""" result = TraktAPIErrorHandler.handle_server_error( endpoint="/api/shows", correlation_id="test-500" ) assert isinstance(result, TraktServerError) assert result.data is not None assert result.data["http_status"] == 500 assert result.data["endpoint"] == "/api/shows" assert result.data["correlation_id"] == "test-500" def test_handle_bad_gateway(self) -> None: """Test 502 Bad Gateway handling.""" result = TraktAPIErrorHandler.handle_bad_gateway( endpoint="/api/movies", correlation_id="test-502" ) assert isinstance(result, TraktServerError) assert result.data is not None assert result.data["http_status"] == 502 assert result.data["endpoint"] == "/api/movies" assert result.data["correlation_id"] == "test-502" assert "bad gateway" in result.message.lower() def test_handle_service_unavailable(self) -> None: """Test 503 Service Unavailable handling.""" result = TraktAPIErrorHandler.handle_service_unavailable( endpoint="/api/users", correlation_id="test-503" ) assert isinstance(result, TraktServerError) assert result.data is not None assert result.data["http_status"] == 503 assert result.data["endpoint"] == "/api/users" assert result.data["correlation_id"] == "test-503" assert "service unavailable" in result.message.lower() def test_handle_unknown_error(self) -> None: """Test unknown status code handling.""" mock_response = MagicMock() mock_response.status_code = 418 # I'm a teapot mock_error = MagicMock() mock_error.response = mock_response result = TraktAPIErrorHandler.handle_unknown_error( error=mock_error, endpoint="/api/teapot", resource_type="teapot", resource_id="tea123", correlation_id="test-418", response_text="I'm a teapot", ) assert isinstance(result, InternalError) assert result.data is not None assert result.data["http_status"] == 418 assert result.data["endpoint"] == "/api/teapot" assert result.data["resource_type"] == "teapot" assert result.data["resource_id"] == "tea123" assert result.data["correlation_id"] == "test-418" assert result.data["response"] == "I'm a teapot" def test_handle_unknown_error_no_error_object(self) -> None: """Test unknown error handling without error object.""" result = TraktAPIErrorHandler.handle_unknown_error( correlation_id="test-unknown" ) assert isinstance(result, InternalError) assert result.data is not None assert result.data["http_status"] == "unknown" assert result.data["correlation_id"] == "test-unknown" class TestStatusCodeMapping: """Test status code to handler mapping.""" def test_get_status_code_handler_known_codes(self) -> None: """Test mapping for known status codes.""" test_cases = [ (400, TraktAPIErrorHandler.handle_bad_request), (401, TraktAPIErrorHandler.handle_unauthorized), (403, TraktAPIErrorHandler.handle_forbidden), (404, TraktAPIErrorHandler.handle_not_found), (409, TraktAPIErrorHandler.handle_conflict), (422, TraktAPIErrorHandler.handle_validation_error), (429, TraktAPIErrorHandler.handle_rate_limit), (500, TraktAPIErrorHandler.handle_server_error), (502, TraktAPIErrorHandler.handle_bad_gateway), (503, TraktAPIErrorHandler.handle_service_unavailable), ] for status_code, expected_handler in test_cases: handler = TraktAPIErrorHandler.get_status_code_handler(status_code) assert handler == expected_handler def test_get_status_code_handler_unknown_code(self) -> None: """Test mapping for unknown status codes.""" handler = TraktAPIErrorHandler.get_status_code_handler(418) assert handler == TraktAPIErrorHandler.handle_unknown_error class TestLogging: """Test error logging functionality.""" @patch("utils.api.error_handler.logger") def test_log_http_error_basic(self, mock_logger: Any) -> None: """Test basic HTTP error logging.""" TraktAPIErrorHandler.log_http_error( status_code=404, endpoint="/shows/test", resource_type="show", resource_id="test-show", correlation_id="test-123", response_text="Not found", ) mock_logger.error.assert_called_once() call_args = mock_logger.error.call_args # Check log message assert "404" in call_args[0][0] # Check context data context = call_args[1]["extra"]["context"] assert context["http_status"] == 404 assert context["endpoint"] == "/shows/test" assert context["resource_type"] == "show" assert context["resource_id"] == "test-show" assert context["correlation_id"] == "test-123" assert context["response_length"] == len("Not found") assert context["response_preview"] == "Not found" @patch("utils.api.error_handler.logger") def test_log_http_error_truncated_response(self, mock_logger: Any) -> None: """Test logging with long response text that gets truncated.""" long_response = "x" * 300 # Longer than 200 chars TraktAPIErrorHandler.log_http_error( status_code=500, endpoint=None, resource_type=None, resource_id=None, correlation_id="test-truncate", response_text=long_response, ) mock_logger.error.assert_called_once() context = mock_logger.error.call_args[1]["extra"]["context"] assert context["response_length"] == 300 assert len(context["response_preview"]) == 203 # 200 + "..." assert context["response_preview"].endswith("...") @patch("utils.api.error_handler.logger") def test_log_http_error_minimal_context(self, mock_logger: Any) -> None: """Test logging with minimal context.""" TraktAPIErrorHandler.log_http_error( status_code=400, endpoint=None, resource_type=None, resource_id=None, correlation_id="minimal-test", response_text="", ) mock_logger.error.assert_called_once() context = mock_logger.error.call_args[1]["extra"]["context"] assert context["http_status"] == 400 assert context["correlation_id"] == "minimal-test" assert context["response_length"] == 0 assert context["response_preview"] == "" # Optional keys should not be present assert "endpoint" not in context assert "resource_type" not in context assert "resource_id" not in context class TestCorrelationId: """Test correlation ID functionality.""" def test_create_correlation_id(self) -> None: """Test correlation ID creation.""" correlation_id = create_correlation_id() assert isinstance(correlation_id, str) # Should be a valid UUID uuid.UUID(correlation_id) def test_create_correlation_id_unique(self) -> None: """Test correlation IDs are unique.""" id1 = create_correlation_id() id2 = create_correlation_id() assert id1 != id2 class TestIntegration: """Test integration scenarios.""" @patch("utils.api.error_handler.logger") def test_handle_http_error_end_to_end_404(self, mock_logger: Any) -> None: """Test complete 404 error handling flow.""" mock_response = MagicMock() mock_response.status_code = 404 mock_response.text = "Show not found" http_error = httpx.HTTPStatusError( message="404 Not Found", request=MagicMock(), response=mock_response ) result = TraktAPIErrorHandler.handle_http_error( error=http_error, endpoint="/shows/nonexistent", resource_type="show", resource_id="nonexistent", correlation_id="integration-test-404", ) # Check error type and properties assert isinstance(result, TraktResourceNotFoundError) assert result.data is not None assert result.data["resource_type"] == "show" assert result.data["resource_id"] == "nonexistent" assert result.data["endpoint"] == "/shows/nonexistent" assert result.data["correlation_id"] == "integration-test-404" # Check logging was called mock_logger.error.assert_called_once() @patch("utils.api.error_handler.logger") def test_handle_http_error_end_to_end_429(self, mock_logger: Any) -> None: """Test complete 429 rate limit error handling flow.""" mock_response = MagicMock() mock_response.status_code = 429 mock_response.text = "Rate limit exceeded" mock_response.headers = {"retry-after": "120"} http_error = httpx.HTTPStatusError( message="429 Too Many Requests", request=MagicMock(), response=mock_response ) result = TraktAPIErrorHandler.handle_http_error( error=http_error, endpoint="/shows/trending", correlation_id="integration-test-429", ) # Check error type and properties assert isinstance(result, TraktRateLimitError) assert result.data is not None assert result.data["http_status"] == 429 assert result.data["retry_after"] == 120 assert result.data["endpoint"] == "/shows/trending" assert result.data["correlation_id"] == "integration-test-429" # Check logging was called mock_logger.error.assert_called_once() @patch("utils.api.error_handler.logger") def test_handle_http_error_end_to_end_401(self, mock_logger: Any) -> None: """Test complete 401 unauthorized error handling flow.""" mock_response = MagicMock() mock_response.status_code = 401 mock_response.text = "Unauthorized" http_error = httpx.HTTPStatusError( message="401 Unauthorized", request=MagicMock(), response=mock_response ) result = TraktAPIErrorHandler.handle_http_error( error=http_error, endpoint="/user/profile", resource_type="user", correlation_id="integration-test-401", ) # Check error type and properties assert isinstance(result, AuthenticationRequiredError) assert result.data is not None assert result.data["error_type"] == "auth_required" assert result.data["action"] == "access user" assert "auth_url" in result.data # Check logging was called mock_logger.error.assert_called_once() @patch("utils.api.error_handler.logger") def test_handle_http_error_end_to_end_403(self, mock_logger: Any) -> None: """Test complete 403 forbidden error handling flow.""" mock_response = MagicMock() mock_response.status_code = 403 mock_response.text = "Forbidden access" http_error = httpx.HTTPStatusError( message="403 Forbidden", request=MagicMock(), response=mock_response ) result = TraktAPIErrorHandler.handle_http_error( error=http_error, endpoint="/admin/settings", correlation_id="integration-test-403", ) # Check error type and properties assert isinstance(result, InvalidRequestError) assert result.data is not None assert result.data["http_status"] == 403 assert result.data["details"] == "Forbidden access" assert result.data["endpoint"] == "/admin/settings" assert result.data["correlation_id"] == "integration-test-403" # Check logging was called mock_logger.error.assert_called_once()

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