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

378
tests/factories.py Normal file
View File

@@ -0,0 +1,378 @@
"""
Test factories for ThrillWiki models.
Following Django styleguide pattern for test data creation using factory_boy.
"""
import factory
from factory import fuzzy
from factory.django import DjangoModelFactory
from django.contrib.auth import get_user_model
from django.contrib.gis.geos import Point
from django.utils.text import slugify
from decimal import Decimal
import random
User = get_user_model()
class UserFactory(DjangoModelFactory):
"""Factory for creating User instances."""
class Meta:
model = User
django_get_or_create = ('username',)
username = factory.Sequence(lambda n: f"testuser{n}")
email = factory.LazyAttribute(lambda obj: f"{obj.username}@example.com")
first_name = factory.Faker('first_name')
last_name = factory.Faker('last_name')
is_active = True
is_staff = False
is_superuser = False
@factory.post_generation
def set_password(obj, create, extracted, **kwargs):
if create:
[PASSWORD-REMOVED] or 'testpass123'
obj.set_password(password)
obj.save()
class StaffUserFactory(UserFactory):
"""Factory for creating staff User instances."""
is_staff = True
class SuperUserFactory(UserFactory):
"""Factory for creating superuser instances."""
is_staff = True
is_superuser = True
class CompanyFactory(DjangoModelFactory):
"""Factory for creating Company instances."""
class Meta:
model = 'parks.Company'
django_get_or_create = ('name',)
name = factory.Faker('company')
slug = factory.LazyAttribute(lambda obj: slugify(obj.name))
description = factory.Faker('text', max_nb_chars=500)
website = factory.Faker('url')
founded_year = fuzzy.FuzzyInteger(1800, 2024)
roles = factory.LazyFunction(lambda: ['OPERATOR'])
@factory.post_generation
def multiple_roles(obj, create, extracted, **kwargs):
"""Optionally add multiple roles."""
if create and extracted:
obj.roles = extracted
obj.save()
class OperatorCompanyFactory(CompanyFactory):
"""Factory for companies that operate parks."""
roles = factory.LazyFunction(lambda: ['OPERATOR'])
class ManufacturerCompanyFactory(CompanyFactory):
"""Factory for companies that manufacture rides."""
roles = factory.LazyFunction(lambda: ['MANUFACTURER'])
class DesignerCompanyFactory(CompanyFactory):
"""Factory for companies that design rides."""
roles = factory.LazyFunction(lambda: ['DESIGNER'])
class LocationFactory(DjangoModelFactory):
"""Factory for creating Location instances."""
class Meta:
model = 'location.Location'
name = factory.Faker('city')
location_type = 'park'
latitude = fuzzy.FuzzyFloat(-90, 90)
longitude = fuzzy.FuzzyFloat(-180, 180)
street_address = factory.Faker('street_address')
city = factory.Faker('city')
state = factory.Faker('state')
country = factory.Faker('country')
postal_code = factory.Faker('postcode')
@factory.lazy_attribute
def point(self):
return Point(float(self.longitude), float(self.latitude))
class ParkFactory(DjangoModelFactory):
"""Factory for creating Park instances."""
class Meta:
model = 'parks.Park'
django_get_or_create = ('slug',)
name = factory.Sequence(lambda n: f"Test Park {n}")
slug = factory.LazyAttribute(lambda obj: slugify(obj.name))
description = factory.Faker('text', max_nb_chars=1000)
status = 'OPERATING'
opening_date = factory.Faker('date_between', start_date='-50y', end_date='today')
closing_date = None
operating_season = factory.Faker('sentence', nb_words=4)
size_acres = fuzzy.FuzzyDecimal(1, 1000, precision=2)
website = factory.Faker('url')
average_rating = fuzzy.FuzzyDecimal(1, 10, precision=2)
ride_count = fuzzy.FuzzyInteger(5, 100)
coaster_count = fuzzy.FuzzyInteger(1, 20)
# Relationships
operator = factory.SubFactory(OperatorCompanyFactory)
property_owner = factory.SubFactory(OperatorCompanyFactory)
@factory.post_generation
def create_location(obj, create, extracted, **kwargs):
"""Create a location for the park."""
if create:
LocationFactory(
content_object=obj,
name=obj.name,
location_type='park'
)
class ClosedParkFactory(ParkFactory):
"""Factory for creating closed parks."""
status = 'CLOSED_PERM'
closing_date = factory.Faker('date_between', start_date='-10y', end_date='today')
class ParkAreaFactory(DjangoModelFactory):
"""Factory for creating ParkArea instances."""
class Meta:
model = 'parks.ParkArea'
django_get_or_create = ('park', 'slug')
name = factory.Faker('word')
slug = factory.LazyAttribute(lambda obj: slugify(obj.name))
description = factory.Faker('text', max_nb_chars=500)
# Relationships
park = factory.SubFactory(ParkFactory)
class RideModelFactory(DjangoModelFactory):
"""Factory for creating RideModel instances."""
class Meta:
model = 'rides.RideModel'
django_get_or_create = ('name', 'manufacturer')
name = factory.Faker('word')
description = factory.Faker('text', max_nb_chars=500)
# Relationships
manufacturer = factory.SubFactory(ManufacturerCompanyFactory)
class RideFactory(DjangoModelFactory):
"""Factory for creating Ride instances."""
class Meta:
model = 'rides.Ride'
django_get_or_create = ('park', 'slug')
name = factory.Sequence(lambda n: f"Test Ride {n}")
slug = factory.LazyAttribute(lambda obj: slugify(obj.name))
description = factory.Faker('text', max_nb_chars=1000)
category = fuzzy.FuzzyChoice(['RC', 'WC', 'TR', 'WR', 'DR', 'CR', 'FR', 'SP'])
status = 'OPERATING'
opening_date = factory.Faker('date_between', start_date='-30y', end_date='today')
closing_date = None
min_height_in = fuzzy.FuzzyInteger(36, 48)
max_height_in = None
capacity_per_hour = fuzzy.FuzzyInteger(500, 3000)
ride_duration_seconds = fuzzy.FuzzyInteger(60, 300)
average_rating = fuzzy.FuzzyDecimal(1, 10, precision=2)
# Relationships
park = factory.SubFactory(ParkFactory)
manufacturer = factory.SubFactory(ManufacturerCompanyFactory)
designer = factory.SubFactory(DesignerCompanyFactory)
ride_model = factory.SubFactory(RideModelFactory)
park_area = factory.SubFactory(ParkAreaFactory, park=factory.SelfAttribute('..park'))
@factory.post_generation
def create_location(obj, create, extracted, **kwargs):
"""Create a location for the ride."""
if create:
LocationFactory(
content_object=obj,
name=obj.name,
location_type='ride'
)
class CoasterFactory(RideFactory):
"""Factory for creating roller coaster rides."""
category = fuzzy.FuzzyChoice(['RC', 'WC'])
min_height_in = fuzzy.FuzzyInteger(42, 54)
ride_duration_seconds = fuzzy.FuzzyInteger(90, 240)
class ParkReviewFactory(DjangoModelFactory):
"""Factory for creating ParkReview instances."""
class Meta:
model = 'parks.ParkReview'
django_get_or_create = ('park', 'user')
rating = fuzzy.FuzzyInteger(1, 10)
title = factory.Faker('sentence', nb_words=6)
content = factory.Faker('text', max_nb_chars=2000)
visit_date = factory.Faker('date_between', start_date='-2y', end_date='today')
is_published = True
moderation_notes = ''
# Relationships
park = factory.SubFactory(ParkFactory)
user = factory.SubFactory(UserFactory)
class RideReviewFactory(DjangoModelFactory):
"""Factory for creating RideReview instances."""
class Meta:
model = 'rides.RideReview'
django_get_or_create = ('ride', 'user')
rating = fuzzy.FuzzyInteger(1, 10)
title = factory.Faker('sentence', nb_words=6)
content = factory.Faker('text', max_nb_chars=2000)
visit_date = factory.Faker('date_between', start_date='-2y', end_date='today')
is_published = True
moderation_notes = ''
# Relationships
ride = factory.SubFactory(RideFactory)
user = factory.SubFactory(UserFactory)
class ModeratedReviewFactory(ParkReviewFactory):
"""Factory for creating moderated reviews."""
moderation_notes = factory.Faker('sentence')
moderated_by = factory.SubFactory(StaffUserFactory)
moderated_at = factory.Faker('date_time_between', start_date='-1y', end_date='now')
class EditSubmissionFactory(DjangoModelFactory):
"""Factory for creating EditSubmission instances."""
class Meta:
model = 'moderation.EditSubmission'
submission_type = 'UPDATE'
changes = factory.LazyFunction(lambda: {'name': 'Updated Name'})
status = 'PENDING'
notes = factory.Faker('sentence')
# Relationships
submitted_by = factory.SubFactory(UserFactory)
content_object = factory.SubFactory(ParkFactory)
# Trait mixins for common scenarios
class Traits:
"""Common trait mixins for factories."""
@staticmethod
def operating_park():
"""Trait for operating parks."""
return {
'status': 'OPERATING',
'closing_date': None
}
@staticmethod
def closed_park():
"""Trait for closed parks."""
return {
'status': 'CLOSED_PERM',
'closing_date': factory.Faker('date_between', start_date='-10y', end_date='today')
}
@staticmethod
def high_rated():
"""Trait for highly rated items."""
return {
'average_rating': fuzzy.FuzzyDecimal(8, 10, precision=2)
}
@staticmethod
def recent_submission():
"""Trait for recent submissions."""
return {
'submitted_at': factory.Faker('date_time_between', start_date='-7d', end_date='now')
}
# Specialized factories for testing scenarios
class TestScenarios:
"""Pre-configured factory combinations for common test scenarios."""
@staticmethod
def complete_park_with_rides(num_rides=5):
"""Create a complete park with rides and reviews."""
park = ParkFactory()
rides = [RideFactory(park=park) for _ in range(num_rides)]
park_review = ParkReviewFactory(park=park)
ride_reviews = [RideReviewFactory(ride=ride) for ride in rides[:2]]
return {
'park': park,
'rides': rides,
'park_review': park_review,
'ride_reviews': ride_reviews
}
@staticmethod
def moderation_workflow():
"""Create a complete moderation workflow scenario."""
user = UserFactory()
moderator = StaffUserFactory()
park = ParkFactory()
submission = EditSubmissionFactory(
submitted_by=user,
content_object=park
)
return {
'user': user,
'moderator': moderator,
'park': park,
'submission': submission
}
@staticmethod
def review_scenario():
"""Create a scenario with multiple reviews and ratings."""
park = ParkFactory()
users = [UserFactory() for _ in range(5)]
reviews = [ParkReviewFactory(park=park, user=user) for user in users]
return {
'park': park,
'users': users,
'reviews': reviews
}

