mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 06:31:09 -05:00
Refactor test utilities and enhance ASGI settings
- Cleaned up and standardized assertions in ApiTestMixin for API response validation. - Updated ASGI settings to use os.environ for setting the DJANGO_SETTINGS_MODULE. - Removed unused imports and improved formatting in settings.py. - Refactored URL patterns in urls.py for better readability and organization. - Enhanced view functions in views.py for consistency and clarity. - Added .flake8 configuration for linting and style enforcement. - Introduced type stubs for django-environ to improve type checking with Pylance.
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
# ThrillWiki Test Package
|
||||
# This file makes the tests directory a Python package for proper module discovery
|
||||
# This file makes the tests directory a Python package for proper module
|
||||
# discovery
|
||||
|
||||
@@ -23,9 +23,11 @@ def setup_page(page: Page):
|
||||
# Listen for console errors
|
||||
page.on(
|
||||
"console",
|
||||
lambda msg: print(f"Browser console {msg.type}: {msg.text}")
|
||||
if msg.type == "error"
|
||||
else None,
|
||||
lambda msg: (
|
||||
print(f"Browser console {msg.type}: {msg.text}")
|
||||
if msg.type == "error"
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
yield page
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import pytest
|
||||
from playwright.sync_api import expect, Page
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import pytest
|
||||
from playwright.sync_api import expect, Page
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import pytest
|
||||
from playwright.sync_api import expect, Page
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import pytest
|
||||
from playwright.sync_api import expect, Page
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import pytest
|
||||
from playwright.sync_api import expect, Page
|
||||
|
||||
|
||||
|
||||
@@ -9,62 +9,60 @@ 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',)
|
||||
|
||||
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')
|
||||
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'
|
||||
password = extracted 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')
|
||||
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')
|
||||
description = factory.Faker("text", max_nb_chars=500)
|
||||
website = factory.Faker("url")
|
||||
founded_year = fuzzy.FuzzyInteger(1800, 2024)
|
||||
roles = factory.LazyFunction(lambda: ['OPERATOR'])
|
||||
|
||||
roles = factory.LazyFunction(lambda: ["OPERATOR"])
|
||||
|
||||
@factory.post_generation
|
||||
def multiple_roles(obj, create, extracted, **kwargs):
|
||||
"""Optionally add multiple roles."""
|
||||
@@ -75,38 +73,38 @@ class CompanyFactory(DjangoModelFactory):
|
||||
|
||||
class OperatorCompanyFactory(CompanyFactory):
|
||||
"""Factory for companies that operate parks."""
|
||||
|
||||
roles = factory.LazyFunction(lambda: ['OPERATOR'])
|
||||
|
||||
roles = factory.LazyFunction(lambda: ["OPERATOR"])
|
||||
|
||||
|
||||
class ManufacturerCompanyFactory(CompanyFactory):
|
||||
"""Factory for companies that manufacture rides."""
|
||||
|
||||
roles = factory.LazyFunction(lambda: ['MANUFACTURER'])
|
||||
|
||||
roles = factory.LazyFunction(lambda: ["MANUFACTURER"])
|
||||
|
||||
|
||||
class DesignerCompanyFactory(CompanyFactory):
|
||||
"""Factory for companies that design rides."""
|
||||
|
||||
roles = factory.LazyFunction(lambda: ['DESIGNER'])
|
||||
|
||||
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'
|
||||
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')
|
||||
|
||||
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))
|
||||
@@ -114,135 +112,129 @@ class LocationFactory(DjangoModelFactory):
|
||||
|
||||
class ParkFactory(DjangoModelFactory):
|
||||
"""Factory for creating Park instances."""
|
||||
|
||||
|
||||
class Meta:
|
||||
model = 'parks.Park'
|
||||
django_get_or_create = ('slug',)
|
||||
|
||||
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')
|
||||
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)
|
||||
operating_season = factory.Faker("sentence", nb_words=4)
|
||||
size_acres = fuzzy.FuzzyDecimal(1, 1000, precision=2)
|
||||
website = factory.Faker('url')
|
||||
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'
|
||||
)
|
||||
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')
|
||||
|
||||
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')
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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')
|
||||
|
||||
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')
|
||||
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'))
|
||||
|
||||
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'
|
||||
)
|
||||
LocationFactory(content_object=obj, name=obj.name, location_type="ride")
|
||||
|
||||
|
||||
class CoasterFactory(RideFactory):
|
||||
"""Factory for creating roller coaster rides."""
|
||||
|
||||
category = fuzzy.FuzzyChoice(['RC', 'WC'])
|
||||
|
||||
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')
|
||||
|
||||
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')
|
||||
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 = ''
|
||||
|
||||
moderation_notes = ""
|
||||
|
||||
# Relationships
|
||||
park = factory.SubFactory(ParkFactory)
|
||||
user = factory.SubFactory(UserFactory)
|
||||
@@ -250,18 +242,18 @@ class ParkReviewFactory(DjangoModelFactory):
|
||||
|
||||
class RideReviewFactory(DjangoModelFactory):
|
||||
"""Factory for creating RideReview instances."""
|
||||
|
||||
|
||||
class Meta:
|
||||
model = 'rides.RideReview'
|
||||
django_get_or_create = ('ride', 'user')
|
||||
|
||||
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')
|
||||
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 = ''
|
||||
|
||||
moderation_notes = ""
|
||||
|
||||
# Relationships
|
||||
ride = factory.SubFactory(RideFactory)
|
||||
user = factory.SubFactory(UserFactory)
|
||||
@@ -269,23 +261,23 @@ class RideReviewFactory(DjangoModelFactory):
|
||||
|
||||
class ModeratedReviewFactory(ParkReviewFactory):
|
||||
"""Factory for creating moderated reviews."""
|
||||
|
||||
moderation_notes = factory.Faker('sentence')
|
||||
|
||||
moderation_notes = factory.Faker("sentence")
|
||||
moderated_by = factory.SubFactory(StaffUserFactory)
|
||||
moderated_at = factory.Faker('date_time_between', start_date='-1y', end_date='now')
|
||||
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')
|
||||
|
||||
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)
|
||||
@@ -294,42 +286,41 @@ class EditSubmissionFactory(DjangoModelFactory):
|
||||
# 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
|
||||
}
|
||||
|
||||
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')
|
||||
"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)
|
||||
}
|
||||
|
||||
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')
|
||||
"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."""
|
||||
@@ -337,42 +328,35 @@ class TestScenarios:
|
||||
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
|
||||
"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
|
||||
)
|
||||
|
||||
|
||||
submission = EditSubmissionFactory(submitted_by=user, content_object=park)
|
||||
|
||||
return {
|
||||
'user': user,
|
||||
'moderator': moderator,
|
||||
'park': park,
|
||||
'submission': submission
|
||||
"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
|
||||
}
|
||||
|
||||
return {"park": park, "users": users, "reviews": reviews}
|
||||
|
||||
@@ -5,17 +5,15 @@ 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,
|
||||
UserFactory,
|
||||
ParkFactory,
|
||||
RideFactory,
|
||||
ParkReviewFactory,
|
||||
RideReviewFactory,
|
||||
CompanyFactory,
|
||||
TestScenarios,
|
||||
Traits
|
||||
Traits,
|
||||
)
|
||||
|
||||
User = get_user_model()
|
||||
@@ -23,151 +21,153 @@ 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)
|
||||
|
||||
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'))
|
||||
|
||||
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'))
|
||||
|
||||
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'])
|
||||
|
||||
operator = CompanyFactory(roles=["OPERATOR"])
|
||||
self.assertEqual(operator.roles, ["OPERATOR"])
|
||||
|
||||
# Manufacturer company
|
||||
manufacturer = CompanyFactory(roles=['MANUFACTURER'])
|
||||
self.assertEqual(manufacturer.roles, ['MANUFACTURER'])
|
||||
|
||||
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'})
|
||||
|
||||
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.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.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.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.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))
|
||||
|
||||
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
|
||||
|
||||
# Should have different ratings
|
||||
self.assertGreater(len(set(ratings)), 1)
|
||||
|
||||
ride_counts = [p.ride_count for p in parks if p.ride_count]
|
||||
self.assertGreater(len(set(ride_counts)), 1) # Should have different counts
|
||||
# Should have different counts
|
||||
self.assertGreater(len(set(ride_counts)), 1)
|
||||
|
||||
|
||||
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:
|
||||
@@ -177,72 +177,68 @@ class TestScenariosTestCase(TestCase):
|
||||
|
||||
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
|
||||
|
||||
from datetime import date
|
||||
|
||||
# Valid dates
|
||||
park = ParkFactory.build(
|
||||
opening_date=date(2020, 1, 1),
|
||||
closing_date=date(2023, 12, 31)
|
||||
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
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
@@ -3,355 +3,380 @@ 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 parks.models import Park
|
||||
from tests.factories import (
|
||||
UserFactory, StaffUserFactory, CompanyFactory, ParkFactory,
|
||||
TestScenarios
|
||||
UserFactory,
|
||||
StaffUserFactory,
|
||||
CompanyFactory,
|
||||
ParkFactory,
|
||||
)
|
||||
|
||||
|
||||
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.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")
|
||||
ParkFactory(operator=self.company, name="Park B", status="CLOSED_TEMP"),
|
||||
ParkFactory(operator=self.company, name="Park C"),
|
||||
]
|
||||
self.url = reverse('parks_api:park-list')
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
self.assertEqual(len(response.data["data"]), 3)
|
||||
|
||||
# Check response structure
|
||||
park_data = response.data['data'][0]
|
||||
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'
|
||||
"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):
|
||||
|
||||
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'})
|
||||
|
||||
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']
|
||||
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'})
|
||||
|
||||
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')
|
||||
|
||||
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'})
|
||||
|
||||
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'])
|
||||
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.company = CompanyFactory(roles=["OPERATOR"])
|
||||
self.park = ParkFactory(operator=self.company)
|
||||
self.url = reverse('parks_api:park-detail', kwargs={'slug': self.park.slug})
|
||||
|
||||
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)
|
||||
|
||||
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'
|
||||
"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'})
|
||||
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')
|
||||
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.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'
|
||||
"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')
|
||||
|
||||
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):
|
||||
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
|
||||
|
||||
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):
|
||||
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
|
||||
})
|
||||
|
||||
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))
|
||||
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.company = CompanyFactory(roles=["OPERATOR"])
|
||||
self.park = ParkFactory(operator=self.company)
|
||||
self.url = reverse('parks_api:park-detail', kwargs={'slug': self.park.slug})
|
||||
|
||||
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'
|
||||
"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')
|
||||
|
||||
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):
|
||||
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
|
||||
"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'])
|
||||
|
||||
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')
|
||||
|
||||
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']
|
||||
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'
|
||||
"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)
|
||||
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')
|
||||
|
||||
url = reverse("parks_api:park-list")
|
||||
|
||||
response = self.client.post(
|
||||
url,
|
||||
data='{"invalid": json}',
|
||||
content_type='application/json'
|
||||
url, data='{"invalid": json}', content_type="application/json"
|
||||
)
|
||||
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(response.data['status'], 'error')
|
||||
|
||||
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})
|
||||
|
||||
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'])
|
||||
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
|
||||
"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
|
||||
)
|
||||
|
||||
|
||||
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']
|
||||
|
||||
park_slug = create_response.data["data"]["slug"]
|
||||
|
||||
# 2. Retrieve park
|
||||
detail_url = reverse('parks_api:park-detail', kwargs={'slug': park_slug})
|
||||
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')
|
||||
|
||||
self.assertEqual(
|
||||
retrieve_response.data["data"]["name"], "Integration Test Park"
|
||||
)
|
||||
|
||||
# 3. Update park
|
||||
update_data = {'description': 'Updated integration test description'}
|
||||
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'
|
||||
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)
|
||||
|
||||
@@ -3,242 +3,247 @@ 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 parks.models import Park, Company
|
||||
from tests.factories import (
|
||||
UserFactory, CompanyFactory, ParkFactory, ParkAreaFactory,
|
||||
ParkReviewFactory, TestScenarios
|
||||
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.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.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
|
||||
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
|
||||
|
||||
# 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):
|
||||
|
||||
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
|
||||
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"
|
||||
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):
|
||||
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'))
|
||||
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.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'])
|
||||
|
||||
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.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):
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
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):
|
||||
|
||||
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']
|
||||
|
||||
|
||||
park = scenario["park"]
|
||||
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)
|
||||
|
||||
@@ -4,62 +4,63 @@ import sys
|
||||
import django
|
||||
from django.conf import settings
|
||||
from django.test.runner import DiscoverRunner
|
||||
import coverage # type: ignore
|
||||
import unittest
|
||||
import coverage # type: ignore
|
||||
|
||||
|
||||
def setup_django():
|
||||
"""Set up Django test environment"""
|
||||
# Add the project root directory to Python path
|
||||
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', 'config.django.test')
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.django.test")
|
||||
django.setup()
|
||||
|
||||
|
||||
# Use PostGIS for GeoDjango support
|
||||
settings.DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.contrib.gis.db.backends.postgis',
|
||||
'NAME': 'test_thrillwiki',
|
||||
'USER': 'postgres',
|
||||
'PASSWORD': 'postgres',
|
||||
'HOST': 'localhost',
|
||||
'PORT': '5432',
|
||||
'TEST': {
|
||||
'NAME': 'test_thrillwiki',
|
||||
}
|
||||
"default": {
|
||||
"ENGINE": "django.contrib.gis.db.backends.postgis",
|
||||
"NAME": "test_thrillwiki",
|
||||
"USER": "postgres",
|
||||
"PASSWORD": "postgres",
|
||||
"HOST": "localhost",
|
||||
"PORT": "5432",
|
||||
"TEST": {
|
||||
"NAME": "test_thrillwiki",
|
||||
},
|
||||
}
|
||||
}
|
||||
settings.DEBUG = False
|
||||
|
||||
|
||||
# Skip problematic migrations during tests
|
||||
settings.MIGRATION_MODULES = {
|
||||
'parks': None,
|
||||
'operators': None,
|
||||
'property_owners': None,
|
||||
'location': None,
|
||||
'rides': None,
|
||||
'reviews': None
|
||||
"parks": None,
|
||||
"operators": None,
|
||||
"property_owners": None,
|
||||
"location": None,
|
||||
"rides": None,
|
||||
"reviews": None,
|
||||
}
|
||||
|
||||
|
||||
class CustomTestRunner(DiscoverRunner):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.cov = coverage.Coverage(
|
||||
source=[
|
||||
'parks',
|
||||
'operators',
|
||||
'property_owners',
|
||||
'location',
|
||||
'rides',
|
||||
'reviews'
|
||||
"parks",
|
||||
"operators",
|
||||
"property_owners",
|
||||
"location",
|
||||
"rides",
|
||||
"reviews",
|
||||
],
|
||||
omit=[
|
||||
'*/migrations/*',
|
||||
'*/management/*',
|
||||
'*/admin.py',
|
||||
'*/apps.py',
|
||||
'manage.py'
|
||||
]
|
||||
"*/migrations/*",
|
||||
"*/management/*",
|
||||
"*/admin.py",
|
||||
"*/apps.py",
|
||||
"manage.py",
|
||||
],
|
||||
)
|
||||
self.cov.start()
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -67,65 +68,59 @@ class CustomTestRunner(DiscoverRunner):
|
||||
def setup_databases(self, **kwargs):
|
||||
"""Set up databases and ensure content types are created"""
|
||||
old_config = super().setup_databases(**kwargs)
|
||||
|
||||
|
||||
# Create necessary content types
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from parks.models import Park
|
||||
|
||||
ContentType.objects.get_or_create(
|
||||
app_label='parks',
|
||||
model='park'
|
||||
)
|
||||
|
||||
|
||||
ContentType.objects.get_or_create(app_label="parks", model="park")
|
||||
|
||||
return old_config
|
||||
|
||||
def run_suite(self, suite, **kwargs):
|
||||
results = super().run_suite(suite, **kwargs)
|
||||
self.cov.stop()
|
||||
self.cov.save()
|
||||
|
||||
|
||||
# Print coverage report
|
||||
print('\nCoverage Report:')
|
||||
print("\nCoverage Report:")
|
||||
self.cov.report()
|
||||
|
||||
|
||||
# Generate HTML coverage report
|
||||
html_dir = os.path.join('tests', 'coverage_html')
|
||||
html_dir = os.path.join("tests", "coverage_html")
|
||||
self.cov.html_report(directory=html_dir)
|
||||
print(f'\nDetailed HTML coverage report generated in: {html_dir}')
|
||||
|
||||
print(f"\nDetailed HTML coverage report generated in: {html_dir}")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def run_tests():
|
||||
# Set up Django
|
||||
setup_django()
|
||||
|
||||
# Initialize test runner
|
||||
test_runner = CustomTestRunner(
|
||||
verbosity=2,
|
||||
interactive=True,
|
||||
keepdb=True
|
||||
)
|
||||
test_runner = CustomTestRunner(verbosity=2, interactive=True, keepdb=True)
|
||||
|
||||
# Define test labels for discovery
|
||||
test_labels = [
|
||||
'parks.tests',
|
||||
'operators.tests',
|
||||
'property_owners.tests',
|
||||
'location.tests',
|
||||
'rides.tests',
|
||||
'reviews.tests'
|
||||
"parks.tests",
|
||||
"operators.tests",
|
||||
"property_owners.tests",
|
||||
"location.tests",
|
||||
"rides.tests",
|
||||
"reviews.tests",
|
||||
]
|
||||
|
||||
# Run tests and collect results
|
||||
failures = test_runner.run_tests(test_labels)
|
||||
|
||||
|
||||
return failures
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Create tests directory if it doesn't exist
|
||||
os.makedirs('tests', exist_ok=True)
|
||||
os.makedirs(os.path.join('tests', 'coverage_html'), exist_ok=True)
|
||||
|
||||
os.makedirs("tests", exist_ok=True)
|
||||
os.makedirs(os.path.join("tests", "coverage_html"), exist_ok=True)
|
||||
|
||||
# Run tests and exit with appropriate status code
|
||||
failures = run_tests()
|
||||
sys.exit(bool(failures))
|
||||
|
||||
@@ -3,9 +3,7 @@ Test utilities and helpers following Django styleguide patterns.
|
||||
Provides reusable testing patterns and assertion helpers.
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import Dict, Any, Optional, List
|
||||
from decimal import Decimal
|
||||
from datetime import date, datetime
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth import get_user_model
|
||||
@@ -17,19 +15,19 @@ User = get_user_model()
|
||||
|
||||
class ApiTestMixin:
|
||||
"""Mixin providing common API testing utilities."""
|
||||
|
||||
|
||||
def assertApiResponse(
|
||||
self,
|
||||
response,
|
||||
*,
|
||||
status_code: int = status.HTTP_200_OK,
|
||||
response_status: str = 'success',
|
||||
response_status: str = "success",
|
||||
data_type: Optional[type] = None,
|
||||
contains_fields: Optional[List[str]] = None
|
||||
contains_fields: Optional[List[str]] = None,
|
||||
):
|
||||
"""
|
||||
Assert API response has correct structure and content.
|
||||
|
||||
|
||||
Args:
|
||||
response: DRF Response object
|
||||
status_code: Expected HTTP status code
|
||||
@@ -38,65 +36,71 @@ class ApiTestMixin:
|
||||
contains_fields: List of fields that should be in response data
|
||||
"""
|
||||
self.assertEqual(response.status_code, status_code)
|
||||
self.assertEqual(response.data['status'], response_status)
|
||||
|
||||
if response_status == 'success':
|
||||
self.assertIn('data', response.data)
|
||||
|
||||
self.assertEqual(response.data["status"], response_status)
|
||||
|
||||
if response_status == "success":
|
||||
self.assertIn("data", response.data)
|
||||
|
||||
if data_type:
|
||||
self.assertIsInstance(response.data['data'], data_type)
|
||||
|
||||
if contains_fields and response.data['data']:
|
||||
data = response.data['data']
|
||||
self.assertIsInstance(response.data["data"], data_type)
|
||||
|
||||
if contains_fields and response.data["data"]:
|
||||
data = response.data["data"]
|
||||
# Handle both single objects and lists
|
||||
if isinstance(data, list) and data:
|
||||
data = data[0]
|
||||
|
||||
|
||||
if isinstance(data, dict):
|
||||
for field in contains_fields:
|
||||
self.assertIn(field, data, f"Field '{field}' missing from response data")
|
||||
|
||||
elif response_status == 'error':
|
||||
self.assertIn('error', response.data)
|
||||
self.assertIn('code', response.data['error'])
|
||||
self.assertIn('message', response.data['error'])
|
||||
|
||||
self.assertIn(
|
||||
field,
|
||||
data,
|
||||
f"Field '{field}' missing from response data",
|
||||
)
|
||||
|
||||
elif response_status == "error":
|
||||
self.assertIn("error", response.data)
|
||||
self.assertIn("code", response.data["error"])
|
||||
self.assertIn("message", response.data["error"])
|
||||
|
||||
def assertApiError(
|
||||
self,
|
||||
response,
|
||||
*,
|
||||
status_code: int,
|
||||
error_code: Optional[str] = None,
|
||||
message_contains: Optional[str] = None
|
||||
message_contains: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
Assert API response is an error with specific characteristics.
|
||||
|
||||
|
||||
Args:
|
||||
response: DRF Response object
|
||||
status_code: Expected HTTP status code
|
||||
error_code: Expected error code in response
|
||||
message_contains: String that should be in error message
|
||||
"""
|
||||
self.assertApiResponse(response, status_code=status_code, response_status='error')
|
||||
|
||||
self.assertApiResponse(
|
||||
response, status_code=status_code, response_status="error"
|
||||
)
|
||||
|
||||
if error_code:
|
||||
self.assertEqual(response.data['error']['code'], error_code)
|
||||
|
||||
self.assertEqual(response.data["error"]["code"], error_code)
|
||||
|
||||
if message_contains:
|
||||
self.assertIn(message_contains, response.data['error']['message'])
|
||||
|
||||
self.assertIn(message_contains, response.data["error"]["message"])
|
||||
|
||||
def assertPaginatedResponse(
|
||||
self,
|
||||
response,
|
||||
*,
|
||||
expected_count: Optional[int] = None,
|
||||
has_next: Optional[bool] = None,
|
||||
has_previous: Optional[bool] = None
|
||||
has_previous: Optional[bool] = None,
|
||||
):
|
||||
"""
|
||||
Assert API response has correct pagination structure.
|
||||
|
||||
|
||||
Args:
|
||||
response: DRF Response object
|
||||
expected_count: Expected number of items in current page
|
||||
@@ -104,31 +108,38 @@ class ApiTestMixin:
|
||||
has_previous: Whether pagination should have previous page
|
||||
"""
|
||||
self.assertApiResponse(response, data_type=list)
|
||||
self.assertIn('pagination', response.data)
|
||||
|
||||
pagination = response.data['pagination']
|
||||
required_fields = ['page', 'page_size', 'total_pages', 'total_count', 'has_next', 'has_previous']
|
||||
|
||||
self.assertIn("pagination", response.data)
|
||||
|
||||
pagination = response.data["pagination"]
|
||||
required_fields = [
|
||||
"page",
|
||||
"page_size",
|
||||
"total_pages",
|
||||
"total_count",
|
||||
"has_next",
|
||||
"has_previous",
|
||||
]
|
||||
|
||||
for field in required_fields:
|
||||
self.assertIn(field, pagination)
|
||||
|
||||
|
||||
if expected_count is not None:
|
||||
self.assertEqual(len(response.data['data']), expected_count)
|
||||
|
||||
self.assertEqual(len(response.data["data"]), expected_count)
|
||||
|
||||
if has_next is not None:
|
||||
self.assertEqual(pagination['has_next'], has_next)
|
||||
|
||||
self.assertEqual(pagination["has_next"], has_next)
|
||||
|
||||
if has_previous is not None:
|
||||
self.assertEqual(pagination['has_previous'], has_previous)
|
||||
self.assertEqual(pagination["has_previous"], has_previous)
|
||||
|
||||
|
||||
class ModelTestMixin:
|
||||
"""Mixin providing common model testing utilities."""
|
||||
|
||||
|
||||
def assertModelFields(self, instance, expected_fields: Dict[str, Any]):
|
||||
"""
|
||||
Assert model instance has expected field values.
|
||||
|
||||
|
||||
Args:
|
||||
instance: Model instance
|
||||
expected_fields: Dict of field_name: expected_value
|
||||
@@ -136,116 +147,120 @@ class ModelTestMixin:
|
||||
for field_name, expected_value in expected_fields.items():
|
||||
actual_value = getattr(instance, field_name)
|
||||
self.assertEqual(
|
||||
actual_value,
|
||||
actual_value,
|
||||
expected_value,
|
||||
f"Field '{field_name}' expected {expected_value}, got {actual_value}"
|
||||
f"Field '{field_name}' expected {expected_value}, got {actual_value}",
|
||||
)
|
||||
|
||||
def assertModelValidation(self, model_class, invalid_data: Dict[str, Any], expected_errors: List[str]):
|
||||
|
||||
def assertModelValidation(
|
||||
self,
|
||||
model_class,
|
||||
invalid_data: Dict[str, Any],
|
||||
expected_errors: List[str],
|
||||
):
|
||||
"""
|
||||
Assert model validation catches expected errors.
|
||||
|
||||
|
||||
Args:
|
||||
model_class: Model class to test
|
||||
invalid_data: Data that should cause validation errors
|
||||
expected_errors: List of error messages that should be raised
|
||||
"""
|
||||
instance = model_class(**invalid_data)
|
||||
|
||||
|
||||
with self.assertRaises(Exception) as context:
|
||||
instance.full_clean()
|
||||
|
||||
|
||||
exception_str = str(context.exception)
|
||||
for expected_error in expected_errors:
|
||||
self.assertIn(expected_error, exception_str)
|
||||
|
||||
|
||||
def assertDatabaseConstraint(self, model_factory, invalid_data: Dict[str, Any]):
|
||||
"""
|
||||
Assert database constraint is enforced.
|
||||
|
||||
|
||||
Args:
|
||||
model_factory: Factory class for creating model instances
|
||||
invalid_data: Data that should violate database constraints
|
||||
"""
|
||||
from django.db import IntegrityError
|
||||
|
||||
|
||||
with self.assertRaises(IntegrityError):
|
||||
model_factory(**invalid_data)
|
||||
|
||||
|
||||
class FactoryTestMixin:
|
||||
"""Mixin providing factory testing utilities."""
|
||||
|
||||
|
||||
def assertFactoryCreatesValidInstance(self, factory_class, **kwargs):
|
||||
"""
|
||||
Assert factory creates valid model instance.
|
||||
|
||||
|
||||
Args:
|
||||
factory_class: Factory class to test
|
||||
**kwargs: Additional factory parameters
|
||||
"""
|
||||
instance = factory_class(**kwargs)
|
||||
|
||||
|
||||
# Basic assertions
|
||||
self.assertIsNotNone(instance.id)
|
||||
self.assertIsNotNone(instance.created_at)
|
||||
|
||||
|
||||
# Run full_clean to ensure validity
|
||||
instance.full_clean()
|
||||
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
def assertFactoryBatchCreation(self, factory_class, count: int = 5, **kwargs):
|
||||
"""
|
||||
Assert factory can create multiple valid instances.
|
||||
|
||||
|
||||
Args:
|
||||
factory_class: Factory class to test
|
||||
count: Number of instances to create
|
||||
**kwargs: Additional factory parameters
|
||||
"""
|
||||
instances = factory_class.create_batch(count, **kwargs)
|
||||
|
||||
|
||||
self.assertEqual(len(instances), count)
|
||||
|
||||
|
||||
for instance in instances:
|
||||
self.assertIsNotNone(instance.id)
|
||||
instance.full_clean()
|
||||
|
||||
|
||||
return instances
|
||||
|
||||
|
||||
class TimestampTestMixin:
|
||||
"""Mixin for testing timestamp-related functionality."""
|
||||
|
||||
|
||||
def assertRecentTimestamp(self, timestamp, tolerance_seconds: int = 5):
|
||||
"""
|
||||
Assert timestamp is recent (within tolerance).
|
||||
|
||||
|
||||
Args:
|
||||
timestamp: Timestamp to check
|
||||
tolerance_seconds: Allowed difference in seconds
|
||||
"""
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
now = timezone.now()
|
||||
if isinstance(timestamp, date) and not isinstance(timestamp, datetime):
|
||||
# Convert date to datetime for comparison
|
||||
timestamp = datetime.combine(timestamp, datetime.min.time())
|
||||
timestamp = timezone.make_aware(timestamp)
|
||||
|
||||
|
||||
time_diff = abs((now - timestamp).total_seconds())
|
||||
self.assertLessEqual(
|
||||
time_diff,
|
||||
time_diff,
|
||||
tolerance_seconds,
|
||||
f"Timestamp {timestamp} is not recent (diff: {time_diff}s)"
|
||||
f"Timestamp {timestamp} is not recent (diff: {time_diff}s)",
|
||||
)
|
||||
|
||||
|
||||
def assertTimestampOrder(self, earlier_timestamp, later_timestamp):
|
||||
"""
|
||||
Assert timestamps are in correct order.
|
||||
|
||||
|
||||
Args:
|
||||
earlier_timestamp: Should be before later_timestamp
|
||||
later_timestamp: Should be after earlier_timestamp
|
||||
@@ -253,17 +268,17 @@ class TimestampTestMixin:
|
||||
self.assertLess(
|
||||
earlier_timestamp,
|
||||
later_timestamp,
|
||||
f"Timestamps not in order: {earlier_timestamp} should be before {later_timestamp}"
|
||||
f"Timestamps not in order: {earlier_timestamp} should be before {later_timestamp}",
|
||||
)
|
||||
|
||||
|
||||
class GeographyTestMixin:
|
||||
"""Mixin for testing geography-related functionality."""
|
||||
|
||||
|
||||
def assertValidCoordinates(self, latitude: float, longitude: float):
|
||||
"""
|
||||
Assert coordinates are within valid ranges.
|
||||
|
||||
|
||||
Args:
|
||||
latitude: Latitude value
|
||||
longitude: Longitude value
|
||||
@@ -272,38 +287,36 @@ class GeographyTestMixin:
|
||||
self.assertLessEqual(latitude, 90, "Latitude above valid range")
|
||||
self.assertGreaterEqual(longitude, -180, "Longitude below valid range")
|
||||
self.assertLessEqual(longitude, 180, "Longitude above valid range")
|
||||
|
||||
|
||||
def assertCoordinateDistance(
|
||||
self,
|
||||
point1: tuple,
|
||||
point2: tuple,
|
||||
max_distance_km: float
|
||||
self, point1: tuple, point2: tuple, max_distance_km: float
|
||||
):
|
||||
"""
|
||||
Assert two geographic points are within specified distance.
|
||||
|
||||
|
||||
Args:
|
||||
point1: (latitude, longitude) tuple
|
||||
point2: (latitude, longitude) tuple
|
||||
max_distance_km: Maximum allowed distance in kilometers
|
||||
"""
|
||||
from math import radians, cos, sin, asin, sqrt
|
||||
|
||||
|
||||
lat1, lon1 = point1
|
||||
lat2, lon2 = point2
|
||||
|
||||
|
||||
# Haversine formula for great circle distance
|
||||
lat1, lon1, lat2, lon2 = map(radians, [lat1, lon1, lat2, lon2])
|
||||
dlat = lat2 - lat1
|
||||
dlon = lon2 - lon1
|
||||
a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2
|
||||
a = sin(dlat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(dlon / 2) ** 2
|
||||
c = 2 * asin(sqrt(a))
|
||||
distance_km = 6371 * c # Earth's radius in km
|
||||
|
||||
|
||||
self.assertLessEqual(
|
||||
distance_km,
|
||||
max_distance_km,
|
||||
f"Points are {distance_km:.2f}km apart, exceeds {max_distance_km}km"
|
||||
f"Points are {
|
||||
distance_km:.2f}km apart, exceeds {max_distance_km}km",
|
||||
)
|
||||
|
||||
|
||||
@@ -313,10 +326,9 @@ class EnhancedTestCase(
|
||||
FactoryTestMixin,
|
||||
TimestampTestMixin,
|
||||
GeographyTestMixin,
|
||||
TestCase
|
||||
TestCase,
|
||||
):
|
||||
"""Enhanced TestCase with all testing mixins."""
|
||||
pass
|
||||
|
||||
|
||||
class EnhancedAPITestCase(
|
||||
@@ -325,7 +337,6 @@ class EnhancedAPITestCase(
|
||||
FactoryTestMixin,
|
||||
TimestampTestMixin,
|
||||
GeographyTestMixin,
|
||||
APITestCase
|
||||
APITestCase,
|
||||
):
|
||||
"""Enhanced APITestCase with all testing mixins."""
|
||||
pass
|
||||
"""Enhanced APITestCase with all testing mixins."""
|
||||
|
||||
Reference in New Issue
Block a user