Files
thrillwiki_django_no_react/backend/tests/test_utils.py
pacnpal e4e36c7899 Add migrations for ParkPhoto and RidePhoto models with associated events
- Created ParkPhoto and ParkPhotoEvent models in the parks app, including fields for image, caption, alt text, and relationships to the Park model.
- Implemented triggers for insert and update operations on ParkPhoto to log changes in ParkPhotoEvent.
- Created RidePhoto and RidePhotoEvent models in the rides app, with similar structure and functionality as ParkPhoto.
- Added fields for photo type in RidePhoto and implemented corresponding triggers for logging changes.
- Established necessary indexes and unique constraints for both models to ensure data integrity and optimize queries.
2025-08-26 14:40:46 -04:00

342 lines
11 KiB
Python

"""
Test utilities and helpers following Django styleguide patterns.
Provides reusable testing patterns and assertion helpers.
"""
from typing import Dict, Any, Optional, List
from datetime import date, datetime
from django.test import TestCase
from django.contrib.auth import get_user_model
from rest_framework.test import APITestCase
from rest_framework import status
User = get_user_model()
class ApiTestMixin:
"""Mixin providing common API testing utilities."""
def assertApiResponse(
self,
response,
*,
status_code: int = status.HTTP_200_OK,
response_status: str = "success",
data_type: Optional[type] = None,
contains_fields: Optional[List[str]] = None,
):
"""
Assert API response has correct structure and content.
Args:
response: DRF Response object
status_code: Expected HTTP status code
response_status: Expected response status ('success' or 'error')
data_type: Expected type of response data (list, dict, etc.)
contains_fields: List of fields that should be in response data
"""
self.assertEqual(response.status_code, status_code)
self.assertEqual(response.data["status"], response_status)
if response_status == "success":
self.assertIn("data", response.data)
if data_type:
self.assertIsInstance(response.data["data"], data_type)
if contains_fields and response.data["data"]:
data = response.data["data"]
# Handle both single objects and lists
if isinstance(data, list) and data:
data = data[0]
if isinstance(data, dict):
for field in contains_fields:
self.assertIn(
field,
data,
f"Field '{field}' missing from response data",
)
elif response_status == "error":
self.assertIn("error", response.data)
self.assertIn("code", response.data["error"])
self.assertIn("message", response.data["error"])
def assertApiError(
self,
response,
*,
status_code: int,
error_code: Optional[str] = None,
message_contains: Optional[str] = None,
):
"""
Assert API response is an error with specific characteristics.
Args:
response: DRF Response object
status_code: Expected HTTP status code
error_code: Expected error code in response
message_contains: String that should be in error message
"""
self.assertApiResponse(
response, status_code=status_code, response_status="error"
)
if error_code:
self.assertEqual(response.data["error"]["code"], error_code)
if message_contains:
self.assertIn(message_contains, response.data["error"]["message"])
def assertPaginatedResponse(
self,
response,
*,
expected_count: Optional[int] = None,
has_next: Optional[bool] = None,
has_previous: Optional[bool] = None,
):
"""
Assert API response has correct pagination structure.
Args:
response: DRF Response object
expected_count: Expected number of items in current page
has_next: Whether pagination should have next page
has_previous: Whether pagination should have previous page
"""
self.assertApiResponse(response, data_type=list)
self.assertIn("pagination", response.data)
pagination = response.data["pagination"]
required_fields = [
"page",
"page_size",
"total_pages",
"total_count",
"has_next",
"has_previous",
]
for field in required_fields:
self.assertIn(field, pagination)
if expected_count is not None:
self.assertEqual(len(response.data["data"]), expected_count)
if has_next is not None:
self.assertEqual(pagination["has_next"], has_next)
if has_previous is not None:
self.assertEqual(pagination["has_previous"], has_previous)
class ModelTestMixin:
"""Mixin providing common model testing utilities."""
def assertModelFields(self, instance, expected_fields: Dict[str, Any]):
"""
Assert model instance has expected field values.
Args:
instance: Model instance
expected_fields: Dict of field_name: expected_value
"""
for field_name, expected_value in expected_fields.items():
actual_value = getattr(instance, field_name)
self.assertEqual(
actual_value,
expected_value,
f"Field '{field_name}' expected {expected_value}, got {actual_value}",
)
def assertModelValidation(
self,
model_class,
invalid_data: Dict[str, Any],
expected_errors: List[str],
):
"""
Assert model validation catches expected errors.
Args:
model_class: Model class to test
invalid_data: Data that should cause validation errors
expected_errors: List of error messages that should be raised
"""
instance = model_class(**invalid_data)
with self.assertRaises(Exception) as context:
instance.full_clean()
exception_str = str(context.exception)
for expected_error in expected_errors:
self.assertIn(expected_error, exception_str)
def assertDatabaseConstraint(self, model_factory, invalid_data: Dict[str, Any]):
"""
Assert database constraint is enforced.
Args:
model_factory: Factory class for creating model instances
invalid_data: Data that should violate database constraints
"""
from django.db import IntegrityError
with self.assertRaises(IntegrityError):
model_factory(**invalid_data)
class FactoryTestMixin:
"""Mixin providing factory testing utilities."""
def assertFactoryCreatesValidInstance(self, factory_class, **kwargs):
"""
Assert factory creates valid model instance.
Args:
factory_class: Factory class to test
**kwargs: Additional factory parameters
"""
instance = factory_class(**kwargs)
# Basic assertions
self.assertIsNotNone(instance.id)
self.assertIsNotNone(instance.created_at)
# Run full_clean to ensure validity
instance.full_clean()
return instance
def assertFactoryBatchCreation(self, factory_class, count: int = 5, **kwargs):
"""
Assert factory can create multiple valid instances.
Args:
factory_class: Factory class to test
count: Number of instances to create
**kwargs: Additional factory parameters
"""
instances = factory_class.create_batch(count, **kwargs)
self.assertEqual(len(instances), count)
for instance in instances:
self.assertIsNotNone(instance.id)
instance.full_clean()
return instances
class TimestampTestMixin:
"""Mixin for testing timestamp-related functionality."""
def assertRecentTimestamp(self, timestamp, tolerance_seconds: int = 5):
"""
Assert timestamp is recent (within tolerance).
Args:
timestamp: Timestamp to check
tolerance_seconds: Allowed difference in seconds
"""
from django.utils import timezone
now = timezone.now()
if isinstance(timestamp, date) and not isinstance(timestamp, datetime):
# Convert date to datetime for comparison
timestamp = datetime.combine(timestamp, datetime.min.time())
timestamp = timezone.make_aware(timestamp)
time_diff = abs((now - timestamp).total_seconds())
self.assertLessEqual(
time_diff,
tolerance_seconds,
f"Timestamp {timestamp} is not recent (diff: {time_diff}s)",
)
def assertTimestampOrder(self, earlier_timestamp, later_timestamp):
"""
Assert timestamps are in correct order.
Args:
earlier_timestamp: Should be before later_timestamp
later_timestamp: Should be after earlier_timestamp
"""
self.assertLess(
earlier_timestamp,
later_timestamp,
f"Timestamps not in order: {earlier_timestamp} should be before {later_timestamp}",
)
class GeographyTestMixin:
"""Mixin for testing geography-related functionality."""
def assertValidCoordinates(self, latitude: float, longitude: float):
"""
Assert coordinates are within valid ranges.
Args:
latitude: Latitude value
longitude: Longitude value
"""
self.assertGreaterEqual(latitude, -90, "Latitude below valid range")
self.assertLessEqual(latitude, 90, "Latitude above valid range")
self.assertGreaterEqual(longitude, -180, "Longitude below valid range")
self.assertLessEqual(longitude, 180, "Longitude above valid range")
def assertCoordinateDistance(
self, point1: tuple, point2: tuple, max_distance_km: float
):
"""
Assert two geographic points are within specified distance.
Args:
point1: (latitude, longitude) tuple
point2: (latitude, longitude) tuple
max_distance_km: Maximum allowed distance in kilometers
"""
from math import radians, cos, sin, asin, sqrt
lat1, lon1 = point1
lat2, lon2 = point2
# Haversine formula for great circle distance
lat1, lon1, lat2, lon2 = map(radians, [lat1, lon1, lat2, lon2])
dlat = lat2 - lat1
dlon = lon2 - lon1
a = sin(dlat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(dlon / 2) ** 2
c = 2 * asin(sqrt(a))
distance_km = 6371 * c # Earth's radius in km
self.assertLessEqual(
distance_km,
max_distance_km,
f"Points are {distance_km:.2f}km apart, exceeds {max_distance_km}km",
)
class EnhancedTestCase(
ApiTestMixin,
ModelTestMixin,
FactoryTestMixin,
TimestampTestMixin,
GeographyTestMixin,
TestCase,
):
"""Enhanced TestCase with all testing mixins."""
class EnhancedAPITestCase(
ApiTestMixin,
ModelTestMixin,
FactoryTestMixin,
TimestampTestMixin,
GeographyTestMixin,
APITestCase,
):
"""Enhanced APITestCase with all testing mixins."""