248
tests/test_factories.py Normal file
View File

@@ -0,0 +1,248 @@
"""
Test cases demonstrating the factory pattern usage.
Following Django styleguide pattern for test data creation.
"""
from django.test import TestCase
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from .factories import (
UserFactory,
ParkFactory,
RideFactory,
ParkReviewFactory,
RideReviewFactory,
CompanyFactory,
TestScenarios,
Traits
)
User = get_user_model()
class FactoryTestCase(TestCase):
"""Test case demonstrating factory usage patterns."""
def test_user_factory_basic(self):
"""Test basic user factory functionality."""
# Build without saving to database
user = UserFactory.build()
self.assertIsInstance(user, User)
self.assertTrue(user.username.startswith('testuser'))
self.assertIn('@example.com', user.email)
# Create and save to database
user = UserFactory()
self.assertTrue(user.pk)
self.assertTrue(user.check_password('testpass123'))
def test_user_factory_with_custom_password(self):
"""Test user factory with custom password."""
user = UserFactory(set_password__password='custompass')
self.assertTrue(user.check_password('custompass'))
def test_staff_user_factory(self):
"""Test staff user factory."""
from .factories import StaffUserFactory
staff = StaffUserFactory()
self.assertTrue(staff.is_staff)
self.assertFalse(staff.is_superuser)
def test_company_factory_with_roles(self):
"""Test company factory with different roles."""
# Operator company
operator = CompanyFactory(roles=['OPERATOR'])
self.assertEqual(operator.roles, ['OPERATOR'])
# Manufacturer company
manufacturer = CompanyFactory(roles=['MANUFACTURER'])
self.assertEqual(manufacturer.roles, ['MANUFACTURER'])
# Multi-role company
multi_role = CompanyFactory(roles=['OPERATOR', 'MANUFACTURER'])
self.assertEqual(set(multi_role.roles), {'OPERATOR', 'MANUFACTURER'})
def test_park_factory_basic(self):
"""Test basic park factory functionality."""
park = ParkFactory.build()
self.assertTrue(park.name.startswith('Test Park'))
self.assertEqual(park.status, 'OPERATING')
self.assertIsNotNone(park.operator)
# Test that constraints are respected
self.assertGreaterEqual(park.average_rating, 1)
self.assertLessEqual(park.average_rating, 10)
self.assertGreaterEqual(park.ride_count, 0)
self.assertGreaterEqual(park.coaster_count, 0)
def test_park_factory_with_traits(self):
"""Test park factory with traits."""
# Closed park
closed_park = ParkFactory.build(**Traits.closed_park())
self.assertEqual(closed_park.status, 'CLOSED_PERM')
self.assertIsNotNone(closed_park.closing_date)
# High rated park
high_rated = ParkFactory.build(**Traits.high_rated())
self.assertGreaterEqual(high_rated.average_rating, 8)
def test_ride_factory_basic(self):
"""Test basic ride factory functionality."""
ride = RideFactory.build()
self.assertTrue(ride.name.startswith('Test Ride'))
self.assertEqual(ride.status, 'OPERATING')
self.assertIsNotNone(ride.park)
self.assertIsNotNone(ride.manufacturer)
# Test constraints
if ride.min_height_in and ride.max_height_in:
self.assertLessEqual(ride.min_height_in, ride.max_height_in)
self.assertGreaterEqual(ride.average_rating, 1)
self.assertLessEqual(ride.average_rating, 10)
def test_coaster_factory(self):
"""Test roller coaster specific factory."""
from .factories import CoasterFactory
coaster = CoasterFactory.build()
self.assertIn(coaster.category, ['RC', 'WC'])
self.assertGreaterEqual(coaster.min_height_in, 42)
self.assertLessEqual(coaster.min_height_in, 54)
def test_review_factories(self):
"""Test review factory functionality."""
park_review = ParkReviewFactory.build()
self.assertGreaterEqual(park_review.rating, 1)
self.assertLessEqual(park_review.rating, 10)
self.assertTrue(park_review.is_published)
ride_review = RideReviewFactory.build()
self.assertGreaterEqual(ride_review.rating, 1)
self.assertLessEqual(ride_review.rating, 10)
def test_sequence_functionality(self):
"""Test that sequences work correctly."""
users = [UserFactory.build() for _ in range(3)]
usernames = [user.username for user in users]
# Should have unique usernames
self.assertEqual(len(set(usernames)), 3)
self.assertTrue(all('testuser' in username for username in usernames))
def test_lazy_attributes(self):
"""Test lazy attribute functionality."""
park = ParkFactory.build(name="Custom Park Name")
self.assertEqual(park.slug, "custom-park-name")
def test_fuzzy_fields(self):
"""Test fuzzy field generation."""
parks = [ParkFactory.build() for _ in range(10)]
# Should have varied values
ratings = [p.average_rating for p in parks if p.average_rating]
self.assertGreater(len(set(ratings)), 1) # Should have different ratings
ride_counts = [p.ride_count for p in parks if p.ride_count]
self.assertGreater(len(set(ride_counts)), 1) # Should have different counts
class TestScenariosTestCase(TestCase):
"""Test case for pre-configured test scenarios."""
def test_build_only_scenario(self):
"""Test scenarios using build() to avoid database operations."""
# Create minimal scenario data using build()
park = ParkFactory.build()
rides = [RideFactory.build(park=park) for _ in range(3)]
# Verify the scenario
self.assertEqual(len(rides), 3)
for ride in rides:
self.assertEqual(ride.park, park)
def test_review_scenario_build(self):
"""Test review scenario using build()."""
park = ParkFactory.build()
users = [UserFactory.build() for _ in range(3)]
reviews = [ParkReviewFactory.build(park=park, user=user) for user in users]
# Verify scenario
self.assertEqual(len(reviews), 3)
for review in reviews:
self.assertEqual(review.park, park)
self.assertIn(review.user, users)
class FactoryValidationTestCase(TestCase):
"""Test that factories respect model validation."""
def test_rating_constraints(self):
"""Test that rating constraints are respected."""
# Valid ratings
valid_review = ParkReviewFactory.build(rating=5)
self.assertEqual(valid_review.rating, 5)
# Edge cases
min_review = ParkReviewFactory.build(rating=1)
self.assertEqual(min_review.rating, 1)
max_review = ParkReviewFactory.build(rating=10)
self.assertEqual(max_review.rating, 10)
def test_date_constraints(self):
"""Test that date constraints are logical."""
from datetime import date, timedelta
# Valid dates
park = ParkFactory.build(
opening_date=date(2020, 1, 1),
closing_date=date(2023, 12, 31)
)
# Verify opening is before closing
if park.opening_date and park.closing_date:
self.assertLessEqual(park.opening_date, park.closing_date)
def test_height_requirements(self):
"""Test that height requirements are logical."""
ride = RideFactory.build(
min_height_in=48,
max_height_in=72
)
if ride.min_height_in and ride.max_height_in:
self.assertLessEqual(ride.min_height_in, ride.max_height_in)
class FactoryPerformanceTestCase(TestCase):
"""Test factory performance and bulk operations."""
def test_bulk_creation_build(self):
"""Test bulk creation using build() for performance."""
import time
start_time = time.time()
users = [UserFactory.build() for _ in range(100)]
build_time = time.time() - start_time
self.assertEqual(len(users), 100)
self.assertLess(build_time, 1.0) # Should be fast with build()
# Verify uniqueness
usernames = [user.username for user in users]
self.assertEqual(len(set(usernames)), 100)
def test_related_object_creation(self):
"""Test creation of objects with relationships."""
# Build park with relationships
park = ParkFactory.build()
# Verify relationships exist
self.assertIsNotNone(park.operator)
self.assertIsNotNone(park.property_owner)
# Build ride with park relationship
ride = RideFactory.build(park=park)
self.assertEqual(ride.park, park)

