mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 07: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,10 +1,32 @@
|
||||
"""
|
||||
Comprehensive tests for the moderation app.
|
||||
|
||||
This module contains tests for:
|
||||
- EditSubmission state machine transitions
|
||||
- PhotoSubmission state machine transitions
|
||||
- ModerationReport state machine transitions
|
||||
- ModerationQueue state machine transitions
|
||||
- BulkOperation state machine transitions
|
||||
- FSM transition logging with django-fsm-log
|
||||
- Mixin functionality tests
|
||||
"""
|
||||
|
||||
from django.test import TestCase, Client
|
||||
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 .models import EditSubmission
|
||||
from django.utils import timezone
|
||||
from django_fsm import TransitionNotAllowed
|
||||
from .models import (
|
||||
EditSubmission,
|
||||
PhotoSubmission,
|
||||
ModerationReport,
|
||||
ModerationQueue,
|
||||
BulkOperation,
|
||||
ModerationAction,
|
||||
)
|
||||
from .mixins import (
|
||||
EditSubmissionMixin,
|
||||
PhotoSubmissionMixin,
|
||||
@@ -349,6 +371,480 @@ class ModerationMixinsTests(TestCase):
|
||||
self.assertEqual(len(context["edit_submissions"]), 1)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# EditSubmission FSM Transition Tests
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class EditSubmissionTransitionTests(TestCase):
|
||||
"""Comprehensive tests for EditSubmission 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'
|
||||
)
|
||||
self.operator = Operator.objects.create(
|
||||
name='Test Operator',
|
||||
description='Test Description'
|
||||
)
|
||||
self.content_type = ContentType.objects.get_for_model(Operator)
|
||||
|
||||
def _create_submission(self, status='PENDING'):
|
||||
"""Helper to create an EditSubmission."""
|
||||
return EditSubmission.objects.create(
|
||||
user=self.user,
|
||||
content_type=self.content_type,
|
||||
object_id=self.operator.id,
|
||||
submission_type='EDIT',
|
||||
changes={'name': 'Updated Name'},
|
||||
status=status,
|
||||
reason='Test reason'
|
||||
)
|
||||
|
||||
def test_pending_to_approved_transition(self):
|
||||
"""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)
|
||||
self.assertIsNotNone(submission.handled_at)
|
||||
|
||||
def test_pending_to_rejected_transition(self):
|
||||
"""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)
|
||||
self.assertIn('Rejected', submission.notes)
|
||||
|
||||
def test_pending_to_escalated_transition(self):
|
||||
"""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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
def test_approve_wrapper_method(self):
|
||||
"""Test the approve() wrapper method."""
|
||||
submission = self._create_submission()
|
||||
|
||||
result = submission.approve(self.moderator)
|
||||
|
||||
submission.refresh_from_db()
|
||||
self.assertEqual(submission.status, 'APPROVED')
|
||||
self.assertEqual(submission.handled_by, self.moderator)
|
||||
self.assertIsNotNone(submission.handled_at)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# ModerationReport FSM Transition Tests
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class ModerationReportTransitionTests(TestCase):
|
||||
"""Comprehensive tests for ModerationReport FSM transitions."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.user = User.objects.create_user(
|
||||
username='reporter',
|
||||
email='reporter@example.com',
|
||||
password='testpass123',
|
||||
role='USER'
|
||||
)
|
||||
self.moderator = User.objects.create_user(
|
||||
username='moderator',
|
||||
email='moderator@example.com',
|
||||
password='testpass123',
|
||||
role='MODERATOR'
|
||||
)
|
||||
self.operator = Operator.objects.create(
|
||||
name='Test Operator',
|
||||
description='Test Description'
|
||||
)
|
||||
self.content_type = ContentType.objects.get_for_model(Operator)
|
||||
|
||||
def _create_report(self, status='PENDING'):
|
||||
"""Helper to create a ModerationReport."""
|
||||
return ModerationReport.objects.create(
|
||||
report_type='CONTENT',
|
||||
status=status,
|
||||
priority='MEDIUM',
|
||||
reported_entity_type='company',
|
||||
reported_entity_id=self.operator.id,
|
||||
content_type=self.content_type,
|
||||
reason='Inaccurate information',
|
||||
description='The company information is incorrect',
|
||||
reported_by=self.user
|
||||
)
|
||||
|
||||
def test_pending_to_under_review_transition(self):
|
||||
"""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)
|
||||
|
||||
def test_under_review_to_resolved_transition(self):
|
||||
"""Test transition from UNDER_REVIEW to RESOLVED."""
|
||||
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)
|
||||
|
||||
def test_under_review_to_dismissed_transition(self):
|
||||
"""Test transition from UNDER_REVIEW to DISMISSED."""
|
||||
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)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# ModerationQueue FSM Transition Tests
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class ModerationQueueTransitionTests(TestCase):
|
||||
"""Comprehensive tests for ModerationQueue FSM transitions."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.moderator = User.objects.create_user(
|
||||
username='moderator',
|
||||
email='moderator@example.com',
|
||||
password='testpass123',
|
||||
role='MODERATOR'
|
||||
)
|
||||
|
||||
def _create_queue_item(self, status='PENDING'):
|
||||
"""Helper to create a ModerationQueue item."""
|
||||
return ModerationQueue.objects.create(
|
||||
item_type='EDIT_SUBMISSION',
|
||||
status=status,
|
||||
priority='MEDIUM',
|
||||
title='Review edit submission',
|
||||
description='User submitted an edit that needs review',
|
||||
flagged_by=self.moderator
|
||||
)
|
||||
|
||||
def test_pending_to_in_progress_transition(self):
|
||||
"""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)
|
||||
|
||||
def test_in_progress_to_completed_transition(self):
|
||||
"""Test transition from IN_PROGRESS to COMPLETED."""
|
||||
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')
|
||||
|
||||
def test_in_progress_to_cancelled_transition(self):
|
||||
"""Test transition from IN_PROGRESS to CANCELLED."""
|
||||
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)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# BulkOperation FSM Transition Tests
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class BulkOperationTransitionTests(TestCase):
|
||||
"""Comprehensive tests for BulkOperation FSM transitions."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.admin = User.objects.create_user(
|
||||
username='admin',
|
||||
email='admin@example.com',
|
||||
password='testpass123',
|
||||
role='ADMIN'
|
||||
)
|
||||
|
||||
def _create_bulk_operation(self, status='PENDING'):
|
||||
"""Helper to create a BulkOperation."""
|
||||
return BulkOperation.objects.create(
|
||||
operation_type='BULK_UPDATE',
|
||||
status=status,
|
||||
priority='MEDIUM',
|
||||
description='Bulk update park statuses',
|
||||
parameters={'target': 'parks', 'action': 'update_status'},
|
||||
created_by=self.admin,
|
||||
total_items=100
|
||||
)
|
||||
|
||||
def test_pending_to_running_transition(self):
|
||||
"""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)
|
||||
|
||||
def test_running_to_completed_transition(self):
|
||||
"""Test transition from RUNNING to COMPLETED."""
|
||||
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)
|
||||
self.assertEqual(operation.processed_items, 100)
|
||||
|
||||
def test_running_to_failed_transition(self):
|
||||
"""Test transition from RUNNING to FAILED."""
|
||||
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)
|
||||
|
||||
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')
|
||||
|
||||
def test_running_to_cancelled_transition(self):
|
||||
"""Test transition from RUNNING to CANCELLED when cancellable."""
|
||||
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)
|
||||
|
||||
def test_progress_percentage_calculation(self):
|
||||
"""Test progress percentage property."""
|
||||
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)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# FSM Transition Logging Tests
|
||||
# ============================================================================
|
||||
@@ -388,7 +884,8 @@ class TransitionLoggingTestCase(TestCase):
|
||||
object_id=self.operator.id,
|
||||
submission_type='EDIT',
|
||||
changes={'name': 'Updated Name'},
|
||||
status='PENDING'
|
||||
status='PENDING',
|
||||
reason='Test reason'
|
||||
)
|
||||
|
||||
# Perform transition
|
||||
@@ -417,7 +914,8 @@ class TransitionLoggingTestCase(TestCase):
|
||||
object_id=self.operator.id,
|
||||
submission_type='EDIT',
|
||||
changes={'name': 'Updated Name'},
|
||||
status='PENDING'
|
||||
status='PENDING',
|
||||
reason='Test reason'
|
||||
)
|
||||
|
||||
submission_ct = ContentType.objects.get_for_model(submission)
|
||||
@@ -443,7 +941,6 @@ class TransitionLoggingTestCase(TestCase):
|
||||
def test_history_endpoint_returns_logs(self):
|
||||
"""Test history API endpoint returns transition logs."""
|
||||
from rest_framework.test import APIClient
|
||||
from django_fsm_log.models import StateLog
|
||||
|
||||
api_client = APIClient()
|
||||
api_client.force_authenticate(user=self.moderator)
|
||||
@@ -454,7 +951,8 @@ class TransitionLoggingTestCase(TestCase):
|
||||
object_id=self.operator.id,
|
||||
submission_type='EDIT',
|
||||
changes={'name': 'Updated Name'},
|
||||
status='PENDING'
|
||||
status='PENDING',
|
||||
reason='Test reason'
|
||||
)
|
||||
|
||||
# Perform transition to create log
|
||||
@@ -463,11 +961,9 @@ class TransitionLoggingTestCase(TestCase):
|
||||
|
||||
# Note: This assumes EditSubmission has a history endpoint
|
||||
# Adjust URL pattern based on actual implementation
|
||||
response = api_client.get(f'/api/moderation/reports/all_history/')
|
||||
response = api_client.get('/api/moderation/reports/all_history/')
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# Response should contain history data
|
||||
# Actual assertions depend on response format
|
||||
|
||||
def test_system_transitions_without_user(self):
|
||||
"""Test that system transitions work without a user."""
|
||||
@@ -479,7 +975,8 @@ class TransitionLoggingTestCase(TestCase):
|
||||
object_id=self.operator.id,
|
||||
submission_type='EDIT',
|
||||
changes={'name': 'Updated Name'},
|
||||
status='PENDING'
|
||||
status='PENDING',
|
||||
reason='Test reason'
|
||||
)
|
||||
|
||||
# Perform transition without user
|
||||
@@ -507,7 +1004,8 @@ class TransitionLoggingTestCase(TestCase):
|
||||
object_id=self.operator.id,
|
||||
submission_type='EDIT',
|
||||
changes={'name': 'Updated Name'},
|
||||
status='PENDING'
|
||||
status='PENDING',
|
||||
reason='Test reason'
|
||||
)
|
||||
|
||||
# Perform transition
|
||||
@@ -525,3 +1023,367 @@ class TransitionLoggingTestCase(TestCase):
|
||||
# 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,
|
||||
object_id=self.operator.id,
|
||||
submission_type='EDIT',
|
||||
changes={'name': 'Updated Name'},
|
||||
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)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# ModerationAction Model Tests
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class ModerationActionTests(TestCase):
|
||||
"""Tests for ModerationAction model."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.moderator = User.objects.create_user(
|
||||
username='moderator',
|
||||
email='moderator@example.com',
|
||||
password='testpass123',
|
||||
role='MODERATOR'
|
||||
)
|
||||
self.target_user = User.objects.create_user(
|
||||
username='target',
|
||||
email='target@example.com',
|
||||
password='testpass123',
|
||||
role='USER'
|
||||
)
|
||||
|
||||
def test_create_action_with_duration(self):
|
||||
"""Test creating an action with duration sets expires_at."""
|
||||
action = ModerationAction.objects.create(
|
||||
action_type='TEMPORARY_BAN',
|
||||
reason='Spam',
|
||||
details='User was spamming the forums',
|
||||
duration_hours=24,
|
||||
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()
|
||||
self.assertAlmostEqual(time_diff.total_seconds(), 24 * 3600, delta=60)
|
||||
|
||||
def test_create_action_without_duration(self):
|
||||
"""Test creating an action without duration has no expires_at."""
|
||||
action = ModerationAction.objects.create(
|
||||
action_type='WARNING',
|
||||
reason='First offense',
|
||||
details='Warning issued for minor violation',
|
||||
moderator=self.moderator,
|
||||
target_user=self.target_user
|
||||
)
|
||||
|
||||
self.assertIsNone(action.expires_at)
|
||||
|
||||
def test_action_is_active_by_default(self):
|
||||
"""Test that new actions are active by default."""
|
||||
action = ModerationAction.objects.create(
|
||||
action_type='WARNING',
|
||||
reason='Test',
|
||||
details='Test warning',
|
||||
moderator=self.moderator,
|
||||
target_user=self.target_user
|
||||
)
|
||||
|
||||
self.assertTrue(action.is_active)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# PhotoSubmission FSM Transition Tests
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class PhotoSubmissionTransitionTests(TestCase):
|
||||
"""Comprehensive tests for PhotoSubmission 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'
|
||||
)
|
||||
self.operator = Operator.objects.create(
|
||||
name='Test Operator',
|
||||
description='Test Description',
|
||||
roles=['OPERATOR']
|
||||
)
|
||||
self.content_type = ContentType.objects.get_for_model(Operator)
|
||||
|
||||
def _create_mock_photo(self):
|
||||
"""Create a mock CloudflareImage for testing."""
|
||||
from unittest.mock import Mock
|
||||
mock_photo = Mock()
|
||||
mock_photo.pk = 1
|
||||
mock_photo.id = 1
|
||||
return mock_photo
|
||||
|
||||
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
|
||||
|
||||
with patch.object(PhotoSubmission, 'photo', Mock()):
|
||||
submission = PhotoSubmission(
|
||||
user=self.user,
|
||||
content_type=self.content_type,
|
||||
object_id=self.operator.id,
|
||||
caption='Test Photo',
|
||||
status=status,
|
||||
)
|
||||
# Bypass model save to avoid FK constraint on photo
|
||||
submission.photo_id = 1
|
||||
submission.save(update_fields=None)
|
||||
# Force status after creation for non-PENDING states
|
||||
if status != 'PENDING':
|
||||
PhotoSubmission.objects.filter(pk=submission.pk).update(status=status)
|
||||
submission.refresh_from_db()
|
||||
return submission
|
||||
|
||||
def test_pending_to_approved_transition(self):
|
||||
"""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)
|
||||
self.assertIsNotNone(submission.handled_at)
|
||||
|
||||
def test_pending_to_rejected_transition(self):
|
||||
"""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: Image quality too low'
|
||||
submission.save()
|
||||
|
||||
submission.refresh_from_db()
|
||||
self.assertEqual(submission.status, 'REJECTED')
|
||||
self.assertEqual(submission.handled_by, self.moderator)
|
||||
self.assertIn('Rejected', submission.notes)
|
||||
|
||||
def test_pending_to_escalated_transition(self):
|
||||
"""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: Copyright concerns'
|
||||
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)
|
||||
|
||||
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 after review'
|
||||
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')
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
submission.transition_to_rejected(user=self.moderator)
|
||||
|
||||
def test_invalid_transition_from_rejected(self):
|
||||
"""Test that transitions from REJECTED state fail."""
|
||||
submission = self._create_submission(status='REJECTED')
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
submission.transition_to_approved(user=self.moderator)
|
||||
|
||||
def test_reject_wrapper_method(self):
|
||||
"""Test the reject() wrapper method."""
|
||||
from unittest.mock import patch
|
||||
|
||||
submission = self._create_submission()
|
||||
|
||||
# Mock the photo creation part since we don't have actual photos
|
||||
with patch.object(submission, 'transition_to_rejected'):
|
||||
submission.reject(self.moderator, notes='Not suitable')
|
||||
|
||||
submission.refresh_from_db()
|
||||
self.assertEqual(submission.status, 'REJECTED')
|
||||
self.assertIn('Not suitable', submission.notes)
|
||||
|
||||
def test_escalate_wrapper_method(self):
|
||||
"""Test the escalate() wrapper method."""
|
||||
from unittest.mock import patch
|
||||
|
||||
submission = self._create_submission()
|
||||
|
||||
with patch.object(submission, 'transition_to_escalated'):
|
||||
submission.escalate(self.moderator, notes='Needs admin review')
|
||||
|
||||
submission.refresh_from_db()
|
||||
self.assertEqual(submission.status, 'ESCALATED')
|
||||
self.assertIn('Needs admin review', submission.notes)
|
||||
|
||||
def test_transition_creates_state_log(self):
|
||||
"""Test that transitions create StateLog entries."""
|
||||
from django_fsm_log.models import StateLog
|
||||
|
||||
submission = self._create_submission()
|
||||
|
||||
# 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)
|
||||
|
||||
def test_multiple_transitions_logged(self):
|
||||
"""Test that multiple transitions are all logged."""
|
||||
from django_fsm_log.models import StateLog
|
||||
|
||||
submission = self._create_submission()
|
||||
submission_ct = ContentType.objects.get_for_model(submission)
|
||||
|
||||
# First transition: PENDING -> ESCALATED
|
||||
submission.transition_to_escalated(user=self.moderator)
|
||||
submission.save()
|
||||
|
||||
# Second transition: ESCALATED -> APPROVED
|
||||
submission.transition_to_approved(user=self.admin)
|
||||
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')
|
||||
|
||||
def test_handled_by_and_handled_at_updated(self):
|
||||
"""Test that handled_by and handled_at are properly updated."""
|
||||
submission = self._create_submission()
|
||||
|
||||
self.assertIsNone(submission.handled_by)
|
||||
self.assertIsNone(submission.handled_at)
|
||||
|
||||
before_time = timezone.now()
|
||||
submission.transition_to_approved(user=self.moderator)
|
||||
submission.handled_by = self.moderator
|
||||
submission.handled_at = timezone.now()
|
||||
submission.save()
|
||||
after_time = timezone.now()
|
||||
|
||||
submission.refresh_from_db()
|
||||
self.assertEqual(submission.handled_by, self.moderator)
|
||||
self.assertIsNotNone(submission.handled_at)
|
||||
self.assertTrue(before_time <= submission.handled_at <= after_time)
|
||||
|
||||
def test_notes_field_updated_on_rejection(self):
|
||||
"""Test that notes field is updated with rejection reason."""
|
||||
submission = self._create_submission()
|
||||
rejection_reason = 'Image contains watermarks'
|
||||
|
||||
submission.transition_to_rejected(user=self.moderator)
|
||||
submission.notes = rejection_reason
|
||||
submission.save()
|
||||
|
||||
submission.refresh_from_db()
|
||||
self.assertEqual(submission.notes, rejection_reason)
|
||||
|
||||
def test_notes_field_updated_on_escalation(self):
|
||||
"""Test that notes field is updated with escalation reason."""
|
||||
submission = self._create_submission()
|
||||
escalation_reason = 'Potentially copyrighted content'
|
||||
|
||||
submission.transition_to_escalated(user=self.moderator)
|
||||
submission.notes = escalation_reason
|
||||
submission.save()
|
||||
|
||||
submission.refresh_from_db()
|
||||
self.assertEqual(submission.notes, escalation_reason)
|
||||
|
||||
7
backend/apps/moderation/tests/__init__.py
Normal file
7
backend/apps/moderation/tests/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""
|
||||
Moderation test package.
|
||||
|
||||
This package contains tests for the moderation app including:
|
||||
- Workflow tests (test_workflows.py)
|
||||
- Permission tests (test_permissions.py - planned)
|
||||
"""
|
||||
532
backend/apps/moderation/tests/test_workflows.py
Normal file
532
backend/apps/moderation/tests/test_workflows.py
Normal file
@@ -0,0 +1,532 @@
|
||||
"""
|
||||
Integration tests for complete moderation workflows.
|
||||
|
||||
This module tests end-to-end moderation workflows including:
|
||||
- Submission approval workflow
|
||||
- Submission rejection workflow
|
||||
- Submission escalation workflow
|
||||
- Report handling workflow
|
||||
- Bulk operation workflow
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils import timezone
|
||||
from unittest.mock import patch, Mock
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class SubmissionApprovalWorkflowTests(TestCase):
|
||||
"""Tests for the complete submission approval workflow."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
"""Set up test data for all tests."""
|
||||
cls.regular_user = User.objects.create_user(
|
||||
username='regular_user',
|
||||
email='user@example.com',
|
||||
password='testpass123',
|
||||
role='USER'
|
||||
)
|
||||
cls.moderator = User.objects.create_user(
|
||||
username='moderator',
|
||||
email='mod@example.com',
|
||||
password='testpass123',
|
||||
role='MODERATOR'
|
||||
)
|
||||
cls.admin = User.objects.create_user(
|
||||
username='admin',
|
||||
email='admin@example.com',
|
||||
password='testpass123',
|
||||
role='ADMIN'
|
||||
)
|
||||
|
||||
def test_edit_submission_approval_workflow(self):
|
||||
"""
|
||||
Test complete edit submission approval workflow.
|
||||
|
||||
Flow: User submits → Moderator reviews → Moderator approves → Changes applied
|
||||
"""
|
||||
from apps.moderation.models import EditSubmission
|
||||
from apps.parks.models import Company
|
||||
|
||||
# Create target object
|
||||
company = Company.objects.create(
|
||||
name='Test Company',
|
||||
description='Original description'
|
||||
)
|
||||
|
||||
# User submits an edit
|
||||
content_type = ContentType.objects.get_for_model(company)
|
||||
submission = EditSubmission.objects.create(
|
||||
user=self.regular_user,
|
||||
content_type=content_type,
|
||||
object_id=company.id,
|
||||
submission_type='EDIT',
|
||||
changes={'description': 'Updated description'},
|
||||
status='PENDING',
|
||||
reason='Fixing typo'
|
||||
)
|
||||
|
||||
self.assertEqual(submission.status, 'PENDING')
|
||||
self.assertIsNone(submission.handled_by)
|
||||
self.assertIsNone(submission.handled_at)
|
||||
|
||||
# Moderator approves
|
||||
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)
|
||||
self.assertIsNotNone(submission.handled_at)
|
||||
|
||||
def test_photo_submission_approval_workflow(self):
|
||||
"""
|
||||
Test complete photo submission approval workflow.
|
||||
|
||||
Flow: User submits photo → Moderator reviews → Moderator approves → Photo created
|
||||
"""
|
||||
from apps.moderation.models import PhotoSubmission
|
||||
from apps.parks.models import Park, Company
|
||||
|
||||
# Create target park
|
||||
operator = Company.objects.create(
|
||||
name='Test Operator',
|
||||
roles=['OPERATOR']
|
||||
)
|
||||
park = Park.objects.create(
|
||||
name='Test Park',
|
||||
slug='test-park',
|
||||
operator=operator,
|
||||
status='OPERATING',
|
||||
timezone='America/New_York'
|
||||
)
|
||||
|
||||
# User submits a photo
|
||||
content_type = ContentType.objects.get_for_model(park)
|
||||
submission = PhotoSubmission.objects.create(
|
||||
user=self.regular_user,
|
||||
content_type=content_type,
|
||||
object_id=park.id,
|
||||
status='PENDING',
|
||||
photo_type='GENERAL',
|
||||
description='Beautiful park entrance'
|
||||
)
|
||||
|
||||
self.assertEqual(submission.status, 'PENDING')
|
||||
|
||||
# Moderator approves
|
||||
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')
|
||||
|
||||
|
||||
class SubmissionRejectionWorkflowTests(TestCase):
|
||||
"""Tests for the submission rejection workflow."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.regular_user = User.objects.create_user(
|
||||
username='user_rej',
|
||||
email='user_rej@example.com',
|
||||
password='testpass123',
|
||||
role='USER'
|
||||
)
|
||||
cls.moderator = User.objects.create_user(
|
||||
username='mod_rej',
|
||||
email='mod_rej@example.com',
|
||||
password='testpass123',
|
||||
role='MODERATOR'
|
||||
)
|
||||
|
||||
def test_edit_submission_rejection_with_reason(self):
|
||||
"""
|
||||
Test rejection workflow with reason.
|
||||
|
||||
Flow: User submits → Moderator rejects with reason → User notified
|
||||
"""
|
||||
from apps.moderation.models import EditSubmission
|
||||
from apps.parks.models import Company
|
||||
|
||||
company = Company.objects.create(
|
||||
name='Test Company',
|
||||
description='Original'
|
||||
)
|
||||
|
||||
content_type = ContentType.objects.get_for_model(company)
|
||||
submission = EditSubmission.objects.create(
|
||||
user=self.regular_user,
|
||||
content_type=content_type,
|
||||
object_id=company.id,
|
||||
submission_type='EDIT',
|
||||
changes={'name': 'Spam Content'},
|
||||
status='PENDING',
|
||||
reason='Name change request'
|
||||
)
|
||||
|
||||
# Moderator rejects
|
||||
submission.transition_to_rejected(user=self.moderator)
|
||||
submission.handled_by = self.moderator
|
||||
submission.handled_at = timezone.now()
|
||||
submission.notes = 'Rejected: Content appears to be spam'
|
||||
submission.save()
|
||||
|
||||
submission.refresh_from_db()
|
||||
self.assertEqual(submission.status, 'REJECTED')
|
||||
self.assertIn('spam', submission.notes.lower())
|
||||
|
||||
|
||||
class SubmissionEscalationWorkflowTests(TestCase):
|
||||
"""Tests for the submission escalation workflow."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.regular_user = User.objects.create_user(
|
||||
username='user_esc',
|
||||
email='user_esc@example.com',
|
||||
password='testpass123',
|
||||
role='USER'
|
||||
)
|
||||
cls.moderator = User.objects.create_user(
|
||||
username='mod_esc',
|
||||
email='mod_esc@example.com',
|
||||
password='testpass123',
|
||||
role='MODERATOR'
|
||||
)
|
||||
cls.admin = User.objects.create_user(
|
||||
username='admin_esc',
|
||||
email='admin_esc@example.com',
|
||||
password='testpass123',
|
||||
role='ADMIN'
|
||||
)
|
||||
|
||||
def test_escalation_workflow(self):
|
||||
"""
|
||||
Test complete escalation workflow.
|
||||
|
||||
Flow: User submits → Moderator escalates → Admin reviews → Admin approves
|
||||
"""
|
||||
from apps.moderation.models import EditSubmission
|
||||
from apps.parks.models import Company
|
||||
|
||||
company = Company.objects.create(
|
||||
name='Sensitive Company',
|
||||
description='Original'
|
||||
)
|
||||
|
||||
content_type = ContentType.objects.get_for_model(company)
|
||||
submission = EditSubmission.objects.create(
|
||||
user=self.regular_user,
|
||||
content_type=content_type,
|
||||
object_id=company.id,
|
||||
submission_type='EDIT',
|
||||
changes={'name': 'New Sensitive Name'},
|
||||
status='PENDING',
|
||||
reason='Major name change'
|
||||
)
|
||||
|
||||
# Moderator escalates
|
||||
submission.transition_to_escalated(user=self.moderator)
|
||||
submission.notes = 'Escalated: Major change needs admin review'
|
||||
submission.save()
|
||||
|
||||
self.assertEqual(submission.status, 'ESCALATED')
|
||||
|
||||
# Admin approves
|
||||
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)
|
||||
|
||||
|
||||
class ReportHandlingWorkflowTests(TestCase):
|
||||
"""Tests for the moderation report handling workflow."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.reporter = User.objects.create_user(
|
||||
username='reporter',
|
||||
email='reporter@example.com',
|
||||
password='testpass123',
|
||||
role='USER'
|
||||
)
|
||||
cls.moderator = User.objects.create_user(
|
||||
username='mod_report',
|
||||
email='mod_report@example.com',
|
||||
password='testpass123',
|
||||
role='MODERATOR'
|
||||
)
|
||||
|
||||
def test_report_resolution_workflow(self):
|
||||
"""
|
||||
Test complete report resolution workflow.
|
||||
|
||||
Flow: User reports → Moderator assigned → Moderator investigates → Resolved
|
||||
"""
|
||||
from apps.moderation.models import ModerationReport
|
||||
from apps.parks.models import Company
|
||||
|
||||
reported_company = Company.objects.create(
|
||||
name='Problematic Company',
|
||||
description='Some inappropriate content'
|
||||
)
|
||||
|
||||
content_type = ContentType.objects.get_for_model(reported_company)
|
||||
|
||||
# User reports content
|
||||
report = ModerationReport.objects.create(
|
||||
report_type='CONTENT',
|
||||
status='PENDING',
|
||||
priority='HIGH',
|
||||
reported_entity_type='company',
|
||||
reported_entity_id=reported_company.id,
|
||||
content_type=content_type,
|
||||
reason='INAPPROPRIATE',
|
||||
description='This content is inappropriate',
|
||||
reported_by=self.reporter
|
||||
)
|
||||
|
||||
self.assertEqual(report.status, 'PENDING')
|
||||
|
||||
# Moderator claims and starts review
|
||||
report.transition_to_under_review(user=self.moderator)
|
||||
report.assigned_moderator = self.moderator
|
||||
report.save()
|
||||
|
||||
self.assertEqual(report.status, 'UNDER_REVIEW')
|
||||
self.assertEqual(report.assigned_moderator, self.moderator)
|
||||
|
||||
# Moderator resolves
|
||||
report.transition_to_resolved(user=self.moderator)
|
||||
report.resolution_action = 'CONTENT_REMOVED'
|
||||
report.resolution_notes = 'Content was removed'
|
||||
report.resolved_at = timezone.now()
|
||||
report.save()
|
||||
|
||||
report.refresh_from_db()
|
||||
self.assertEqual(report.status, 'RESOLVED')
|
||||
self.assertIsNotNone(report.resolved_at)
|
||||
|
||||
def test_report_dismissal_workflow(self):
|
||||
"""
|
||||
Test report dismissal workflow for invalid reports.
|
||||
|
||||
Flow: User reports → Moderator reviews → Moderator dismisses
|
||||
"""
|
||||
from apps.moderation.models import ModerationReport
|
||||
from apps.parks.models import Company
|
||||
|
||||
company = Company.objects.create(
|
||||
name='Valid Company',
|
||||
description='Normal content'
|
||||
)
|
||||
|
||||
content_type = ContentType.objects.get_for_model(company)
|
||||
|
||||
report = ModerationReport.objects.create(
|
||||
report_type='CONTENT',
|
||||
status='PENDING',
|
||||
priority='LOW',
|
||||
reported_entity_type='company',
|
||||
reported_entity_id=company.id,
|
||||
content_type=content_type,
|
||||
reason='OTHER',
|
||||
description='I just do not like this',
|
||||
reported_by=self.reporter
|
||||
)
|
||||
|
||||
# Moderator claims
|
||||
report.transition_to_under_review(user=self.moderator)
|
||||
report.assigned_moderator = self.moderator
|
||||
report.save()
|
||||
|
||||
# Moderator dismisses as invalid
|
||||
report.transition_to_dismissed(user=self.moderator)
|
||||
report.resolution_notes = 'Report does not violate any guidelines'
|
||||
report.resolved_at = timezone.now()
|
||||
report.save()
|
||||
|
||||
report.refresh_from_db()
|
||||
self.assertEqual(report.status, 'DISMISSED')
|
||||
|
||||
|
||||
class BulkOperationWorkflowTests(TestCase):
|
||||
"""Tests for bulk operation workflows."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.admin = User.objects.create_user(
|
||||
username='admin_bulk',
|
||||
email='admin_bulk@example.com',
|
||||
password='testpass123',
|
||||
role='ADMIN'
|
||||
)
|
||||
|
||||
def test_bulk_operation_success_workflow(self):
|
||||
"""
|
||||
Test successful bulk operation workflow.
|
||||
|
||||
Flow: Admin creates → Operation runs → Progress tracked → Completed
|
||||
"""
|
||||
from apps.moderation.models import BulkOperation
|
||||
|
||||
operation = BulkOperation.objects.create(
|
||||
operation_type='APPROVE_SUBMISSIONS',
|
||||
status='PENDING',
|
||||
total_items=10,
|
||||
processed_items=0,
|
||||
created_by=self.admin,
|
||||
parameters={'submission_ids': list(range(1, 11))}
|
||||
)
|
||||
|
||||
self.assertEqual(operation.status, 'PENDING')
|
||||
|
||||
# Start operation
|
||||
operation.transition_to_running(user=self.admin)
|
||||
operation.started_at = timezone.now()
|
||||
operation.save()
|
||||
|
||||
self.assertEqual(operation.status, 'RUNNING')
|
||||
|
||||
# Simulate progress
|
||||
for i in range(1, 11):
|
||||
operation.processed_items = i
|
||||
operation.save()
|
||||
|
||||
# Complete operation
|
||||
operation.transition_to_completed(user=self.admin)
|
||||
operation.completed_at = timezone.now()
|
||||
operation.results = {'approved': 10, 'failed': 0}
|
||||
operation.save()
|
||||
|
||||
operation.refresh_from_db()
|
||||
self.assertEqual(operation.status, 'COMPLETED')
|
||||
self.assertEqual(operation.processed_items, 10)
|
||||
|
||||
def test_bulk_operation_failure_workflow(self):
|
||||
"""
|
||||
Test bulk operation failure workflow.
|
||||
|
||||
Flow: Admin creates → Operation runs → Error occurs → Failed
|
||||
"""
|
||||
from apps.moderation.models import BulkOperation
|
||||
|
||||
operation = BulkOperation.objects.create(
|
||||
operation_type='DELETE_CONTENT',
|
||||
status='PENDING',
|
||||
total_items=5,
|
||||
processed_items=0,
|
||||
created_by=self.admin,
|
||||
parameters={'content_ids': list(range(1, 6))}
|
||||
)
|
||||
|
||||
operation.transition_to_running(user=self.admin)
|
||||
operation.started_at = timezone.now()
|
||||
operation.save()
|
||||
|
||||
# Simulate partial progress then failure
|
||||
operation.processed_items = 2
|
||||
operation.failed_items = 3
|
||||
operation.transition_to_failed(user=self.admin)
|
||||
operation.completed_at = timezone.now()
|
||||
operation.results = {'error': 'Database connection lost', 'processed': 2}
|
||||
operation.save()
|
||||
|
||||
operation.refresh_from_db()
|
||||
self.assertEqual(operation.status, 'FAILED')
|
||||
self.assertEqual(operation.failed_items, 3)
|
||||
|
||||
def test_bulk_operation_cancellation_workflow(self):
|
||||
"""
|
||||
Test bulk operation cancellation workflow.
|
||||
|
||||
Flow: Admin creates → Operation runs → Admin cancels
|
||||
"""
|
||||
from apps.moderation.models import BulkOperation
|
||||
|
||||
operation = BulkOperation.objects.create(
|
||||
operation_type='BATCH_UPDATE',
|
||||
status='PENDING',
|
||||
total_items=100,
|
||||
processed_items=0,
|
||||
created_by=self.admin,
|
||||
parameters={'update_field': 'status'},
|
||||
can_cancel=True
|
||||
)
|
||||
|
||||
operation.transition_to_running(user=self.admin)
|
||||
operation.save()
|
||||
|
||||
# Partial progress
|
||||
operation.processed_items = 30
|
||||
operation.save()
|
||||
|
||||
# Admin cancels
|
||||
operation.transition_to_cancelled(user=self.admin)
|
||||
operation.completed_at = timezone.now()
|
||||
operation.results = {'cancelled_at': 30, 'reason': 'User requested cancellation'}
|
||||
operation.save()
|
||||
|
||||
operation.refresh_from_db()
|
||||
self.assertEqual(operation.status, 'CANCELLED')
|
||||
self.assertEqual(operation.processed_items, 30)
|
||||
|
||||
|
||||
class ModerationQueueWorkflowTests(TestCase):
|
||||
"""Tests for moderation queue workflows."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.moderator = User.objects.create_user(
|
||||
username='mod_queue',
|
||||
email='mod_queue@example.com',
|
||||
password='testpass123',
|
||||
role='MODERATOR'
|
||||
)
|
||||
|
||||
def test_queue_completion_workflow(self):
|
||||
"""
|
||||
Test queue item completion workflow.
|
||||
|
||||
Flow: Item created → Moderator claims → Work done → Completed
|
||||
"""
|
||||
from apps.moderation.models import ModerationQueue
|
||||
|
||||
queue_item = ModerationQueue.objects.create(
|
||||
queue_type='SUBMISSION_REVIEW',
|
||||
status='PENDING',
|
||||
priority='MEDIUM',
|
||||
item_type='edit_submission',
|
||||
item_id=123
|
||||
)
|
||||
|
||||
self.assertEqual(queue_item.status, 'PENDING')
|
||||
|
||||
# Moderator claims
|
||||
queue_item.transition_to_in_progress(user=self.moderator)
|
||||
queue_item.assigned_to = self.moderator
|
||||
queue_item.assigned_at = timezone.now()
|
||||
queue_item.save()
|
||||
|
||||
self.assertEqual(queue_item.status, 'IN_PROGRESS')
|
||||
|
||||
# Work completed
|
||||
queue_item.transition_to_completed(user=self.moderator)
|
||||
queue_item.completed_at = timezone.now()
|
||||
queue_item.save()
|
||||
|
||||
queue_item.refresh_from_db()
|
||||
self.assertEqual(queue_item.status, 'COMPLETED')
|
||||
Reference in New Issue
Block a user