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 .mixins import ( EditSubmissionMixin, PhotoSubmissionMixin, ModeratorRequiredMixin, AdminRequiredMixin, InlineEditMixin, HistoryMixin, ) from apps.parks.models import Company as Operator from django.views.generic import DetailView from django.test import RequestFactory import json 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) # ============================================================================ # 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' ) # 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' ) 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 from django_fsm_log.models import StateLog 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' ) # 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(f'/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.""" 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' ) # 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' ) # 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'))