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

@@ -5,7 +5,6 @@ These tests verify the functionality of ride, model, stats, company,
review, and ranking admin classes including query optimization and custom actions.
"""
import pytest
from django.contrib.admin.sites import AdminSite
from django.contrib.auth import get_user_model
from django.test import RequestFactory, TestCase
@@ -14,7 +13,6 @@ from apps.rides.admin import (
CompanyAdmin,
RankingSnapshotAdmin,
RideAdmin,
RideLocationAdmin,
RideModelAdmin,
RidePairComparisonAdmin,
RideRankingAdmin,
@@ -22,7 +20,6 @@ from apps.rides.admin import (
RollerCoasterStatsAdmin,
)
from apps.rides.models.company import Company
from apps.rides.models.location import RideLocation
from apps.rides.models.rankings import RankingSnapshot, RidePairComparison, RideRanking
from apps.rides.models.reviews import RideReview
from apps.rides.models.rides import Ride, RideModel, RollerCoasterStats

View File

@@ -10,17 +10,18 @@ This module tests end-to-end ride lifecycle workflows including:
- 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
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.utils import timezone
User = get_user_model()
class RideOpeningWorkflowTests(TestCase):
"""Tests for ride opening workflow."""
@classmethod
def setUpTestData(cls):
cls.user = User.objects.create_user(
@@ -29,18 +30,18 @@ class RideOpeningWorkflowTests(TestCase):
password='testpass123',
role='USER'
)
def _create_ride(self, status='OPERATING', **kwargs):
"""Helper to create a ride with park."""
from apps.parks.models import Company, 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()}',
@@ -53,7 +54,7 @@ class RideOpeningWorkflowTests(TestCase):
status='OPERATING',
timezone='America/New_York'
)
defaults = {
'name': f'Test Ride {timezone.now().timestamp()}',
'slug': f'test-ride-{timezone.now().timestamp()}',
@@ -63,28 +64,28 @@ class RideOpeningWorkflowTests(TestCase):
}
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(
@@ -93,11 +94,11 @@ class RideMaintenanceWorkflowTests(TestCase):
password='testpass123',
role='USER'
)
def _create_ride(self, status='OPERATING', **kwargs):
from apps.parks.models import Company, Park
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']
@@ -113,7 +114,7 @@ class RideMaintenanceWorkflowTests(TestCase):
status='OPERATING',
timezone='America/New_York'
)
defaults = {
'name': f'Ride Maint {timezone.now().timestamp()}',
'slug': f'ride-maint-{timezone.now().timestamp()}',
@@ -123,33 +124,33 @@ class RideMaintenanceWorkflowTests(TestCase):
}
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(
@@ -158,11 +159,11 @@ class RideSBNOWorkflowTests(TestCase):
password='testpass123',
role='MODERATOR'
)
def _create_ride(self, status='OPERATING', **kwargs):
from apps.parks.models import Company, Park
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']
@@ -178,7 +179,7 @@ class RideSBNOWorkflowTests(TestCase):
status='OPERATING',
timezone='America/New_York'
)
defaults = {
'name': f'Ride SBNO {timezone.now().timestamp()}',
'slug': f'ride-sbno-{timezone.now().timestamp()}',
@@ -188,71 +189,71 @@ class RideSBNOWorkflowTests(TestCase):
}
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(
@@ -261,11 +262,11 @@ class RideScheduledClosureWorkflowTests(TestCase):
password='testpass123',
role='MODERATOR'
)
def _create_ride(self, status='OPERATING', **kwargs):
from apps.parks.models import Company, Park
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']
@@ -281,7 +282,7 @@ class RideScheduledClosureWorkflowTests(TestCase):
status='OPERATING',
timezone='America/New_York'
)
defaults = {
'name': f'Ride Closing {timezone.now().timestamp()}',
'slug': f'ride-closing-{timezone.now().timestamp()}',
@@ -291,67 +292,67 @@ class RideScheduledClosureWorkflowTests(TestCase):
}
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(
@@ -360,11 +361,11 @@ class RideDemolitionWorkflowTests(TestCase):
password='testpass123',
role='MODERATOR'
)
def _create_ride(self, status='CLOSED_PERM', **kwargs):
from apps.parks.models import Company, Park
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']
@@ -380,7 +381,7 @@ class RideDemolitionWorkflowTests(TestCase):
status='OPERATING',
timezone='America/New_York'
)
defaults = {
'name': f'Ride Demo {timezone.now().timestamp()}',
'slug': f'ride-demo-{timezone.now().timestamp()}',
@@ -390,28 +391,28 @@ class RideDemolitionWorkflowTests(TestCase):
}
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)
@@ -419,7 +420,7 @@ class RideDemolitionWorkflowTests(TestCase):
class RideRelocationWorkflowTests(TestCase):
"""Tests for ride relocation workflow."""
@classmethod
def setUpTestData(cls):
cls.moderator = User.objects.create_user(
@@ -428,11 +429,11 @@ class RideRelocationWorkflowTests(TestCase):
password='testpass123',
role='MODERATOR'
)
def _create_ride(self, status='CLOSED_PERM', **kwargs):
from apps.parks.models import Company, Park
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']
@@ -448,7 +449,7 @@ class RideRelocationWorkflowTests(TestCase):
status='OPERATING',
timezone='America/New_York'
)
defaults = {
'name': f'Ride Reloc {timezone.now().timestamp()}',
'slug': f'ride-reloc-{timezone.now().timestamp()}',
@@ -458,28 +459,28 @@ class RideRelocationWorkflowTests(TestCase):
}
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)
@@ -487,7 +488,7 @@ class RideRelocationWorkflowTests(TestCase):
class RideWrapperMethodTests(TestCase):
"""Tests for ride wrapper methods."""
@classmethod
def setUpTestData(cls):
cls.user = User.objects.create_user(
@@ -502,11 +503,11 @@ class RideWrapperMethodTests(TestCase):
password='testpass123',
role='MODERATOR'
)
def _create_ride(self, status='OPERATING', **kwargs):
from apps.parks.models import Company, Park
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']
@@ -522,7 +523,7 @@ class RideWrapperMethodTests(TestCase):
status='OPERATING',
timezone='America/New_York'
)
defaults = {
'name': f'Ride Wrapper {timezone.now().timestamp()}',
'slug': f'ride-wrapper-{timezone.now().timestamp()}',
@@ -532,38 +533,38 @@ class RideWrapperMethodTests(TestCase):
}
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,
@@ -575,66 +576,66 @@ class RideWrapperMethodTests(TestCase):
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(
@@ -643,11 +644,11 @@ class RidePostClosingStatusAutomationTests(TestCase):
password='testpass123',
role='MODERATOR'
)
def _create_ride(self, status='CLOSING', **kwargs):
from apps.parks.models import Company, Park
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']
@@ -663,7 +664,7 @@ class RidePostClosingStatusAutomationTests(TestCase):
status='OPERATING',
timezone='America/New_York'
)
defaults = {
'name': f'Ride Auto {timezone.now().timestamp()}',
'slug': f'ride-auto-{timezone.now().timestamp()}',
@@ -673,40 +674,40 @@ class RidePostClosingStatusAutomationTests(TestCase):
}
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')
@@ -743,8 +744,8 @@ class RideStateLogTests(TestCase):
)
def _create_ride(self, status='OPERATING', **kwargs):
from apps.parks.models import Company, Park
from apps.rides.models import Ride
from apps.parks.models import Park, Company
manufacturer = Company.objects.create(
name=f'Mfr Log {timezone.now().timestamp()}',
@@ -774,8 +775,8 @@ class RideStateLogTests(TestCase):
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
from django_fsm_log.models import StateLog
ride = self._create_ride(status='OPERATING')
ride_ct = ContentType.objects.get_for_model(ride)
@@ -796,8 +797,8 @@ class RideStateLogTests(TestCase):
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
from django_fsm_log.models import StateLog
ride = self._create_ride(status='OPERATING')
ride_ct = ContentType.objects.get_for_model(ride)
@@ -822,8 +823,8 @@ class RideStateLogTests(TestCase):
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
from django_fsm_log.models import StateLog
ride = self._create_ride(status='SBNO')
ride_ct = ContentType.objects.get_for_model(ride)
@@ -844,8 +845,8 @@ class RideStateLogTests(TestCase):
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
from django_fsm_log.models import StateLog
ride = self._create_ride(status='OPERATING')
ride_ct = ContentType.objects.get_for_model(ride)
@@ -875,8 +876,8 @@ class RideStateLogTests(TestCase):
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
from django_fsm_log.models import StateLog
ride = self._create_ride(status='OPERATING')
ride_ct = ContentType.objects.get_for_model(ride)