mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-02-06 13:54:42 -05:00
Add standardized HTMX conventions, interaction patterns, and migration guide for ThrillWiki UX
This commit is contained in:
6
backend/tests/serializers/__init__.py
Normal file
6
backend/tests/serializers/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""
|
||||
Serializer tests.
|
||||
|
||||
This module contains tests for DRF serializers to verify
|
||||
validation, field mapping, and custom logic.
|
||||
"""
|
||||
514
backend/tests/serializers/test_account_serializers.py
Normal file
514
backend/tests/serializers/test_account_serializers.py
Normal file
@@ -0,0 +1,514 @@
|
||||
"""
|
||||
Tests for Account serializers.
|
||||
|
||||
Following Django styleguide pattern: test__<context>__<action>__<expected_outcome>
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from django.test import TestCase, RequestFactory
|
||||
|
||||
from apps.accounts.serializers import (
|
||||
UserSerializer,
|
||||
LoginSerializer,
|
||||
SignupSerializer,
|
||||
PasswordResetSerializer,
|
||||
PasswordChangeSerializer,
|
||||
SocialProviderSerializer,
|
||||
)
|
||||
|
||||
from apps.api.v1.accounts.serializers import (
|
||||
UserProfileCreateInputSerializer,
|
||||
UserProfileUpdateInputSerializer,
|
||||
UserProfileOutputSerializer,
|
||||
TopListCreateInputSerializer,
|
||||
TopListUpdateInputSerializer,
|
||||
TopListOutputSerializer,
|
||||
TopListItemCreateInputSerializer,
|
||||
TopListItemUpdateInputSerializer,
|
||||
TopListItemOutputSerializer,
|
||||
)
|
||||
|
||||
from tests.factories import (
|
||||
UserFactory,
|
||||
StaffUserFactory,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestUserSerializer(TestCase):
|
||||
"""Tests for UserSerializer."""
|
||||
|
||||
def test__serialize__user__returns_expected_fields(self):
|
||||
"""Test serializing a user returns expected fields."""
|
||||
user = UserFactory()
|
||||
|
||||
serializer = UserSerializer(user)
|
||||
data = serializer.data
|
||||
|
||||
assert "id" in data
|
||||
assert "username" in data
|
||||
assert "email" in data
|
||||
assert "display_name" in data
|
||||
assert "date_joined" in data
|
||||
assert "is_active" in data
|
||||
assert "avatar_url" in data
|
||||
|
||||
def test__serialize__user_without_profile__returns_none_avatar(self):
|
||||
"""Test serializing user without profile returns None for avatar."""
|
||||
user = UserFactory()
|
||||
# Ensure no profile
|
||||
if hasattr(user, "profile"):
|
||||
user.profile.delete()
|
||||
|
||||
serializer = UserSerializer(user)
|
||||
data = serializer.data
|
||||
|
||||
assert data["avatar_url"] is None
|
||||
|
||||
def test__get_display_name__user_with_display_name__returns_display_name(self):
|
||||
"""Test get_display_name returns user's display name."""
|
||||
user = UserFactory()
|
||||
user.display_name = "John Doe"
|
||||
user.save()
|
||||
|
||||
serializer = UserSerializer(user)
|
||||
|
||||
# get_display_name calls the model method
|
||||
assert "display_name" in serializer.data
|
||||
|
||||
def test__meta__read_only_fields__includes_id_and_dates(self):
|
||||
"""Test Meta.read_only_fields includes id and date fields."""
|
||||
assert "id" in UserSerializer.Meta.read_only_fields
|
||||
assert "date_joined" in UserSerializer.Meta.read_only_fields
|
||||
assert "is_active" in UserSerializer.Meta.read_only_fields
|
||||
|
||||
|
||||
class TestLoginSerializer(TestCase):
|
||||
"""Tests for LoginSerializer."""
|
||||
|
||||
def test__validate__valid_credentials__returns_data(self):
|
||||
"""Test validation passes with valid credentials."""
|
||||
data = {
|
||||
"username": "testuser",
|
||||
"password": "testpassword123",
|
||||
}
|
||||
|
||||
serializer = LoginSerializer(data=data)
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
assert serializer.validated_data["username"] == "testuser"
|
||||
assert serializer.validated_data["password"] == "testpassword123"
|
||||
|
||||
def test__validate__email_as_username__returns_data(self):
|
||||
"""Test validation passes with email as username."""
|
||||
data = {
|
||||
"username": "user@example.com",
|
||||
"password": "testpassword123",
|
||||
}
|
||||
|
||||
serializer = LoginSerializer(data=data)
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
assert serializer.validated_data["username"] == "user@example.com"
|
||||
|
||||
def test__validate__missing_username__returns_error(self):
|
||||
"""Test validation fails with missing username."""
|
||||
data = {"password": "testpassword123"}
|
||||
|
||||
serializer = LoginSerializer(data=data)
|
||||
|
||||
assert not serializer.is_valid()
|
||||
assert "username" in serializer.errors
|
||||
|
||||
def test__validate__missing_password__returns_error(self):
|
||||
"""Test validation fails with missing password."""
|
||||
data = {"username": "testuser"}
|
||||
|
||||
serializer = LoginSerializer(data=data)
|
||||
|
||||
assert not serializer.is_valid()
|
||||
assert "password" in serializer.errors
|
||||
|
||||
def test__validate__empty_credentials__returns_error(self):
|
||||
"""Test validation fails with empty credentials."""
|
||||
data = {"username": "", "password": ""}
|
||||
|
||||
serializer = LoginSerializer(data=data)
|
||||
|
||||
assert not serializer.is_valid()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestSignupSerializer(TestCase):
|
||||
"""Tests for SignupSerializer."""
|
||||
|
||||
def test__validate__valid_data__returns_validated_data(self):
|
||||
"""Test validation passes with valid signup data."""
|
||||
data = {
|
||||
"username": "newuser",
|
||||
"email": "newuser@example.com",
|
||||
"display_name": "New User",
|
||||
"password": "SecurePass123!",
|
||||
"password_confirm": "SecurePass123!",
|
||||
}
|
||||
|
||||
serializer = SignupSerializer(data=data)
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
|
||||
def test__validate__mismatched_passwords__returns_error(self):
|
||||
"""Test validation fails with mismatched passwords."""
|
||||
data = {
|
||||
"username": "newuser",
|
||||
"email": "newuser@example.com",
|
||||
"display_name": "New User",
|
||||
"password": "SecurePass123!",
|
||||
"password_confirm": "DifferentPass456!",
|
||||
}
|
||||
|
||||
serializer = SignupSerializer(data=data)
|
||||
|
||||
assert not serializer.is_valid()
|
||||
assert "password_confirm" in serializer.errors
|
||||
|
||||
def test__validate_email__duplicate_email__returns_error(self):
|
||||
"""Test validation fails with duplicate email."""
|
||||
existing_user = UserFactory(email="existing@example.com")
|
||||
data = {
|
||||
"username": "newuser",
|
||||
"email": "existing@example.com",
|
||||
"display_name": "New User",
|
||||
"password": "SecurePass123!",
|
||||
"password_confirm": "SecurePass123!",
|
||||
}
|
||||
|
||||
serializer = SignupSerializer(data=data)
|
||||
|
||||
assert not serializer.is_valid()
|
||||
assert "email" in serializer.errors
|
||||
|
||||
def test__validate_email__case_insensitive__returns_error(self):
|
||||
"""Test email validation is case insensitive."""
|
||||
existing_user = UserFactory(email="existing@example.com")
|
||||
data = {
|
||||
"username": "newuser",
|
||||
"email": "EXISTING@EXAMPLE.COM",
|
||||
"display_name": "New User",
|
||||
"password": "SecurePass123!",
|
||||
"password_confirm": "SecurePass123!",
|
||||
}
|
||||
|
||||
serializer = SignupSerializer(data=data)
|
||||
|
||||
assert not serializer.is_valid()
|
||||
assert "email" in serializer.errors
|
||||
|
||||
def test__validate_username__duplicate_username__returns_error(self):
|
||||
"""Test validation fails with duplicate username."""
|
||||
existing_user = UserFactory(username="existinguser")
|
||||
data = {
|
||||
"username": "existinguser",
|
||||
"email": "new@example.com",
|
||||
"display_name": "New User",
|
||||
"password": "SecurePass123!",
|
||||
"password_confirm": "SecurePass123!",
|
||||
}
|
||||
|
||||
serializer = SignupSerializer(data=data)
|
||||
|
||||
assert not serializer.is_valid()
|
||||
assert "username" in serializer.errors
|
||||
|
||||
def test__validate__weak_password__returns_error(self):
|
||||
"""Test validation fails with weak password."""
|
||||
data = {
|
||||
"username": "newuser",
|
||||
"email": "newuser@example.com",
|
||||
"display_name": "New User",
|
||||
"password": "123", # Too weak
|
||||
"password_confirm": "123",
|
||||
}
|
||||
|
||||
serializer = SignupSerializer(data=data)
|
||||
|
||||
assert not serializer.is_valid()
|
||||
# Password validation error could be in 'password' or 'non_field_errors'
|
||||
assert "password" in serializer.errors or "non_field_errors" in serializer.errors
|
||||
|
||||
def test__create__valid_data__creates_user(self):
|
||||
"""Test create method creates user correctly."""
|
||||
data = {
|
||||
"username": "createuser",
|
||||
"email": "createuser@example.com",
|
||||
"display_name": "Create User",
|
||||
"password": "SecurePass123!",
|
||||
"password_confirm": "SecurePass123!",
|
||||
}
|
||||
|
||||
serializer = SignupSerializer(data=data)
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
|
||||
user = serializer.save()
|
||||
|
||||
assert user.username == "createuser"
|
||||
assert user.email == "createuser@example.com"
|
||||
assert user.check_password("SecurePass123!")
|
||||
|
||||
def test__meta__password_write_only__excludes_from_output(self):
|
||||
"""Test password field is write-only."""
|
||||
assert "password" in SignupSerializer.Meta.fields
|
||||
assert SignupSerializer.Meta.extra_kwargs.get("password", {}).get("write_only") is True
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestPasswordResetSerializer(TestCase):
|
||||
"""Tests for PasswordResetSerializer."""
|
||||
|
||||
def test__validate__valid_email__returns_normalized_email(self):
|
||||
"""Test validation normalizes email."""
|
||||
user = UserFactory(email="test@example.com")
|
||||
data = {"email": " TEST@EXAMPLE.COM "}
|
||||
|
||||
serializer = PasswordResetSerializer(data=data)
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
assert serializer.validated_data["email"] == "test@example.com"
|
||||
|
||||
def test__validate__nonexistent_email__still_valid(self):
|
||||
"""Test validation passes with nonexistent email (security)."""
|
||||
data = {"email": "nonexistent@example.com"}
|
||||
|
||||
serializer = PasswordResetSerializer(data=data)
|
||||
|
||||
# Should pass validation to prevent email enumeration
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
|
||||
def test__validate__existing_email__attaches_user(self):
|
||||
"""Test validation attaches user when email exists."""
|
||||
user = UserFactory(email="exists@example.com")
|
||||
data = {"email": "exists@example.com"}
|
||||
|
||||
serializer = PasswordResetSerializer(data=data)
|
||||
serializer.is_valid()
|
||||
|
||||
assert hasattr(serializer, "user")
|
||||
assert serializer.user == user
|
||||
|
||||
def test__validate__nonexistent_email__no_user_attached(self):
|
||||
"""Test validation doesn't attach user for nonexistent email."""
|
||||
data = {"email": "notfound@example.com"}
|
||||
|
||||
serializer = PasswordResetSerializer(data=data)
|
||||
serializer.is_valid()
|
||||
|
||||
assert not hasattr(serializer, "user")
|
||||
|
||||
@patch("apps.accounts.serializers.EmailService.send_email")
|
||||
def test__save__existing_user__sends_email(self, mock_send_email):
|
||||
"""Test save sends email for existing user."""
|
||||
user = UserFactory(email="reset@example.com")
|
||||
data = {"email": "reset@example.com"}
|
||||
|
||||
factory = RequestFactory()
|
||||
request = factory.post("/password-reset/")
|
||||
|
||||
serializer = PasswordResetSerializer(data=data, context={"request": request})
|
||||
serializer.is_valid()
|
||||
serializer.save()
|
||||
|
||||
# Email should be sent
|
||||
mock_send_email.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestPasswordChangeSerializer(TestCase):
|
||||
"""Tests for PasswordChangeSerializer."""
|
||||
|
||||
def test__validate__valid_data__returns_validated_data(self):
|
||||
"""Test validation passes with valid password change data."""
|
||||
user = UserFactory()
|
||||
user.set_password("OldPass123!")
|
||||
user.save()
|
||||
|
||||
factory = RequestFactory()
|
||||
request = factory.post("/password-change/")
|
||||
request.user = user
|
||||
|
||||
data = {
|
||||
"old_password": "OldPass123!",
|
||||
"new_password": "NewSecurePass456!",
|
||||
"new_password_confirm": "NewSecurePass456!",
|
||||
}
|
||||
|
||||
serializer = PasswordChangeSerializer(data=data, context={"request": request})
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
|
||||
def test__validate_old_password__incorrect__returns_error(self):
|
||||
"""Test validation fails with incorrect old password."""
|
||||
user = UserFactory()
|
||||
user.set_password("CorrectOldPass!")
|
||||
user.save()
|
||||
|
||||
factory = RequestFactory()
|
||||
request = factory.post("/password-change/")
|
||||
request.user = user
|
||||
|
||||
data = {
|
||||
"old_password": "WrongOldPass!",
|
||||
"new_password": "NewSecurePass456!",
|
||||
"new_password_confirm": "NewSecurePass456!",
|
||||
}
|
||||
|
||||
serializer = PasswordChangeSerializer(data=data, context={"request": request})
|
||||
|
||||
assert not serializer.is_valid()
|
||||
assert "old_password" in serializer.errors
|
||||
|
||||
def test__validate__mismatched_new_passwords__returns_error(self):
|
||||
"""Test validation fails with mismatched new passwords."""
|
||||
user = UserFactory()
|
||||
user.set_password("OldPass123!")
|
||||
user.save()
|
||||
|
||||
factory = RequestFactory()
|
||||
request = factory.post("/password-change/")
|
||||
request.user = user
|
||||
|
||||
data = {
|
||||
"old_password": "OldPass123!",
|
||||
"new_password": "NewSecurePass456!",
|
||||
"new_password_confirm": "DifferentPass789!",
|
||||
}
|
||||
|
||||
serializer = PasswordChangeSerializer(data=data, context={"request": request})
|
||||
|
||||
assert not serializer.is_valid()
|
||||
assert "new_password_confirm" in serializer.errors
|
||||
|
||||
def test__save__valid_data__changes_password(self):
|
||||
"""Test save changes the password."""
|
||||
user = UserFactory()
|
||||
user.set_password("OldPass123!")
|
||||
user.save()
|
||||
|
||||
factory = RequestFactory()
|
||||
request = factory.post("/password-change/")
|
||||
request.user = user
|
||||
|
||||
data = {
|
||||
"old_password": "OldPass123!",
|
||||
"new_password": "NewSecurePass456!",
|
||||
"new_password_confirm": "NewSecurePass456!",
|
||||
}
|
||||
|
||||
serializer = PasswordChangeSerializer(data=data, context={"request": request})
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
serializer.save()
|
||||
|
||||
user.refresh_from_db()
|
||||
assert user.check_password("NewSecurePass456!")
|
||||
|
||||
|
||||
class TestSocialProviderSerializer(TestCase):
|
||||
"""Tests for SocialProviderSerializer."""
|
||||
|
||||
def test__validate__valid_provider__returns_data(self):
|
||||
"""Test validation passes with valid provider data."""
|
||||
data = {
|
||||
"id": "google",
|
||||
"name": "Google",
|
||||
"login_url": "https://accounts.google.com/oauth/login",
|
||||
}
|
||||
|
||||
serializer = SocialProviderSerializer(data=data)
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
assert serializer.validated_data["id"] == "google"
|
||||
assert serializer.validated_data["name"] == "Google"
|
||||
|
||||
def test__validate__invalid_url__returns_error(self):
|
||||
"""Test validation fails with invalid URL."""
|
||||
data = {
|
||||
"id": "invalid",
|
||||
"name": "Invalid Provider",
|
||||
"login_url": "not-a-valid-url",
|
||||
}
|
||||
|
||||
serializer = SocialProviderSerializer(data=data)
|
||||
|
||||
assert not serializer.is_valid()
|
||||
assert "login_url" in serializer.errors
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestUserProfileOutputSerializer(TestCase):
|
||||
"""Tests for UserProfileOutputSerializer."""
|
||||
|
||||
def test__serialize__profile__returns_expected_fields(self):
|
||||
"""Test serializing profile returns expected fields."""
|
||||
user = UserFactory()
|
||||
|
||||
# Create mock profile
|
||||
mock_profile = Mock()
|
||||
mock_profile.user = user
|
||||
mock_profile.avatar = None
|
||||
|
||||
serializer = UserProfileOutputSerializer(mock_profile)
|
||||
|
||||
# Should include user nested serializer
|
||||
assert "user" in serializer.data or serializer.data is not None
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestUserProfileCreateInputSerializer(TestCase):
|
||||
"""Tests for UserProfileCreateInputSerializer."""
|
||||
|
||||
def test__meta__fields__includes_all_fields(self):
|
||||
"""Test Meta.fields is set to __all__."""
|
||||
assert UserProfileCreateInputSerializer.Meta.fields == "__all__"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestUserProfileUpdateInputSerializer(TestCase):
|
||||
"""Tests for UserProfileUpdateInputSerializer."""
|
||||
|
||||
def test__meta__user_read_only(self):
|
||||
"""Test user field is read-only for updates."""
|
||||
extra_kwargs = UserProfileUpdateInputSerializer.Meta.extra_kwargs
|
||||
assert extra_kwargs.get("user", {}).get("read_only") is True
|
||||
|
||||
|
||||
class TestTopListCreateInputSerializer(TestCase):
|
||||
"""Tests for TopListCreateInputSerializer."""
|
||||
|
||||
def test__meta__fields__includes_all_fields(self):
|
||||
"""Test Meta.fields is set to __all__."""
|
||||
assert TopListCreateInputSerializer.Meta.fields == "__all__"
|
||||
|
||||
|
||||
class TestTopListUpdateInputSerializer(TestCase):
|
||||
"""Tests for TopListUpdateInputSerializer."""
|
||||
|
||||
def test__meta__user_read_only(self):
|
||||
"""Test user field is read-only for updates."""
|
||||
extra_kwargs = TopListUpdateInputSerializer.Meta.extra_kwargs
|
||||
assert extra_kwargs.get("user", {}).get("read_only") is True
|
||||
|
||||
|
||||
class TestTopListItemCreateInputSerializer(TestCase):
|
||||
"""Tests for TopListItemCreateInputSerializer."""
|
||||
|
||||
def test__meta__fields__includes_all_fields(self):
|
||||
"""Test Meta.fields is set to __all__."""
|
||||
assert TopListItemCreateInputSerializer.Meta.fields == "__all__"
|
||||
|
||||
|
||||
class TestTopListItemUpdateInputSerializer(TestCase):
|
||||
"""Tests for TopListItemUpdateInputSerializer."""
|
||||
|
||||
def test__meta__top_list_not_read_only(self):
|
||||
"""Test top_list field is not read-only for updates."""
|
||||
extra_kwargs = TopListItemUpdateInputSerializer.Meta.extra_kwargs
|
||||
assert extra_kwargs.get("top_list", {}).get("read_only") is False
|
||||
477
backend/tests/serializers/test_park_serializers.py
Normal file
477
backend/tests/serializers/test_park_serializers.py
Normal file
@@ -0,0 +1,477 @@
|
||||
"""
|
||||
Tests for Park serializers.
|
||||
|
||||
Following Django styleguide pattern: test__<context>__<action>__<expected_outcome>
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, MagicMock
|
||||
from django.test import TestCase
|
||||
|
||||
from apps.api.v1.parks.serializers import (
|
||||
ParkPhotoOutputSerializer,
|
||||
ParkPhotoCreateInputSerializer,
|
||||
ParkPhotoUpdateInputSerializer,
|
||||
ParkPhotoListOutputSerializer,
|
||||
ParkPhotoApprovalInputSerializer,
|
||||
ParkPhotoStatsOutputSerializer,
|
||||
ParkPhotoSerializer,
|
||||
HybridParkSerializer,
|
||||
ParkSerializer,
|
||||
)
|
||||
|
||||
from tests.factories import (
|
||||
ParkFactory,
|
||||
ParkPhotoFactory,
|
||||
UserFactory,
|
||||
CloudflareImageFactory,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestParkPhotoOutputSerializer(TestCase):
|
||||
"""Tests for ParkPhotoOutputSerializer."""
|
||||
|
||||
def test__serialize__valid_photo__returns_all_fields(self):
|
||||
"""Test serializing a park photo returns all expected fields."""
|
||||
user = UserFactory()
|
||||
park = ParkFactory()
|
||||
image = CloudflareImageFactory()
|
||||
photo = ParkPhotoFactory(
|
||||
park=park,
|
||||
uploaded_by=user,
|
||||
image=image,
|
||||
caption="Test caption",
|
||||
alt_text="Test alt text",
|
||||
is_primary=True,
|
||||
is_approved=True,
|
||||
)
|
||||
|
||||
serializer = ParkPhotoOutputSerializer(photo)
|
||||
data = serializer.data
|
||||
|
||||
assert "id" in data
|
||||
assert data["caption"] == "Test caption"
|
||||
assert data["alt_text"] == "Test alt text"
|
||||
assert data["is_primary"] is True
|
||||
assert data["is_approved"] is True
|
||||
assert data["uploaded_by_username"] == user.username
|
||||
assert data["park_slug"] == park.slug
|
||||
assert data["park_name"] == park.name
|
||||
|
||||
def test__serialize__photo_with_image__returns_image_url(self):
|
||||
"""Test serializing a photo with image returns URL."""
|
||||
photo = ParkPhotoFactory()
|
||||
|
||||
serializer = ParkPhotoOutputSerializer(photo)
|
||||
data = serializer.data
|
||||
|
||||
assert "image_url" in data
|
||||
assert "image_variants" in data
|
||||
|
||||
def test__serialize__photo_without_image__returns_none_for_image_fields(self):
|
||||
"""Test serializing photo without image returns None for image fields."""
|
||||
photo = ParkPhotoFactory()
|
||||
photo.image = None
|
||||
photo.save()
|
||||
|
||||
serializer = ParkPhotoOutputSerializer(photo)
|
||||
data = serializer.data
|
||||
|
||||
assert data["image_url"] is None
|
||||
assert data["image_variants"] == {}
|
||||
|
||||
def test__get_file_size__photo_with_image__returns_file_size(self):
|
||||
"""Test get_file_size method returns file size."""
|
||||
photo = ParkPhotoFactory()
|
||||
|
||||
serializer = ParkPhotoOutputSerializer(photo)
|
||||
|
||||
# file_size comes from the model property
|
||||
assert "file_size" in serializer.data
|
||||
|
||||
def test__get_dimensions__photo_with_image__returns_dimensions(self):
|
||||
"""Test get_dimensions method returns [width, height]."""
|
||||
photo = ParkPhotoFactory()
|
||||
|
||||
serializer = ParkPhotoOutputSerializer(photo)
|
||||
|
||||
assert "dimensions" in serializer.data
|
||||
|
||||
def test__get_image_variants__photo_with_image__returns_variant_urls(self):
|
||||
"""Test get_image_variants returns all variant URLs."""
|
||||
image = CloudflareImageFactory()
|
||||
photo = ParkPhotoFactory(image=image)
|
||||
|
||||
serializer = ParkPhotoOutputSerializer(photo)
|
||||
data = serializer.data
|
||||
|
||||
if photo.image:
|
||||
variants = data["image_variants"]
|
||||
assert "thumbnail" in variants
|
||||
assert "medium" in variants
|
||||
assert "large" in variants
|
||||
assert "public" in variants
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestParkPhotoCreateInputSerializer(TestCase):
|
||||
"""Tests for ParkPhotoCreateInputSerializer."""
|
||||
|
||||
def test__serialize__valid_data__returns_expected_fields(self):
|
||||
"""Test serializing valid create data."""
|
||||
image = CloudflareImageFactory()
|
||||
data = {
|
||||
"image": image.pk,
|
||||
"caption": "New photo caption",
|
||||
"alt_text": "Description of the image",
|
||||
"is_primary": False,
|
||||
}
|
||||
|
||||
serializer = ParkPhotoCreateInputSerializer(data=data)
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
assert "caption" in serializer.validated_data
|
||||
assert "alt_text" in serializer.validated_data
|
||||
assert "is_primary" in serializer.validated_data
|
||||
|
||||
def test__validate__missing_required_fields__returns_error(self):
|
||||
"""Test validation fails with missing required fields."""
|
||||
data = {}
|
||||
|
||||
serializer = ParkPhotoCreateInputSerializer(data=data)
|
||||
|
||||
# image is required since it's not in read_only_fields
|
||||
assert not serializer.is_valid()
|
||||
|
||||
def test__meta__fields__includes_expected_fields(self):
|
||||
"""Test Meta.fields includes the expected input fields."""
|
||||
expected_fields = ["image", "caption", "alt_text", "is_primary"]
|
||||
|
||||
assert list(ParkPhotoCreateInputSerializer.Meta.fields) == expected_fields
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestParkPhotoUpdateInputSerializer(TestCase):
|
||||
"""Tests for ParkPhotoUpdateInputSerializer."""
|
||||
|
||||
def test__serialize__valid_data__returns_expected_fields(self):
|
||||
"""Test serializing valid update data."""
|
||||
data = {
|
||||
"caption": "Updated caption",
|
||||
"alt_text": "Updated alt text",
|
||||
"is_primary": True,
|
||||
}
|
||||
|
||||
serializer = ParkPhotoUpdateInputSerializer(data=data)
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
assert serializer.validated_data["caption"] == "Updated caption"
|
||||
assert serializer.validated_data["alt_text"] == "Updated alt text"
|
||||
assert serializer.validated_data["is_primary"] is True
|
||||
|
||||
def test__serialize__partial_update__validates_partial_data(self):
|
||||
"""Test partial update with only some fields."""
|
||||
photo = ParkPhotoFactory()
|
||||
data = {"caption": "Only caption updated"}
|
||||
|
||||
serializer = ParkPhotoUpdateInputSerializer(photo, data=data, partial=True)
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
assert serializer.validated_data["caption"] == "Only caption updated"
|
||||
|
||||
def test__meta__fields__excludes_image_field(self):
|
||||
"""Test Meta.fields excludes image field for updates."""
|
||||
expected_fields = ["caption", "alt_text", "is_primary"]
|
||||
|
||||
assert list(ParkPhotoUpdateInputSerializer.Meta.fields) == expected_fields
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestParkPhotoListOutputSerializer(TestCase):
|
||||
"""Tests for ParkPhotoListOutputSerializer."""
|
||||
|
||||
def test__serialize__photo__returns_list_fields_only(self):
|
||||
"""Test serializing returns only list-appropriate fields."""
|
||||
user = UserFactory()
|
||||
photo = ParkPhotoFactory(uploaded_by=user)
|
||||
|
||||
serializer = ParkPhotoListOutputSerializer(photo)
|
||||
data = serializer.data
|
||||
|
||||
assert "id" in data
|
||||
assert "image" in data
|
||||
assert "caption" in data
|
||||
assert "is_primary" in data
|
||||
assert "is_approved" in data
|
||||
assert "created_at" in data
|
||||
assert "uploaded_by_username" in data
|
||||
# Should NOT include detailed fields
|
||||
assert "image_variants" not in data
|
||||
assert "file_size" not in data
|
||||
assert "dimensions" not in data
|
||||
|
||||
def test__serialize__multiple_photos__returns_list(self):
|
||||
"""Test serializing multiple photos returns a list."""
|
||||
photos = [ParkPhotoFactory() for _ in range(3)]
|
||||
|
||||
serializer = ParkPhotoListOutputSerializer(photos, many=True)
|
||||
|
||||
assert len(serializer.data) == 3
|
||||
|
||||
def test__meta__all_fields_read_only(self):
|
||||
"""Test all fields are read-only for list serializer."""
|
||||
assert (
|
||||
ParkPhotoListOutputSerializer.Meta.read_only_fields
|
||||
== ParkPhotoListOutputSerializer.Meta.fields
|
||||
)
|
||||
|
||||
|
||||
class TestParkPhotoApprovalInputSerializer(TestCase):
|
||||
"""Tests for ParkPhotoApprovalInputSerializer."""
|
||||
|
||||
def test__validate__valid_photo_ids__returns_validated_data(self):
|
||||
"""Test validation with valid photo IDs."""
|
||||
data = {
|
||||
"photo_ids": [1, 2, 3],
|
||||
"approve": True,
|
||||
}
|
||||
|
||||
serializer = ParkPhotoApprovalInputSerializer(data=data)
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
assert serializer.validated_data["photo_ids"] == [1, 2, 3]
|
||||
assert serializer.validated_data["approve"] is True
|
||||
|
||||
def test__validate__approve_default__defaults_to_true(self):
|
||||
"""Test approve field defaults to True."""
|
||||
data = {"photo_ids": [1, 2]}
|
||||
|
||||
serializer = ParkPhotoApprovalInputSerializer(data=data)
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
assert serializer.validated_data["approve"] is True
|
||||
|
||||
def test__validate__empty_photo_ids__is_valid(self):
|
||||
"""Test empty photo_ids list is valid."""
|
||||
data = {"photo_ids": [], "approve": False}
|
||||
|
||||
serializer = ParkPhotoApprovalInputSerializer(data=data)
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
assert serializer.validated_data["photo_ids"] == []
|
||||
|
||||
def test__validate__missing_photo_ids__returns_error(self):
|
||||
"""Test validation fails without photo_ids."""
|
||||
data = {"approve": True}
|
||||
|
||||
serializer = ParkPhotoApprovalInputSerializer(data=data)
|
||||
|
||||
assert not serializer.is_valid()
|
||||
assert "photo_ids" in serializer.errors
|
||||
|
||||
def test__validate__invalid_photo_ids__returns_error(self):
|
||||
"""Test validation fails with non-integer photo IDs."""
|
||||
data = {"photo_ids": ["invalid", "ids"]}
|
||||
|
||||
serializer = ParkPhotoApprovalInputSerializer(data=data)
|
||||
|
||||
assert not serializer.is_valid()
|
||||
|
||||
|
||||
class TestParkPhotoStatsOutputSerializer(TestCase):
|
||||
"""Tests for ParkPhotoStatsOutputSerializer."""
|
||||
|
||||
def test__serialize__stats_dict__returns_all_fields(self):
|
||||
"""Test serializing stats dictionary."""
|
||||
stats = {
|
||||
"total_photos": 100,
|
||||
"approved_photos": 80,
|
||||
"pending_photos": 20,
|
||||
"has_primary": True,
|
||||
"recent_uploads": 5,
|
||||
}
|
||||
|
||||
serializer = ParkPhotoStatsOutputSerializer(data=stats)
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
assert serializer.validated_data["total_photos"] == 100
|
||||
assert serializer.validated_data["approved_photos"] == 80
|
||||
assert serializer.validated_data["pending_photos"] == 20
|
||||
assert serializer.validated_data["has_primary"] is True
|
||||
assert serializer.validated_data["recent_uploads"] == 5
|
||||
|
||||
def test__validate__missing_fields__returns_error(self):
|
||||
"""Test validation fails with missing stats fields."""
|
||||
stats = {"total_photos": 100} # Missing other required fields
|
||||
|
||||
serializer = ParkPhotoStatsOutputSerializer(data=stats)
|
||||
|
||||
assert not serializer.is_valid()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestHybridParkSerializer(TestCase):
|
||||
"""Tests for HybridParkSerializer."""
|
||||
|
||||
def test__serialize__park_with_all_fields__returns_complete_data(self):
|
||||
"""Test serializing park with all fields populated."""
|
||||
park = ParkFactory()
|
||||
|
||||
serializer = HybridParkSerializer(park)
|
||||
data = serializer.data
|
||||
|
||||
assert "id" in data
|
||||
assert "name" in data
|
||||
assert "slug" in data
|
||||
assert "status" in data
|
||||
assert "operator_name" in data
|
||||
|
||||
def test__serialize__park_without_location__returns_null_location_fields(self):
|
||||
"""Test serializing park without location returns null for location fields."""
|
||||
park = ParkFactory()
|
||||
# Remove location if it exists
|
||||
if hasattr(park, 'location') and park.location:
|
||||
park.location.delete()
|
||||
|
||||
serializer = HybridParkSerializer(park)
|
||||
data = serializer.data
|
||||
|
||||
# Location fields should be None when no location
|
||||
assert "city" in data
|
||||
assert "state" in data
|
||||
assert "country" in data
|
||||
|
||||
def test__get_city__park_with_location__returns_city(self):
|
||||
"""Test get_city returns city from location."""
|
||||
park = ParkFactory()
|
||||
|
||||
# Create a mock location
|
||||
mock_location = Mock()
|
||||
mock_location.city = "Orlando"
|
||||
mock_location.state = "FL"
|
||||
mock_location.country = "USA"
|
||||
mock_location.continent = "North America"
|
||||
mock_location.coordinates = [-81.3792, 28.5383] # [lon, lat]
|
||||
|
||||
park.location = mock_location
|
||||
|
||||
serializer = HybridParkSerializer(park)
|
||||
|
||||
assert serializer.get_city(park) == "Orlando"
|
||||
|
||||
def test__get_latitude__park_with_coordinates__returns_latitude(self):
|
||||
"""Test get_latitude returns correct value from coordinates."""
|
||||
park = ParkFactory()
|
||||
|
||||
mock_location = Mock()
|
||||
mock_location.coordinates = [-81.3792, 28.5383] # [lon, lat]
|
||||
park.location = mock_location
|
||||
|
||||
serializer = HybridParkSerializer(park)
|
||||
|
||||
# Latitude is index 1 in PostGIS [lon, lat] format
|
||||
assert serializer.get_latitude(park) == 28.5383
|
||||
|
||||
def test__get_longitude__park_with_coordinates__returns_longitude(self):
|
||||
"""Test get_longitude returns correct value from coordinates."""
|
||||
park = ParkFactory()
|
||||
|
||||
mock_location = Mock()
|
||||
mock_location.coordinates = [-81.3792, 28.5383] # [lon, lat]
|
||||
park.location = mock_location
|
||||
|
||||
serializer = HybridParkSerializer(park)
|
||||
|
||||
# Longitude is index 0 in PostGIS [lon, lat] format
|
||||
assert serializer.get_longitude(park) == -81.3792
|
||||
|
||||
def test__get_banner_image_url__park_with_banner__returns_url(self):
|
||||
"""Test get_banner_image_url returns URL when banner exists."""
|
||||
park = ParkFactory()
|
||||
|
||||
mock_image = Mock()
|
||||
mock_image.url = "https://example.com/banner.jpg"
|
||||
|
||||
mock_banner = Mock()
|
||||
mock_banner.image = mock_image
|
||||
|
||||
park.banner_image = mock_banner
|
||||
|
||||
serializer = HybridParkSerializer(park)
|
||||
|
||||
assert serializer.get_banner_image_url(park) == "https://example.com/banner.jpg"
|
||||
|
||||
def test__get_banner_image_url__park_without_banner__returns_none(self):
|
||||
"""Test get_banner_image_url returns None when no banner."""
|
||||
park = ParkFactory()
|
||||
park.banner_image = None
|
||||
|
||||
serializer = HybridParkSerializer(park)
|
||||
|
||||
assert serializer.get_banner_image_url(park) is None
|
||||
|
||||
def test__meta__all_fields_read_only(self):
|
||||
"""Test all fields in HybridParkSerializer are read-only."""
|
||||
assert (
|
||||
HybridParkSerializer.Meta.read_only_fields
|
||||
== HybridParkSerializer.Meta.fields
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestParkSerializer(TestCase):
|
||||
"""Tests for ParkSerializer (legacy)."""
|
||||
|
||||
def test__serialize__park__returns_basic_fields(self):
|
||||
"""Test serializing park returns basic fields."""
|
||||
park = ParkFactory()
|
||||
|
||||
serializer = ParkSerializer(park)
|
||||
data = serializer.data
|
||||
|
||||
assert "id" in data
|
||||
assert "name" in data
|
||||
assert "slug" in data
|
||||
assert "status" in data
|
||||
assert "website" in data
|
||||
|
||||
def test__serialize__multiple_parks__returns_list(self):
|
||||
"""Test serializing multiple parks returns a list."""
|
||||
parks = [ParkFactory() for _ in range(3)]
|
||||
|
||||
serializer = ParkSerializer(parks, many=True)
|
||||
|
||||
assert len(serializer.data) == 3
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestParkPhotoSerializer(TestCase):
|
||||
"""Tests for legacy ParkPhotoSerializer."""
|
||||
|
||||
def test__serialize__photo__returns_legacy_fields(self):
|
||||
"""Test serializing photo returns legacy field set."""
|
||||
photo = ParkPhotoFactory()
|
||||
|
||||
serializer = ParkPhotoSerializer(photo)
|
||||
data = serializer.data
|
||||
|
||||
assert "id" in data
|
||||
assert "image" in data
|
||||
assert "caption" in data
|
||||
assert "alt_text" in data
|
||||
assert "is_primary" in data
|
||||
|
||||
def test__meta__fields__matches_legacy_format(self):
|
||||
"""Test Meta.fields matches legacy format."""
|
||||
expected_fields = (
|
||||
"id",
|
||||
"image",
|
||||
"caption",
|
||||
"alt_text",
|
||||
"is_primary",
|
||||
"uploaded_at",
|
||||
"uploaded_by",
|
||||
)
|
||||
|
||||
assert ParkPhotoSerializer.Meta.fields == expected_fields
|
||||
573
backend/tests/serializers/test_ride_serializers.py
Normal file
573
backend/tests/serializers/test_ride_serializers.py
Normal file
@@ -0,0 +1,573 @@
|
||||
"""
|
||||
Tests for Ride serializers.
|
||||
|
||||
Following Django styleguide pattern: test__<context>__<action>__<expected_outcome>
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, MagicMock
|
||||
from django.test import TestCase
|
||||
|
||||
from apps.api.v1.rides.serializers import (
|
||||
RidePhotoOutputSerializer,
|
||||
RidePhotoCreateInputSerializer,
|
||||
RidePhotoUpdateInputSerializer,
|
||||
RidePhotoListOutputSerializer,
|
||||
RidePhotoApprovalInputSerializer,
|
||||
RidePhotoStatsOutputSerializer,
|
||||
RidePhotoTypeFilterSerializer,
|
||||
RidePhotoSerializer,
|
||||
HybridRideSerializer,
|
||||
RideSerializer,
|
||||
)
|
||||
|
||||
from tests.factories import (
|
||||
RideFactory,
|
||||
RidePhotoFactory,
|
||||
ParkFactory,
|
||||
UserFactory,
|
||||
CloudflareImageFactory,
|
||||
ManufacturerCompanyFactory,
|
||||
DesignerCompanyFactory,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestRidePhotoOutputSerializer(TestCase):
|
||||
"""Tests for RidePhotoOutputSerializer."""
|
||||
|
||||
def test__serialize__valid_photo__returns_all_fields(self):
|
||||
"""Test serializing a ride photo returns all expected fields."""
|
||||
user = UserFactory()
|
||||
ride = RideFactory()
|
||||
image = CloudflareImageFactory()
|
||||
photo = RidePhotoFactory(
|
||||
ride=ride,
|
||||
uploaded_by=user,
|
||||
image=image,
|
||||
caption="Test caption",
|
||||
alt_text="Test alt text",
|
||||
is_primary=True,
|
||||
is_approved=True,
|
||||
)
|
||||
|
||||
serializer = RidePhotoOutputSerializer(photo)
|
||||
data = serializer.data
|
||||
|
||||
assert "id" in data
|
||||
assert data["caption"] == "Test caption"
|
||||
assert data["alt_text"] == "Test alt text"
|
||||
assert data["is_primary"] is True
|
||||
assert data["is_approved"] is True
|
||||
assert data["uploaded_by_username"] == user.username
|
||||
assert data["ride_slug"] == ride.slug
|
||||
assert data["ride_name"] == ride.name
|
||||
assert data["park_slug"] == ride.park.slug
|
||||
assert data["park_name"] == ride.park.name
|
||||
|
||||
def test__serialize__photo_with_image__returns_image_url(self):
|
||||
"""Test serializing a photo with image returns URL."""
|
||||
photo = RidePhotoFactory()
|
||||
|
||||
serializer = RidePhotoOutputSerializer(photo)
|
||||
data = serializer.data
|
||||
|
||||
assert "image_url" in data
|
||||
assert "image_variants" in data
|
||||
|
||||
def test__serialize__photo_without_image__returns_none_for_image_fields(self):
|
||||
"""Test serializing photo without image returns None for image fields."""
|
||||
photo = RidePhotoFactory()
|
||||
photo.image = None
|
||||
photo.save()
|
||||
|
||||
serializer = RidePhotoOutputSerializer(photo)
|
||||
data = serializer.data
|
||||
|
||||
assert data["image_url"] is None
|
||||
assert data["image_variants"] == {}
|
||||
|
||||
def test__get_image_variants__photo_with_image__returns_variant_urls(self):
|
||||
"""Test get_image_variants returns all variant URLs."""
|
||||
image = CloudflareImageFactory()
|
||||
photo = RidePhotoFactory(image=image)
|
||||
|
||||
serializer = RidePhotoOutputSerializer(photo)
|
||||
data = serializer.data
|
||||
|
||||
if photo.image:
|
||||
variants = data["image_variants"]
|
||||
assert "thumbnail" in variants
|
||||
assert "medium" in variants
|
||||
assert "large" in variants
|
||||
assert "public" in variants
|
||||
|
||||
def test__serialize__includes_photo_type(self):
|
||||
"""Test serializing includes photo_type field."""
|
||||
photo = RidePhotoFactory()
|
||||
|
||||
serializer = RidePhotoOutputSerializer(photo)
|
||||
data = serializer.data
|
||||
|
||||
assert "photo_type" in data
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestRidePhotoCreateInputSerializer(TestCase):
|
||||
"""Tests for RidePhotoCreateInputSerializer."""
|
||||
|
||||
def test__serialize__valid_data__returns_expected_fields(self):
|
||||
"""Test serializing valid create data."""
|
||||
image = CloudflareImageFactory()
|
||||
data = {
|
||||
"image": image.pk,
|
||||
"caption": "New photo caption",
|
||||
"alt_text": "Description of the image",
|
||||
"photo_type": "exterior",
|
||||
"is_primary": False,
|
||||
}
|
||||
|
||||
serializer = RidePhotoCreateInputSerializer(data=data)
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
assert "caption" in serializer.validated_data
|
||||
assert "alt_text" in serializer.validated_data
|
||||
assert "photo_type" in serializer.validated_data
|
||||
assert "is_primary" in serializer.validated_data
|
||||
|
||||
def test__validate__missing_required_fields__returns_error(self):
|
||||
"""Test validation fails with missing required fields."""
|
||||
data = {}
|
||||
|
||||
serializer = RidePhotoCreateInputSerializer(data=data)
|
||||
|
||||
assert not serializer.is_valid()
|
||||
|
||||
def test__meta__fields__includes_photo_type(self):
|
||||
"""Test Meta.fields includes photo_type for ride photos."""
|
||||
expected_fields = ["image", "caption", "alt_text", "photo_type", "is_primary"]
|
||||
|
||||
assert list(RidePhotoCreateInputSerializer.Meta.fields) == expected_fields
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestRidePhotoUpdateInputSerializer(TestCase):
|
||||
"""Tests for RidePhotoUpdateInputSerializer."""
|
||||
|
||||
def test__serialize__valid_data__returns_expected_fields(self):
|
||||
"""Test serializing valid update data."""
|
||||
data = {
|
||||
"caption": "Updated caption",
|
||||
"alt_text": "Updated alt text",
|
||||
"photo_type": "queue",
|
||||
"is_primary": True,
|
||||
}
|
||||
|
||||
serializer = RidePhotoUpdateInputSerializer(data=data)
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
assert serializer.validated_data["caption"] == "Updated caption"
|
||||
assert serializer.validated_data["photo_type"] == "queue"
|
||||
|
||||
def test__serialize__partial_update__validates_partial_data(self):
|
||||
"""Test partial update with only some fields."""
|
||||
photo = RidePhotoFactory()
|
||||
data = {"caption": "Only caption updated"}
|
||||
|
||||
serializer = RidePhotoUpdateInputSerializer(photo, data=data, partial=True)
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
|
||||
def test__meta__fields__includes_photo_type(self):
|
||||
"""Test Meta.fields includes photo_type for updates."""
|
||||
expected_fields = ["caption", "alt_text", "photo_type", "is_primary"]
|
||||
|
||||
assert list(RidePhotoUpdateInputSerializer.Meta.fields) == expected_fields
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestRidePhotoListOutputSerializer(TestCase):
|
||||
"""Tests for RidePhotoListOutputSerializer."""
|
||||
|
||||
def test__serialize__photo__returns_list_fields_only(self):
|
||||
"""Test serializing returns only list-appropriate fields."""
|
||||
user = UserFactory()
|
||||
photo = RidePhotoFactory(uploaded_by=user)
|
||||
|
||||
serializer = RidePhotoListOutputSerializer(photo)
|
||||
data = serializer.data
|
||||
|
||||
assert "id" in data
|
||||
assert "image" in data
|
||||
assert "caption" in data
|
||||
assert "photo_type" in data
|
||||
assert "is_primary" in data
|
||||
assert "is_approved" in data
|
||||
assert "created_at" in data
|
||||
assert "uploaded_by_username" in data
|
||||
# Should NOT include detailed fields
|
||||
assert "image_variants" not in data
|
||||
assert "file_size" not in data
|
||||
assert "dimensions" not in data
|
||||
|
||||
def test__serialize__multiple_photos__returns_list(self):
|
||||
"""Test serializing multiple photos returns a list."""
|
||||
photos = [RidePhotoFactory() for _ in range(3)]
|
||||
|
||||
serializer = RidePhotoListOutputSerializer(photos, many=True)
|
||||
|
||||
assert len(serializer.data) == 3
|
||||
|
||||
def test__meta__all_fields_read_only(self):
|
||||
"""Test all fields are read-only for list serializer."""
|
||||
assert (
|
||||
RidePhotoListOutputSerializer.Meta.read_only_fields
|
||||
== RidePhotoListOutputSerializer.Meta.fields
|
||||
)
|
||||
|
||||
|
||||
class TestRidePhotoApprovalInputSerializer(TestCase):
|
||||
"""Tests for RidePhotoApprovalInputSerializer."""
|
||||
|
||||
def test__validate__valid_photo_ids__returns_validated_data(self):
|
||||
"""Test validation with valid photo IDs."""
|
||||
data = {
|
||||
"photo_ids": [1, 2, 3],
|
||||
"approve": True,
|
||||
}
|
||||
|
||||
serializer = RidePhotoApprovalInputSerializer(data=data)
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
assert serializer.validated_data["photo_ids"] == [1, 2, 3]
|
||||
assert serializer.validated_data["approve"] is True
|
||||
|
||||
def test__validate__approve_default__defaults_to_true(self):
|
||||
"""Test approve field defaults to True."""
|
||||
data = {"photo_ids": [1, 2]}
|
||||
|
||||
serializer = RidePhotoApprovalInputSerializer(data=data)
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
assert serializer.validated_data["approve"] is True
|
||||
|
||||
def test__validate__empty_photo_ids__is_valid(self):
|
||||
"""Test empty photo_ids list is valid."""
|
||||
data = {"photo_ids": [], "approve": False}
|
||||
|
||||
serializer = RidePhotoApprovalInputSerializer(data=data)
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
|
||||
def test__validate__missing_photo_ids__returns_error(self):
|
||||
"""Test validation fails without photo_ids."""
|
||||
data = {"approve": True}
|
||||
|
||||
serializer = RidePhotoApprovalInputSerializer(data=data)
|
||||
|
||||
assert not serializer.is_valid()
|
||||
assert "photo_ids" in serializer.errors
|
||||
|
||||
|
||||
class TestRidePhotoStatsOutputSerializer(TestCase):
|
||||
"""Tests for RidePhotoStatsOutputSerializer."""
|
||||
|
||||
def test__serialize__stats_dict__returns_all_fields(self):
|
||||
"""Test serializing stats dictionary."""
|
||||
stats = {
|
||||
"total_photos": 50,
|
||||
"approved_photos": 40,
|
||||
"pending_photos": 10,
|
||||
"has_primary": True,
|
||||
"recent_uploads": 3,
|
||||
"by_type": {"exterior": 20, "queue": 10, "onride": 10, "other": 10},
|
||||
}
|
||||
|
||||
serializer = RidePhotoStatsOutputSerializer(data=stats)
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
assert serializer.validated_data["total_photos"] == 50
|
||||
assert serializer.validated_data["by_type"]["exterior"] == 20
|
||||
|
||||
def test__validate__includes_by_type_field(self):
|
||||
"""Test stats include by_type breakdown."""
|
||||
stats = {
|
||||
"total_photos": 10,
|
||||
"approved_photos": 8,
|
||||
"pending_photos": 2,
|
||||
"has_primary": False,
|
||||
"recent_uploads": 1,
|
||||
"by_type": {"exterior": 10},
|
||||
}
|
||||
|
||||
serializer = RidePhotoStatsOutputSerializer(data=stats)
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
assert "by_type" in serializer.validated_data
|
||||
|
||||
|
||||
class TestRidePhotoTypeFilterSerializer(TestCase):
|
||||
"""Tests for RidePhotoTypeFilterSerializer."""
|
||||
|
||||
def test__validate__valid_photo_type__returns_validated_data(self):
|
||||
"""Test validation with valid photo type."""
|
||||
data = {"photo_type": "exterior"}
|
||||
|
||||
serializer = RidePhotoTypeFilterSerializer(data=data)
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
assert serializer.validated_data["photo_type"] == "exterior"
|
||||
|
||||
def test__validate__all_photo_types__are_valid(self):
|
||||
"""Test all defined photo types are valid."""
|
||||
valid_types = ["exterior", "queue", "station", "onride", "construction", "other"]
|
||||
|
||||
for photo_type in valid_types:
|
||||
serializer = RidePhotoTypeFilterSerializer(data={"photo_type": photo_type})
|
||||
assert serializer.is_valid(), f"Photo type {photo_type} should be valid"
|
||||
|
||||
def test__validate__invalid_photo_type__returns_error(self):
|
||||
"""Test invalid photo type returns error."""
|
||||
data = {"photo_type": "invalid_type"}
|
||||
|
||||
serializer = RidePhotoTypeFilterSerializer(data=data)
|
||||
|
||||
assert not serializer.is_valid()
|
||||
assert "photo_type" in serializer.errors
|
||||
|
||||
def test__validate__empty_photo_type__is_valid(self):
|
||||
"""Test empty/missing photo_type is valid (optional field)."""
|
||||
data = {}
|
||||
|
||||
serializer = RidePhotoTypeFilterSerializer(data=data)
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestHybridRideSerializer(TestCase):
|
||||
"""Tests for HybridRideSerializer."""
|
||||
|
||||
def test__serialize__ride_with_all_fields__returns_complete_data(self):
|
||||
"""Test serializing ride with all fields populated."""
|
||||
ride = RideFactory()
|
||||
|
||||
serializer = HybridRideSerializer(ride)
|
||||
data = serializer.data
|
||||
|
||||
assert "id" in data
|
||||
assert "name" in data
|
||||
assert "slug" in data
|
||||
assert "category" in data
|
||||
assert "status" in data
|
||||
assert "park_name" in data
|
||||
assert "park_slug" in data
|
||||
assert "manufacturer_name" in data
|
||||
|
||||
def test__serialize__ride_with_manufacturer__returns_manufacturer_fields(self):
|
||||
"""Test serializing includes manufacturer information."""
|
||||
manufacturer = ManufacturerCompanyFactory(name="Test Manufacturer")
|
||||
ride = RideFactory(manufacturer=manufacturer)
|
||||
|
||||
serializer = HybridRideSerializer(ride)
|
||||
data = serializer.data
|
||||
|
||||
assert data["manufacturer_name"] == "Test Manufacturer"
|
||||
assert "manufacturer_slug" in data
|
||||
|
||||
def test__serialize__ride_with_designer__returns_designer_fields(self):
|
||||
"""Test serializing includes designer information."""
|
||||
designer = DesignerCompanyFactory(name="Test Designer")
|
||||
ride = RideFactory(designer=designer)
|
||||
|
||||
serializer = HybridRideSerializer(ride)
|
||||
data = serializer.data
|
||||
|
||||
assert data["designer_name"] == "Test Designer"
|
||||
assert "designer_slug" in data
|
||||
|
||||
def test__get_park_city__ride_with_park_location__returns_city(self):
|
||||
"""Test get_park_city returns city from park location."""
|
||||
ride = RideFactory()
|
||||
|
||||
mock_location = Mock()
|
||||
mock_location.city = "Orlando"
|
||||
mock_location.state = "FL"
|
||||
mock_location.country = "USA"
|
||||
|
||||
ride.park.location = mock_location
|
||||
|
||||
serializer = HybridRideSerializer(ride)
|
||||
|
||||
assert serializer.get_park_city(ride) == "Orlando"
|
||||
|
||||
def test__get_park_city__ride_without_park_location__returns_none(self):
|
||||
"""Test get_park_city returns None when no location."""
|
||||
ride = RideFactory()
|
||||
ride.park.location = None
|
||||
|
||||
serializer = HybridRideSerializer(ride)
|
||||
|
||||
assert serializer.get_park_city(ride) is None
|
||||
|
||||
def test__get_coaster_height_ft__ride_with_stats__returns_height(self):
|
||||
"""Test get_coaster_height_ft returns height from coaster stats."""
|
||||
ride = RideFactory()
|
||||
|
||||
mock_stats = Mock()
|
||||
mock_stats.height_ft = 205.5
|
||||
mock_stats.length_ft = 5000
|
||||
mock_stats.speed_mph = 70
|
||||
mock_stats.inversions = 4
|
||||
|
||||
ride.coaster_stats = mock_stats
|
||||
|
||||
serializer = HybridRideSerializer(ride)
|
||||
|
||||
assert serializer.get_coaster_height_ft(ride) == 205.5
|
||||
|
||||
def test__get_coaster_inversions__ride_with_stats__returns_inversions(self):
|
||||
"""Test get_coaster_inversions returns inversions count."""
|
||||
ride = RideFactory()
|
||||
|
||||
mock_stats = Mock()
|
||||
mock_stats.inversions = 7
|
||||
|
||||
ride.coaster_stats = mock_stats
|
||||
|
||||
serializer = HybridRideSerializer(ride)
|
||||
|
||||
assert serializer.get_coaster_inversions(ride) == 7
|
||||
|
||||
def test__get_coaster_height_ft__ride_without_stats__returns_none(self):
|
||||
"""Test coaster stat methods return None when no stats."""
|
||||
ride = RideFactory()
|
||||
ride.coaster_stats = None
|
||||
|
||||
serializer = HybridRideSerializer(ride)
|
||||
|
||||
assert serializer.get_coaster_height_ft(ride) is None
|
||||
assert serializer.get_coaster_length_ft(ride) is None
|
||||
assert serializer.get_coaster_speed_mph(ride) is None
|
||||
assert serializer.get_coaster_inversions(ride) is None
|
||||
|
||||
def test__get_banner_image_url__ride_with_banner__returns_url(self):
|
||||
"""Test get_banner_image_url returns URL when banner exists."""
|
||||
ride = RideFactory()
|
||||
|
||||
mock_image = Mock()
|
||||
mock_image.url = "https://example.com/ride-banner.jpg"
|
||||
|
||||
mock_banner = Mock()
|
||||
mock_banner.image = mock_image
|
||||
|
||||
ride.banner_image = mock_banner
|
||||
|
||||
serializer = HybridRideSerializer(ride)
|
||||
|
||||
assert serializer.get_banner_image_url(ride) == "https://example.com/ride-banner.jpg"
|
||||
|
||||
def test__get_banner_image_url__ride_without_banner__returns_none(self):
|
||||
"""Test get_banner_image_url returns None when no banner."""
|
||||
ride = RideFactory()
|
||||
ride.banner_image = None
|
||||
|
||||
serializer = HybridRideSerializer(ride)
|
||||
|
||||
assert serializer.get_banner_image_url(ride) is None
|
||||
|
||||
def test__meta__all_fields_read_only(self):
|
||||
"""Test all fields in HybridRideSerializer are read-only."""
|
||||
assert (
|
||||
HybridRideSerializer.Meta.read_only_fields
|
||||
== HybridRideSerializer.Meta.fields
|
||||
)
|
||||
|
||||
def test__serialize__includes_ride_model_fields(self):
|
||||
"""Test serializing includes ride model information."""
|
||||
ride = RideFactory()
|
||||
|
||||
serializer = HybridRideSerializer(ride)
|
||||
data = serializer.data
|
||||
|
||||
assert "ride_model_name" in data
|
||||
assert "ride_model_slug" in data
|
||||
assert "ride_model_category" in data
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestRideSerializer(TestCase):
|
||||
"""Tests for RideSerializer (legacy)."""
|
||||
|
||||
def test__serialize__ride__returns_basic_fields(self):
|
||||
"""Test serializing ride returns basic fields."""
|
||||
ride = RideFactory()
|
||||
|
||||
serializer = RideSerializer(ride)
|
||||
data = serializer.data
|
||||
|
||||
assert "id" in data
|
||||
assert "name" in data
|
||||
assert "slug" in data
|
||||
assert "category" in data
|
||||
assert "status" in data
|
||||
assert "opening_date" in data
|
||||
|
||||
def test__serialize__multiple_rides__returns_list(self):
|
||||
"""Test serializing multiple rides returns a list."""
|
||||
rides = [RideFactory() for _ in range(3)]
|
||||
|
||||
serializer = RideSerializer(rides, many=True)
|
||||
|
||||
assert len(serializer.data) == 3
|
||||
|
||||
def test__meta__fields__matches_expected(self):
|
||||
"""Test Meta.fields matches expected field list."""
|
||||
expected_fields = [
|
||||
"id",
|
||||
"name",
|
||||
"slug",
|
||||
"park",
|
||||
"manufacturer",
|
||||
"designer",
|
||||
"category",
|
||||
"status",
|
||||
"opening_date",
|
||||
"closing_date",
|
||||
]
|
||||
|
||||
assert list(RideSerializer.Meta.fields) == expected_fields
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestRidePhotoSerializer(TestCase):
|
||||
"""Tests for legacy RidePhotoSerializer."""
|
||||
|
||||
def test__serialize__photo__returns_legacy_fields(self):
|
||||
"""Test serializing photo returns legacy field set."""
|
||||
photo = RidePhotoFactory()
|
||||
|
||||
serializer = RidePhotoSerializer(photo)
|
||||
data = serializer.data
|
||||
|
||||
assert "id" in data
|
||||
assert "image" in data
|
||||
assert "caption" in data
|
||||
assert "alt_text" in data
|
||||
assert "is_primary" in data
|
||||
assert "photo_type" in data
|
||||
|
||||
def test__meta__fields__matches_legacy_format(self):
|
||||
"""Test Meta.fields matches legacy format."""
|
||||
expected_fields = [
|
||||
"id",
|
||||
"image",
|
||||
"caption",
|
||||
"alt_text",
|
||||
"is_primary",
|
||||
"photo_type",
|
||||
"uploaded_at",
|
||||
"uploaded_by",
|
||||
]
|
||||
|
||||
assert list(RidePhotoSerializer.Meta.fields) == expected_fields
|
||||
Reference in New Issue
Block a user