Refactor test utilities and enhance ASGI settings

- Cleaned up and standardized assertions in ApiTestMixin for API response validation.
- Updated ASGI settings to use os.environ for setting the DJANGO_SETTINGS_MODULE.
- Removed unused imports and improved formatting in settings.py.
- Refactored URL patterns in urls.py for better readability and organization.
- Enhanced view functions in views.py for consistency and clarity.
- Added .flake8 configuration for linting and style enforcement.
- Introduced type stubs for django-environ to improve type checking with Pylance.
This commit is contained in:
pacnpal
2025-08-20 19:51:59 -04:00
parent 69c07d1381
commit 66ed4347a9
230 changed files with 15094 additions and 11578 deletions

View File

@@ -3,9 +3,7 @@ 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
@@ -17,19 +15,19 @@ 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',
response_status: str = "success",
data_type: Optional[type] = None,
contains_fields: Optional[List[str]] = 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
@@ -38,65 +36,71 @@ class ApiTestMixin:
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)
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']
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'])
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
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')
self.assertApiResponse(
response, status_code=status_code, response_status="error"
)
if error_code:
self.assertEqual(response.data['error']['code'], error_code)
self.assertEqual(response.data["error"]["code"], error_code)
if message_contains:
self.assertIn(message_contains, response.data['error']['message'])
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
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
@@ -104,31 +108,38 @@ class ApiTestMixin:
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']
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)
self.assertEqual(len(response.data["data"]), expected_count)
if has_next is not None:
self.assertEqual(pagination['has_next'], has_next)
self.assertEqual(pagination["has_next"], has_next)
if has_previous is not None:
self.assertEqual(pagination['has_previous'], has_previous)
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
@@ -136,116 +147,120 @@ class ModelTestMixin:
for field_name, expected_value in expected_fields.items():
actual_value = getattr(instance, field_name)
self.assertEqual(
actual_value,
actual_value,
expected_value,
f"Field '{field_name}' expected {expected_value}, got {actual_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]):
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,
time_diff,
tolerance_seconds,
f"Timestamp {timestamp} is not recent (diff: {time_diff}s)"
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
@@ -253,17 +268,17 @@ class TimestampTestMixin:
self.assertLess(
earlier_timestamp,
later_timestamp,
f"Timestamps not in order: {earlier_timestamp} should be before {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
@@ -272,38 +287,36 @@ class GeographyTestMixin:
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
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
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"
f"Points are {
distance_km:.2f}km apart, exceeds {max_distance_km}km",
)
@@ -313,10 +326,9 @@ class EnhancedTestCase(
FactoryTestMixin,
TimestampTestMixin,
GeographyTestMixin,
TestCase
TestCase,
):
"""Enhanced TestCase with all testing mixins."""
pass
class EnhancedAPITestCase(
@@ -325,7 +337,6 @@ class EnhancedAPITestCase(
FactoryTestMixin,
TimestampTestMixin,
GeographyTestMixin,
APITestCase
APITestCase,
):
"""Enhanced APITestCase with all testing mixins."""
pass
"""Enhanced APITestCase with all testing mixins."""