Add comprehensive tests for Parks API and models

- Implemented extensive test cases for the Parks API, covering endpoints for listing, retrieving, creating, updating, and deleting parks.
- Added tests for filtering, searching, and ordering parks in the API.
- Created tests for error handling in the API, including malformed JSON and unsupported methods.
- Developed model tests for Park, ParkArea, Company, and ParkReview models, ensuring validation and constraints are enforced.
- Introduced utility mixins for API and model testing to streamline assertions and enhance test readability.
- Included integration tests to validate complete workflows involving park creation, retrieval, updating, and deletion.
This commit is contained in:
pacnpal
2025-08-17 19:36:20 -04:00
parent 17228e9935
commit c26414ff74
210 changed files with 24155 additions and 833 deletions

349
rides/api/serializers.py Normal file
View File

@@ -0,0 +1,349 @@
"""
Serializers for Rides API following Django styleguide patterns.
"""
from rest_framework import serializers
from ..models import Ride, RideModel, Company
class RideModelOutputSerializer(serializers.Serializer):
"""Output serializer for ride model data."""
id = serializers.IntegerField()
name = serializers.CharField()
description = serializers.CharField()
category = serializers.CharField()
manufacturer = serializers.SerializerMethodField()
def get_manufacturer(self, obj):
if obj.manufacturer:
return {
'id': obj.manufacturer.id,
'name': obj.manufacturer.name,
'slug': obj.manufacturer.slug
}
return None
class RideParkOutputSerializer(serializers.Serializer):
"""Output serializer for ride's park data."""
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
class RideListOutputSerializer(serializers.Serializer):
"""Output serializer for ride list view."""
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
category = serializers.CharField()
status = serializers.CharField()
description = serializers.CharField()
# Park info
park = RideParkOutputSerializer()
# Statistics
average_rating = serializers.DecimalField(max_digits=3, decimal_places=2, allow_null=True)
capacity_per_hour = serializers.IntegerField(allow_null=True)
# Dates
opening_date = serializers.DateField(allow_null=True)
closing_date = serializers.DateField(allow_null=True)
# Metadata
created_at = serializers.DateTimeField()
updated_at = serializers.DateTimeField()
class RideDetailOutputSerializer(serializers.Serializer):
"""Output serializer for ride detail view."""
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
category = serializers.CharField()
status = serializers.CharField()
post_closing_status = serializers.CharField(allow_null=True)
description = serializers.CharField()
# Park info
park = RideParkOutputSerializer()
park_area = serializers.SerializerMethodField()
# Dates
opening_date = serializers.DateField(allow_null=True)
closing_date = serializers.DateField(allow_null=True)
status_since = serializers.DateField(allow_null=True)
# Physical specs
min_height_in = serializers.IntegerField(allow_null=True)
max_height_in = serializers.IntegerField(allow_null=True)
capacity_per_hour = serializers.IntegerField(allow_null=True)
ride_duration_seconds = serializers.IntegerField(allow_null=True)
# Statistics
average_rating = serializers.DecimalField(max_digits=3, decimal_places=2, allow_null=True)
# Companies
manufacturer = serializers.SerializerMethodField()
designer = serializers.SerializerMethodField()
# Model
ride_model = RideModelOutputSerializer(allow_null=True)
# Metadata
created_at = serializers.DateTimeField()
updated_at = serializers.DateTimeField()
def get_park_area(self, obj):
if obj.park_area:
return {
'id': obj.park_area.id,
'name': obj.park_area.name,
'slug': obj.park_area.slug
}
return None
def get_manufacturer(self, obj):
if obj.manufacturer:
return {
'id': obj.manufacturer.id,
'name': obj.manufacturer.name,
'slug': obj.manufacturer.slug
}
return None
def get_designer(self, obj):
if obj.designer:
return {
'id': obj.designer.id,
'name': obj.designer.name,
'slug': obj.designer.slug
}
return None
class RideCreateInputSerializer(serializers.Serializer):
"""Input serializer for creating rides."""
name = serializers.CharField(max_length=255)
description = serializers.CharField(allow_blank=True, default="")
category = serializers.ChoiceField(choices=Ride.CATEGORY_CHOICES)
status = serializers.ChoiceField(
choices=Ride.STATUS_CHOICES,
default="OPERATING"
)
# Required park
park_id = serializers.IntegerField()
# Optional area
park_area_id = serializers.IntegerField(required=False, allow_null=True)
# Optional dates
opening_date = serializers.DateField(required=False, allow_null=True)
closing_date = serializers.DateField(required=False, allow_null=True)
status_since = serializers.DateField(required=False, allow_null=True)
# Optional specs
min_height_in = serializers.IntegerField(
required=False,
allow_null=True,
min_value=30,
max_value=90
)
max_height_in = serializers.IntegerField(
required=False,
allow_null=True,
min_value=30,
max_value=90
)
capacity_per_hour = serializers.IntegerField(
required=False,
allow_null=True,
min_value=1
)
ride_duration_seconds = serializers.IntegerField(
required=False,
allow_null=True,
min_value=1
)
# Optional companies
manufacturer_id = serializers.IntegerField(required=False, allow_null=True)
designer_id = serializers.IntegerField(required=False, allow_null=True)
# Optional model
ride_model_id = serializers.IntegerField(required=False, allow_null=True)
def validate(self, data):
"""Cross-field validation."""
# Date validation
opening_date = data.get('opening_date')
closing_date = data.get('closing_date')
if opening_date and closing_date and closing_date < opening_date:
raise serializers.ValidationError(
"Closing date cannot be before opening date"
)
# Height validation
min_height = data.get('min_height_in')
max_height = data.get('max_height_in')
if min_height and max_height and min_height > max_height:
raise serializers.ValidationError(
"Minimum height cannot be greater than maximum height"
)
return data
class RideUpdateInputSerializer(serializers.Serializer):
"""Input serializer for updating rides."""
name = serializers.CharField(max_length=255, required=False)
description = serializers.CharField(allow_blank=True, required=False)
category = serializers.ChoiceField(choices=Ride.CATEGORY_CHOICES, required=False)
status = serializers.ChoiceField(choices=Ride.STATUS_CHOICES, required=False)
post_closing_status = serializers.ChoiceField(
choices=Ride.POST_CLOSING_STATUS_CHOICES,
required=False,
allow_null=True
)
# Park and area
park_id = serializers.IntegerField(required=False)
park_area_id = serializers.IntegerField(required=False, allow_null=True)
# Dates
opening_date = serializers.DateField(required=False, allow_null=True)
closing_date = serializers.DateField(required=False, allow_null=True)
status_since = serializers.DateField(required=False, allow_null=True)
# Specs
min_height_in = serializers.IntegerField(
required=False,
allow_null=True,
min_value=30,
max_value=90
)
max_height_in = serializers.IntegerField(
required=False,
allow_null=True,
min_value=30,
max_value=90
)
capacity_per_hour = serializers.IntegerField(
required=False,
allow_null=True,
min_value=1
)
ride_duration_seconds = serializers.IntegerField(
required=False,
allow_null=True,
min_value=1
)
# Companies
manufacturer_id = serializers.IntegerField(required=False, allow_null=True)
designer_id = serializers.IntegerField(required=False, allow_null=True)
# Model
ride_model_id = serializers.IntegerField(required=False, allow_null=True)
def validate(self, data):
"""Cross-field validation."""
# Date validation
opening_date = data.get('opening_date')
closing_date = data.get('closing_date')
if opening_date and closing_date and closing_date < opening_date:
raise serializers.ValidationError(
"Closing date cannot be before opening date"
)
# Height validation
min_height = data.get('min_height_in')
max_height = data.get('max_height_in')
if min_height and max_height and min_height > max_height:
raise serializers.ValidationError(
"Minimum height cannot be greater than maximum height"
)
return data
class RideFilterInputSerializer(serializers.Serializer):
"""Input serializer for ride filtering and search."""
# Search
search = serializers.CharField(required=False, allow_blank=True)
# Category filter
category = serializers.MultipleChoiceField(
choices=Ride.CATEGORY_CHOICES,
required=False
)
# Status filter
status = serializers.MultipleChoiceField(
choices=Ride.STATUS_CHOICES,
required=False
)
# Park filter
park_id = serializers.IntegerField(required=False)
park_slug = serializers.CharField(required=False, allow_blank=True)
# Company filters
manufacturer_id = serializers.IntegerField(required=False)
designer_id = serializers.IntegerField(required=False)
# Rating filter
min_rating = serializers.DecimalField(
max_digits=3,
decimal_places=2,
required=False,
min_value=1,
max_value=10
)
# Height filters
min_height_requirement = serializers.IntegerField(required=False)
max_height_requirement = serializers.IntegerField(required=False)
# Capacity filter
min_capacity = serializers.IntegerField(required=False)
# Ordering
ordering = serializers.ChoiceField(
choices=[
'name', '-name',
'opening_date', '-opening_date',
'average_rating', '-average_rating',
'capacity_per_hour', '-capacity_per_hour',
'created_at', '-created_at'
],
required=False,
default='name'
)
class RideStatsOutputSerializer(serializers.Serializer):
"""Output serializer for ride statistics."""
total_rides = serializers.IntegerField()
operating_rides = serializers.IntegerField()
closed_rides = serializers.IntegerField()
under_construction = serializers.IntegerField()
# By category
rides_by_category = serializers.DictField()
# Averages
average_rating = serializers.DecimalField(max_digits=3, decimal_places=2, allow_null=True)
average_capacity = serializers.DecimalField(max_digits=8, decimal_places=2, allow_null=True)
# Top manufacturers
top_manufacturers = serializers.ListField(child=serializers.DictField())
# Recently added
recently_added_count = serializers.IntegerField()