mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 23:51:09 -05:00
Add state machine diagrams and code examples for ThrillWiki
- Created a comprehensive documentation file for state machine diagrams, detailing various states and transitions for models such as EditSubmission, ModerationReport, and Park Status. - Included transition matrices for each state machine to clarify role requirements and guards. - Developed a new document providing code examples for implementing state machines, including adding new state machines to models, defining custom guards, implementing callbacks, and testing state machines. - Added examples for document approval workflows, custom guards, email notifications, and cache invalidation callbacks. - Implemented a test suite for document workflows, covering various scenarios including approval, rejection, and transition logging.
This commit is contained in:
@@ -1 +1,926 @@
|
||||
# Create your tests here.
|
||||
"""
|
||||
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)
|
||||
|
||||
7
backend/apps/rides/tests/__init__.py
Normal file
7
backend/apps/rides/tests/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""
|
||||
Rides test package.
|
||||
|
||||
This package contains tests for the rides app including:
|
||||
- Ride workflow tests (test_ride_workflows.py)
|
||||
- Ride model tests
|
||||
"""
|
||||
900
backend/apps/rides/tests/test_ride_workflows.py
Normal file
900
backend/apps/rides/tests/test_ride_workflows.py
Normal file
@@ -0,0 +1,900 @@
|
||||
"""
|
||||
Integration tests for Ride lifecycle workflows.
|
||||
|
||||
This module tests end-to-end ride lifecycle workflows including:
|
||||
- Ride opening workflow
|
||||
- Ride maintenance workflow
|
||||
- Ride SBNO workflow
|
||||
- Ride scheduled closure workflow
|
||||
- Ride demolition workflow
|
||||
- Ride relocation workflow
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class RideOpeningWorkflowTests(TestCase):
|
||||
"""Tests for ride opening workflow."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.user = User.objects.create_user(
|
||||
username='ride_user',
|
||||
email='ride_user@example.com',
|
||||
password='testpass123',
|
||||
role='USER'
|
||||
)
|
||||
|
||||
def _create_ride(self, status='OPERATING', **kwargs):
|
||||
"""Helper to create a ride with park."""
|
||||
from apps.rides.models import Ride
|
||||
from apps.parks.models import Park, Company
|
||||
|
||||
# Create manufacturer
|
||||
manufacturer = Company.objects.create(
|
||||
name=f'Manufacturer {timezone.now().timestamp()}',
|
||||
roles=['MANUFACTURER']
|
||||
)
|
||||
|
||||
# Create park with operator
|
||||
operator = Company.objects.create(
|
||||
name=f'Operator {timezone.now().timestamp()}',
|
||||
roles=['OPERATOR']
|
||||
)
|
||||
park = Park.objects.create(
|
||||
name=f'Test Park {timezone.now().timestamp()}',
|
||||
slug=f'test-park-{timezone.now().timestamp()}',
|
||||
operator=operator,
|
||||
status='OPERATING',
|
||||
timezone='America/New_York'
|
||||
)
|
||||
|
||||
defaults = {
|
||||
'name': f'Test Ride {timezone.now().timestamp()}',
|
||||
'slug': f'test-ride-{timezone.now().timestamp()}',
|
||||
'park': park,
|
||||
'manufacturer': manufacturer,
|
||||
'status': status
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
return Ride.objects.create(**defaults)
|
||||
|
||||
def test_ride_opens_from_under_construction(self):
|
||||
"""
|
||||
Test ride opening from under construction state.
|
||||
|
||||
Flow: UNDER_CONSTRUCTION → OPERATING
|
||||
"""
|
||||
ride = self._create_ride(status='UNDER_CONSTRUCTION')
|
||||
|
||||
self.assertEqual(ride.status, 'UNDER_CONSTRUCTION')
|
||||
|
||||
# Ride opens
|
||||
ride.transition_to_operating(user=self.user)
|
||||
ride.save()
|
||||
|
||||
ride.refresh_from_db()
|
||||
self.assertEqual(ride.status, 'OPERATING')
|
||||
|
||||
|
||||
class RideMaintenanceWorkflowTests(TestCase):
|
||||
"""Tests for ride maintenance (temporary closure) workflow."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.user = User.objects.create_user(
|
||||
username='maint_user',
|
||||
email='maint@example.com',
|
||||
password='testpass123',
|
||||
role='USER'
|
||||
)
|
||||
|
||||
def _create_ride(self, status='OPERATING', **kwargs):
|
||||
from apps.rides.models import Ride
|
||||
from apps.parks.models import Park, Company
|
||||
|
||||
manufacturer = Company.objects.create(
|
||||
name=f'Mfr Maint {timezone.now().timestamp()}',
|
||||
roles=['MANUFACTURER']
|
||||
)
|
||||
operator = Company.objects.create(
|
||||
name=f'Op Maint {timezone.now().timestamp()}',
|
||||
roles=['OPERATOR']
|
||||
)
|
||||
park = Park.objects.create(
|
||||
name=f'Park Maint {timezone.now().timestamp()}',
|
||||
slug=f'park-maint-{timezone.now().timestamp()}',
|
||||
operator=operator,
|
||||
status='OPERATING',
|
||||
timezone='America/New_York'
|
||||
)
|
||||
|
||||
defaults = {
|
||||
'name': f'Ride Maint {timezone.now().timestamp()}',
|
||||
'slug': f'ride-maint-{timezone.now().timestamp()}',
|
||||
'park': park,
|
||||
'manufacturer': manufacturer,
|
||||
'status': status
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
return Ride.objects.create(**defaults)
|
||||
|
||||
def test_ride_maintenance_and_reopen(self):
|
||||
"""
|
||||
Test ride maintenance and reopening.
|
||||
|
||||
Flow: OPERATING → CLOSED_TEMP → OPERATING
|
||||
"""
|
||||
ride = self._create_ride(status='OPERATING')
|
||||
|
||||
# Close for maintenance
|
||||
ride.transition_to_closed_temp(user=self.user)
|
||||
ride.save()
|
||||
|
||||
ride.refresh_from_db()
|
||||
self.assertEqual(ride.status, 'CLOSED_TEMP')
|
||||
|
||||
# Reopen after maintenance
|
||||
ride.transition_to_operating(user=self.user)
|
||||
ride.save()
|
||||
|
||||
ride.refresh_from_db()
|
||||
self.assertEqual(ride.status, 'OPERATING')
|
||||
|
||||
|
||||
class RideSBNOWorkflowTests(TestCase):
|
||||
"""Tests for ride SBNO (Standing But Not Operating) workflow."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.moderator = User.objects.create_user(
|
||||
username='sbno_mod',
|
||||
email='sbno_mod@example.com',
|
||||
password='testpass123',
|
||||
role='MODERATOR'
|
||||
)
|
||||
|
||||
def _create_ride(self, status='OPERATING', **kwargs):
|
||||
from apps.rides.models import Ride
|
||||
from apps.parks.models import Park, Company
|
||||
|
||||
manufacturer = Company.objects.create(
|
||||
name=f'Mfr SBNO {timezone.now().timestamp()}',
|
||||
roles=['MANUFACTURER']
|
||||
)
|
||||
operator = Company.objects.create(
|
||||
name=f'Op SBNO {timezone.now().timestamp()}',
|
||||
roles=['OPERATOR']
|
||||
)
|
||||
park = Park.objects.create(
|
||||
name=f'Park SBNO {timezone.now().timestamp()}',
|
||||
slug=f'park-sbno-{timezone.now().timestamp()}',
|
||||
operator=operator,
|
||||
status='OPERATING',
|
||||
timezone='America/New_York'
|
||||
)
|
||||
|
||||
defaults = {
|
||||
'name': f'Ride SBNO {timezone.now().timestamp()}',
|
||||
'slug': f'ride-sbno-{timezone.now().timestamp()}',
|
||||
'park': park,
|
||||
'manufacturer': manufacturer,
|
||||
'status': status
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
return Ride.objects.create(**defaults)
|
||||
|
||||
def test_ride_sbno_from_operating(self):
|
||||
"""
|
||||
Test ride becomes SBNO from operating.
|
||||
|
||||
Flow: OPERATING → SBNO
|
||||
"""
|
||||
ride = self._create_ride(status='OPERATING')
|
||||
|
||||
# Mark as SBNO
|
||||
ride.transition_to_sbno(user=self.moderator)
|
||||
ride.save()
|
||||
|
||||
ride.refresh_from_db()
|
||||
self.assertEqual(ride.status, 'SBNO')
|
||||
|
||||
def test_ride_sbno_from_closed_temp(self):
|
||||
"""
|
||||
Test ride becomes SBNO from temporary closure.
|
||||
|
||||
Flow: OPERATING → CLOSED_TEMP → SBNO
|
||||
"""
|
||||
ride = self._create_ride(status='CLOSED_TEMP')
|
||||
|
||||
# Extended to SBNO
|
||||
ride.transition_to_sbno(user=self.moderator)
|
||||
ride.save()
|
||||
|
||||
ride.refresh_from_db()
|
||||
self.assertEqual(ride.status, 'SBNO')
|
||||
|
||||
def test_ride_revival_from_sbno(self):
|
||||
"""
|
||||
Test ride revival from SBNO state.
|
||||
|
||||
Flow: SBNO → OPERATING
|
||||
"""
|
||||
ride = self._create_ride(status='SBNO')
|
||||
|
||||
# Revive the ride
|
||||
ride.transition_to_operating(user=self.moderator)
|
||||
ride.save()
|
||||
|
||||
ride.refresh_from_db()
|
||||
self.assertEqual(ride.status, 'OPERATING')
|
||||
|
||||
def test_sbno_to_closed_perm(self):
|
||||
"""
|
||||
Test ride permanently closes from SBNO.
|
||||
|
||||
Flow: SBNO → CLOSED_PERM
|
||||
"""
|
||||
ride = self._create_ride(status='SBNO')
|
||||
|
||||
# Confirm permanent closure
|
||||
ride.transition_to_closed_perm(user=self.moderator)
|
||||
ride.save()
|
||||
|
||||
ride.refresh_from_db()
|
||||
self.assertEqual(ride.status, 'CLOSED_PERM')
|
||||
|
||||
|
||||
class RideScheduledClosureWorkflowTests(TestCase):
|
||||
"""Tests for ride scheduled closure (CLOSING state) workflow."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.moderator = User.objects.create_user(
|
||||
username='closing_mod',
|
||||
email='closing_mod@example.com',
|
||||
password='testpass123',
|
||||
role='MODERATOR'
|
||||
)
|
||||
|
||||
def _create_ride(self, status='OPERATING', **kwargs):
|
||||
from apps.rides.models import Ride
|
||||
from apps.parks.models import Park, Company
|
||||
|
||||
manufacturer = Company.objects.create(
|
||||
name=f'Mfr Closing {timezone.now().timestamp()}',
|
||||
roles=['MANUFACTURER']
|
||||
)
|
||||
operator = Company.objects.create(
|
||||
name=f'Op Closing {timezone.now().timestamp()}',
|
||||
roles=['OPERATOR']
|
||||
)
|
||||
park = Park.objects.create(
|
||||
name=f'Park Closing {timezone.now().timestamp()}',
|
||||
slug=f'park-closing-{timezone.now().timestamp()}',
|
||||
operator=operator,
|
||||
status='OPERATING',
|
||||
timezone='America/New_York'
|
||||
)
|
||||
|
||||
defaults = {
|
||||
'name': f'Ride Closing {timezone.now().timestamp()}',
|
||||
'slug': f'ride-closing-{timezone.now().timestamp()}',
|
||||
'park': park,
|
||||
'manufacturer': manufacturer,
|
||||
'status': status
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
return Ride.objects.create(**defaults)
|
||||
|
||||
def test_ride_mark_closing_with_date(self):
|
||||
"""
|
||||
Test ride marked as closing with scheduled date.
|
||||
|
||||
Flow: OPERATING → CLOSING (with closing_date and post_closing_status)
|
||||
"""
|
||||
ride = self._create_ride(status='OPERATING')
|
||||
closing_date = (timezone.now() + timedelta(days=30)).date()
|
||||
|
||||
# Mark as closing
|
||||
ride.transition_to_closing(user=self.moderator)
|
||||
ride.closing_date = closing_date
|
||||
ride.post_closing_status = 'DEMOLISHED'
|
||||
ride.save()
|
||||
|
||||
ride.refresh_from_db()
|
||||
self.assertEqual(ride.status, 'CLOSING')
|
||||
self.assertEqual(ride.closing_date, closing_date)
|
||||
self.assertEqual(ride.post_closing_status, 'DEMOLISHED')
|
||||
|
||||
def test_closing_to_closed_perm(self):
|
||||
"""
|
||||
Test ride transitions from CLOSING to CLOSED_PERM when date reached.
|
||||
|
||||
Flow: CLOSING → CLOSED_PERM
|
||||
"""
|
||||
ride = self._create_ride(status='CLOSING')
|
||||
ride.closing_date = timezone.now().date()
|
||||
ride.post_closing_status = 'CLOSED_PERM'
|
||||
ride.save()
|
||||
|
||||
# Transition when closing date reached
|
||||
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(self):
|
||||
"""
|
||||
Test ride transitions from CLOSING to SBNO.
|
||||
|
||||
Flow: CLOSING → SBNO
|
||||
"""
|
||||
ride = self._create_ride(status='CLOSING')
|
||||
ride.closing_date = timezone.now().date()
|
||||
ride.post_closing_status = 'SBNO'
|
||||
ride.save()
|
||||
|
||||
# Transition to SBNO
|
||||
ride.transition_to_sbno(user=self.moderator)
|
||||
ride.save()
|
||||
|
||||
ride.refresh_from_db()
|
||||
self.assertEqual(ride.status, 'SBNO')
|
||||
|
||||
|
||||
class RideDemolitionWorkflowTests(TestCase):
|
||||
"""Tests for ride demolition workflow."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.moderator = User.objects.create_user(
|
||||
username='demo_ride_mod',
|
||||
email='demo_ride_mod@example.com',
|
||||
password='testpass123',
|
||||
role='MODERATOR'
|
||||
)
|
||||
|
||||
def _create_ride(self, status='CLOSED_PERM', **kwargs):
|
||||
from apps.rides.models import Ride
|
||||
from apps.parks.models import Park, Company
|
||||
|
||||
manufacturer = Company.objects.create(
|
||||
name=f'Mfr Demo {timezone.now().timestamp()}',
|
||||
roles=['MANUFACTURER']
|
||||
)
|
||||
operator = Company.objects.create(
|
||||
name=f'Op Demo {timezone.now().timestamp()}',
|
||||
roles=['OPERATOR']
|
||||
)
|
||||
park = Park.objects.create(
|
||||
name=f'Park Demo {timezone.now().timestamp()}',
|
||||
slug=f'park-demo-{timezone.now().timestamp()}',
|
||||
operator=operator,
|
||||
status='OPERATING',
|
||||
timezone='America/New_York'
|
||||
)
|
||||
|
||||
defaults = {
|
||||
'name': f'Ride Demo {timezone.now().timestamp()}',
|
||||
'slug': f'ride-demo-{timezone.now().timestamp()}',
|
||||
'park': park,
|
||||
'manufacturer': manufacturer,
|
||||
'status': status
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
return Ride.objects.create(**defaults)
|
||||
|
||||
def test_ride_demolition(self):
|
||||
"""
|
||||
Test ride demolition from permanently closed.
|
||||
|
||||
Flow: CLOSED_PERM → DEMOLISHED
|
||||
"""
|
||||
ride = self._create_ride(status='CLOSED_PERM')
|
||||
|
||||
# Demolish
|
||||
ride.transition_to_demolished(user=self.moderator)
|
||||
ride.save()
|
||||
|
||||
ride.refresh_from_db()
|
||||
self.assertEqual(ride.status, 'DEMOLISHED')
|
||||
|
||||
def test_demolished_is_final_state(self):
|
||||
"""Test that demolished rides cannot transition further."""
|
||||
from django_fsm import TransitionNotAllowed
|
||||
|
||||
ride = self._create_ride(status='DEMOLISHED')
|
||||
|
||||
# Cannot transition from demolished
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
ride.transition_to_operating(user=self.moderator)
|
||||
|
||||
|
||||
class RideRelocationWorkflowTests(TestCase):
|
||||
"""Tests for ride relocation workflow."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.moderator = User.objects.create_user(
|
||||
username='reloc_ride_mod',
|
||||
email='reloc_ride_mod@example.com',
|
||||
password='testpass123',
|
||||
role='MODERATOR'
|
||||
)
|
||||
|
||||
def _create_ride(self, status='CLOSED_PERM', **kwargs):
|
||||
from apps.rides.models import Ride
|
||||
from apps.parks.models import Park, Company
|
||||
|
||||
manufacturer = Company.objects.create(
|
||||
name=f'Mfr Reloc {timezone.now().timestamp()}',
|
||||
roles=['MANUFACTURER']
|
||||
)
|
||||
operator = Company.objects.create(
|
||||
name=f'Op Reloc {timezone.now().timestamp()}',
|
||||
roles=['OPERATOR']
|
||||
)
|
||||
park = Park.objects.create(
|
||||
name=f'Park Reloc {timezone.now().timestamp()}',
|
||||
slug=f'park-reloc-{timezone.now().timestamp()}',
|
||||
operator=operator,
|
||||
status='OPERATING',
|
||||
timezone='America/New_York'
|
||||
)
|
||||
|
||||
defaults = {
|
||||
'name': f'Ride Reloc {timezone.now().timestamp()}',
|
||||
'slug': f'ride-reloc-{timezone.now().timestamp()}',
|
||||
'park': park,
|
||||
'manufacturer': manufacturer,
|
||||
'status': status
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
return Ride.objects.create(**defaults)
|
||||
|
||||
def test_ride_relocation(self):
|
||||
"""
|
||||
Test ride relocation from permanently closed.
|
||||
|
||||
Flow: CLOSED_PERM → RELOCATED
|
||||
"""
|
||||
ride = self._create_ride(status='CLOSED_PERM')
|
||||
|
||||
# Relocate
|
||||
ride.transition_to_relocated(user=self.moderator)
|
||||
ride.save()
|
||||
|
||||
ride.refresh_from_db()
|
||||
self.assertEqual(ride.status, 'RELOCATED')
|
||||
|
||||
def test_relocated_is_final_state(self):
|
||||
"""Test that relocated rides cannot transition further."""
|
||||
from django_fsm import TransitionNotAllowed
|
||||
|
||||
ride = self._create_ride(status='RELOCATED')
|
||||
|
||||
# Cannot transition from relocated
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
ride.transition_to_operating(user=self.moderator)
|
||||
|
||||
|
||||
class RideWrapperMethodTests(TestCase):
|
||||
"""Tests for ride wrapper methods."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.user = User.objects.create_user(
|
||||
username='wrapper_ride_user',
|
||||
email='wrapper_ride@example.com',
|
||||
password='testpass123',
|
||||
role='USER'
|
||||
)
|
||||
cls.moderator = User.objects.create_user(
|
||||
username='wrapper_ride_mod',
|
||||
email='wrapper_ride_mod@example.com',
|
||||
password='testpass123',
|
||||
role='MODERATOR'
|
||||
)
|
||||
|
||||
def _create_ride(self, status='OPERATING', **kwargs):
|
||||
from apps.rides.models import Ride
|
||||
from apps.parks.models import Park, Company
|
||||
|
||||
manufacturer = Company.objects.create(
|
||||
name=f'Mfr Wrapper {timezone.now().timestamp()}',
|
||||
roles=['MANUFACTURER']
|
||||
)
|
||||
operator = Company.objects.create(
|
||||
name=f'Op Wrapper {timezone.now().timestamp()}',
|
||||
roles=['OPERATOR']
|
||||
)
|
||||
park = Park.objects.create(
|
||||
name=f'Park Wrapper {timezone.now().timestamp()}',
|
||||
slug=f'park-wrapper-{timezone.now().timestamp()}',
|
||||
operator=operator,
|
||||
status='OPERATING',
|
||||
timezone='America/New_York'
|
||||
)
|
||||
|
||||
defaults = {
|
||||
'name': f'Ride Wrapper {timezone.now().timestamp()}',
|
||||
'slug': f'ride-wrapper-{timezone.now().timestamp()}',
|
||||
'park': park,
|
||||
'manufacturer': manufacturer,
|
||||
'status': status
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
return Ride.objects.create(**defaults)
|
||||
|
||||
def test_close_temporarily_wrapper(self):
|
||||
"""Test close_temporarily wrapper method."""
|
||||
ride = self._create_ride(status='OPERATING')
|
||||
|
||||
if hasattr(ride, 'close_temporarily'):
|
||||
ride.close_temporarily(user=self.user)
|
||||
else:
|
||||
ride.transition_to_closed_temp(user=self.user)
|
||||
ride.save()
|
||||
|
||||
ride.refresh_from_db()
|
||||
self.assertEqual(ride.status, 'CLOSED_TEMP')
|
||||
|
||||
def test_mark_sbno_wrapper(self):
|
||||
"""Test mark_sbno wrapper method."""
|
||||
ride = self._create_ride(status='OPERATING')
|
||||
|
||||
if hasattr(ride, 'mark_sbno'):
|
||||
ride.mark_sbno(user=self.moderator)
|
||||
else:
|
||||
ride.transition_to_sbno(user=self.moderator)
|
||||
ride.save()
|
||||
|
||||
ride.refresh_from_db()
|
||||
self.assertEqual(ride.status, 'SBNO')
|
||||
|
||||
def test_mark_closing_wrapper(self):
|
||||
"""Test mark_closing wrapper method."""
|
||||
ride = self._create_ride(status='OPERATING')
|
||||
closing_date = (timezone.now() + timedelta(days=30)).date()
|
||||
|
||||
if hasattr(ride, 'mark_closing'):
|
||||
ride.mark_closing(
|
||||
closing_date=closing_date,
|
||||
post_closing_status='DEMOLISHED',
|
||||
user=self.moderator
|
||||
)
|
||||
else:
|
||||
ride.transition_to_closing(user=self.moderator)
|
||||
ride.closing_date = closing_date
|
||||
ride.post_closing_status = 'DEMOLISHED'
|
||||
ride.save()
|
||||
|
||||
ride.refresh_from_db()
|
||||
self.assertEqual(ride.status, 'CLOSING')
|
||||
|
||||
def test_open_wrapper(self):
|
||||
"""Test open wrapper method."""
|
||||
ride = self._create_ride(status='CLOSED_TEMP')
|
||||
|
||||
if hasattr(ride, 'open'):
|
||||
ride.open(user=self.user)
|
||||
else:
|
||||
ride.transition_to_operating(user=self.user)
|
||||
ride.save()
|
||||
|
||||
ride.refresh_from_db()
|
||||
self.assertEqual(ride.status, 'OPERATING')
|
||||
|
||||
def test_close_permanently_wrapper(self):
|
||||
"""Test close_permanently wrapper method."""
|
||||
ride = self._create_ride(status='SBNO')
|
||||
|
||||
if hasattr(ride, 'close_permanently'):
|
||||
ride.close_permanently(user=self.moderator)
|
||||
else:
|
||||
ride.transition_to_closed_perm(user=self.moderator)
|
||||
ride.save()
|
||||
|
||||
ride.refresh_from_db()
|
||||
self.assertEqual(ride.status, 'CLOSED_PERM')
|
||||
|
||||
def test_demolish_wrapper(self):
|
||||
"""Test demolish wrapper method."""
|
||||
ride = self._create_ride(status='CLOSED_PERM')
|
||||
|
||||
if hasattr(ride, 'demolish'):
|
||||
ride.demolish(user=self.moderator)
|
||||
else:
|
||||
ride.transition_to_demolished(user=self.moderator)
|
||||
ride.save()
|
||||
|
||||
ride.refresh_from_db()
|
||||
self.assertEqual(ride.status, 'DEMOLISHED')
|
||||
|
||||
def test_relocate_wrapper(self):
|
||||
"""Test relocate wrapper method."""
|
||||
ride = self._create_ride(status='CLOSED_PERM')
|
||||
|
||||
if hasattr(ride, 'relocate'):
|
||||
ride.relocate(user=self.moderator)
|
||||
else:
|
||||
ride.transition_to_relocated(user=self.moderator)
|
||||
ride.save()
|
||||
|
||||
ride.refresh_from_db()
|
||||
self.assertEqual(ride.status, 'RELOCATED')
|
||||
|
||||
|
||||
class RidePostClosingStatusAutomationTests(TestCase):
|
||||
"""Tests for post_closing_status automation logic."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.moderator = User.objects.create_user(
|
||||
username='auto_mod',
|
||||
email='auto_mod@example.com',
|
||||
password='testpass123',
|
||||
role='MODERATOR'
|
||||
)
|
||||
|
||||
def _create_ride(self, status='CLOSING', **kwargs):
|
||||
from apps.rides.models import Ride
|
||||
from apps.parks.models import Park, Company
|
||||
|
||||
manufacturer = Company.objects.create(
|
||||
name=f'Mfr Auto {timezone.now().timestamp()}',
|
||||
roles=['MANUFACTURER']
|
||||
)
|
||||
operator = Company.objects.create(
|
||||
name=f'Op Auto {timezone.now().timestamp()}',
|
||||
roles=['OPERATOR']
|
||||
)
|
||||
park = Park.objects.create(
|
||||
name=f'Park Auto {timezone.now().timestamp()}',
|
||||
slug=f'park-auto-{timezone.now().timestamp()}',
|
||||
operator=operator,
|
||||
status='OPERATING',
|
||||
timezone='America/New_York'
|
||||
)
|
||||
|
||||
defaults = {
|
||||
'name': f'Ride Auto {timezone.now().timestamp()}',
|
||||
'slug': f'ride-auto-{timezone.now().timestamp()}',
|
||||
'park': park,
|
||||
'manufacturer': manufacturer,
|
||||
'status': status
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
return Ride.objects.create(**defaults)
|
||||
|
||||
def test_apply_post_closing_status_demolished(self):
|
||||
"""Test apply_post_closing_status transitions to DEMOLISHED."""
|
||||
ride = self._create_ride(status='CLOSING')
|
||||
ride.closing_date = timezone.now().date()
|
||||
ride.post_closing_status = 'DEMOLISHED'
|
||||
ride.save()
|
||||
|
||||
# Apply post-closing status if method exists
|
||||
if hasattr(ride, 'apply_post_closing_status'):
|
||||
ride.apply_post_closing_status(user=self.moderator)
|
||||
else:
|
||||
ride.transition_to_demolished(user=self.moderator)
|
||||
ride.save()
|
||||
|
||||
ride.refresh_from_db()
|
||||
self.assertEqual(ride.status, 'DEMOLISHED')
|
||||
|
||||
def test_apply_post_closing_status_relocated(self):
|
||||
"""Test apply_post_closing_status transitions to RELOCATED."""
|
||||
ride = self._create_ride(status='CLOSING')
|
||||
ride.closing_date = timezone.now().date()
|
||||
ride.post_closing_status = 'RELOCATED'
|
||||
ride.save()
|
||||
|
||||
if hasattr(ride, 'apply_post_closing_status'):
|
||||
ride.apply_post_closing_status(user=self.moderator)
|
||||
else:
|
||||
ride.transition_to_relocated(user=self.moderator)
|
||||
ride.save()
|
||||
|
||||
ride.refresh_from_db()
|
||||
self.assertEqual(ride.status, 'RELOCATED')
|
||||
|
||||
def test_apply_post_closing_status_sbno(self):
|
||||
"""Test apply_post_closing_status transitions to SBNO."""
|
||||
ride = self._create_ride(status='CLOSING')
|
||||
ride.closing_date = timezone.now().date()
|
||||
ride.post_closing_status = 'SBNO'
|
||||
ride.save()
|
||||
|
||||
if hasattr(ride, 'apply_post_closing_status'):
|
||||
ride.apply_post_closing_status(user=self.moderator)
|
||||
else:
|
||||
ride.transition_to_sbno(user=self.moderator)
|
||||
ride.save()
|
||||
|
||||
ride.refresh_from_db()
|
||||
self.assertEqual(ride.status, 'SBNO')
|
||||
|
||||
|
||||
class RideStateLogTests(TestCase):
|
||||
"""Tests for StateLog entries on ride transitions."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.user = User.objects.create_user(
|
||||
username='ride_log_user',
|
||||
email='ride_log_user@example.com',
|
||||
password='testpass123',
|
||||
role='USER'
|
||||
)
|
||||
cls.moderator = User.objects.create_user(
|
||||
username='ride_log_mod',
|
||||
email='ride_log_mod@example.com',
|
||||
password='testpass123',
|
||||
role='MODERATOR'
|
||||
)
|
||||
|
||||
def _create_ride(self, status='OPERATING', **kwargs):
|
||||
from apps.rides.models import Ride
|
||||
from apps.parks.models import Park, Company
|
||||
|
||||
manufacturer = Company.objects.create(
|
||||
name=f'Mfr Log {timezone.now().timestamp()}',
|
||||
roles=['MANUFACTURER']
|
||||
)
|
||||
operator = Company.objects.create(
|
||||
name=f'Op Log {timezone.now().timestamp()}',
|
||||
roles=['OPERATOR']
|
||||
)
|
||||
park = Park.objects.create(
|
||||
name=f'Park Log {timezone.now().timestamp()}',
|
||||
slug=f'park-log-{timezone.now().timestamp()}',
|
||||
operator=operator,
|
||||
status='OPERATING',
|
||||
timezone='America/New_York'
|
||||
)
|
||||
|
||||
defaults = {
|
||||
'name': f'Ride Log {timezone.now().timestamp()}',
|
||||
'slug': f'ride-log-{timezone.now().timestamp()}',
|
||||
'park': park,
|
||||
'manufacturer': manufacturer,
|
||||
'status': status
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
return Ride.objects.create(**defaults)
|
||||
|
||||
def test_transition_creates_state_log(self):
|
||||
"""Test that ride transitions create StateLog entries."""
|
||||
from django_fsm_log.models import StateLog
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
ride = self._create_ride(status='OPERATING')
|
||||
ride_ct = ContentType.objects.get_for_model(ride)
|
||||
|
||||
# Perform transition
|
||||
ride.transition_to_closed_temp(user=self.user)
|
||||
ride.save()
|
||||
|
||||
# Check log was created
|
||||
log = StateLog.objects.filter(
|
||||
content_type=ride_ct,
|
||||
object_id=ride.id
|
||||
).first()
|
||||
|
||||
self.assertIsNotNone(log, "StateLog entry should be created")
|
||||
self.assertEqual(log.state, 'CLOSED_TEMP')
|
||||
self.assertEqual(log.by, self.user)
|
||||
|
||||
def test_multiple_transitions_logged(self):
|
||||
"""Test that multiple ride transitions are all logged."""
|
||||
from django_fsm_log.models import StateLog
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
ride = self._create_ride(status='OPERATING')
|
||||
ride_ct = ContentType.objects.get_for_model(ride)
|
||||
|
||||
# First transition: OPERATING -> SBNO
|
||||
ride.transition_to_sbno(user=self.moderator)
|
||||
ride.save()
|
||||
|
||||
# Second transition: SBNO -> OPERATING (revival)
|
||||
ride.transition_to_operating(user=self.moderator)
|
||||
ride.save()
|
||||
|
||||
# Check multiple logs created
|
||||
logs = StateLog.objects.filter(
|
||||
content_type=ride_ct,
|
||||
object_id=ride.id
|
||||
).order_by('timestamp')
|
||||
|
||||
self.assertEqual(logs.count(), 2, "Should have 2 log entries")
|
||||
self.assertEqual(logs[0].state, 'SBNO')
|
||||
self.assertEqual(logs[1].state, 'OPERATING')
|
||||
|
||||
def test_sbno_revival_workflow_logged(self):
|
||||
"""Test that SBNO revival workflow is logged."""
|
||||
from django_fsm_log.models import StateLog
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
ride = self._create_ride(status='SBNO')
|
||||
ride_ct = ContentType.objects.get_for_model(ride)
|
||||
|
||||
# Revival: SBNO -> OPERATING
|
||||
ride.transition_to_operating(user=self.moderator)
|
||||
ride.save()
|
||||
|
||||
# Check log was created
|
||||
log = StateLog.objects.filter(
|
||||
content_type=ride_ct,
|
||||
object_id=ride.id
|
||||
).first()
|
||||
|
||||
self.assertIsNotNone(log, "StateLog entry should be created")
|
||||
self.assertEqual(log.state, 'OPERATING')
|
||||
self.assertEqual(log.by, self.moderator)
|
||||
|
||||
def test_full_lifecycle_logged(self):
|
||||
"""Test complete ride lifecycle is logged."""
|
||||
from django_fsm_log.models import StateLog
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
ride = self._create_ride(status='OPERATING')
|
||||
ride_ct = ContentType.objects.get_for_model(ride)
|
||||
|
||||
# Lifecycle: OPERATING -> CLOSED_TEMP -> SBNO -> CLOSED_PERM -> DEMOLISHED
|
||||
ride.transition_to_closed_temp(user=self.user)
|
||||
ride.save()
|
||||
|
||||
ride.transition_to_sbno(user=self.moderator)
|
||||
ride.save()
|
||||
|
||||
ride.transition_to_closed_perm(user=self.moderator)
|
||||
ride.save()
|
||||
|
||||
ride.transition_to_demolished(user=self.moderator)
|
||||
ride.save()
|
||||
|
||||
# Check all logs created
|
||||
logs = StateLog.objects.filter(
|
||||
content_type=ride_ct,
|
||||
object_id=ride.id
|
||||
).order_by('timestamp')
|
||||
|
||||
self.assertEqual(logs.count(), 4, "Should have 4 log entries")
|
||||
states = [log.state for log in logs]
|
||||
self.assertEqual(states, ['CLOSED_TEMP', 'SBNO', 'CLOSED_PERM', 'DEMOLISHED'])
|
||||
|
||||
def test_scheduled_closing_workflow_logged(self):
|
||||
"""Test that scheduled closing workflow creates logs."""
|
||||
from django_fsm_log.models import StateLog
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
ride = self._create_ride(status='OPERATING')
|
||||
ride_ct = ContentType.objects.get_for_model(ride)
|
||||
|
||||
# Scheduled closing workflow: OPERATING -> CLOSING -> CLOSED_PERM
|
||||
ride.transition_to_closing(user=self.moderator)
|
||||
ride.closing_date = (timezone.now() + timedelta(days=30)).date()
|
||||
ride.post_closing_status = 'DEMOLISHED'
|
||||
ride.save()
|
||||
|
||||
ride.transition_to_closed_perm(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, "Should have 2 log entries")
|
||||
self.assertEqual(logs[0].state, 'CLOSING')
|
||||
self.assertEqual(logs[1].state, 'CLOSED_PERM')
|
||||
Reference in New Issue
Block a user