""" Comprehensive tests for the rides app state machine. This module contains tests for: - Ride status FSM transitions - Ride transition wrapper methods - Post-closing status automation - Transition history logging - Related model updates during transitions """ from datetime import date, timedelta from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError from django.test import TestCase from django_fsm import TransitionNotAllowed from apps.parks.models import Company as ParkCompany from apps.parks.models import Park from .models import Company, Ride User = get_user_model() # ============================================================================ # Ride FSM Transition Tests # ============================================================================ class RideTransitionTests(TestCase): """Comprehensive tests for Ride FSM transitions.""" def setUp(self): """Set up test fixtures.""" self.user = User.objects.create_user( username="testuser", email="test@example.com", password="testpass123", role="USER" ) self.moderator = User.objects.create_user( username="moderator", email="moderator@example.com", password="testpass123", role="MODERATOR" ) self.admin = User.objects.create_user( username="admin", email="admin@example.com", password="testpass123", role="ADMIN" ) # Create operator and park self.operator = ParkCompany.objects.create( name="Test Operator", description="Test operator company", roles=["OPERATOR"] ) self.park = Park.objects.create( name="Test Park", slug="test-park", description="A test park", operator=self.operator, timezone="America/New_York", ) # Create manufacturer self.manufacturer = Company.objects.create( name="Test Manufacturer", description="Test manufacturer company", roles=["MANUFACTURER"] ) def _create_ride(self, status="OPERATING", **kwargs): """Helper to create a Ride with specified status.""" defaults = { "name": "Test Ride", "slug": "test-ride", "description": "A test ride", "park": self.park, "manufacturer": self.manufacturer, } defaults.update(kwargs) return Ride.objects.create(status=status, **defaults) # ------------------------------------------------------------------------- # Operating status transitions # ------------------------------------------------------------------------- def test_operating_to_closed_temp_transition(self): """Test transition from OPERATING to CLOSED_TEMP.""" ride = self._create_ride(status="OPERATING") self.assertEqual(ride.status, "OPERATING") ride.transition_to_closed_temp(user=self.user) ride.save() ride.refresh_from_db() self.assertEqual(ride.status, "CLOSED_TEMP") def test_operating_to_sbno_transition(self): """Test transition from OPERATING to SBNO.""" ride = self._create_ride(status="OPERATING") ride.transition_to_sbno(user=self.moderator) ride.save() ride.refresh_from_db() self.assertEqual(ride.status, "SBNO") def test_operating_to_closing_transition(self): """Test transition from OPERATING to CLOSING.""" ride = self._create_ride(status="OPERATING") ride.transition_to_closing(user=self.moderator) ride.save() ride.refresh_from_db() self.assertEqual(ride.status, "CLOSING") # ------------------------------------------------------------------------- # Under construction transitions # ------------------------------------------------------------------------- def test_under_construction_to_operating_transition(self): """Test transition from UNDER_CONSTRUCTION to OPERATING.""" ride = self._create_ride(status="UNDER_CONSTRUCTION") self.assertEqual(ride.status, "UNDER_CONSTRUCTION") ride.transition_to_operating(user=self.user) ride.save() ride.refresh_from_db() self.assertEqual(ride.status, "OPERATING") # ------------------------------------------------------------------------- # Closed temp transitions # ------------------------------------------------------------------------- def test_closed_temp_to_operating_transition(self): """Test transition from CLOSED_TEMP to OPERATING (reopen).""" ride = self._create_ride(status="CLOSED_TEMP") ride.transition_to_operating(user=self.user) ride.save() ride.refresh_from_db() self.assertEqual(ride.status, "OPERATING") def test_closed_temp_to_sbno_transition(self): """Test transition from CLOSED_TEMP to SBNO.""" ride = self._create_ride(status="CLOSED_TEMP") ride.transition_to_sbno(user=self.moderator) ride.save() ride.refresh_from_db() self.assertEqual(ride.status, "SBNO") def test_closed_temp_to_closed_perm_transition(self): """Test transition from CLOSED_TEMP to CLOSED_PERM.""" ride = self._create_ride(status="CLOSED_TEMP") ride.transition_to_closed_perm(user=self.moderator) ride.closing_date = date.today() ride.save() ride.refresh_from_db() self.assertEqual(ride.status, "CLOSED_PERM") # ------------------------------------------------------------------------- # SBNO transitions # ------------------------------------------------------------------------- def test_sbno_to_operating_transition(self): """Test transition from SBNO to OPERATING (revival).""" ride = self._create_ride(status="SBNO") ride.transition_to_operating(user=self.moderator) ride.save() ride.refresh_from_db() self.assertEqual(ride.status, "OPERATING") def test_sbno_to_closed_perm_transition(self): """Test transition from SBNO to CLOSED_PERM.""" ride = self._create_ride(status="SBNO") ride.transition_to_closed_perm(user=self.moderator) ride.save() ride.refresh_from_db() self.assertEqual(ride.status, "CLOSED_PERM") # ------------------------------------------------------------------------- # Closing transitions # ------------------------------------------------------------------------- def test_closing_to_closed_perm_transition(self): """Test transition from CLOSING to CLOSED_PERM.""" ride = self._create_ride(status="CLOSING") ride.transition_to_closed_perm(user=self.moderator) ride.save() ride.refresh_from_db() self.assertEqual(ride.status, "CLOSED_PERM") def test_closing_to_sbno_transition(self): """Test transition from CLOSING to SBNO.""" ride = self._create_ride(status="CLOSING") ride.transition_to_sbno(user=self.moderator) ride.save() ride.refresh_from_db() self.assertEqual(ride.status, "SBNO") # ------------------------------------------------------------------------- # Closed perm transitions (to final states) # ------------------------------------------------------------------------- def test_closed_perm_to_demolished_transition(self): """Test transition from CLOSED_PERM to DEMOLISHED.""" ride = self._create_ride(status="CLOSED_PERM") ride.transition_to_demolished(user=self.moderator) ride.save() ride.refresh_from_db() self.assertEqual(ride.status, "DEMOLISHED") def test_closed_perm_to_relocated_transition(self): """Test transition from CLOSED_PERM to RELOCATED.""" ride = self._create_ride(status="CLOSED_PERM") ride.transition_to_relocated(user=self.moderator) ride.save() ride.refresh_from_db() self.assertEqual(ride.status, "RELOCATED") # ------------------------------------------------------------------------- # Invalid transitions (final states) # ------------------------------------------------------------------------- def test_demolished_cannot_transition(self): """Test that DEMOLISHED state cannot transition further.""" ride = self._create_ride(status="DEMOLISHED") with self.assertRaises(TransitionNotAllowed): ride.transition_to_operating(user=self.moderator) def test_relocated_cannot_transition(self): """Test that RELOCATED state cannot transition further.""" ride = self._create_ride(status="RELOCATED") with self.assertRaises(TransitionNotAllowed): ride.transition_to_operating(user=self.moderator) def test_operating_cannot_directly_demolish(self): """Test that OPERATING cannot directly transition to DEMOLISHED.""" ride = self._create_ride(status="OPERATING") with self.assertRaises(TransitionNotAllowed): ride.transition_to_demolished(user=self.moderator) def test_operating_cannot_directly_relocate(self): """Test that OPERATING cannot directly transition to RELOCATED.""" ride = self._create_ride(status="OPERATING") with self.assertRaises(TransitionNotAllowed): ride.transition_to_relocated(user=self.moderator) # ------------------------------------------------------------------------- # Wrapper method tests # ------------------------------------------------------------------------- def test_open_wrapper_method(self): """Test the open() wrapper method.""" ride = self._create_ride(status="CLOSED_TEMP") ride.open(user=self.user) ride.refresh_from_db() self.assertEqual(ride.status, "OPERATING") def test_close_temporarily_wrapper_method(self): """Test the close_temporarily() wrapper method.""" ride = self._create_ride(status="OPERATING") ride.close_temporarily(user=self.user) ride.refresh_from_db() self.assertEqual(ride.status, "CLOSED_TEMP") def test_mark_sbno_wrapper_method(self): """Test the mark_sbno() wrapper method.""" ride = self._create_ride(status="OPERATING") ride.mark_sbno(user=self.moderator) ride.refresh_from_db() self.assertEqual(ride.status, "SBNO") def test_mark_closing_wrapper_method(self): """Test the mark_closing() wrapper method.""" ride = self._create_ride(status="OPERATING") closing = date(2025, 12, 31) ride.mark_closing(closing_date=closing, post_closing_status="DEMOLISHED", user=self.moderator) ride.refresh_from_db() self.assertEqual(ride.status, "CLOSING") self.assertEqual(ride.closing_date, closing) self.assertEqual(ride.post_closing_status, "DEMOLISHED") def test_mark_closing_requires_post_closing_status(self): """Test that mark_closing() requires post_closing_status.""" ride = self._create_ride(status="OPERATING") with self.assertRaises(ValidationError): ride.mark_closing(closing_date=date(2025, 12, 31), post_closing_status="", user=self.moderator) def test_close_permanently_wrapper_method(self): """Test the close_permanently() wrapper method.""" ride = self._create_ride(status="SBNO") ride.close_permanently(user=self.moderator) ride.refresh_from_db() self.assertEqual(ride.status, "CLOSED_PERM") def test_demolish_wrapper_method(self): """Test the demolish() wrapper method.""" ride = self._create_ride(status="CLOSED_PERM") ride.demolish(user=self.moderator) ride.refresh_from_db() self.assertEqual(ride.status, "DEMOLISHED") def test_relocate_wrapper_method(self): """Test the relocate() wrapper method.""" ride = self._create_ride(status="CLOSED_PERM") ride.relocate(user=self.moderator) ride.refresh_from_db() self.assertEqual(ride.status, "RELOCATED") # ============================================================================ # Ride Post-Closing Status Tests # ============================================================================ class RidePostClosingTests(TestCase): """Tests for Ride post_closing_status automation.""" def setUp(self): """Set up test fixtures.""" self.moderator = User.objects.create_user( username="moderator", email="moderator@example.com", password="testpass123", role="MODERATOR" ) self.operator = ParkCompany.objects.create( name="Test Operator", description="Test operator company", roles=["OPERATOR"] ) self.park = Park.objects.create( name="Test Park", slug="test-park", description="A test park", operator=self.operator, timezone="America/New_York", ) self.manufacturer = Company.objects.create( name="Test Manufacturer", description="Test manufacturer company", roles=["MANUFACTURER"] ) def _create_ride(self, status="OPERATING", **kwargs): """Helper to create a Ride.""" defaults = { "name": "Test Ride", "slug": "test-ride", "description": "A test ride", "park": self.park, "manufacturer": self.manufacturer, } defaults.update(kwargs) return Ride.objects.create(status=status, **defaults) def test_apply_post_closing_status_to_demolished(self): """Test apply_post_closing_status transitions to DEMOLISHED.""" yesterday = date.today() - timedelta(days=1) ride = self._create_ride(status="CLOSING", closing_date=yesterday, post_closing_status="DEMOLISHED") ride.apply_post_closing_status(user=self.moderator) ride.refresh_from_db() self.assertEqual(ride.status, "DEMOLISHED") def test_apply_post_closing_status_to_relocated(self): """Test apply_post_closing_status transitions to RELOCATED.""" yesterday = date.today() - timedelta(days=1) ride = self._create_ride(status="CLOSING", closing_date=yesterday, post_closing_status="RELOCATED") ride.apply_post_closing_status(user=self.moderator) ride.refresh_from_db() self.assertEqual(ride.status, "RELOCATED") def test_apply_post_closing_status_to_sbno(self): """Test apply_post_closing_status transitions to SBNO.""" yesterday = date.today() - timedelta(days=1) ride = self._create_ride(status="CLOSING", closing_date=yesterday, post_closing_status="SBNO") ride.apply_post_closing_status(user=self.moderator) ride.refresh_from_db() self.assertEqual(ride.status, "SBNO") def test_apply_post_closing_status_to_closed_perm(self): """Test apply_post_closing_status transitions to CLOSED_PERM.""" yesterday = date.today() - timedelta(days=1) ride = self._create_ride(status="CLOSING", closing_date=yesterday, post_closing_status="CLOSED_PERM") ride.apply_post_closing_status(user=self.moderator) ride.refresh_from_db() self.assertEqual(ride.status, "CLOSED_PERM") def test_apply_post_closing_status_not_yet_reached(self): """Test apply_post_closing_status does nothing if date not reached.""" tomorrow = date.today() + timedelta(days=1) ride = self._create_ride(status="CLOSING", closing_date=tomorrow, post_closing_status="DEMOLISHED") ride.apply_post_closing_status(user=self.moderator) ride.refresh_from_db() # Status should remain CLOSING since date hasn't been reached self.assertEqual(ride.status, "CLOSING") def test_apply_post_closing_status_requires_closing_status(self): """Test apply_post_closing_status requires CLOSING status.""" ride = self._create_ride(status="OPERATING") with self.assertRaises(ValidationError) as ctx: ride.apply_post_closing_status(user=self.moderator) self.assertIn("CLOSING", str(ctx.exception)) def test_apply_post_closing_status_requires_closing_date(self): """Test apply_post_closing_status requires closing_date.""" ride = self._create_ride(status="CLOSING", post_closing_status="DEMOLISHED") ride.closing_date = None ride.save() with self.assertRaises(ValidationError) as ctx: ride.apply_post_closing_status(user=self.moderator) self.assertIn("closing_date", str(ctx.exception)) def test_apply_post_closing_status_requires_post_closing_status(self): """Test apply_post_closing_status requires post_closing_status.""" yesterday = date.today() - timedelta(days=1) ride = self._create_ride(status="CLOSING", closing_date=yesterday) ride.post_closing_status = None ride.save() with self.assertRaises(ValidationError) as ctx: ride.apply_post_closing_status(user=self.moderator) self.assertIn("post_closing_status", str(ctx.exception)) # ============================================================================ # Ride Transition History Tests # ============================================================================ class RideTransitionHistoryTests(TestCase): """Tests for Ride transition history logging.""" def setUp(self): """Set up test fixtures.""" self.moderator = User.objects.create_user( username="moderator", email="moderator@example.com", password="testpass123", role="MODERATOR" ) self.operator = ParkCompany.objects.create( name="Test Operator", description="Test operator company", roles=["OPERATOR"] ) self.park = Park.objects.create( name="Test Park", slug="test-park", description="A test park", operator=self.operator, timezone="America/New_York", ) self.manufacturer = Company.objects.create( name="Test Manufacturer", description="Test manufacturer company", roles=["MANUFACTURER"] ) def _create_ride(self, status="OPERATING"): """Helper to create a Ride.""" return Ride.objects.create( name="Test Ride", slug="test-ride", description="A test ride", park=self.park, manufacturer=self.manufacturer, status=status, ) def test_transition_creates_state_log(self): """Test that transitions create StateLog entries.""" from django_fsm_log.models import StateLog ride = self._create_ride(status="OPERATING") ride.transition_to_closed_temp(user=self.moderator) ride.save() ride_ct = ContentType.objects.get_for_model(ride) log = StateLog.objects.filter(content_type=ride_ct, object_id=ride.id).first() self.assertIsNotNone(log) self.assertEqual(log.state, "CLOSED_TEMP") self.assertEqual(log.by, self.moderator) def test_multiple_transitions_create_multiple_logs(self): """Test that multiple transitions create multiple log entries.""" from django_fsm_log.models import StateLog ride = self._create_ride(status="OPERATING") ride_ct = ContentType.objects.get_for_model(ride) # First transition ride.transition_to_closed_temp(user=self.moderator) ride.save() # Second transition ride.transition_to_operating(user=self.moderator) ride.save() logs = StateLog.objects.filter(content_type=ride_ct, object_id=ride.id).order_by("timestamp") self.assertEqual(logs.count(), 2) self.assertEqual(logs[0].state, "CLOSED_TEMP") self.assertEqual(logs[1].state, "OPERATING") def test_transition_log_includes_user(self): """Test that transition logs include the user who made the change.""" from django_fsm_log.models import StateLog ride = self._create_ride(status="OPERATING") ride.transition_to_sbno(user=self.moderator) ride.save() ride_ct = ContentType.objects.get_for_model(ride) log = StateLog.objects.filter(content_type=ride_ct, object_id=ride.id).first() self.assertEqual(log.by, self.moderator) def test_post_closing_transition_logged(self): """Test that post_closing_status transitions are logged.""" from django_fsm_log.models import StateLog yesterday = date.today() - timedelta(days=1) ride = self._create_ride(status="CLOSING") ride.closing_date = yesterday ride.post_closing_status = "DEMOLISHED" ride.save() ride.apply_post_closing_status(user=self.moderator) ride_ct = ContentType.objects.get_for_model(ride) log = StateLog.objects.filter(content_type=ride_ct, object_id=ride.id, state="DEMOLISHED").first() self.assertIsNotNone(log) self.assertEqual(log.by, self.moderator) # ============================================================================ # Ride Model Business Logic Tests # ============================================================================ class RideBusinessLogicTests(TestCase): """Tests for Ride model business logic.""" def setUp(self): """Set up test fixtures.""" self.operator = ParkCompany.objects.create( name="Test Operator", description="Test operator company", roles=["OPERATOR"] ) self.park = Park.objects.create( name="Test Park", slug="test-park", description="A test park", operator=self.operator, timezone="America/New_York", ) self.manufacturer = Company.objects.create( name="Test Manufacturer", description="Test manufacturer company", roles=["MANUFACTURER"] ) def test_ride_creates_with_valid_park(self): """Test ride can be created with valid park.""" ride = Ride.objects.create( name="Test Ride", slug="test-ride", description="A test ride", park=self.park, manufacturer=self.manufacturer, ) self.assertEqual(ride.park, self.park) def test_ride_slug_auto_generated(self): """Test that ride slug is auto-generated from name.""" ride = Ride.objects.create( name="My Amazing Roller Coaster", description="A test ride", park=self.park, manufacturer=self.manufacturer ) self.assertEqual(ride.slug, "my-amazing-roller-coaster") def test_ride_url_generated(self): """Test that frontend URL is generated on save.""" ride = Ride.objects.create( name="Test Ride", slug="test-ride", description="A test ride", park=self.park, manufacturer=self.manufacturer, ) self.assertIn("test-park", ride.url) self.assertIn("test-ride", ride.url) def test_opening_year_computed_from_opening_date(self): """Test that opening_year is computed from opening_date.""" ride = Ride.objects.create( name="Test Ride", slug="test-ride", description="A test ride", park=self.park, manufacturer=self.manufacturer, opening_date=date(2020, 6, 15), ) self.assertEqual(ride.opening_year, 2020) def test_search_text_populated(self): """Test that search_text is populated on save.""" ride = Ride.objects.create( name="Test Ride", slug="test-ride", description="A thrilling roller coaster", park=self.park, manufacturer=self.manufacturer, ) self.assertIn("test ride", ride.search_text) self.assertIn("thrilling roller coaster", ride.search_text) self.assertIn("test park", ride.search_text) self.assertIn("test manufacturer", ride.search_text) def test_ride_slug_unique_within_park(self): """Test that ride slugs are unique within a park.""" Ride.objects.create( name="Test Ride", slug="test-ride", description="First ride", park=self.park, manufacturer=self.manufacturer ) # Creating another ride with same name should get different slug ride2 = Ride.objects.create( name="Test Ride", description="Second ride", park=self.park, manufacturer=self.manufacturer ) self.assertNotEqual(ride2.slug, "test-ride") self.assertTrue(ride2.slug.startswith("test-ride")) # ============================================================================ # Ride Move to Park Tests # ============================================================================ class RideMoveTests(TestCase): """Tests for moving rides between parks.""" def setUp(self): """Set up test fixtures.""" self.operator = ParkCompany.objects.create( name="Test Operator", description="Test operator company", roles=["OPERATOR"] ) self.park1 = Park.objects.create( name="Park One", slug="park-one", description="First park", operator=self.operator, timezone="America/New_York", ) self.park2 = Park.objects.create( name="Park Two", slug="park-two", description="Second park", operator=self.operator, timezone="America/Los_Angeles", ) self.manufacturer = Company.objects.create( name="Test Manufacturer", description="Test manufacturer company", roles=["MANUFACTURER"] ) def test_move_ride_to_different_park(self): """Test moving a ride to a different park.""" ride = Ride.objects.create( name="Test Ride", slug="test-ride", description="A test ride", park=self.park1, manufacturer=self.manufacturer, ) changes = ride.move_to_park(self.park2) ride.refresh_from_db() self.assertEqual(ride.park, self.park2) self.assertEqual(changes["old_park"]["id"], self.park1.id) self.assertEqual(changes["new_park"]["id"], self.park2.id) def test_move_ride_updates_url(self): """Test that moving a ride updates the URL.""" ride = Ride.objects.create( name="Test Ride", slug="test-ride", description="A test ride", park=self.park1, manufacturer=self.manufacturer, ) old_url = ride.url changes = ride.move_to_park(self.park2) ride.refresh_from_db() self.assertNotEqual(ride.url, old_url) self.assertIn("park-two", ride.url) self.assertTrue(changes["url_changed"]) def test_move_ride_handles_slug_conflict(self): """Test that moving a ride handles slug conflicts in destination park.""" # Create ride in park1 ride1 = Ride.objects.create( name="Test Ride", slug="test-ride", description="A test ride", park=self.park1, manufacturer=self.manufacturer, ) # Create ride with same slug in park2 Ride.objects.create( name="Test Ride", slug="test-ride", description="Another test ride", park=self.park2, manufacturer=self.manufacturer, ) # Move ride1 to park2 changes = ride1.move_to_park(self.park2) ride1.refresh_from_db() self.assertEqual(ride1.park, self.park2) # Slug should have been modified to avoid conflict self.assertNotEqual(ride1.slug, "test-ride") self.assertTrue(changes["slug_changed"]) # ============================================================================ # Ride Historical Slug Tests # ============================================================================ class RideSlugHistoryTests(TestCase): """Tests for Ride historical slug tracking.""" def setUp(self): """Set up test fixtures.""" self.operator = ParkCompany.objects.create( name="Test Operator", description="Test operator company", roles=["OPERATOR"] ) self.park = Park.objects.create( name="Test Park", slug="test-park", description="A test park", operator=self.operator, timezone="America/New_York", ) self.manufacturer = Company.objects.create( name="Test Manufacturer", description="Test manufacturer company", roles=["MANUFACTURER"] ) def test_get_by_slug_finds_current_slug(self): """Test get_by_slug finds ride by current slug.""" ride = Ride.objects.create( name="Test Ride", slug="test-ride", description="A test ride", park=self.park, manufacturer=self.manufacturer, ) found_ride, is_historical = Ride.get_by_slug("test-ride", park=self.park) self.assertEqual(found_ride, ride) self.assertFalse(is_historical) def test_get_by_slug_with_park_filter(self): """Test get_by_slug filters by park.""" ride = Ride.objects.create( name="Test Ride", slug="test-ride", description="A test ride", park=self.park, manufacturer=self.manufacturer, ) # Should find ride in correct park found_ride, is_historical = Ride.get_by_slug("test-ride", park=self.park) self.assertEqual(found_ride, ride) # Should not find ride in different park other_park = Park.objects.create( name="Other Park", slug="other-park", description="Another park", operator=self.operator, timezone="America/New_York", ) with self.assertRaises(Ride.DoesNotExist): Ride.get_by_slug("test-ride", park=other_park)