""" 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 django.test import TestCase from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError from django.utils import timezone from django_fsm import TransitionNotAllowed from .models import Ride, RideModel, Company from apps.parks.models import Park, Company as ParkCompany from datetime import date, timedelta 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)