357
tests/test_parks_api.py Normal file
View File

@@ -0,0 +1,357 @@
"""
Test cases for Parks API following Django styleguide patterns.
Comprehensive API endpoint testing with proper naming conventions.
"""
import json
from decimal import Decimal
from datetime import date
from django.test import TestCase
from django.urls import reverse
from rest_framework.test import APITestCase, APIClient
from rest_framework import status
from parks.models import Park, Company
from accounts.models import User
from tests.factories import (
UserFactory, StaffUserFactory, CompanyFactory, ParkFactory,
TestScenarios
)
class TestParkListApi(APITestCase):
"""Test cases for Park list API endpoint."""
def setUp(self):
"""Set up test data."""
self.client = APIClient()
self.user = UserFactory()
self.company = CompanyFactory(roles=['OPERATOR'])
self.parks = [
ParkFactory(operator=self.company, name="Park A"),
ParkFactory(operator=self.company, name="Park B", status='CLOSED_TEMP'),
ParkFactory(operator=self.company, name="Park C")
]
self.url = reverse('parks_api:park-list')
def test__park_list_api__unauthenticated_user__can_access(self):
"""Test that unauthenticated users can access park list."""
response = self.client.get(self.url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['status'], 'success')
self.assertIsInstance(response.data['data'], list)
def test__park_list_api__returns_all_parks__in_correct_format(self):
"""Test that park list returns all parks in correct format."""
response = self.client.get(self.url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data['data']), 3)
# Check response structure
park_data = response.data['data'][0]
expected_fields = [
'id', 'name', 'slug', 'status', 'description',
'average_rating', 'coaster_count', 'ride_count',
'location', 'operator', 'created_at', 'updated_at'
]
for field in expected_fields:
self.assertIn(field, park_data)
def test__park_list_api__with_status_filter__returns_filtered_results(self):
"""Test that status filter works correctly."""
response = self.client.get(self.url, {'status': 'OPERATING'})
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Should return only operating parks (2 out of 3)
operating_parks = [p for p in response.data['data'] if p['status'] == 'OPERATING']
self.assertEqual(len(operating_parks), 2)
def test__park_list_api__with_search_query__returns_matching_results(self):
"""Test that search functionality works correctly."""
response = self.client.get(self.url, {'search': 'Park A'})
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data['data']), 1)
self.assertEqual(response.data['data'][0]['name'], 'Park A')
def test__park_list_api__with_ordering__returns_ordered_results(self):
"""Test that ordering functionality works correctly."""
response = self.client.get(self.url, {'ordering': '-name'})
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Should be ordered by name descending (C, B, A)
names = [park['name'] for park in response.data['data']]
self.assertEqual(names, ['Park C', 'Park B', 'Park A'])
class TestParkDetailApi(APITestCase):
"""Test cases for Park detail API endpoint."""
def setUp(self):
"""Set up test data."""
self.client = APIClient()
self.company = CompanyFactory(roles=['OPERATOR'])
self.park = ParkFactory(operator=self.company)
self.url = reverse('parks_api:park-detail', kwargs={'slug': self.park.slug})
def test__park_detail_api__with_valid_slug__returns_park_details(self):
"""Test that park detail API returns correct park information."""
response = self.client.get(self.url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['status'], 'success')
park_data = response.data['data']
self.assertEqual(park_data['id'], self.park.id)
self.assertEqual(park_data['name'], self.park.name)
self.assertEqual(park_data['slug'], self.park.slug)
# Check that detailed fields are included
detailed_fields = [
'opening_date', 'closing_date', 'operating_season',
'size_acres', 'website', 'areas', 'operator', 'property_owner'
]
for field in detailed_fields:
self.assertIn(field, park_data)
def test__park_detail_api__with_invalid_slug__returns_404(self):
"""Test that invalid slug returns 404 error."""
invalid_url = reverse('parks_api:park-detail', kwargs={'slug': 'nonexistent'})
response = self.client.get(invalid_url)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertEqual(response.data['status'], 'error')
self.assertEqual(response.data['error']['code'], 'NOT_FOUND')
class TestParkCreateApi(APITestCase):
"""Test cases for Park creation API endpoint."""
def setUp(self):
"""Set up test data."""
self.client = APIClient()
self.user = UserFactory()
self.staff_user = StaffUserFactory()
self.company = CompanyFactory(roles=['OPERATOR'])
self.url = reverse('parks_api:park-list') # POST to list endpoint
self.valid_park_data = {
'name': 'New Test Park',
'description': 'A test park for API testing',
'operator_id': self.company.id,
'status': 'OPERATING'
}
def test__park_create_api__unauthenticated_user__returns_401(self):
"""Test that unauthenticated users cannot create parks."""
response = self.client.post(self.url, self.valid_park_data)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test__park_create_api__authenticated_user__can_create_park(self):
"""Test that authenticated users can create parks."""
self.client.force_authenticate(user=self.user)
response = self.client.post(self.url, self.valid_park_data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['status'], 'success')
# Verify park was created
park_data = response.data['data']
self.assertEqual(park_data['name'], 'New Test Park')
self.assertTrue(Park.objects.filter(name='New Test Park').exists())
def test__park_create_api__with_invalid_data__returns_validation_errors(self):
"""Test that invalid data returns proper validation errors."""
self.client.force_authenticate(user=self.user)
invalid_data = self.valid_park_data.copy()
invalid_data['name'] = '' # Empty name should be invalid
response = self.client.post(self.url, invalid_data)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data['status'], 'error')
self.assertIn('name', response.data['error']['details'])
def test__park_create_api__with_invalid_date_range__returns_validation_error(self):
"""Test that invalid date ranges are caught by validation."""
self.client.force_authenticate(user=self.user)
invalid_data = self.valid_park_data.copy()
invalid_data.update({
'opening_date': '2020-06-01',
'closing_date': '2020-05-01' # Before opening date
})
response = self.client.post(self.url, invalid_data)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('Closing date cannot be before opening date', str(response.data))
class TestParkUpdateApi(APITestCase):
"""Test cases for Park update API endpoint."""
def setUp(self):
"""Set up test data."""
self.client = APIClient()
self.user = UserFactory()
self.company = CompanyFactory(roles=['OPERATOR'])
self.park = ParkFactory(operator=self.company)
self.url = reverse('parks_api:park-detail', kwargs={'slug': self.park.slug})
def test__park_update_api__authenticated_user__can_update_park(self):
"""Test that authenticated users can update parks."""
self.client.force_authenticate(user=self.user)
update_data = {
'name': 'Updated Park Name',
'description': 'Updated description'
}
response = self.client.patch(self.url, update_data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['status'], 'success')
# Verify park was updated
self.park.refresh_from_db()
self.assertEqual(self.park.name, 'Updated Park Name')
self.assertEqual(self.park.description, 'Updated description')
def test__park_update_api__with_invalid_data__returns_validation_errors(self):
"""Test that invalid update data returns validation errors."""
self.client.force_authenticate(user=self.user)
invalid_data = {
'opening_date': '2020-06-01',
'closing_date': '2020-05-01' # Invalid date range
}
response = self.client.patch(self.url, invalid_data)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
class TestParkStatsApi(APITestCase):
"""Test cases for Park statistics API endpoint."""
def setUp(self):
"""Set up test data."""
self.client = APIClient()
self.company = CompanyFactory(roles=['OPERATOR'])
# Create parks with different statuses
ParkFactory(operator=self.company, status='OPERATING')
ParkFactory(operator=self.company, status='OPERATING')
ParkFactory(operator=self.company, status='CLOSED_TEMP')
self.url = reverse('parks_api:park-stats')
def test__park_stats_api__returns_correct_statistics(self):
"""Test that park statistics API returns correct data."""
response = self.client.get(self.url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['status'], 'success')
stats = response.data['data']
expected_fields = [
'total_parks', 'operating_parks', 'closed_parks',
'under_construction', 'average_rating', 'recently_added_count'
]
for field in expected_fields:
self.assertIn(field, stats)
# Verify counts are correct
self.assertEqual(stats['total_parks'], 3)
self.assertEqual(stats['operating_parks'], 2)
class TestParkApiErrorHandling(APITestCase):
"""Test cases for Park API error handling."""
def setUp(self):
"""Set up test data."""
self.client = APIClient()
def test__park_api__with_malformed_json__returns_parse_error(self):
"""Test that malformed JSON returns proper error."""
url = reverse('parks_api:park-list')
response = self.client.post(
url,
data='{"invalid": json}',
content_type='application/json'
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data['status'], 'error')
def test__park_api__with_unsupported_method__returns_405(self):
"""Test that unsupported HTTP methods return 405."""
park = ParkFactory()
url = reverse('parks_api:park-detail', kwargs={'slug': park.slug})
response = self.client.head(url) # HEAD not supported
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
class TestParkApiIntegration(APITestCase):
"""Integration tests for Park API with complete scenarios."""
def test__complete_park_workflow__create_update_retrieve_delete(self):
"""Test complete CRUD workflow for parks."""
user = UserFactory()
company = CompanyFactory(roles=['OPERATOR'])
self.client.force_authenticate(user=user)
# 1. Create park
create_data = {
'name': 'Integration Test Park',
'description': 'A park for integration testing',
'operator_id': company.id
}
create_response = self.client.post(
reverse('parks_api:park-list'),
create_data
)
self.assertEqual(create_response.status_code, status.HTTP_201_CREATED)
park_slug = create_response.data['data']['slug']
# 2. Retrieve park
detail_url = reverse('parks_api:park-detail', kwargs={'slug': park_slug})
retrieve_response = self.client.get(detail_url)
self.assertEqual(retrieve_response.status_code, status.HTTP_200_OK)
self.assertEqual(retrieve_response.data['data']['name'], 'Integration Test Park')
# 3. Update park
update_data = {'description': 'Updated integration test description'}
update_response = self.client.patch(detail_url, update_data)
self.assertEqual(update_response.status_code, status.HTTP_200_OK)
self.assertEqual(
update_response.data['data']['description'],
'Updated integration test description'
)
# 4. Delete park
delete_response = self.client.delete(detail_url)
self.assertEqual(delete_response.status_code, status.HTTP_204_NO_CONTENT)
# 5. Verify park is deleted
verify_response = self.client.get(detail_url)
self.assertEqual(verify_response.status_code, status.HTTP_404_NOT_FOUND)

