mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-23 01:11:09 -05:00
Add test utilities and state machine diagrams for FSM models
- Introduced reusable test utilities in `backend/tests/utils` for FSM transitions, HTMX interactions, and common scenarios. - Added factory functions for creating test submissions, parks, rides, and photo submissions. - Implemented assertion helpers for verifying state changes, toast notifications, and transition logs. - Created comprehensive state machine diagrams for all FSM-enabled models in `docs/STATE_DIAGRAMS.md`, detailing states, transitions, and guard conditions.
This commit is contained in:
6
backend/tests/integration/__init__.py
Normal file
6
backend/tests/integration/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""
|
||||
Integration tests for ThrillWiki.
|
||||
|
||||
These tests verify that different components of the application work together correctly
|
||||
without requiring a browser (unlike E2E tests).
|
||||
"""
|
||||
792
backend/tests/integration/test_fsm_transition_view.py
Normal file
792
backend/tests/integration/test_fsm_transition_view.py
Normal file
@@ -0,0 +1,792 @@
|
||||
"""
|
||||
Integration Tests for FSMTransitionView
|
||||
|
||||
Tests the FSMTransitionView without a browser, using Django's test client.
|
||||
These tests verify:
|
||||
- FSMTransitionView handles HTMX requests correctly
|
||||
- HX-Trigger headers contain proper toast data
|
||||
- Correct partial templates rendered for each model
|
||||
- Permission validation before transition execution
|
||||
|
||||
These are faster than E2E tests and don't require Playwright.
|
||||
"""
|
||||
|
||||
import json
|
||||
import pytest
|
||||
from django.test import TestCase, Client
|
||||
from django.urls import reverse
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class TestFSMTransitionViewHTMX(TestCase):
|
||||
"""Tests for FSMTransitionView with HTMX requests."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
"""Set up test data for all tests in this class."""
|
||||
# Create regular user
|
||||
cls.user = User.objects.create_user(
|
||||
username="testuser",
|
||||
email="testuser@example.com",
|
||||
password="testpass123"
|
||||
)
|
||||
|
||||
# Create moderator user
|
||||
cls.moderator = User.objects.create_user(
|
||||
username="moderator",
|
||||
email="moderator@example.com",
|
||||
password="modpass123",
|
||||
is_staff=True
|
||||
)
|
||||
|
||||
# Create admin user
|
||||
cls.admin = User.objects.create_user(
|
||||
username="admin",
|
||||
email="admin@example.com",
|
||||
password="adminpass123",
|
||||
is_staff=True,
|
||||
is_superuser=True
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test client for each test."""
|
||||
self.client = Client()
|
||||
|
||||
def test_fsm_transition_view_with_htmx_header(self):
|
||||
"""Test that FSMTransitionView handles HTMX requests correctly."""
|
||||
from apps.moderation.models import EditSubmission
|
||||
from apps.parks.models import Park
|
||||
|
||||
self.client.login(username="moderator", password="modpass123")
|
||||
|
||||
# Create a pending submission
|
||||
park = Park.objects.first()
|
||||
if not park:
|
||||
self.skipTest("No park available for testing")
|
||||
|
||||
content_type = ContentType.objects.get_for_model(Park)
|
||||
|
||||
submission = EditSubmission.objects.create(
|
||||
user=self.user,
|
||||
content_type=content_type,
|
||||
object_id=park.pk,
|
||||
submission_type="EDIT",
|
||||
changes={"description": "Test change"},
|
||||
reason="Integration test",
|
||||
status="PENDING"
|
||||
)
|
||||
|
||||
url = reverse(
|
||||
"core:fsm_transition",
|
||||
kwargs={
|
||||
"app_label": "moderation",
|
||||
"model_name": "editsubmission",
|
||||
"pk": submission.pk,
|
||||
"transition_name": "transition_to_approved"
|
||||
}
|
||||
)
|
||||
|
||||
# Make request with HTMX header
|
||||
response = self.client.post(
|
||||
url,
|
||||
HTTP_HX_REQUEST="true"
|
||||
)
|
||||
|
||||
# Should return 200 OK
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Should have HX-Trigger header
|
||||
self.assertIn("HX-Trigger", response)
|
||||
|
||||
# Parse HX-Trigger header
|
||||
trigger_data = json.loads(response["HX-Trigger"])
|
||||
self.assertIn("showToast", trigger_data)
|
||||
self.assertEqual(trigger_data["showToast"]["type"], "success")
|
||||
|
||||
# Cleanup
|
||||
submission.delete()
|
||||
|
||||
def test_fsm_transition_view_without_htmx_header(self):
|
||||
"""Test that FSMTransitionView handles non-HTMX requests correctly."""
|
||||
from apps.moderation.models import EditSubmission
|
||||
from apps.parks.models import Park
|
||||
|
||||
self.client.login(username="moderator", password="modpass123")
|
||||
|
||||
park = Park.objects.first()
|
||||
if not park:
|
||||
self.skipTest("No park available for testing")
|
||||
|
||||
content_type = ContentType.objects.get_for_model(Park)
|
||||
|
||||
submission = EditSubmission.objects.create(
|
||||
user=self.user,
|
||||
content_type=content_type,
|
||||
object_id=park.pk,
|
||||
submission_type="EDIT",
|
||||
changes={"description": "Test change non-htmx"},
|
||||
reason="Integration test non-htmx",
|
||||
status="PENDING"
|
||||
)
|
||||
|
||||
url = reverse(
|
||||
"core:fsm_transition",
|
||||
kwargs={
|
||||
"app_label": "moderation",
|
||||
"model_name": "editsubmission",
|
||||
"pk": submission.pk,
|
||||
"transition_name": "transition_to_approved"
|
||||
}
|
||||
)
|
||||
|
||||
# Make request WITHOUT HTMX header
|
||||
response = self.client.post(url)
|
||||
|
||||
# Should return 200 OK with JSON response
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response["Content-Type"], "application/json")
|
||||
|
||||
# Parse JSON response
|
||||
data = response.json()
|
||||
self.assertTrue(data["success"])
|
||||
self.assertIn("message", data)
|
||||
|
||||
# Cleanup
|
||||
submission.delete()
|
||||
|
||||
def test_fsm_transition_view_returns_correct_partial(self):
|
||||
"""Test that FSMTransitionView returns correct partial template."""
|
||||
from apps.moderation.models import EditSubmission
|
||||
from apps.parks.models import Park
|
||||
|
||||
self.client.login(username="moderator", password="modpass123")
|
||||
|
||||
park = Park.objects.first()
|
||||
if not park:
|
||||
self.skipTest("No park available for testing")
|
||||
|
||||
content_type = ContentType.objects.get_for_model(Park)
|
||||
|
||||
submission = EditSubmission.objects.create(
|
||||
user=self.user,
|
||||
content_type=content_type,
|
||||
object_id=park.pk,
|
||||
submission_type="EDIT",
|
||||
changes={"description": "Test partial"},
|
||||
reason="Partial test",
|
||||
status="PENDING"
|
||||
)
|
||||
|
||||
url = reverse(
|
||||
"core:fsm_transition",
|
||||
kwargs={
|
||||
"app_label": "moderation",
|
||||
"model_name": "editsubmission",
|
||||
"pk": submission.pk,
|
||||
"transition_name": "transition_to_approved"
|
||||
}
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
url,
|
||||
HTTP_HX_REQUEST="true"
|
||||
)
|
||||
|
||||
# Response should contain HTML (partial template)
|
||||
self.assertIn("text/html", response["Content-Type"])
|
||||
|
||||
# Cleanup
|
||||
submission.delete()
|
||||
|
||||
def test_fsm_transition_view_adds_toast_trigger(self):
|
||||
"""Test that FSMTransitionView adds correct HX-Trigger for toast."""
|
||||
from apps.parks.models import Park
|
||||
|
||||
self.client.login(username="moderator", password="modpass123")
|
||||
|
||||
park = Park.objects.filter(status="OPERATING").first()
|
||||
if not park:
|
||||
self.skipTest("No operating park available for testing")
|
||||
|
||||
url = reverse(
|
||||
"core:fsm_transition",
|
||||
kwargs={
|
||||
"app_label": "parks",
|
||||
"model_name": "park",
|
||||
"pk": park.pk,
|
||||
"transition_name": "transition_to_closed_temp"
|
||||
}
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
url,
|
||||
HTTP_HX_REQUEST="true"
|
||||
)
|
||||
|
||||
# Parse HX-Trigger header
|
||||
trigger_data = json.loads(response["HX-Trigger"])
|
||||
|
||||
# Verify toast structure
|
||||
self.assertIn("showToast", trigger_data)
|
||||
self.assertIn("message", trigger_data["showToast"])
|
||||
self.assertIn("type", trigger_data["showToast"])
|
||||
self.assertEqual(trigger_data["showToast"]["type"], "success")
|
||||
|
||||
# Reset park status for other tests
|
||||
park.status = "OPERATING"
|
||||
park.save()
|
||||
|
||||
def test_fsm_transition_view_handles_invalid_model(self):
|
||||
"""Test that FSMTransitionView handles invalid model gracefully."""
|
||||
self.client.login(username="moderator", password="modpass123")
|
||||
|
||||
url = reverse(
|
||||
"core:fsm_transition",
|
||||
kwargs={
|
||||
"app_label": "nonexistent",
|
||||
"model_name": "fakemodel",
|
||||
"pk": 1,
|
||||
"transition_name": "fake_transition"
|
||||
}
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
url,
|
||||
HTTP_HX_REQUEST="true"
|
||||
)
|
||||
|
||||
# Should return 404
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
# Should have error toast trigger
|
||||
self.assertIn("HX-Trigger", response)
|
||||
trigger_data = json.loads(response["HX-Trigger"])
|
||||
self.assertEqual(trigger_data["showToast"]["type"], "error")
|
||||
|
||||
def test_fsm_transition_view_handles_invalid_transition(self):
|
||||
"""Test that FSMTransitionView handles invalid transition name."""
|
||||
from apps.parks.models import Park
|
||||
|
||||
self.client.login(username="moderator", password="modpass123")
|
||||
|
||||
park = Park.objects.first()
|
||||
if not park:
|
||||
self.skipTest("No park available for testing")
|
||||
|
||||
url = reverse(
|
||||
"core:fsm_transition",
|
||||
kwargs={
|
||||
"app_label": "parks",
|
||||
"model_name": "park",
|
||||
"pk": park.pk,
|
||||
"transition_name": "nonexistent_transition"
|
||||
}
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
url,
|
||||
HTTP_HX_REQUEST="true"
|
||||
)
|
||||
|
||||
# Should return 400 Bad Request
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
# Should have error toast trigger
|
||||
self.assertIn("HX-Trigger", response)
|
||||
trigger_data = json.loads(response["HX-Trigger"])
|
||||
self.assertEqual(trigger_data["showToast"]["type"], "error")
|
||||
|
||||
def test_fsm_transition_view_validates_permissions(self):
|
||||
"""Test that FSMTransitionView validates user permissions."""
|
||||
from apps.moderation.models import EditSubmission
|
||||
from apps.parks.models import Park
|
||||
|
||||
# Login as regular user (not moderator)
|
||||
self.client.login(username="testuser", password="testpass123")
|
||||
|
||||
park = Park.objects.first()
|
||||
if not park:
|
||||
self.skipTest("No park available for testing")
|
||||
|
||||
content_type = ContentType.objects.get_for_model(Park)
|
||||
|
||||
submission = EditSubmission.objects.create(
|
||||
user=self.user,
|
||||
content_type=content_type,
|
||||
object_id=park.pk,
|
||||
submission_type="EDIT",
|
||||
changes={"description": "Permission test"},
|
||||
reason="Permission test",
|
||||
status="PENDING"
|
||||
)
|
||||
|
||||
url = reverse(
|
||||
"core:fsm_transition",
|
||||
kwargs={
|
||||
"app_label": "moderation",
|
||||
"model_name": "editsubmission",
|
||||
"pk": submission.pk,
|
||||
"transition_name": "transition_to_approved"
|
||||
}
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
url,
|
||||
HTTP_HX_REQUEST="true"
|
||||
)
|
||||
|
||||
# Should return 400 or 403 (permission denied)
|
||||
self.assertIn(response.status_code, [400, 403])
|
||||
|
||||
# Verify submission was NOT changed
|
||||
submission.refresh_from_db()
|
||||
self.assertEqual(submission.status, "PENDING")
|
||||
|
||||
# Cleanup
|
||||
submission.delete()
|
||||
|
||||
|
||||
class TestFSMTransitionViewParkModel(TestCase):
|
||||
"""Tests for FSMTransitionView with Park model."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.moderator = User.objects.create_user(
|
||||
username="mod_park",
|
||||
email="mod_park@example.com",
|
||||
password="modpass123",
|
||||
is_staff=True
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
self.client.login(username="mod_park", password="modpass123")
|
||||
|
||||
def test_park_close_temporarily_transition(self):
|
||||
"""Test park close temporarily transition via view."""
|
||||
from apps.parks.models import Park
|
||||
|
||||
park = Park.objects.filter(status="OPERATING").first()
|
||||
if not park:
|
||||
self.skipTest("No operating park available")
|
||||
|
||||
url = reverse(
|
||||
"core:fsm_transition",
|
||||
kwargs={
|
||||
"app_label": "parks",
|
||||
"model_name": "park",
|
||||
"pk": park.pk,
|
||||
"transition_name": "transition_to_closed_temp"
|
||||
}
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
url,
|
||||
HTTP_HX_REQUEST="true"
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Verify park status changed
|
||||
park.refresh_from_db()
|
||||
self.assertEqual(park.status, "CLOSED_TEMP")
|
||||
|
||||
# Reset for other tests
|
||||
park.status = "OPERATING"
|
||||
park.save()
|
||||
|
||||
def test_park_reopen_transition(self):
|
||||
"""Test park reopen transition via view."""
|
||||
from apps.parks.models import Park
|
||||
|
||||
park = Park.objects.filter(status="OPERATING").first()
|
||||
if not park:
|
||||
self.skipTest("No park available")
|
||||
|
||||
# First close the park
|
||||
park.status = "CLOSED_TEMP"
|
||||
park.save()
|
||||
|
||||
url = reverse(
|
||||
"core:fsm_transition",
|
||||
kwargs={
|
||||
"app_label": "parks",
|
||||
"model_name": "park",
|
||||
"pk": park.pk,
|
||||
"transition_name": "transition_to_operating"
|
||||
}
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
url,
|
||||
HTTP_HX_REQUEST="true"
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Verify park status changed
|
||||
park.refresh_from_db()
|
||||
self.assertEqual(park.status, "OPERATING")
|
||||
|
||||
def test_park_slug_based_transition(self):
|
||||
"""Test FSM transition using slug instead of pk."""
|
||||
from apps.parks.models import Park
|
||||
|
||||
park = Park.objects.filter(status="OPERATING").first()
|
||||
if not park:
|
||||
self.skipTest("No operating park available")
|
||||
|
||||
url = reverse(
|
||||
"core:fsm_transition_by_slug",
|
||||
kwargs={
|
||||
"app_label": "parks",
|
||||
"model_name": "park",
|
||||
"slug": park.slug,
|
||||
"transition_name": "transition_to_closed_temp"
|
||||
}
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
url,
|
||||
HTTP_HX_REQUEST="true"
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Verify park status changed
|
||||
park.refresh_from_db()
|
||||
self.assertEqual(park.status, "CLOSED_TEMP")
|
||||
|
||||
# Reset for other tests
|
||||
park.status = "OPERATING"
|
||||
park.save()
|
||||
|
||||
|
||||
class TestFSMTransitionViewRideModel(TestCase):
|
||||
"""Tests for FSMTransitionView with Ride model."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.moderator = User.objects.create_user(
|
||||
username="mod_ride",
|
||||
email="mod_ride@example.com",
|
||||
password="modpass123",
|
||||
is_staff=True
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
self.client.login(username="mod_ride", password="modpass123")
|
||||
|
||||
def test_ride_close_temporarily_transition(self):
|
||||
"""Test ride close temporarily transition via view."""
|
||||
from apps.rides.models import Ride
|
||||
|
||||
ride = Ride.objects.filter(status="OPERATING").first()
|
||||
if not ride:
|
||||
self.skipTest("No operating ride available")
|
||||
|
||||
url = reverse(
|
||||
"core:fsm_transition",
|
||||
kwargs={
|
||||
"app_label": "rides",
|
||||
"model_name": "ride",
|
||||
"pk": ride.pk,
|
||||
"transition_name": "transition_to_closed_temp"
|
||||
}
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
url,
|
||||
HTTP_HX_REQUEST="true"
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Verify ride status changed
|
||||
ride.refresh_from_db()
|
||||
self.assertEqual(ride.status, "CLOSED_TEMP")
|
||||
|
||||
# Reset for other tests
|
||||
ride.status = "OPERATING"
|
||||
ride.save()
|
||||
|
||||
def test_ride_mark_sbno_transition(self):
|
||||
"""Test ride mark SBNO transition via view."""
|
||||
from apps.rides.models import Ride
|
||||
|
||||
ride = Ride.objects.filter(status="OPERATING").first()
|
||||
if not ride:
|
||||
self.skipTest("No operating ride available")
|
||||
|
||||
url = reverse(
|
||||
"core:fsm_transition",
|
||||
kwargs={
|
||||
"app_label": "rides",
|
||||
"model_name": "ride",
|
||||
"pk": ride.pk,
|
||||
"transition_name": "transition_to_sbno"
|
||||
}
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
url,
|
||||
HTTP_HX_REQUEST="true"
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Verify ride status changed
|
||||
ride.refresh_from_db()
|
||||
self.assertEqual(ride.status, "SBNO")
|
||||
|
||||
# Reset for other tests
|
||||
ride.status = "OPERATING"
|
||||
ride.save()
|
||||
|
||||
|
||||
class TestFSMTransitionViewModerationModels(TestCase):
|
||||
"""Tests for FSMTransitionView with moderation models."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.user = User.objects.create_user(
|
||||
username="submitter",
|
||||
email="submitter@example.com",
|
||||
password="testpass123"
|
||||
)
|
||||
|
||||
cls.moderator = User.objects.create_user(
|
||||
username="mod_moderation",
|
||||
email="mod_moderation@example.com",
|
||||
password="modpass123",
|
||||
is_staff=True
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
self.client.login(username="mod_moderation", password="modpass123")
|
||||
|
||||
def test_edit_submission_approve_transition(self):
|
||||
"""Test EditSubmission approve transition."""
|
||||
from apps.moderation.models import EditSubmission
|
||||
from apps.parks.models import Park
|
||||
|
||||
park = Park.objects.first()
|
||||
if not park:
|
||||
self.skipTest("No park available")
|
||||
|
||||
content_type = ContentType.objects.get_for_model(Park)
|
||||
|
||||
submission = EditSubmission.objects.create(
|
||||
user=self.user,
|
||||
content_type=content_type,
|
||||
object_id=park.pk,
|
||||
submission_type="EDIT",
|
||||
changes={"description": "Approve test"},
|
||||
reason="Approve test",
|
||||
status="PENDING"
|
||||
)
|
||||
|
||||
url = reverse(
|
||||
"core:fsm_transition",
|
||||
kwargs={
|
||||
"app_label": "moderation",
|
||||
"model_name": "editsubmission",
|
||||
"pk": submission.pk,
|
||||
"transition_name": "transition_to_approved"
|
||||
}
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
url,
|
||||
HTTP_HX_REQUEST="true"
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Verify submission status changed
|
||||
submission.refresh_from_db()
|
||||
self.assertEqual(submission.status, "APPROVED")
|
||||
|
||||
# Cleanup
|
||||
submission.delete()
|
||||
|
||||
def test_edit_submission_reject_transition(self):
|
||||
"""Test EditSubmission reject transition."""
|
||||
from apps.moderation.models import EditSubmission
|
||||
from apps.parks.models import Park
|
||||
|
||||
park = Park.objects.first()
|
||||
if not park:
|
||||
self.skipTest("No park available")
|
||||
|
||||
content_type = ContentType.objects.get_for_model(Park)
|
||||
|
||||
submission = EditSubmission.objects.create(
|
||||
user=self.user,
|
||||
content_type=content_type,
|
||||
object_id=park.pk,
|
||||
submission_type="EDIT",
|
||||
changes={"description": "Reject test"},
|
||||
reason="Reject test",
|
||||
status="PENDING"
|
||||
)
|
||||
|
||||
url = reverse(
|
||||
"core:fsm_transition",
|
||||
kwargs={
|
||||
"app_label": "moderation",
|
||||
"model_name": "editsubmission",
|
||||
"pk": submission.pk,
|
||||
"transition_name": "transition_to_rejected"
|
||||
}
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
url,
|
||||
HTTP_HX_REQUEST="true"
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Verify submission status changed
|
||||
submission.refresh_from_db()
|
||||
self.assertEqual(submission.status, "REJECTED")
|
||||
|
||||
# Cleanup
|
||||
submission.delete()
|
||||
|
||||
def test_edit_submission_escalate_transition(self):
|
||||
"""Test EditSubmission escalate transition."""
|
||||
from apps.moderation.models import EditSubmission
|
||||
from apps.parks.models import Park
|
||||
|
||||
park = Park.objects.first()
|
||||
if not park:
|
||||
self.skipTest("No park available")
|
||||
|
||||
content_type = ContentType.objects.get_for_model(Park)
|
||||
|
||||
submission = EditSubmission.objects.create(
|
||||
user=self.user,
|
||||
content_type=content_type,
|
||||
object_id=park.pk,
|
||||
submission_type="EDIT",
|
||||
changes={"description": "Escalate test"},
|
||||
reason="Escalate test",
|
||||
status="PENDING"
|
||||
)
|
||||
|
||||
url = reverse(
|
||||
"core:fsm_transition",
|
||||
kwargs={
|
||||
"app_label": "moderation",
|
||||
"model_name": "editsubmission",
|
||||
"pk": submission.pk,
|
||||
"transition_name": "transition_to_escalated"
|
||||
}
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
url,
|
||||
HTTP_HX_REQUEST="true"
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Verify submission status changed
|
||||
submission.refresh_from_db()
|
||||
self.assertEqual(submission.status, "ESCALATED")
|
||||
|
||||
# Cleanup
|
||||
submission.delete()
|
||||
|
||||
|
||||
class TestFSMTransitionViewStateLog(TestCase):
|
||||
"""Tests that FSM transitions create proper StateLog entries."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.user = User.objects.create_user(
|
||||
username="submitter_log",
|
||||
email="submitter_log@example.com",
|
||||
password="testpass123"
|
||||
)
|
||||
|
||||
cls.moderator = User.objects.create_user(
|
||||
username="mod_log",
|
||||
email="mod_log@example.com",
|
||||
password="modpass123",
|
||||
is_staff=True
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
self.client.login(username="mod_log", password="modpass123")
|
||||
|
||||
def test_transition_creates_state_log(self):
|
||||
"""Test that FSM transition creates a StateLog entry."""
|
||||
from apps.moderation.models import EditSubmission
|
||||
from apps.parks.models import Park
|
||||
from django_fsm_log.models import StateLog
|
||||
|
||||
park = Park.objects.first()
|
||||
if not park:
|
||||
self.skipTest("No park available")
|
||||
|
||||
content_type = ContentType.objects.get_for_model(Park)
|
||||
|
||||
submission = EditSubmission.objects.create(
|
||||
user=self.user,
|
||||
content_type=content_type,
|
||||
object_id=park.pk,
|
||||
submission_type="EDIT",
|
||||
changes={"description": "StateLog test"},
|
||||
reason="StateLog test",
|
||||
status="PENDING"
|
||||
)
|
||||
|
||||
# Count existing StateLog entries
|
||||
initial_log_count = StateLog.objects.filter(
|
||||
content_type=ContentType.objects.get_for_model(EditSubmission),
|
||||
object_id=submission.pk
|
||||
).count()
|
||||
|
||||
url = reverse(
|
||||
"core:fsm_transition",
|
||||
kwargs={
|
||||
"app_label": "moderation",
|
||||
"model_name": "editsubmission",
|
||||
"pk": submission.pk,
|
||||
"transition_name": "transition_to_approved"
|
||||
}
|
||||
)
|
||||
|
||||
self.client.post(
|
||||
url,
|
||||
HTTP_HX_REQUEST="true"
|
||||
)
|
||||
|
||||
# Check that a new StateLog entry was created
|
||||
new_log_count = StateLog.objects.filter(
|
||||
content_type=ContentType.objects.get_for_model(EditSubmission),
|
||||
object_id=submission.pk
|
||||
).count()
|
||||
|
||||
self.assertEqual(new_log_count, initial_log_count + 1)
|
||||
|
||||
# Verify the StateLog entry details
|
||||
latest_log = StateLog.objects.filter(
|
||||
content_type=ContentType.objects.get_for_model(EditSubmission),
|
||||
object_id=submission.pk
|
||||
).latest('timestamp')
|
||||
|
||||
self.assertEqual(latest_log.state, "APPROVED")
|
||||
self.assertEqual(latest_log.by, self.moderator)
|
||||
|
||||
# Cleanup
|
||||
submission.delete()
|
||||
Reference in New Issue
Block a user