mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 09:11: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.
332 lines
11 KiB
Python
332 lines
11 KiB
Python
"""
|
|
Test utilities and helpers following Django styleguide patterns.
|
|
Provides reusable testing patterns and assertion helpers.
|
|
"""
|
|
|
|
import json
|
|
from typing import Dict, Any, Optional, List
|
|
from decimal import Decimal
|
|
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
|
|
from datetime import timedelta
|
|
|
|
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."""
|
|
pass
|
|
|
|
|
|
class EnhancedAPITestCase(
|
|
ApiTestMixin,
|
|
ModelTestMixin,
|
|
FactoryTestMixin,
|
|
TimestampTestMixin,
|
|
GeographyTestMixin,
|
|
APITestCase
|
|
):
|
|
"""Enhanced APITestCase with all testing mixins."""
|
|
pass
|