mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 02:31:08 -05:00
- 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.
173 lines
6.2 KiB
Python
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
|