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

@@ -1 +1 @@
# Parks app test suite
# Parks app test suite

View File

@@ -2,31 +2,29 @@
Tests for park filtering functionality including search, status filtering,
date ranges, and numeric validations.
"""
from django.test import TestCase
from django.contrib.contenttypes.models import ContentType
from django.utils import timezone
from datetime import date, timedelta
from parks.models import Park, ParkLocation
from django.test import TestCase
from datetime import date
from parks.models import Park, ParkLocation, Company
from parks.filters import ParkFilter
from parks.models.companies import Company
# NOTE: These tests need to be updated to work with the new ParkLocation model
# instead of the generic Location model
class ParkFilterTests(TestCase):
@classmethod
def setUpTestData(cls):
"""Set up test data for all filter tests"""
# Create operators
cls.operator1 = Company.objects.create(
name="Thrilling Adventures Inc",
slug="thrilling-adventures"
name="Thrilling Adventures Inc", slug="thrilling-adventures"
)
cls.operator2 = Company.objects.create(
name="Family Fun Corp",
slug="family-fun"
name="Family Fun Corp", slug="family-fun"
)
# Create parks with various attributes for testing all filters
cls.park1 = Park.objects.create(
name="Thrilling Adventures Park",
@@ -37,7 +35,7 @@ class ParkFilterTests(TestCase):
size_acres=100,
ride_count=20,
coaster_count=5,
average_rating=4.5
average_rating=4.5,
)
ParkLocation.objects.create(
park=cls.park1,
@@ -45,9 +43,9 @@ class ParkFilterTests(TestCase):
city="Thrill City",
state="Thrill State",
country="USA",
postal_code="12345"
postal_code="12345",
)
cls.park2 = Park.objects.create(
name="Family Fun Park",
description="Family-friendly entertainment and attractions",
@@ -57,7 +55,7 @@ class ParkFilterTests(TestCase):
size_acres=50,
ride_count=15,
coaster_count=2,
average_rating=4.0
average_rating=4.0,
)
ParkLocation.objects.create(
park=cls.park2,
@@ -65,159 +63,161 @@ class ParkFilterTests(TestCase):
city="Fun City",
state="Fun State",
country="Canada",
postal_code="54321"
postal_code="54321",
)
# Park with minimal data for edge case testing
cls.park3 = Park.objects.create(
name="Incomplete Park",
status="UNDER_CONSTRUCTION",
operator=cls.operator1
operator=cls.operator1,
)
def test_text_search(self):
"""Test search functionality across different fields"""
# Test name search
queryset = ParkFilter(data={"search": "Thrilling"}).qs
self.assertEqual(queryset.count(), 1)
self.assertIn(self.park1, queryset)
# Test description search
queryset = ParkFilter(data={"search": "family-friendly"}).qs
self.assertEqual(queryset.count(), 1)
self.assertIn(self.park2, queryset)
# Test location search
queryset = ParkFilter(data={"search": "Thrill City"}).qs
self.assertEqual(queryset.count(), 1)
self.assertIn(self.park1, queryset)
# Test combined field search
queryset = ParkFilter(data={"search": "Park"}).qs
self.assertEqual(queryset.count(), 3)
# Test empty search
queryset = ParkFilter(data={}).qs
self.assertEqual(queryset.count(), 3)
def test_status_filtering(self):
"""Test status filter with various values"""
# Test each status
status_tests = {
"OPERATING": [self.park1],
"CLOSED_TEMP": [self.park2],
"UNDER_CONSTRUCTION": [self.park3]
"UNDER_CONSTRUCTION": [self.park3],
}
for status, expected_parks in status_tests.items():
queryset = ParkFilter(data={"status": status}).qs
self.assertEqual(queryset.count(), len(expected_parks))
for park in expected_parks:
self.assertIn(park, queryset)
# Test empty status (should return all)
queryset = ParkFilter(data={}).qs
self.assertEqual(queryset.count(), 3)
# Test empty string status (should return all)
queryset = ParkFilter(data={"status": ""}).qs
self.assertEqual(queryset.count(), 3)
# Test invalid status (should return no results)
queryset = ParkFilter(data={"status": "INVALID"}).qs
self.assertEqual(queryset.count(), 0)
def test_date_range_filtering(self):
"""Test date range filter functionality"""
# Test various date range scenarios
test_cases = [
# Start date only
({
"opening_date_after": "2019-01-01"
}, [self.park1]),
({"opening_date_after": "2019-01-01"}, [self.park1]),
# End date only
({
"opening_date_before": "2016-01-01"
}, [self.park2]),
({"opening_date_before": "2016-01-01"}, [self.park2]),
# Date range including one park
({
"opening_date_after": "2014-01-01",
"opening_date_before": "2016-01-01"
}, [self.park2]),
(
{
"opening_date_after": "2014-01-01",
"opening_date_before": "2016-01-01",
},
[self.park2],
),
# Date range including multiple parks
({
"opening_date_after": "2014-01-01",
"opening_date_before": "2022-01-01"
}, [self.park1, self.park2]),
(
{
"opening_date_after": "2014-01-01",
"opening_date_before": "2022-01-01",
},
[self.park1, self.park2],
),
# Empty filter (should return all)
({}, [self.park1, self.park2, self.park3]),
# Future date (should return none)
({
"opening_date_after": "2030-01-01"
}, []),
({"opening_date_after": "2030-01-01"}, []),
]
for filter_data, expected_parks in test_cases:
queryset = ParkFilter(data=filter_data).qs
self.assertEqual(
set(queryset),
set(expected_parks),
f"Failed for filter: {filter_data}"
f"Failed for filter: {filter_data}",
)
# Test invalid date formats
invalid_dates = [
{"opening_date_after": "invalid-date"},
{"opening_date_before": "2023-13-01"}, # Invalid month
{"opening_date_after": "2023-01-32"}, # Invalid day
{"opening_date_after": "2023-01-32"}, # Invalid day
{"opening_date_before": "not-a-date"},
]
for invalid_data in invalid_dates:
filter_instance = ParkFilter(data=invalid_data)
self.assertFalse(
filter_instance.is_valid(),
f"Filter should be invalid for data: {invalid_data}"
f"Filter should be invalid for data: {invalid_data}",
)
def test_numeric_filtering(self):
"""Test numeric filters with validation"""
# Test minimum rides filter
test_cases = [
({"min_rides": "18"}, [self.park1]), # Only park1 has >= 18 rides
({"min_rides": "10"}, [self.park1, self.park2]), # Both park1 and park2 have >= 10 rides
({"min_rides": "0"}, [self.park1, self.park2, self.park3]), # All parks have >= 0 rides
({}, [self.park1, self.park2, self.park3]), # No filter should return all
(
{"min_rides": "10"},
[self.park1, self.park2],
), # Both park1 and park2 have >= 10 rides
(
{"min_rides": "0"},
[self.park1, self.park2, self.park3],
), # All parks have >= 0 rides
# No filter should return all
({}, [self.park1, self.park2, self.park3]),
]
for filter_data, expected_parks in test_cases:
queryset = ParkFilter(data=filter_data).qs
self.assertEqual(
set(queryset),
set(expected_parks),
f"Failed for filter: {filter_data}"
f"Failed for filter: {filter_data}",
)
# Test coaster count filter
queryset = ParkFilter(data={"min_coasters": "3"}).qs
self.assertEqual(queryset.count(), 1)
self.assertIn(self.park1, queryset)
# Test size filter
queryset = ParkFilter(data={"min_size": "75"}).qs
self.assertEqual(queryset.count(), 1)
self.assertIn(self.park1, queryset)
# Test validation
invalid_values = ["-1", "invalid", "0.5"]
for value in invalid_values:
filter_instance = ParkFilter(data={"min_rides": value})
self.assertFalse(
filter_instance.is_valid(),
f"Filter should be invalid for value: {value}"
)
f"Filter should be invalid for value: {value}",
)

View File

@@ -2,33 +2,29 @@
Tests for park models functionality including CRUD operations,
slug handling, status management, and location integration.
"""
from django.test import TestCase
from django.core.exceptions import ValidationError
from django.db import IntegrityError
from django.utils import timezone
from datetime import date
from parks.models import Park, ParkArea, ParkLocation
from parks.models.companies import Company
from django.test import TestCase
from django.db import IntegrityError
from parks.models import Park, ParkArea, ParkLocation, Company
# NOTE: These tests need to be updated to work with the new ParkLocation model
# instead of the generic Location model
class ParkModelTests(TestCase):
def setUp(self):
"""Set up test data"""
self.operator = Company.objects.create(
name="Test Company",
slug="test-company"
)
self.operator = Company.objects.create(name="Test Company", slug="test-company")
# Create a basic park
self.park = Park.objects.create(
name="Test Park",
description="A test park",
status="OPERATING",
operator=self.operator
operator=self.operator,
)
# Create location for the park
self.location = ParkLocation.objects.create(
park=self.park,
@@ -53,7 +49,7 @@ class ParkModelTests(TestCase):
park = Park.objects.create(
name="Another Test Park",
status="OPERATING",
operator=self.operator
operator=self.operator,
)
self.assertEqual(park.slug, "another-test-park")
@@ -62,40 +58,40 @@ class ParkModelTests(TestCase):
from django.db import transaction
from django.contrib.contenttypes.models import ContentType
from core.history import HistoricalSlug
with transaction.atomic():
# Create initial park with a specific name/slug
park = Park.objects.create(
name="Original Park Name",
description="Test description",
status="OPERATING",
operator=self.operator
operator=self.operator,
)
original_slug = park.slug
print(f"\nInitial park created with slug: {original_slug}")
# Ensure we have a save to trigger history
park.save()
# Modify name to trigger slug change
park.name = "Updated Park Name"
park.save()
new_slug = park.slug
print(f"Park updated with new slug: {new_slug}")
# Check HistoricalSlug records
historical_slugs = HistoricalSlug.objects.filter(
content_type=ContentType.objects.get_for_model(Park),
object_id=park.id
object_id=park.id,
)
print(f"Historical slug records: {[h.slug for h in historical_slugs]}")
# Check pghistory records
event_model = getattr(Park, 'event_model', None)
event_model = getattr(Park, "event_model", None)
if event_model:
historical_records = event_model.objects.filter(
pgh_obj_id=park.id
).order_by('-pgh_created_at')
).order_by("-pgh_created_at")
print(f"\nPG History records:")
for record in historical_records:
print(f"- Event ID: {record.pgh_id}")
@@ -104,56 +100,57 @@ class ParkModelTests(TestCase):
print(f" Created At: {record.pgh_created_at}")
else:
print("\nNo pghistory event model available")
# Try to find by old slug
found_park, is_historical = Park.get_by_slug(original_slug)
self.assertEqual(found_park.id, park.id)
print(f"Found park by old slug: {found_park.slug}, is_historical: {is_historical}")
print(
f"Found park by old slug: {
found_park.slug}, is_historical: {is_historical}"
)
self.assertTrue(is_historical)
# Try current slug
found_park, is_historical = Park.get_by_slug(new_slug)
self.assertEqual(found_park.id, park.id)
print(f"Found park by new slug: {found_park.slug}, is_historical: {is_historical}")
print(
f"Found park by new slug: {
found_park.slug}, is_historical: {is_historical}"
)
self.assertFalse(is_historical)
def test_status_color_mapping(self):
"""Test status color class mapping"""
status_tests = {
'OPERATING': 'bg-green-100 text-green-800',
'CLOSED_TEMP': 'bg-yellow-100 text-yellow-800',
'CLOSED_PERM': 'bg-red-100 text-red-800',
'UNDER_CONSTRUCTION': 'bg-blue-100 text-blue-800',
'DEMOLISHED': 'bg-gray-100 text-gray-800',
'RELOCATED': 'bg-purple-100 text-purple-800'
"OPERATING": "bg-green-100 text-green-800",
"CLOSED_TEMP": "bg-yellow-100 text-yellow-800",
"CLOSED_PERM": "bg-red-100 text-red-800",
"UNDER_CONSTRUCTION": "bg-blue-100 text-blue-800",
"DEMOLISHED": "bg-gray-100 text-gray-800",
"RELOCATED": "bg-purple-100 text-purple-800",
}
for status, expected_color in status_tests.items():
self.park.status = status
self.assertEqual(self.park.get_status_color(), expected_color)
def test_absolute_url(self):
"""Test get_absolute_url method"""
expected_url = f"/parks/{self.park.slug}/"
self.assertEqual(self.park.get_absolute_url(), expected_url)
class ParkAreaModelTests(TestCase):
def setUp(self):
"""Set up test data"""
self.operator = Company.objects.create(
name="Test Company 2",
slug="test-company-2"
name="Test Company 2", slug="test-company-2"
)
self.park = Park.objects.create(
name="Test Park",
status="OPERATING",
operator=self.operator
name="Test Park", status="OPERATING", operator=self.operator
)
self.area = ParkArea.objects.create(
park=self.park,
name="Test Area",
description="A test area"
park=self.park, name="Test Area", description="A test area"
)
def test_area_creation(self):
@@ -162,23 +159,18 @@ class ParkAreaModelTests(TestCase):
self.assertEqual(self.area.slug, "test-area")
self.assertEqual(self.area.park, self.park)
def test_unique_together_constraint(self):
"""Test unique_together constraint for park and slug"""
from django.db import transaction
# Try to create area with same slug in same park
with transaction.atomic():
with self.assertRaises(IntegrityError):
ParkArea.objects.create(
park=self.park,
name="Test Area" # Will generate same slug
park=self.park, name="Test Area" # Will generate same slug
)
# Should be able to use same name in different park
other_park = Park.objects.create(name="Other Park", operator=self.operator)
area = ParkArea.objects.create(
park=other_park,
name="Test Area"
)
area = ParkArea.objects.create(park=other_park, name="Test Area")
self.assertEqual(area.slug, "test-area")