244
tests/test_parks_models.py Normal file
View File

@@ -0,0 +1,244 @@
"""
Test cases for Parks models following Django styleguide patterns.
Uses proper naming conventions and comprehensive coverage.
"""
from decimal import Decimal
from datetime import date, timedelta
from django.test import TestCase
from django.core.exceptions import ValidationError
from django.db import IntegrityError
from django.utils import timezone
from parks.models import Park, ParkArea, Company, ParkReview
from accounts.models import User
from tests.factories import (
UserFactory, CompanyFactory, ParkFactory, ParkAreaFactory,
ParkReviewFactory, TestScenarios
)
class TestParkModel(TestCase):
"""Test cases for the Park model."""
def setUp(self):
"""Set up test data."""
self.company = CompanyFactory(roles=['OPERATOR'])
self.user = UserFactory()
def test__park_creation__with_valid_data__succeeds(self):
"""Test that park can be created with valid data."""
park = ParkFactory(operator=self.company)
self.assertIsInstance(park.id, int)
self.assertEqual(park.operator, self.company)
self.assertEqual(park.status, 'OPERATING')
self.assertIsNotNone(park.created_at)
def test__park_str_representation__returns_park_name(self):
"""Test that park string representation returns the name."""
park = ParkFactory(name="Test Park", operator=self.company)
self.assertEqual(str(park), "Test Park")
def test__park_slug__is_automatically_generated(self):
"""Test that park slug is generated from name."""
park = ParkFactory(name="Amazing Theme Park", operator=self.company)
self.assertEqual(park.slug, "amazing-theme-park")
def test__park_constraints__closing_date_after_opening__is_enforced(self):
"""Test that closing date must be after opening date."""
with self.assertRaises(IntegrityError):
ParkFactory(
operator=self.company,
opening_date=date(2020, 6, 1),
closing_date=date(2020, 5, 1) # Before opening
)
def test__park_constraints__positive_size__is_enforced(self):
"""Test that park size must be positive."""
with self.assertRaises(IntegrityError):
ParkFactory(operator=self.company, size_acres=-10)
def test__park_constraints__rating_range__is_enforced(self):
"""Test that rating must be within valid range."""
# Test upper bound
with self.assertRaises(IntegrityError):
ParkFactory(operator=self.company, average_rating=11)
# Test lower bound
with self.assertRaises(IntegrityError):
ParkFactory(operator=self.company, average_rating=0)
def test__park_constraints__coaster_count_lte_ride_count__is_enforced(self):
"""Test that coaster count cannot exceed ride count."""
with self.assertRaises(IntegrityError):
ParkFactory(
operator=self.company,
ride_count=5,
coaster_count=10 # More coasters than total rides
)
def test__park_relationships__operator_is_required(self):
"""Test that park must have an operator."""
with self.assertRaises(IntegrityError):
Park.objects.create(
name="Test Park",
slug="test-park"
# Missing operator
)
def test__park_relationships__property_owner_is_optional(self):
"""Test that property owner is optional."""
park = ParkFactory(operator=self.company, property_owner=None)
self.assertIsNone(park.property_owner)
self.assertEqual(park.operator, self.company)
class TestParkModelManagers(TestCase):
"""Test cases for Park model custom managers."""
def setUp(self):
"""Set up test data."""
self.company = CompanyFactory(roles=['OPERATOR'])
self.operating_park = ParkFactory(operator=self.company, status='OPERATING')
self.closed_park = ParkFactory(operator=self.company, status='CLOSED_TEMP')
def test__park_manager__operating_filter__returns_only_operating_parks(self):
"""Test that operating() filter returns only operating parks."""
operating_parks = Park.objects.operating()
self.assertEqual(operating_parks.count(), 1)
self.assertEqual(operating_parks.first(), self.operating_park)
def test__park_manager__closed_filter__returns_only_closed_parks(self):
"""Test that closed() filter returns only closed parks."""
closed_parks = Park.objects.closed()
self.assertEqual(closed_parks.count(), 1)
self.assertEqual(closed_parks.first(), self.closed_park)
def test__park_manager__optimized_for_list__includes_stats(self):
"""Test that optimized_for_list includes statistical annotations."""
parks = Park.objects.optimized_for_list()
park = parks.first()
# Check that statistical fields are available
self.assertTrue(hasattr(park, 'ride_count_calculated'))
self.assertTrue(hasattr(park, 'coaster_count_calculated'))
self.assertTrue(hasattr(park, 'area_count'))
class TestParkAreaModel(TestCase):
"""Test cases for the ParkArea model."""
def setUp(self):
"""Set up test data."""
self.company = CompanyFactory(roles=['OPERATOR'])
self.park = ParkFactory(operator=self.company)
def test__park_area_creation__with_valid_data__succeeds(self):
"""Test that park area can be created with valid data."""
area = ParkAreaFactory(park=self.park)
self.assertIsInstance(area.id, int)
self.assertEqual(area.park, self.park)
self.assertIsNotNone(area.created_at)
def test__park_area_unique_constraint__park_and_slug__is_enforced(self):
"""Test that park+slug combination must be unique."""
ParkAreaFactory(park=self.park, slug="test-area")
with self.assertRaises(IntegrityError):
ParkAreaFactory(park=self.park, slug="test-area") # Duplicate
class TestCompanyModel(TestCase):
"""Test cases for the Company model."""
def test__company_creation__with_valid_data__succeeds(self):
"""Test that company can be created with valid data."""
company = CompanyFactory()
self.assertIsInstance(company.id, int)
self.assertIsInstance(company.roles, list)
self.assertIsNotNone(company.created_at)
def test__company_manager__operators_filter__returns_only_operators(self):
"""Test that operators() filter works correctly."""
operator = CompanyFactory(roles=['OPERATOR'])
manufacturer = CompanyFactory(roles=['MANUFACTURER'])
operators = Company.objects.operators()
self.assertIn(operator, operators)
self.assertNotIn(manufacturer, operators)
class TestParkReviewModel(TestCase):
"""Test cases for the ParkReview model."""
def setUp(self):
"""Set up test data."""
self.company = CompanyFactory(roles=['OPERATOR'])
self.park = ParkFactory(operator=self.company)
self.user = UserFactory()
def test__park_review_creation__with_valid_data__succeeds(self):
"""Test that park review can be created with valid data."""
review = ParkReviewFactory(park=self.park, user=self.user)
self.assertIsInstance(review.id, int)
self.assertEqual(review.park, self.park)
self.assertEqual(review.user, self.user)
self.assertTrue(1 <= review.rating <= 10)
def test__park_review_constraints__rating_range__is_enforced(self):
"""Test that review rating must be within valid range."""
with self.assertRaises(IntegrityError):
ParkReviewFactory(park=self.park, user=self.user, rating=11)
def test__park_review_constraints__visit_date_not_future__is_enforced(self):
"""Test that visit date cannot be in the future."""
future_date = timezone.now().date() + timedelta(days=1)
with self.assertRaises(IntegrityError):
ParkReviewFactory(
park=self.park,
user=self.user,
visit_date=future_date
)
def test__park_review_unique_constraint__park_and_user__is_enforced(self):
"""Test that user can only review each park once."""
ParkReviewFactory(park=self.park, user=self.user)
with self.assertRaises(IntegrityError):
ParkReviewFactory(park=self.park, user=self.user) # Duplicate
class TestParkModelIntegration(TestCase):
"""Integration tests for Park model with related models."""
def test__complete_park_scenario__with_all_relationships__works_correctly(self):
"""Test complete park creation with all relationships."""
scenario = TestScenarios.complete_park_with_rides(num_rides=3)
park = scenario['park']
rides = scenario['rides']
areas = scenario['areas']
reviews = scenario['reviews']
# Verify all relationships are properly created
self.assertEqual(park.rides.count(), 3)
self.assertEqual(park.areas.count(), len(areas))
self.assertEqual(park.reviews.filter(is_published=True).count(), len(reviews))
# Test that park statistics are calculated correctly
parks_with_stats = Park.objects.with_complete_stats()
park_with_stats = parks_with_stats.get(id=park.id)
self.assertEqual(park_with_stats.ride_count_calculated, 3)
self.assertIsNotNone(park_with_stats.average_rating_calculated)

View File

@@ -13,7 +13,7 @@ def setup_django():
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, project_root)
os***REMOVED***iron.setdefault('DJANGO_SETTINGS_MODULE', 'thrillwiki.settings')
os***REMOVED***iron.setdefault('DJANGO_SETTINGS_MODULE', 'config.django.test')
django.setup()
# Use PostGIS for GeoDjango support

331
tests/test_utils.py Normal file
View File

@@ -0,0 +1,331 @@
"""
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