""" 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______ - Factory-based test data creation - Comprehensive edge case coverage - Permission and authorization testing """ from unittest.mock import patch from rest_framework import status from rest_framework.test import APIClient from apps.parks.models import Park from tests.factories import ( CompanyFactory, ParkFactory, StaffUserFactory, SuperUserFactory, UserFactory, ) 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", "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])