Files
thrillwiki_django_no_react/core/api/exceptions.py
pacnpal c26414ff74 Add comprehensive tests for Parks API and models
- Implemented extensive test cases for the Parks API, covering endpoints for listing, retrieving, creating, updating, and deleting parks.
- Added tests for filtering, searching, and ordering parks in the API.
- Created tests for error handling in the API, including malformed JSON and unsupported methods.
- Developed model tests for Park, ParkArea, Company, and ParkReview models, ensuring validation and constraints are enforced.
- Introduced utility mixins for API and model testing to streamline assertions and enhance test readability.
- Included integration tests to validate complete workflows involving park creation, retrieval, updating, and deletion.
2025-08-17 19:36:20 -04:00

173 lines
6.2 KiB
Python

"""
Custom exception handling for ThrillWiki API.
Provides standardized error responses following Django styleguide patterns.
"""
import logging
from typing import Any, Dict, Optional
from django.http import Http404
from django.core.exceptions import PermissionDenied, ValidationError as DjangoValidationError
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import exception_handler
from rest_framework.exceptions import ValidationError as DRFValidationError, NotFound, PermissionDenied as DRFPermissionDenied
from ..exceptions import ThrillWikiException
from ..logging import get_logger, log_exception
logger = get_logger(__name__)
def custom_exception_handler(exc: Exception, context: Dict[str, Any]) -> Optional[Response]:
"""
Custom exception handler for DRF that provides standardized error responses.
Returns:
Response with standardized error format or None to fallback to default handler
"""
# Call REST framework's default exception handler first
response = exception_handler(exc, context)
if response is not None:
# Standardize the error response format
custom_response_data = {
'status': 'error',
'error': {
'code': _get_error_code(exc),
'message': _get_error_message(exc, response.data),
'details': _get_error_details(exc, response.data),
},
'data': None,
}
# Add request context for debugging
if hasattr(context.get('request'), 'user'):
custom_response_data['error']['request_user'] = str(context['request'].user)
# Log the error for monitoring
log_exception(logger, exc, context={'response_status': response.status_code}, request=context.get('request'))
response.data = custom_response_data
# Handle ThrillWiki custom exceptions
elif isinstance(exc, ThrillWikiException):
custom_response_data = {
'status': 'error',
'error': exc.to_dict(),
'data': None,
}
log_exception(logger, exc, context={'response_status': exc.status_code}, request=context.get('request'))
response = Response(custom_response_data, status=exc.status_code)
# Handle specific Django exceptions that DRF doesn't catch
elif isinstance(exc, DjangoValidationError):
custom_response_data = {
'status': 'error',
'error': {
'code': 'VALIDATION_ERROR',
'message': 'Validation failed',
'details': _format_django_validation_errors(exc),
},
'data': None,
}
log_exception(logger, exc, context={'response_status': status.HTTP_400_BAD_REQUEST}, request=context.get('request'))
response = Response(custom_response_data, status=status.HTTP_400_BAD_REQUEST)
elif isinstance(exc, Http404):
custom_response_data = {
'status': 'error',
'error': {
'code': 'NOT_FOUND',
'message': 'Resource not found',
'details': str(exc) if str(exc) else None,
},
'data': None,
}
log_exception(logger, exc, context={'response_status': status.HTTP_404_NOT_FOUND}, request=context.get('request'))
response = Response(custom_response_data, status=status.HTTP_404_NOT_FOUND)
elif isinstance(exc, PermissionDenied):
custom_response_data = {
'status': 'error',
'error': {
'code': 'PERMISSION_DENIED',
'message': 'Permission denied',
'details': str(exc) if str(exc) else None,
},
'data': None,
}
log_exception(logger, exc, context={'response_status': status.HTTP_403_FORBIDDEN}, request=context.get('request'))
response = Response(custom_response_data, status=status.HTTP_403_FORBIDDEN)
return response
def _get_error_code(exc: Exception) -> str:
"""Extract or determine error code from exception."""
if hasattr(exc, 'default_code'):
return exc.default_code.upper()
if isinstance(exc, DRFValidationError):
return 'VALIDATION_ERROR'
elif isinstance(exc, NotFound):
return 'NOT_FOUND'
elif isinstance(exc, DRFPermissionDenied):
return 'PERMISSION_DENIED'
return exc.__class__.__name__.upper()
def _get_error_message(exc: Exception, response_data: Any) -> str:
"""Extract user-friendly error message."""
if isinstance(response_data, dict):
# Handle DRF validation errors
if 'detail' in response_data:
return str(response_data['detail'])
elif 'non_field_errors' in response_data:
errors = response_data['non_field_errors']
return errors[0] if isinstance(errors, list) and errors else str(errors)
elif isinstance(response_data, dict) and len(response_data) == 1:
key, value = next(iter(response_data.items()))
if isinstance(value, list) and value:
return f"{key}: {value[0]}"
return f"{key}: {value}"
# Fallback to exception message
return str(exc) if str(exc) else 'An error occurred'
def _get_error_details(exc: Exception, response_data: Any) -> Optional[Dict[str, Any]]:
"""Extract detailed error information for debugging."""
if isinstance(response_data, dict) and len(response_data) > 1:
return response_data
if hasattr(exc, 'detail') and isinstance(exc.detail, dict):
return exc.detail
return None
def _format_django_validation_errors(exc: DjangoValidationError) -> Dict[str, Any]:
"""Format Django ValidationError for API response."""
if hasattr(exc, 'error_dict'):
# Field-specific errors
return {
field: [str(error) for error in errors]
for field, errors in exc.error_dict.items()
}
elif hasattr(exc, 'error_list'):
# Non-field errors
return {
'non_field_errors': [str(error) for error in exc.error_list]
}
return {'non_field_errors': [str(exc)]}
# Removed _log_api_error - using centralized logging instead