mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 06:31:09 -05:00
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:
378
tests/factories.py
Normal file
378
tests/factories.py
Normal 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
248
tests/test_factories.py
Normal 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
357
tests/test_parks_api.py
Normal 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
244
tests/test_parks_models.py
Normal 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)
|
||||
@@ -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
331
tests/test_utils.py
Normal 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
|
||||
Reference in New Issue
Block a user