feat: Implement MFA authentication, add ride statistics model, and update various services, APIs, and tests across the application.

This commit is contained in:
pacnpal
2025-12-28 17:32:53 -05:00
parent aa56c46c27
commit c95f99ca10
452 changed files with 7948 additions and 6073 deletions

View File

@@ -9,15 +9,18 @@ This module contains tests for:
- Related model updates during transitions
"""
from django.test import TestCase
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.utils import timezone
from django.test import TestCase
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
from apps.parks.models import Company as ParkCompany
from apps.parks.models import Park
from .models import Company, Ride
User = get_user_model()
@@ -50,7 +53,7 @@ class RideTransitionTests(TestCase):
password='testpass123',
role='ADMIN'
)
# Create operator and park
self.operator = ParkCompany.objects.create(
name='Test Operator',
@@ -64,7 +67,7 @@ class RideTransitionTests(TestCase):
operator=self.operator,
timezone='America/New_York'
)
# Create manufacturer
self.manufacturer = Company.objects.create(
name='Test Manufacturer',
@@ -92,30 +95,30 @@ class RideTransitionTests(TestCase):
"""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')
@@ -127,10 +130,10 @@ class RideTransitionTests(TestCase):
"""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')
@@ -141,31 +144,31 @@ class RideTransitionTests(TestCase):
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')
@@ -176,20 +179,20 @@ class RideTransitionTests(TestCase):
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')
@@ -200,20 +203,20 @@ class RideTransitionTests(TestCase):
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')
@@ -224,20 +227,20 @@ class RideTransitionTests(TestCase):
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')
@@ -248,28 +251,28 @@ class RideTransitionTests(TestCase):
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)
@@ -280,27 +283,27 @@ class RideTransitionTests(TestCase):
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')
@@ -308,13 +311,13 @@ class RideTransitionTests(TestCase):
"""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)
@@ -323,7 +326,7 @@ class RideTransitionTests(TestCase):
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),
@@ -334,27 +337,27 @@ class RideTransitionTests(TestCase):
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')
@@ -413,9 +416,9 @@ class RidePostClosingTests(TestCase):
closing_date=yesterday,
post_closing_status='DEMOLISHED'
)
ride.apply_post_closing_status(user=self.moderator)
ride.refresh_from_db()
self.assertEqual(ride.status, 'DEMOLISHED')
@@ -427,9 +430,9 @@ class RidePostClosingTests(TestCase):
closing_date=yesterday,
post_closing_status='RELOCATED'
)
ride.apply_post_closing_status(user=self.moderator)
ride.refresh_from_db()
self.assertEqual(ride.status, 'RELOCATED')
@@ -441,9 +444,9 @@ class RidePostClosingTests(TestCase):
closing_date=yesterday,
post_closing_status='SBNO'
)
ride.apply_post_closing_status(user=self.moderator)
ride.refresh_from_db()
self.assertEqual(ride.status, 'SBNO')
@@ -455,9 +458,9 @@ class RidePostClosingTests(TestCase):
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')
@@ -469,9 +472,9 @@ class RidePostClosingTests(TestCase):
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')
@@ -479,10 +482,10 @@ class RidePostClosingTests(TestCase):
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):
@@ -493,10 +496,10 @@ class RidePostClosingTests(TestCase):
)
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):
@@ -508,10 +511,10 @@ class RidePostClosingTests(TestCase):
)
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))
@@ -563,18 +566,18 @@ class RideTransitionHistoryTests(TestCase):
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)
@@ -582,23 +585,23 @@ class RideTransitionHistoryTests(TestCase):
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')
@@ -606,39 +609,39 @@ class RideTransitionHistoryTests(TestCase):
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)
@@ -680,7 +683,7 @@ class RideBusinessLogicTests(TestCase):
park=self.park,
manufacturer=self.manufacturer
)
self.assertEqual(ride.park, self.park)
def test_ride_slug_auto_generated(self):
@@ -691,7 +694,7 @@ class RideBusinessLogicTests(TestCase):
park=self.park,
manufacturer=self.manufacturer
)
self.assertEqual(ride.slug, 'my-amazing-roller-coaster')
def test_ride_url_generated(self):
@@ -703,7 +706,7 @@ class RideBusinessLogicTests(TestCase):
park=self.park,
manufacturer=self.manufacturer
)
self.assertIn('test-park', ride.url)
self.assertIn('test-ride', ride.url)
@@ -717,7 +720,7 @@ class RideBusinessLogicTests(TestCase):
manufacturer=self.manufacturer,
opening_date=date(2020, 6, 15)
)
self.assertEqual(ride.opening_year, 2020)
def test_search_text_populated(self):
@@ -729,7 +732,7 @@ class RideBusinessLogicTests(TestCase):
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)
@@ -744,7 +747,7 @@ class RideBusinessLogicTests(TestCase):
park=self.park,
manufacturer=self.manufacturer
)
# Creating another ride with same name should get different slug
ride2 = Ride.objects.create(
name='Test Ride',
@@ -752,7 +755,7 @@ class RideBusinessLogicTests(TestCase):
park=self.park,
manufacturer=self.manufacturer
)
self.assertNotEqual(ride2.slug, 'test-ride')
self.assertTrue(ride2.slug.startswith('test-ride'))
@@ -801,9 +804,9 @@ class RideMoveTests(TestCase):
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)
@@ -819,9 +822,9 @@ class RideMoveTests(TestCase):
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)
@@ -837,7 +840,7 @@ class RideMoveTests(TestCase):
park=self.park1,
manufacturer=self.manufacturer
)
# Create ride with same slug in park2
Ride.objects.create(
name='Test Ride',
@@ -846,10 +849,10 @@ class RideMoveTests(TestCase):
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
@@ -894,9 +897,9 @@ class RideSlugHistoryTests(TestCase):
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)
@@ -909,11 +912,11 @@ class RideSlugHistoryTests(TestCase):
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',