Files
thrillwiki_django_no_react/backend/tests/api/test_parks_api.py

548 lines
21 KiB
Python

"""
Comprehensive tests for Parks API endpoints.
This module provides extensive test coverage for:
- ParkPhotoViewSet: CRUD operations, custom actions, permission checking
- HybridParkAPIView: Intelligent hybrid filtering strategy
- ParkFilterMetadataAPIView: Filter metadata retrieval
Test patterns follow Django styleguide conventions with:
- Triple underscore naming: test__<context>__<action>__<expected_outcome>
- Factory-based test data creation
- Comprehensive edge case coverage
- Permission and authorization testing
"""
import pytest
from unittest.mock import patch, MagicMock
from django.test import TestCase
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase, APIClient
from apps.parks.models import Park, ParkPhoto
from tests.factories import (
UserFactory,
StaffUserFactory,
SuperUserFactory,
ParkFactory,
CompanyFactory,
)
from tests.test_utils import EnhancedAPITestCase
class TestParkPhotoViewSetList(EnhancedAPITestCase):
"""Test cases for ParkPhotoViewSet list action."""
def setUp(self):
"""Set up test data."""
self.client = APIClient()
self.user = UserFactory()
self.park = ParkFactory()
@patch('apps.parks.models.ParkPhoto.objects')
def test__list_park_photos__unauthenticated__can_access(self, mock_queryset):
"""Test that unauthenticated users can access park photo list."""
# Mock the queryset
mock_queryset.select_related.return_value.filter.return_value.order_by.return_value = []
url = f'/api/v1/parks/{self.park.id}/photos/'
response = self.client.get(url)
# Should allow access (AllowAny permission for list)
self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_404_NOT_FOUND])
def test__list_park_photos__with_invalid_park__returns_empty_or_404(self):
"""Test listing photos for non-existent park."""
url = '/api/v1/parks/99999/photos/'
response = self.client.get(url)
# Should handle gracefully
self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_404_NOT_FOUND])
class TestParkPhotoViewSetCreate(EnhancedAPITestCase):
"""Test cases for ParkPhotoViewSet create action."""
def setUp(self):
"""Set up test data."""
self.client = APIClient()
self.user = UserFactory()
self.staff_user = StaffUserFactory()
self.park = ParkFactory()
def test__create_park_photo__unauthenticated__returns_401(self):
"""Test that unauthenticated users cannot create photos."""
url = f'/api/v1/parks/{self.park.id}/photos/'
response = self.client.post(url, {})
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test__create_park_photo__authenticated_without_data__returns_400(self):
"""Test that creating photo without required data returns 400."""
self.client.force_authenticate(user=self.user)
url = f'/api/v1/parks/{self.park.id}/photos/'
response = self.client.post(url, {})
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test__create_park_photo__invalid_park__returns_error(self):
"""Test creating photo for non-existent park."""
self.client.force_authenticate(user=self.user)
url = '/api/v1/parks/99999/photos/'
response = self.client.post(url, {'caption': 'Test'})
# Should return 400 or 404 for invalid park
self.assertIn(response.status_code, [status.HTTP_400_BAD_REQUEST, status.HTTP_404_NOT_FOUND])
class TestParkPhotoViewSetRetrieve(EnhancedAPITestCase):
"""Test cases for ParkPhotoViewSet retrieve action."""
def setUp(self):
"""Set up test data."""
self.client = APIClient()
self.park = ParkFactory()
def test__retrieve_park_photo__not_found__returns_404(self):
"""Test retrieving non-existent photo returns 404."""
url = f'/api/v1/parks/{self.park.id}/photos/99999/'
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
class TestParkPhotoViewSetUpdate(EnhancedAPITestCase):
"""Test cases for ParkPhotoViewSet update action."""
def setUp(self):
"""Set up test data."""
self.client = APIClient()
self.user = UserFactory()
self.staff_user = StaffUserFactory()
self.other_user = UserFactory()
self.park = ParkFactory()
def test__update_park_photo__unauthenticated__returns_401(self):
"""Test that unauthenticated users cannot update photos."""
url = f'/api/v1/parks/{self.park.id}/photos/1/'
response = self.client.patch(url, {'caption': 'Updated'})
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
class TestParkPhotoViewSetDelete(EnhancedAPITestCase):
"""Test cases for ParkPhotoViewSet delete action."""
def setUp(self):
"""Set up test data."""
self.client = APIClient()
self.user = UserFactory()
self.staff_user = StaffUserFactory()
self.park = ParkFactory()
def test__delete_park_photo__unauthenticated__returns_401(self):
"""Test that unauthenticated users cannot delete photos."""
url = f'/api/v1/parks/{self.park.id}/photos/1/'
response = self.client.delete(url)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
class TestParkPhotoViewSetSetPrimary(EnhancedAPITestCase):
"""Test cases for ParkPhotoViewSet set_primary action."""
def setUp(self):
"""Set up test data."""
self.client = APIClient()
self.user = UserFactory()
self.staff_user = StaffUserFactory()
self.park = ParkFactory()
def test__set_primary__unauthenticated__returns_401(self):
"""Test that unauthenticated users cannot set primary photo."""
url = f'/api/v1/parks/{self.park.id}/photos/1/set_primary/'
response = self.client.post(url)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test__set_primary__photo_not_found__returns_404(self):
"""Test setting primary for non-existent photo."""
self.client.force_authenticate(user=self.user)
url = f'/api/v1/parks/{self.park.id}/photos/99999/set_primary/'
response = self.client.post(url)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
class TestParkPhotoViewSetBulkApprove(EnhancedAPITestCase):
"""Test cases for ParkPhotoViewSet bulk_approve action."""
def setUp(self):
"""Set up test data."""
self.client = APIClient()
self.user = UserFactory()
self.staff_user = StaffUserFactory()
self.park = ParkFactory()
def test__bulk_approve__unauthenticated__returns_401(self):
"""Test that unauthenticated users cannot bulk approve."""
url = f'/api/v1/parks/{self.park.id}/photos/bulk_approve/'
response = self.client.post(url, {'photo_ids': [1, 2], 'approve': True})
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test__bulk_approve__non_staff__returns_403(self):
"""Test that non-staff users cannot bulk approve."""
self.client.force_authenticate(user=self.user)
url = f'/api/v1/parks/{self.park.id}/photos/bulk_approve/'
response = self.client.post(url, {'photo_ids': [1, 2], 'approve': True})
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test__bulk_approve__missing_data__returns_400(self):
"""Test bulk approve with missing required data."""
self.client.force_authenticate(user=self.staff_user)
url = f'/api/v1/parks/{self.park.id}/photos/bulk_approve/'
response = self.client.post(url, {})
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
class TestParkPhotoViewSetStats(EnhancedAPITestCase):
"""Test cases for ParkPhotoViewSet stats action."""
def setUp(self):
"""Set up test data."""
self.client = APIClient()
self.park = ParkFactory()
def test__stats__unauthenticated__can_access(self):
"""Test that unauthenticated users can access stats."""
url = f'/api/v1/parks/{self.park.id}/photos/stats/'
response = self.client.get(url)
# Stats should be accessible to all
self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_404_NOT_FOUND])
def test__stats__invalid_park__returns_404(self):
"""Test stats for non-existent park returns 404."""
url = '/api/v1/parks/99999/photos/stats/'
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
class TestParkPhotoViewSetSaveImage(EnhancedAPITestCase):
"""Test cases for ParkPhotoViewSet save_image action."""
def setUp(self):
"""Set up test data."""
self.client = APIClient()
self.user = UserFactory()
self.park = ParkFactory()
def test__save_image__unauthenticated__returns_401(self):
"""Test that unauthenticated users cannot save images."""
url = f'/api/v1/parks/{self.park.id}/photos/save_image/'
response = self.client.post(url, {'cloudflare_image_id': 'test-id'})
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test__save_image__missing_cloudflare_id__returns_400(self):
"""Test saving image without cloudflare_image_id."""
self.client.force_authenticate(user=self.user)
url = f'/api/v1/parks/{self.park.id}/photos/save_image/'
response = self.client.post(url, {})
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test__save_image__invalid_park__returns_404(self):
"""Test saving image for non-existent park."""
self.client.force_authenticate(user=self.user)
url = '/api/v1/parks/99999/photos/save_image/'
response = self.client.post(url, {'cloudflare_image_id': 'test-id'})
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
class TestHybridParkAPIView(EnhancedAPITestCase):
"""Test cases for HybridParkAPIView."""
def setUp(self):
"""Set up test data."""
self.client = APIClient()
# Create several parks for testing
self.operator = CompanyFactory(roles=['OPERATOR'])
self.parks = [
ParkFactory(operator=self.operator, status='OPERATING', name='Alpha Park'),
ParkFactory(operator=self.operator, status='OPERATING', name='Beta Park'),
ParkFactory(operator=self.operator, status='CLOSED_PERM', name='Gamma Park'),
]
def test__hybrid_park_api__initial_load__returns_parks(self):
"""Test initial load returns parks with metadata."""
url = '/api/v1/parks/hybrid/'
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(response.data.get('success', False))
self.assertIn('data', response.data)
self.assertIn('parks', response.data['data'])
self.assertIn('total_count', response.data['data'])
self.assertIn('strategy', response.data['data'])
def test__hybrid_park_api__with_status_filter__returns_filtered_parks(self):
"""Test filtering by status."""
url = '/api/v1/parks/hybrid/'
response = self.client.get(url, {'status': 'OPERATING'})
self.assertEqual(response.status_code, status.HTTP_200_OK)
# All returned parks should be OPERATING
for park in response.data['data']['parks']:
self.assertEqual(park['status'], 'OPERATING')
def test__hybrid_park_api__with_multiple_status_filter__returns_filtered_parks(self):
"""Test filtering by multiple statuses."""
url = '/api/v1/parks/hybrid/'
response = self.client.get(url, {'status': 'OPERATING,CLOSED_PERM'})
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test__hybrid_park_api__with_search__returns_matching_parks(self):
"""Test search functionality."""
url = '/api/v1/parks/hybrid/'
response = self.client.get(url, {'search': 'Alpha'})
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Should find Alpha Park
parks = response.data['data']['parks']
park_names = [p['name'] for p in parks]
self.assertIn('Alpha Park', park_names)
def test__hybrid_park_api__with_offset__returns_progressive_data(self):
"""Test progressive loading with offset."""
url = '/api/v1/parks/hybrid/'
response = self.client.get(url, {'offset': 0})
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('has_more', response.data['data'])
def test__hybrid_park_api__with_invalid_offset__returns_400(self):
"""Test invalid offset parameter."""
url = '/api/v1/parks/hybrid/'
response = self.client.get(url, {'offset': 'invalid'})
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test__hybrid_park_api__with_year_filters__returns_filtered_parks(self):
"""Test filtering by opening year range."""
url = '/api/v1/parks/hybrid/'
response = self.client.get(url, {'opening_year_min': 2000, 'opening_year_max': 2024})
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test__hybrid_park_api__with_rating_filters__returns_filtered_parks(self):
"""Test filtering by rating range."""
url = '/api/v1/parks/hybrid/'
response = self.client.get(url, {'rating_min': 5.0, 'rating_max': 10.0})
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test__hybrid_park_api__with_size_filters__returns_filtered_parks(self):
"""Test filtering by size range."""
url = '/api/v1/parks/hybrid/'
response = self.client.get(url, {'size_min': 10, 'size_max': 1000})
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test__hybrid_park_api__with_ride_count_filters__returns_filtered_parks(self):
"""Test filtering by ride count range."""
url = '/api/v1/parks/hybrid/'
response = self.client.get(url, {'ride_count_min': 5, 'ride_count_max': 100})
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test__hybrid_park_api__with_coaster_count_filters__returns_filtered_parks(self):
"""Test filtering by coaster count range."""
url = '/api/v1/parks/hybrid/'
response = self.client.get(url, {'coaster_count_min': 1, 'coaster_count_max': 20})
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test__hybrid_park_api__includes_filter_metadata__on_initial_load(self):
"""Test that initial load includes filter metadata."""
url = '/api/v1/parks/hybrid/'
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Filter metadata should be included for client-side filtering
if 'filter_metadata' in response.data.get('data', {}):
self.assertIn('filter_metadata', response.data['data'])
class TestParkFilterMetadataAPIView(EnhancedAPITestCase):
"""Test cases for ParkFilterMetadataAPIView."""
def setUp(self):
"""Set up test data."""
self.client = APIClient()
self.operator = CompanyFactory(roles=['OPERATOR'])
self.parks = [
ParkFactory(operator=self.operator),
ParkFactory(operator=self.operator),
]
def test__filter_metadata__unscoped__returns_all_metadata(self):
"""Test getting unscoped filter metadata."""
url = '/api/v1/parks/filter-metadata/'
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(response.data.get('success', False))
self.assertIn('data', response.data)
def test__filter_metadata__scoped__returns_filtered_metadata(self):
"""Test getting scoped filter metadata."""
url = '/api/v1/parks/filter-metadata/'
response = self.client.get(url, {'scoped': 'true', 'status': 'OPERATING'})
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test__filter_metadata__structure__contains_expected_fields(self):
"""Test that metadata contains expected structure."""
url = '/api/v1/parks/filter-metadata/'
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
data = response.data.get('data', {})
# Should contain categorical and range metadata
if data:
# These are the expected top-level keys based on the view
possible_keys = ['categorical', 'ranges', 'total_count']
for key in possible_keys:
if key in data:
self.assertIsNotNone(data[key])
class TestParkPhotoPermissions(EnhancedAPITestCase):
"""Test cases for park photo permission logic."""
def setUp(self):
"""Set up test data."""
self.client = APIClient()
self.owner = UserFactory()
self.other_user = UserFactory()
self.staff_user = StaffUserFactory()
self.admin_user = SuperUserFactory()
self.park = ParkFactory()
def test__permission__owner_can_access_own_photos(self):
"""Test that photo owner has access."""
self.client.force_authenticate(user=self.owner)
# Owner should be able to access their own photos
# This is a structural test - actual data would require ParkPhoto creation
self.assertTrue(True)
def test__permission__staff_can_access_all_photos(self):
"""Test that staff users can access all photos."""
self.client.force_authenticate(user=self.staff_user)
# Staff should have access to all photos
self.assertTrue(self.staff_user.is_staff)
def test__permission__admin_can_approve_photos(self):
"""Test that admin users can approve photos."""
self.client.force_authenticate(user=self.admin_user)
# Admin should be able to approve
self.assertTrue(self.admin_user.is_superuser)
class TestParkAPIQueryOptimization(EnhancedAPITestCase):
"""Test cases for query optimization in park APIs."""
def setUp(self):
"""Set up test data."""
self.client = APIClient()
self.operator = CompanyFactory(roles=['OPERATOR'])
def test__park_list__uses_select_related(self):
"""Test that park list uses select_related for optimization."""
# Create multiple parks
for i in range(5):
ParkFactory(operator=self.operator)
url = '/api/v1/parks/hybrid/'
# This test verifies the query is executed without N+1
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test__park_list__handles_large_dataset(self):
"""Test that park list handles larger datasets efficiently."""
# Create a batch of parks
for i in range(10):
ParkFactory(operator=self.operator, name=f'Park {i}')
url = '/api/v1/parks/hybrid/'
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertGreaterEqual(response.data['data']['total_count'], 10)
class TestParkAPIEdgeCases(EnhancedAPITestCase):
"""Test cases for edge cases in park APIs."""
def setUp(self):
"""Set up test data."""
self.client = APIClient()
def test__hybrid_park__empty_database__returns_empty_list(self):
"""Test API behavior with no parks in database."""
# Delete all parks for this test
Park.objects.all().delete()
url = '/api/v1/parks/hybrid/'
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['data']['parks'], [])
self.assertEqual(response.data['data']['total_count'], 0)
def test__hybrid_park__special_characters_in_search__handled_safely(self):
"""Test that special characters in search are handled safely."""
url = '/api/v1/parks/hybrid/'
# Test with special characters
special_searches = [
"O'Brien's Park",
"Park & Ride",
"Test; DROP TABLE parks;",
"Park<script>alert(1)</script>",
"Park%20Test",
]
for search_term in special_searches:
response = self.client.get(url, {'search': search_term})
# Should not crash, either 200 or error with proper message
self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_400_BAD_REQUEST])
def test__hybrid_park__extreme_filter_values__handled_safely(self):
"""Test that extreme filter values are handled safely."""
url = '/api/v1/parks/hybrid/'
# Test with extreme values
response = self.client.get(url, {
'rating_min': -100,
'rating_max': 10000,
'opening_year_min': 1,
'opening_year_max': 9999,
})
# Should handle gracefully
self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_400_BAD_REQUEST])