View File

@@ -16,8 +16,8 @@ class TestParkSearch:
park3 = Park.objects.create(name="Test Garden")
# Get autocomplete results
url = reverse('parks:suggest_parks')
response = client.get(url, {'search': 'Test'})
url = reverse("parks:suggest_parks")
response = client.get(url, {"search": "Test"})
# Check response
assert response.status_code == 200
@@ -35,18 +35,15 @@ class TestParkSearch:
"""Test ParkAutocomplete configuration"""
ac = ParkAutocomplete()
assert ac.model == Park
assert 'name' in ac.search_attrs
assert "name" in ac.search_attrs
def test_search_with_filters(self, client: Client):
"""Test search works with filters"""
park = Park.objects.create(name="Test Park", status="OPERATING")
# Search with status filter
url = reverse('parks:park_list')
response = client.get(url, {
'park': str(park.pk),
'status': 'OPERATING'
})
url = reverse("parks:park_list")
response = client.get(url, {"park": str(park.pk), "status": "OPERATING"})
assert response.status_code == 200
assert park.name in response.content.decode()
@@ -56,7 +53,7 @@ class TestParkSearch:
Park.objects.create(name="Test Park")
Park.objects.create(name="Another Park")
url = reverse('parks:park_list')
url = reverse("parks:park_list")
response = client.get(url)
assert response.status_code == 200
@@ -69,8 +66,8 @@ class TestParkSearch:
Park.objects.create(name="Adventure World")
Park.objects.create(name="Water Adventure")
url = reverse('parks:suggest_parks')
response = client.get(url, {'search': 'Adv'})
url = reverse("parks:suggest_parks")
response = client.get(url, {"search": "Adv"})
assert response.status_code == 200
content = response.content.decode()
@@ -81,12 +78,8 @@ class TestParkSearch:
"""Test HTMX-specific request handling"""
Park.objects.create(name="Test Park")
url = reverse('parks:suggest_parks')
response = client.get(
url,
{'search': 'Test'},
HTTP_HX_REQUEST='true'
)
url = reverse("parks:suggest_parks")
response = client.get(url, {"search": "Test"}, HTTP_HX_REQUEST="true")
assert response.status_code == 200
assert "Test Park" in response.content.decode()
@@ -95,11 +88,8 @@ class TestParkSearch:
"""Test view mode is maintained during search"""
Park.objects.create(name="Test Park")
url = reverse('parks:park_list')
response = client.get(url, {
'park': 'Test',
'view_mode': 'list'
})
url = reverse("parks:park_list")
response = client.get(url, {"park": "Test", "view_mode": "list"})
assert response.status_code == 200
assert 'data-view-mode="list"' in response.content.decode()
@@ -110,11 +100,11 @@ class TestParkSearch:
for i in range(10):
Park.objects.create(name=f"Test Park {i}")
url = reverse('parks:suggest_parks')
response = client.get(url, {'search': 'Test'})
url = reverse("parks:suggest_parks")
response = client.get(url, {"search": "Test"})
content = response.content.decode()
result_count = content.count('Test Park')
result_count = content.count("Test Park")
assert result_count == 8 # Verify limit is enforced
def test_search_json_format(self, client: Client):
@@ -123,61 +113,61 @@ class TestParkSearch:
name="Test Park",
status="OPERATING",
city="Test City",
state="Test State"
state="Test State",
)
url = reverse('parks:suggest_parks')
response = client.get(url, {'search': 'Test'})
url = reverse("parks:suggest_parks")
response = client.get(url, {"search": "Test"})
assert response.status_code == 200
data = response.json()
assert 'results' in data
assert len(data['results']) == 1
result = data['results'][0]
assert result['id'] == str(park.pk)
assert result['name'] == "Test Park"
assert result['status'] == "Operating"
assert result['location'] == park.formatted_location
assert result['url'] == reverse('parks:park_detail', kwargs={'slug': park.slug})
assert "results" in data
assert len(data["results"]) == 1
result = data["results"][0]
assert result["id"] == str(park.pk)
assert result["name"] == "Test Park"
assert result["status"] == "Operating"
assert result["location"] == park.formatted_location
assert result["url"] == reverse("parks:park_detail", kwargs={"slug": park.slug})
def test_empty_search_json(self, client: Client):
"""Test empty search returns empty results array"""
url = reverse('parks:suggest_parks')
response = client.get(url, {'search': ''})
url = reverse("parks:suggest_parks")
response = client.get(url, {"search": ""})
assert response.status_code == 200
data = response.json()
assert 'results' in data
assert len(data['results']) == 0
assert "results" in data
assert len(data["results"]) == 0
def test_search_format_validation(self, client: Client):
"""Test that all fields are properly formatted in search results"""
park = Park.objects.create(
Park.objects.create(
name="Test Park",
status="OPERATING",
city="Test City",
state="Test State",
country="Test Country"
country="Test Country",
)
expected_fields = {'id', 'name', 'status', 'location', 'url'}
url = reverse('parks:suggest_parks')
response = client.get(url, {'search': 'Test'})
expected_fields = {"id", "name", "status", "location", "url"}
url = reverse("parks:suggest_parks")
response = client.get(url, {"search": "Test"})
data = response.json()
result = data['results'][0]
result = data["results"][0]
# Check all expected fields are present
assert set(result.keys()) == expected_fields
# Check field types
assert isinstance(result['id'], str)
assert isinstance(result['name'], str)
assert isinstance(result['status'], str)
assert isinstance(result['location'], str)
assert isinstance(result['url'], str)
assert isinstance(result["id"], str)
assert isinstance(result["name"], str)
assert isinstance(result["status"], str)
assert isinstance(result["location"], str)
assert isinstance(result["url"], str)
# Check formatted location includes city and state
assert 'Test City' in result['location']
assert 'Test State' in result['location']
assert "Test City" in result["location"]
assert "Test State" in result["location"]