Files
thrillwiki_django_no_react/backend/apps/moderation/tests.py

1392 lines
49 KiB
Python

"""
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
"""
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 HttpRequest, JsonResponse
from django.test import Client, RequestFactory, TestCase
from django.utils import timezone
from django.views.generic import DetailView
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()
class TestView(
EditSubmissionMixin,
PhotoSubmissionMixin,
InlineEditMixin,
HistoryMixin,
DetailView,
):
model = Operator
template_name = "test.html"
pk_url_kwarg = "pk"
slug_url_kwarg = "slug"
def get_context_data(self, **kwargs):
if not hasattr(self, "object"):
self.object = self.get_object()
return super().get_context_data(**kwargs)
def setup(self, request: HttpRequest, *args, **kwargs):
super().setup(request, *args, **kwargs)
self.request = request
class ModerationMixinsTests(TestCase):
def setUp(self):
self.client = Client()
self.factory = RequestFactory()
# Create users with different roles
self.user = User.objects.create_user(
username="testuser",
email="test@example.com",
password="testpass123",
)
self.moderator = User.objects.create_user(
username="moderator",
email="moderator@example.com",
password="modpass123",
role="MODERATOR",
)
self.admin = User.objects.create_user(
username="admin",
email="admin@example.com",
password="adminpass123",
role="ADMIN",
)
# Create test company
self.operator = Operator.objects.create(
name="Test Operator",
website="http://example.com",
description="Test Description",
)
def test_edit_submission_mixin_unauthenticated(self):
"""Test edit submission when not logged in"""
view = TestView()
request = self.factory.post(f"/test/{self.operator.pk}/")
request.user = AnonymousUser()
view.setup(request, pk=self.operator.pk)
view.kwargs = {"pk": self.operator.pk}
response = view.handle_edit_submission(request, {})
self.assertIsInstance(response, JsonResponse)
self.assertEqual(response.status_code, 403)
def test_edit_submission_mixin_no_changes(self):
"""Test edit submission with no changes"""
view = TestView()
request = self.factory.post(
f"/test/{self.operator.pk}/",
data=json.dumps({}),
content_type="application/json",
)
request.user = self.user
view.setup(request, pk=self.operator.pk)
view.kwargs = {"pk": self.operator.pk}
response = view.post(request)
self.assertIsInstance(response, JsonResponse)
self.assertEqual(response.status_code, 400)
def test_edit_submission_mixin_invalid_json(self):
"""Test edit submission with invalid JSON"""
view = TestView()
request = self.factory.post(
f"/test/{self.operator.pk}/",
data="invalid json",
content_type="application/json",
)
request.user = self.user
view.setup(request, pk=self.operator.pk)
view.kwargs = {"pk": self.operator.pk}
response = view.post(request)
self.assertIsInstance(response, JsonResponse)
self.assertEqual(response.status_code, 400)
def test_edit_submission_mixin_regular_user(self):
"""Test edit submission as regular user"""
view = TestView()
request = self.factory.post(f"/test/{self.operator.pk}/")
request.user = self.user
view.setup(request, pk=self.operator.pk)
view.kwargs = {"pk": self.operator.pk}
changes = {"name": "New Name"}
response = view.handle_edit_submission(
request, changes, "Test reason", "Test source"
)
self.assertIsInstance(response, JsonResponse)
self.assertEqual(response.status_code, 200)
data = json.loads(response.content.decode())
self.assertFalse(data["auto_approved"])
def test_edit_submission_mixin_moderator(self):
"""Test edit submission as moderator"""
view = TestView()
request = self.factory.post(f"/test/{self.operator.pk}/")
request.user = self.moderator
view.setup(request, pk=self.operator.pk)
view.kwargs = {"pk": self.operator.pk}
changes = {"name": "New Name"}
response = view.handle_edit_submission(
request, changes, "Test reason", "Test source"
)
self.assertIsInstance(response, JsonResponse)
self.assertEqual(response.status_code, 200)
data = json.loads(response.content.decode())
self.assertTrue(data["auto_approved"])
def test_photo_submission_mixin_unauthenticated(self):
"""Test photo submission when not logged in"""
view = TestView()
view.kwargs = {"pk": self.operator.pk}
view.object = self.operator
request = self.factory.post(
f"/test/{self.operator.pk}/", data={}, format="multipart"
)
request.user = AnonymousUser()
view.setup(request, pk=self.operator.pk)
response = view.handle_photo_submission(request)
self.assertIsInstance(response, JsonResponse)
self.assertEqual(response.status_code, 403)
def test_photo_submission_mixin_no_photo(self):
"""Test photo submission with no photo"""
view = TestView()
view.kwargs = {"pk": self.operator.pk}
view.object = self.operator
request = self.factory.post(
f"/test/{self.operator.pk}/", data={}, format="multipart"
)
request.user = self.user
view.setup(request, pk=self.operator.pk)
response = view.handle_photo_submission(request)
self.assertIsInstance(response, JsonResponse)
self.assertEqual(response.status_code, 400)
def test_photo_submission_mixin_regular_user(self):
"""Test photo submission as regular user"""
view = TestView()
view.kwargs = {"pk": self.operator.pk}
view.object = self.operator
# Create a test photo file
photo = SimpleUploadedFile(
"test.gif",
b"GIF87a\x01\x00\x01\x00\x80\x01\x00\x00\x00\x00ccc,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02D\x01\x00;",
content_type="image/gif",
)
request = self.factory.post(
f"/test/{self.operator.pk}/",
data={
"photo": photo,
"caption": "Test Photo",
"date_taken": "2024-01-01",
},
format="multipart",
)
request.user = self.user
view.setup(request, pk=self.operator.pk)
response = view.handle_photo_submission(request)
self.assertIsInstance(response, JsonResponse)
self.assertEqual(response.status_code, 200)
data = json.loads(response.content.decode())
self.assertFalse(data["auto_approved"])
def test_photo_submission_mixin_moderator(self):
"""Test photo submission as moderator"""
view = TestView()
view.kwargs = {"pk": self.operator.pk}
view.object = self.operator
# Create a test photo file
photo = SimpleUploadedFile(
"test.gif",
b"GIF87a\x01\x00\x01\x00\x80\x01\x00\x00\x00\x00ccc,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02D\x01\x00;",
content_type="image/gif",
)
request = self.factory.post(
f"/test/{self.operator.pk}/",
data={
"photo": photo,
"caption": "Test Photo",
"date_taken": "2024-01-01",
},
format="multipart",
)
request.user = self.moderator
view.setup(request, pk=self.operator.pk)
response = view.handle_photo_submission(request)
self.assertIsInstance(response, JsonResponse)
self.assertEqual(response.status_code, 200)
data = json.loads(response.content.decode())
self.assertTrue(data["auto_approved"])
def test_moderator_required_mixin(self):
"""Test moderator required mixin"""
class TestModeratorView(ModeratorRequiredMixin):
pass
view = TestModeratorView()
# Test unauthenticated user
request = self.factory.get("/test/")
request.user = AnonymousUser()
view.request = request
self.assertFalse(view.test_func())
# Test regular user
request.user = self.user
view.request = request
self.assertFalse(view.test_func())
# Test moderator
request.user = self.moderator
view.request = request
self.assertTrue(view.test_func())
# Test admin
request.user = self.admin
view.request = request
self.assertTrue(view.test_func())
def test_admin_required_mixin(self):
"""Test admin required mixin"""
class TestAdminView(AdminRequiredMixin):
pass
view = TestAdminView()
# Test unauthenticated user
request = self.factory.get("/test/")
request.user = AnonymousUser()
view.request = request
self.assertFalse(view.test_func())
# Test regular user
request.user = self.user
view.request = request
self.assertFalse(view.test_func())
# Test moderator
request.user = self.moderator
view.request = request
self.assertFalse(view.test_func())
# Test admin
request.user = self.admin
view.request = request
self.assertTrue(view.test_func())
def test_inline_edit_mixin(self):
"""Test inline edit mixin"""
view = TestView()
view.kwargs = {"pk": self.operator.pk}
view.object = self.operator
# Test unauthenticated user
request = self.factory.get(f"/test/{self.operator.pk}/")
request.user = AnonymousUser()
view.setup(request, pk=self.operator.pk)
context = view.get_context_data()
self.assertNotIn("can_edit", context)
# Test regular user
request.user = self.user
view.setup(request, pk=self.operator.pk)
context = view.get_context_data()
self.assertTrue(context["can_edit"])
self.assertFalse(context["can_auto_approve"])
# Test moderator
request.user = self.moderator
view.setup(request, pk=self.operator.pk)
context = view.get_context_data()
self.assertTrue(context["can_edit"])
self.assertTrue(context["can_auto_approve"])
def test_history_mixin(self):
"""Test history mixin"""
view = TestView()
view.kwargs = {"pk": self.operator.pk}
view.object = self.operator
request = self.factory.get(f"/test/{self.operator.pk}/")
request.user = self.user
view.setup(request, pk=self.operator.pk)
# Create some edit submissions
EditSubmission.objects.create(
user=self.user,
content_type=ContentType.objects.get_for_model(Operator),
object_id=getattr(self.operator, "id", None),
submission_type="EDIT",
changes={"name": "New Name"},
status="APPROVED",
)
context = view.get_context_data()
self.assertIn("history", context)
self.assertIn("edit_submissions", context)
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()
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
# ============================================================================
class TransitionLoggingTestCase(TestCase):
"""Test cases for FSM transition logging with django-fsm-log."""
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.operator = Operator.objects.create(
name='Test Operator',
description='Test Description'
)
self.content_type = ContentType.objects.get_for_model(Operator)
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,
content_type=self.content_type,
object_id=self.operator.id,
submission_type='EDIT',
changes={'name': 'Updated Name'},
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)
self.assertIn('approved', log.transition.lower())
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,
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)
# 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')
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,
object_id=self.operator.id,
submission_type='EDIT',
changes={'name': 'Updated Name'},
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,
object_id=self.operator.id,
submission_type='EDIT',
changes={'name': 'Updated Name'},
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")
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,
object_id=self.operator.id,
submission_type='EDIT',
changes={'name': 'Updated Name'},
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
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 Mock, patch
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)