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.
This commit is contained in:
pacnpal
2025-08-17 19:36:20 -04:00
parent 17228e9935
commit c26414ff74
210 changed files with 24155 additions and 833 deletions

View File

@@ -0,0 +1,22 @@
# Core middleware modules
# Import middleware classes from the analytics module
from .analytics import PageViewMiddleware, PgHistoryContextMiddleware
# Import middleware classes from the performance_middleware.py module
from .performance_middleware import (
PerformanceMiddleware,
QueryCountMiddleware,
DatabaseConnectionMiddleware,
CachePerformanceMiddleware
)
# Make all middleware classes available at the package level
__all__ = [
'PageViewMiddleware',
'PgHistoryContextMiddleware',
'PerformanceMiddleware',
'QueryCountMiddleware',
'DatabaseConnectionMiddleware',
'CachePerformanceMiddleware'
]

View File

@@ -0,0 +1,76 @@
"""
Analytics and tracking middleware for Django application.
"""
import pghistory
from django.contrib.auth.models import AnonymousUser
from django.core.handlers.wsgi import WSGIRequest
from django.utils.deprecation import MiddlewareMixin
from django.contrib.contenttypes.models import ContentType
from django.views.generic.detail import DetailView
from core.analytics import PageView
class RequestContextProvider(pghistory.context):
"""Custom context provider for pghistory that extracts information from the request."""
def __call__(self, request: WSGIRequest) -> dict:
return {
'user': str(request.user) if request.user and not isinstance(request.user, AnonymousUser) else None,
'ip': request.META.get('REMOTE_ADDR'),
'user_agent': request.META.get('HTTP_USER_AGENT'),
'session_key': request.session.session_key if hasattr(request, 'session') else None
}
# Initialize the context provider
request_context = RequestContextProvider()
class PgHistoryContextMiddleware:
"""
Middleware that ensures request object is available to pghistory context.
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
return response
class PageViewMiddleware(MiddlewareMixin):
"""Middleware to track page views for DetailView-based pages."""
def process_view(self, request, view_func, view_args, view_kwargs):
# Only track GET requests
if request.method != 'GET':
return None
# Get view class if it exists
view_class = getattr(view_func, 'view_class', None)
if not view_class or not issubclass(view_class, DetailView):
return None
# Get the object if it's a detail view
try:
view_instance = view_class()
view_instance.request = request
view_instance.args = view_args
view_instance.kwargs = view_kwargs
obj = view_instance.get_object()
except (AttributeError, Exception):
return None
# Record the page view
try:
PageView.objects.create(
content_type=ContentType.objects.get_for_model(obj.__class__),
object_id=obj.pk,
ip_address=request.META.get('REMOTE_ADDR', ''),
user_agent=request.META.get('HTTP_USER_AGENT', '')[:512]
)
except Exception:
# Fail silently to not interrupt the request
pass
return None

View File

@@ -0,0 +1,268 @@
"""
Performance monitoring middleware for tracking request metrics.
"""
import time
import logging
from django.db import connection
from django.utils.deprecation import MiddlewareMixin
from django.conf import settings
performance_logger = logging.getLogger('performance')
logger = logging.getLogger(__name__)
class PerformanceMiddleware(MiddlewareMixin):
"""Middleware to collect performance metrics for each request"""
def process_request(self, request):
"""Initialize performance tracking for the request"""
request._performance_start_time = time.time()
request._performance_initial_queries = len(connection.queries) if hasattr(connection, 'queries') else 0
return None
def process_response(self, request, response):
"""Log performance metrics after response is ready"""
# Skip performance tracking for certain paths
skip_paths = ['/health/', '/admin/jsi18n/', '/static/', '/media/', '/__debug__/']
if any(request.path.startswith(path) for path in skip_paths):
return response
# Calculate metrics
end_time = time.time()
start_time = getattr(request, '_performance_start_time', end_time)
duration = end_time - start_time
initial_queries = getattr(request, '_performance_initial_queries', 0)
total_queries = len(connection.queries) - initial_queries if hasattr(connection, 'queries') else 0
# Get content length
content_length = 0
if hasattr(response, 'content'):
content_length = len(response.content)
elif hasattr(response, 'streaming_content'):
# For streaming responses, we can't easily measure content length
content_length = -1
# Build performance data
performance_data = {
'path': request.path,
'method': request.method,
'status_code': response.status_code,
'duration_ms': round(duration * 1000, 2),
'duration_seconds': round(duration, 3),
'query_count': total_queries,
'content_length_bytes': content_length,
'user_id': getattr(request.user, 'id', None) if hasattr(request, 'user') and request.user.is_authenticated else None,
'user_agent': request.META.get('HTTP_USER_AGENT', '')[:100], # Truncate user agent
'remote_addr': self._get_client_ip(request),
}
# Add query details in debug mode
if settings.DEBUG and hasattr(connection, 'queries') and total_queries > 0:
recent_queries = connection.queries[-total_queries:]
performance_data['queries'] = [
{
'sql': query['sql'][:200] + '...' if len(query['sql']) > 200 else query['sql'],
'time': float(query['time'])
}
for query in recent_queries[-10:] # Last 10 queries only
]
# Identify slow queries
slow_queries = [q for q in recent_queries if float(q['time']) > 0.1]
if slow_queries:
performance_data['slow_query_count'] = len(slow_queries)
performance_data['slowest_query_time'] = max(float(q['time']) for q in slow_queries)
# Determine log level based on performance
log_level = self._get_log_level(duration, total_queries, response.status_code)
# Log the performance data
performance_logger.log(
log_level,
f"Request performance: {request.method} {request.path} - "
f"{duration:.3f}s, {total_queries} queries, {response.status_code}",
extra=performance_data
)
# Add performance headers for debugging (only in debug mode)
if settings.DEBUG:
response['X-Response-Time'] = f"{duration * 1000:.2f}ms"
response['X-Query-Count'] = str(total_queries)
if total_queries > 0 and hasattr(connection, 'queries'):
total_query_time = sum(float(q['time']) for q in connection.queries[-total_queries:])
response['X-Query-Time'] = f"{total_query_time * 1000:.2f}ms"
return response
def process_exception(self, request, exception):
"""Log performance data even when an exception occurs"""
end_time = time.time()
start_time = getattr(request, '_performance_start_time', end_time)
duration = end_time - start_time
initial_queries = getattr(request, '_performance_initial_queries', 0)
total_queries = len(connection.queries) - initial_queries if hasattr(connection, 'queries') else 0
performance_data = {
'path': request.path,
'method': request.method,
'status_code': 500, # Exception occurred
'duration_ms': round(duration * 1000, 2),
'query_count': total_queries,
'exception': str(exception),
'exception_type': type(exception).__name__,
'user_id': getattr(request.user, 'id', None) if hasattr(request, 'user') and request.user.is_authenticated else None,
}
performance_logger.error(
f"Request exception: {request.method} {request.path} - "
f"{duration:.3f}s, {total_queries} queries, {type(exception).__name__}: {exception}",
extra=performance_data
)
return None # Don't handle the exception, just log it
def _get_client_ip(self, request):
"""Extract client IP address from request"""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0].strip()
else:
ip = request.META.get('REMOTE_ADDR', '')
return ip
def _get_log_level(self, duration, query_count, status_code):
"""Determine appropriate log level based on performance metrics"""
# Error responses
if status_code >= 500:
return logging.ERROR
elif status_code >= 400:
return logging.WARNING
# Performance-based log levels
if duration > 5.0: # Very slow requests
return logging.ERROR
elif duration > 2.0 or query_count > 20: # Slow requests or high query count
return logging.WARNING
elif duration > 1.0 or query_count > 10: # Moderately slow
return logging.INFO
else:
return logging.DEBUG
class QueryCountMiddleware(MiddlewareMixin):
"""Middleware to track and limit query counts per request"""
def __init__(self, get_response):
self.get_response = get_response
self.query_limit = getattr(settings, 'MAX_QUERIES_PER_REQUEST', 50)
super().__init__(get_response)
def process_request(self, request):
"""Initialize query tracking"""
request._query_count_start = len(connection.queries) if hasattr(connection, 'queries') else 0
return None
def process_response(self, request, response):
"""Check query count and warn if excessive"""
if not hasattr(connection, 'queries'):
return response
start_count = getattr(request, '_query_count_start', 0)
current_count = len(connection.queries)
request_query_count = current_count - start_count
if request_query_count > self.query_limit:
logger.warning(
f"Excessive query count: {request.path} executed {request_query_count} queries "
f"(limit: {self.query_limit})",
extra={
'path': request.path,
'method': request.method,
'query_count': request_query_count,
'query_limit': self.query_limit,
'excessive_queries': True
}
)
return response
class DatabaseConnectionMiddleware(MiddlewareMixin):
"""Middleware to monitor database connection health"""
def process_request(self, request):
"""Check database connection at start of request"""
try:
# Simple connection test
from django.db import connection
with connection.cursor() as cursor:
cursor.execute("SELECT 1")
cursor.fetchone()
except Exception as e:
logger.error(
f"Database connection failed at request start: {e}",
extra={
'path': request.path,
'method': request.method,
'database_error': str(e)
}
)
# Don't block the request, let Django handle the database error
return None
def process_response(self, request, response):
"""Close database connections properly"""
try:
from django.db import connection
connection.close()
except Exception as e:
logger.warning(f"Error closing database connection: {e}")
return response
class CachePerformanceMiddleware(MiddlewareMixin):
"""Middleware to monitor cache performance"""
def process_request(self, request):
"""Initialize cache performance tracking"""
request._cache_hits = 0
request._cache_misses = 0
request._cache_start_time = time.time()
return None
def process_response(self, request, response):
"""Log cache performance metrics"""
cache_duration = time.time() - getattr(request, '_cache_start_time', time.time())
cache_hits = getattr(request, '_cache_hits', 0)
cache_misses = getattr(request, '_cache_misses', 0)
if cache_hits + cache_misses > 0:
hit_rate = (cache_hits / (cache_hits + cache_misses)) * 100
cache_data = {
'path': request.path,
'cache_hits': cache_hits,
'cache_misses': cache_misses,
'cache_hit_rate': round(hit_rate, 2),
'cache_operations': cache_hits + cache_misses,
'cache_duration': round(cache_duration * 1000, 2) # milliseconds
}
# Log cache performance
if hit_rate < 50 and cache_hits + cache_misses > 5:
logger.warning(
f"Low cache hit rate for {request.path}: {hit_rate:.1f}%",
extra=cache_data
)
else:
logger.debug(
f"Cache performance for {request.path}: {hit_rate:.1f}% hit rate",
extra=cache_data
)
return response