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

@@ -11,34 +11,36 @@ This module contains tests for:
- Mixin functionality tests
"""
from django.test import TestCase, Client
import json
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from django.contrib.contenttypes.models import ContentType
from django.core.files.uploadedfile import SimpleUploadedFile
from django.http import JsonResponse, HttpRequest
from django.http import HttpRequest, JsonResponse
from django.test import Client, RequestFactory, TestCase
from django.utils import timezone
from django_fsm import TransitionNotAllowed
from .models import (
EditSubmission,
PhotoSubmission,
ModerationReport,
ModerationQueue,
BulkOperation,
ModerationAction,
)
from .mixins import (
EditSubmissionMixin,
PhotoSubmissionMixin,
ModeratorRequiredMixin,
AdminRequiredMixin,
InlineEditMixin,
HistoryMixin,
)
from apps.parks.models import Company as Operator
from django.views.generic import DetailView
from django.test import RequestFactory
import json
from django_fsm import TransitionNotAllowed
from apps.parks.models import Company as Operator
from .mixins import (
AdminRequiredMixin,
EditSubmissionMixin,
HistoryMixin,
InlineEditMixin,
ModeratorRequiredMixin,
PhotoSubmissionMixin,
)
from .models import (
BulkOperation,
EditSubmission,
ModerationAction,
ModerationQueue,
ModerationReport,
PhotoSubmission,
)
User = get_user_model()
@@ -421,12 +423,12 @@ class EditSubmissionTransitionTests(TestCase):
"""Test transition from PENDING to APPROVED."""
submission = self._create_submission()
self.assertEqual(submission.status, 'PENDING')
submission.transition_to_approved(user=self.moderator)
submission.handled_by = self.moderator
submission.handled_at = timezone.now()
submission.save()
submission.refresh_from_db()
self.assertEqual(submission.status, 'APPROVED')
self.assertEqual(submission.handled_by, self.moderator)
@@ -436,13 +438,13 @@ class EditSubmissionTransitionTests(TestCase):
"""Test transition from PENDING to REJECTED."""
submission = self._create_submission()
self.assertEqual(submission.status, 'PENDING')
submission.transition_to_rejected(user=self.moderator)
submission.handled_by = self.moderator
submission.handled_at = timezone.now()
submission.notes = 'Rejected: Insufficient evidence'
submission.save()
submission.refresh_from_db()
self.assertEqual(submission.status, 'REJECTED')
self.assertEqual(submission.handled_by, self.moderator)
@@ -452,25 +454,25 @@ class EditSubmissionTransitionTests(TestCase):
"""Test transition from PENDING to ESCALATED."""
submission = self._create_submission()
self.assertEqual(submission.status, 'PENDING')
submission.transition_to_escalated(user=self.moderator)
submission.handled_by = self.moderator
submission.handled_at = timezone.now()
submission.notes = 'Escalated: Needs admin review'
submission.save()
submission.refresh_from_db()
self.assertEqual(submission.status, 'ESCALATED')
def test_escalated_to_approved_transition(self):
"""Test transition from ESCALATED to APPROVED."""
submission = self._create_submission(status='ESCALATED')
submission.transition_to_approved(user=self.admin)
submission.handled_by = self.admin
submission.handled_at = timezone.now()
submission.save()
submission.refresh_from_db()
self.assertEqual(submission.status, 'APPROVED')
self.assertEqual(submission.handled_by, self.admin)
@@ -478,20 +480,20 @@ class EditSubmissionTransitionTests(TestCase):
def test_escalated_to_rejected_transition(self):
"""Test transition from ESCALATED to REJECTED."""
submission = self._create_submission(status='ESCALATED')
submission.transition_to_rejected(user=self.admin)
submission.handled_by = self.admin
submission.handled_at = timezone.now()
submission.notes = 'Rejected by admin'
submission.save()
submission.refresh_from_db()
self.assertEqual(submission.status, 'REJECTED')
def test_invalid_transition_from_approved(self):
"""Test that transitions from APPROVED state fail."""
submission = self._create_submission(status='APPROVED')
# Attempting to transition from APPROVED should raise TransitionNotAllowed
with self.assertRaises(TransitionNotAllowed):
submission.transition_to_rejected(user=self.moderator)
@@ -499,7 +501,7 @@ class EditSubmissionTransitionTests(TestCase):
def test_invalid_transition_from_rejected(self):
"""Test that transitions from REJECTED state fail."""
submission = self._create_submission(status='REJECTED')
# Attempting to transition from REJECTED should raise TransitionNotAllowed
with self.assertRaises(TransitionNotAllowed):
submission.transition_to_approved(user=self.moderator)
@@ -507,9 +509,9 @@ class EditSubmissionTransitionTests(TestCase):
def test_approve_wrapper_method(self):
"""Test the approve() wrapper method."""
submission = self._create_submission()
result = submission.approve(self.moderator)
submission.approve(self.moderator)
submission.refresh_from_db()
self.assertEqual(submission.status, 'APPROVED')
self.assertEqual(submission.handled_by, self.moderator)
@@ -518,9 +520,9 @@ class EditSubmissionTransitionTests(TestCase):
def test_reject_wrapper_method(self):
"""Test the reject() wrapper method."""
submission = self._create_submission()
submission.reject(self.moderator, reason='Not enough evidence')
submission.refresh_from_db()
self.assertEqual(submission.status, 'REJECTED')
self.assertIn('Not enough evidence', submission.notes)
@@ -528,9 +530,9 @@ class EditSubmissionTransitionTests(TestCase):
def test_escalate_wrapper_method(self):
"""Test the escalate() wrapper method."""
submission = self._create_submission()
submission.escalate(self.moderator, reason='Needs admin approval')
submission.refresh_from_db()
self.assertEqual(submission.status, 'ESCALATED')
self.assertIn('Needs admin approval', submission.notes)
@@ -582,11 +584,11 @@ class ModerationReportTransitionTests(TestCase):
"""Test transition from PENDING to UNDER_REVIEW."""
report = self._create_report()
self.assertEqual(report.status, 'PENDING')
report.transition_to_under_review(user=self.moderator)
report.assigned_moderator = self.moderator
report.save()
report.refresh_from_db()
self.assertEqual(report.status, 'UNDER_REVIEW')
self.assertEqual(report.assigned_moderator, self.moderator)
@@ -596,13 +598,13 @@ class ModerationReportTransitionTests(TestCase):
report = self._create_report(status='UNDER_REVIEW')
report.assigned_moderator = self.moderator
report.save()
report.transition_to_resolved(user=self.moderator)
report.resolution_action = 'Content updated'
report.resolution_notes = 'Fixed the incorrect information'
report.resolved_at = timezone.now()
report.save()
report.refresh_from_db()
self.assertEqual(report.status, 'RESOLVED')
self.assertIsNotNone(report.resolved_at)
@@ -612,26 +614,26 @@ class ModerationReportTransitionTests(TestCase):
report = self._create_report(status='UNDER_REVIEW')
report.assigned_moderator = self.moderator
report.save()
report.transition_to_dismissed(user=self.moderator)
report.resolution_notes = 'Report is not valid'
report.resolved_at = timezone.now()
report.save()
report.refresh_from_db()
self.assertEqual(report.status, 'DISMISSED')
def test_invalid_transition_from_resolved(self):
"""Test that transitions from RESOLVED state fail."""
report = self._create_report(status='RESOLVED')
with self.assertRaises(TransitionNotAllowed):
report.transition_to_dismissed(user=self.moderator)
def test_invalid_transition_from_dismissed(self):
"""Test that transitions from DISMISSED state fail."""
report = self._create_report(status='DISMISSED')
with self.assertRaises(TransitionNotAllowed):
report.transition_to_resolved(user=self.moderator)
@@ -668,12 +670,12 @@ class ModerationQueueTransitionTests(TestCase):
"""Test transition from PENDING to IN_PROGRESS."""
item = self._create_queue_item()
self.assertEqual(item.status, 'PENDING')
item.transition_to_in_progress(user=self.moderator)
item.assigned_to = self.moderator
item.assigned_at = timezone.now()
item.save()
item.refresh_from_db()
self.assertEqual(item.status, 'IN_PROGRESS')
self.assertEqual(item.assigned_to, self.moderator)
@@ -683,10 +685,10 @@ class ModerationQueueTransitionTests(TestCase):
item = self._create_queue_item(status='IN_PROGRESS')
item.assigned_to = self.moderator
item.save()
item.transition_to_completed(user=self.moderator)
item.save()
item.refresh_from_db()
self.assertEqual(item.status, 'COMPLETED')
@@ -695,27 +697,27 @@ class ModerationQueueTransitionTests(TestCase):
item = self._create_queue_item(status='IN_PROGRESS')
item.assigned_to = self.moderator
item.save()
item.transition_to_cancelled(user=self.moderator)
item.save()
item.refresh_from_db()
self.assertEqual(item.status, 'CANCELLED')
def test_pending_to_cancelled_transition(self):
"""Test transition from PENDING to CANCELLED."""
item = self._create_queue_item()
item.transition_to_cancelled(user=self.moderator)
item.save()
item.refresh_from_db()
self.assertEqual(item.status, 'CANCELLED')
def test_invalid_transition_from_completed(self):
"""Test that transitions from COMPLETED state fail."""
item = self._create_queue_item(status='COMPLETED')
with self.assertRaises(TransitionNotAllowed):
item.transition_to_in_progress(user=self.moderator)
@@ -753,11 +755,11 @@ class BulkOperationTransitionTests(TestCase):
"""Test transition from PENDING to RUNNING."""
operation = self._create_bulk_operation()
self.assertEqual(operation.status, 'PENDING')
operation.transition_to_running(user=self.admin)
operation.started_at = timezone.now()
operation.save()
operation.refresh_from_db()
self.assertEqual(operation.status, 'RUNNING')
self.assertIsNotNone(operation.started_at)
@@ -767,12 +769,12 @@ class BulkOperationTransitionTests(TestCase):
operation = self._create_bulk_operation(status='RUNNING')
operation.started_at = timezone.now()
operation.save()
operation.transition_to_completed(user=self.admin)
operation.completed_at = timezone.now()
operation.processed_items = 100
operation.save()
operation.refresh_from_db()
self.assertEqual(operation.status, 'COMPLETED')
self.assertIsNotNone(operation.completed_at)
@@ -783,13 +785,13 @@ class BulkOperationTransitionTests(TestCase):
operation = self._create_bulk_operation(status='RUNNING')
operation.started_at = timezone.now()
operation.save()
operation.transition_to_failed(user=self.admin)
operation.completed_at = timezone.now()
operation.results = {'error': 'Database connection failed'}
operation.failed_items = 50
operation.save()
operation.refresh_from_db()
self.assertEqual(operation.status, 'FAILED')
self.assertEqual(operation.failed_items, 50)
@@ -797,10 +799,10 @@ class BulkOperationTransitionTests(TestCase):
def test_pending_to_cancelled_transition(self):
"""Test transition from PENDING to CANCELLED."""
operation = self._create_bulk_operation()
operation.transition_to_cancelled(user=self.admin)
operation.save()
operation.refresh_from_db()
self.assertEqual(operation.status, 'CANCELLED')
@@ -809,24 +811,24 @@ class BulkOperationTransitionTests(TestCase):
operation = self._create_bulk_operation(status='RUNNING')
operation.can_cancel = True
operation.save()
operation.transition_to_cancelled(user=self.admin)
operation.save()
operation.refresh_from_db()
self.assertEqual(operation.status, 'CANCELLED')
def test_invalid_transition_from_completed(self):
"""Test that transitions from COMPLETED state fail."""
operation = self._create_bulk_operation(status='COMPLETED')
with self.assertRaises(TransitionNotAllowed):
operation.transition_to_running(user=self.admin)
def test_invalid_transition_from_failed(self):
"""Test that transitions from FAILED state fail."""
operation = self._create_bulk_operation(status='FAILED')
with self.assertRaises(TransitionNotAllowed):
operation.transition_to_completed(user=self.admin)
@@ -835,12 +837,12 @@ class BulkOperationTransitionTests(TestCase):
operation = self._create_bulk_operation()
operation.total_items = 100
operation.processed_items = 50
self.assertEqual(operation.progress_percentage, 50.0)
operation.processed_items = 0
self.assertEqual(operation.progress_percentage, 0.0)
operation.total_items = 0
self.assertEqual(operation.progress_percentage, 0.0)
@@ -876,7 +878,7 @@ class TransitionLoggingTestCase(TestCase):
def test_transition_creates_log(self):
"""Test that transitions create StateLog entries."""
from django_fsm_log.models import StateLog
# Create a submission
submission = EditSubmission.objects.create(
user=self.user,
@@ -887,18 +889,18 @@ class TransitionLoggingTestCase(TestCase):
status='PENDING',
reason='Test reason'
)
# Perform transition
submission.transition_to_approved(user=self.moderator)
submission.save()
# Check log was created
submission_ct = ContentType.objects.get_for_model(submission)
log = StateLog.objects.filter(
content_type=submission_ct,
object_id=submission.id
).first()
self.assertIsNotNone(log, "StateLog entry should be created")
self.assertEqual(log.state, 'APPROVED')
self.assertEqual(log.by, self.moderator)
@@ -907,7 +909,7 @@ class TransitionLoggingTestCase(TestCase):
def test_multiple_transitions_logged(self):
"""Test that multiple transitions are all logged."""
from django_fsm_log.models import StateLog
submission = EditSubmission.objects.create(
user=self.user,
content_type=self.content_type,
@@ -917,23 +919,23 @@ class TransitionLoggingTestCase(TestCase):
status='PENDING',
reason='Test reason'
)
submission_ct = ContentType.objects.get_for_model(submission)
# First transition
submission.transition_to_escalated(user=self.moderator)
submission.save()
# Second transition
submission.transition_to_approved(user=self.moderator)
submission.save()
# Check multiple logs created
logs = StateLog.objects.filter(
content_type=submission_ct,
object_id=submission.id
).order_by('timestamp')
self.assertEqual(logs.count(), 2, "Should have 2 log entries")
self.assertEqual(logs[0].state, 'ESCALATED')
self.assertEqual(logs[1].state, 'APPROVED')
@@ -941,10 +943,10 @@ class TransitionLoggingTestCase(TestCase):
def test_history_endpoint_returns_logs(self):
"""Test history API endpoint returns transition logs."""
from rest_framework.test import APIClient
api_client = APIClient()
api_client.force_authenticate(user=self.moderator)
submission = EditSubmission.objects.create(
user=self.user,
content_type=self.content_type,
@@ -954,21 +956,21 @@ class TransitionLoggingTestCase(TestCase):
status='PENDING',
reason='Test reason'
)
# Perform transition to create log
submission.transition_to_approved(user=self.moderator)
submission.save()
# Note: This assumes EditSubmission has a history endpoint
# Adjust URL pattern based on actual implementation
response = api_client.get('/api/moderation/reports/all_history/')
self.assertEqual(response.status_code, 200)
def test_system_transitions_without_user(self):
"""Test that system transitions work without a user."""
from django_fsm_log.models import StateLog
submission = EditSubmission.objects.create(
user=self.user,
content_type=self.content_type,
@@ -978,18 +980,18 @@ class TransitionLoggingTestCase(TestCase):
status='PENDING',
reason='Test reason'
)
# Perform transition without user
submission.transition_to_rejected(user=None)
submission.save()
# Check log was created even without user
submission_ct = ContentType.objects.get_for_model(submission)
log = StateLog.objects.filter(
content_type=submission_ct,
object_id=submission.id
).first()
self.assertIsNotNone(log)
self.assertEqual(log.state, 'REJECTED')
self.assertIsNone(log.by, "System transitions should have no user")
@@ -997,7 +999,7 @@ class TransitionLoggingTestCase(TestCase):
def test_transition_log_includes_description(self):
"""Test that transition logs can include descriptions."""
from django_fsm_log.models import StateLog
submission = EditSubmission.objects.create(
user=self.user,
content_type=self.content_type,
@@ -1007,27 +1009,27 @@ class TransitionLoggingTestCase(TestCase):
status='PENDING',
reason='Test reason'
)
# Perform transition
submission.transition_to_approved(user=self.moderator)
submission.save()
# Check log
submission_ct = ContentType.objects.get_for_model(submission)
log = StateLog.objects.filter(
content_type=submission_ct,
object_id=submission.id
).first()
self.assertIsNotNone(log)
# Description field exists and can be used for audit trails
self.assertTrue(hasattr(log, 'description'))
def test_log_ordering_by_timestamp(self):
"""Test that logs are properly ordered by timestamp."""
from django_fsm_log.models import StateLog
import time
submission = EditSubmission.objects.create(
user=self.user,
content_type=self.content_type,
@@ -1037,22 +1039,22 @@ class TransitionLoggingTestCase(TestCase):
status='PENDING',
reason='Test reason'
)
submission_ct = ContentType.objects.get_for_model(submission)
# Create multiple transitions
submission.transition_to_escalated(user=self.moderator)
submission.save()
submission.transition_to_approved(user=self.moderator)
submission.save()
# Get logs ordered by timestamp
logs = list(StateLog.objects.filter(
content_type=submission_ct,
object_id=submission.id
).order_by('timestamp'))
# Verify ordering
self.assertEqual(len(logs), 2)
self.assertTrue(logs[0].timestamp <= logs[1].timestamp)
@@ -1091,7 +1093,7 @@ class ModerationActionTests(TestCase):
moderator=self.moderator,
target_user=self.target_user
)
self.assertIsNotNone(action.expires_at)
# expires_at should be approximately 24 hours from now
time_diff = action.expires_at - timezone.now()
@@ -1106,7 +1108,7 @@ class ModerationActionTests(TestCase):
moderator=self.moderator,
target_user=self.target_user
)
self.assertIsNone(action.expires_at)
def test_action_is_active_by_default(self):
@@ -1168,7 +1170,7 @@ class PhotoSubmissionTransitionTests(TestCase):
def _create_submission(self, status='PENDING'):
"""Helper to create a PhotoSubmission."""
# Create using direct database creation to bypass FK validation
from unittest.mock import patch, Mock
from unittest.mock import Mock, patch
with patch.object(PhotoSubmission, 'photo', Mock()):
submission = PhotoSubmission(