feat: Implement initial schema and add various API, service, and management command enhancements across the application.

This commit is contained in:
pacnpal
2026-01-01 15:13:01 -05:00
parent c95f99ca10
commit b243b17af7
413 changed files with 11164 additions and 17433 deletions

View File

@@ -22,9 +22,7 @@ from playwright.sync_api import Page, expect
class TestUnauthenticatedUserPermissions:
"""Tests for unauthenticated user permission guards."""
def test_unauthenticated_user_cannot_see_moderation_dashboard(
self, page: Page, live_server
):
def test_unauthenticated_user_cannot_see_moderation_dashboard(self, page: Page, live_server):
"""Test that unauthenticated users are redirected from moderation dashboard."""
# Navigate to moderation dashboard without logging in
response = page.goto(f"{live_server.url}/moderation/dashboard/")
@@ -34,9 +32,7 @@ class TestUnauthenticatedUserPermissions:
current_url = page.url
assert "login" in current_url or "denied" in current_url or response.status == 403
def test_unauthenticated_user_cannot_see_transition_buttons(
self, page: Page, live_server, db
):
def test_unauthenticated_user_cannot_see_transition_buttons(self, page: Page, live_server, db):
"""Test that unauthenticated users cannot see transition buttons on park detail."""
from apps.parks.models import Park
@@ -48,18 +44,14 @@ class TestUnauthenticatedUserPermissions:
page.wait_for_load_state("networkidle")
# Status action buttons should NOT be visible
status_actions = page.locator('[data-park-status-actions]')
status_actions = page.locator("[data-park-status-actions]")
# Either the section doesn't exist or the buttons are not there
if status_actions.is_visible():
close_temp_btn = status_actions.get_by_role(
"button", name="Close Temporarily"
)
close_temp_btn = status_actions.get_by_role("button", name="Close Temporarily")
expect(close_temp_btn).not_to_be_visible()
def test_unauthenticated_direct_post_returns_403(
self, page: Page, live_server, db
):
def test_unauthenticated_direct_post_returns_403(self, page: Page, live_server, db):
"""Test that direct POST to FSM endpoint returns 403 for unauthenticated user."""
from apps.parks.models import Park
@@ -70,7 +62,7 @@ class TestUnauthenticatedUserPermissions:
# Attempt to POST directly to FSM transition endpoint
response = page.request.post(
f"{live_server.url}/core/fsm/parks/park/{park.pk}/transition/transition_to_closed_temp/",
headers={"HX-Request": "true"}
headers={"HX-Request": "true"},
)
# Should get 403 Forbidden
@@ -84,9 +76,7 @@ class TestUnauthenticatedUserPermissions:
class TestRegularUserPermissions:
"""Tests for regular (non-moderator) user permission guards."""
def test_regular_user_cannot_approve_submission(
self, auth_page: Page, live_server, db
):
def test_regular_user_cannot_approve_submission(self, auth_page: Page, live_server, db):
"""Test that regular users cannot approve submissions."""
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
@@ -114,7 +104,7 @@ class TestRegularUserPermissions:
submission_type="EDIT",
changes={"description": "Test change"},
reason="Permission test",
status="PENDING"
status="PENDING",
)
try:
@@ -126,9 +116,7 @@ class TestRegularUserPermissions:
# If somehow on dashboard, verify no approve button
if "dashboard" in current_url:
submission_row = auth_page.locator(
f'[data-submission-id="{submission.pk}"]'
)
submission_row = auth_page.locator(f'[data-submission-id="{submission.pk}"]')
if submission_row.is_visible():
approve_btn = submission_row.get_by_role("button", name="Approve")
expect(approve_btn).not_to_be_visible()
@@ -136,7 +124,7 @@ class TestRegularUserPermissions:
# Try direct POST - should be denied
response = auth_page.request.post(
f"{live_server.url}/core/fsm/moderation/editsubmission/{submission.pk}/transition/transition_to_approved/",
headers={"HX-Request": "true"}
headers={"HX-Request": "true"},
)
# Should be denied (403 or 302 redirect)
@@ -149,9 +137,7 @@ class TestRegularUserPermissions:
finally:
submission.delete()
def test_regular_user_cannot_change_park_status(
self, auth_page: Page, live_server, db
):
def test_regular_user_cannot_change_park_status(self, auth_page: Page, live_server, db):
"""Test that regular users cannot change park status."""
from apps.parks.models import Park
@@ -163,18 +149,16 @@ class TestRegularUserPermissions:
auth_page.wait_for_load_state("networkidle")
# Status action buttons should NOT be visible to regular user
status_actions = auth_page.locator('[data-park-status-actions]')
status_actions = auth_page.locator("[data-park-status-actions]")
if status_actions.is_visible():
close_temp_btn = status_actions.get_by_role(
"button", name="Close Temporarily"
)
close_temp_btn = status_actions.get_by_role("button", name="Close Temporarily")
expect(close_temp_btn).not_to_be_visible()
# Try direct POST - should be denied
response = auth_page.request.post(
f"{live_server.url}/core/fsm/parks/park/{park.pk}/transition/transition_to_closed_temp/",
headers={"HX-Request": "true"}
headers={"HX-Request": "true"},
)
# Should be denied
@@ -184,9 +168,7 @@ class TestRegularUserPermissions:
park.refresh_from_db()
assert park.status == "OPERATING"
def test_regular_user_cannot_change_ride_status(
self, auth_page: Page, live_server, db
):
def test_regular_user_cannot_change_ride_status(self, auth_page: Page, live_server, db):
"""Test that regular users cannot change ride status."""
from apps.rides.models import Ride
@@ -194,24 +176,20 @@ class TestRegularUserPermissions:
if not ride:
pytest.skip("No operating ride available")
auth_page.goto(
f"{live_server.url}/parks/{ride.park.slug}/rides/{ride.slug}/"
)
auth_page.goto(f"{live_server.url}/parks/{ride.park.slug}/rides/{ride.slug}/")
auth_page.wait_for_load_state("networkidle")
# Status action buttons should NOT be visible to regular user
status_actions = auth_page.locator('[data-ride-status-actions]')
status_actions = auth_page.locator("[data-ride-status-actions]")
if status_actions.is_visible():
close_temp_btn = status_actions.get_by_role(
"button", name="Close Temporarily"
)
close_temp_btn = status_actions.get_by_role("button", name="Close Temporarily")
expect(close_temp_btn).not_to_be_visible()
# Try direct POST - should be denied
response = auth_page.request.post(
f"{live_server.url}/core/fsm/rides/ride/{ride.pk}/transition/transition_to_closed_temp/",
headers={"HX-Request": "true"}
headers={"HX-Request": "true"},
)
# Should be denied
@@ -225,9 +203,7 @@ class TestRegularUserPermissions:
class TestModeratorPermissions:
"""Tests for moderator-specific permission guards."""
def test_moderator_can_approve_submission(
self, mod_page: Page, live_server, db
):
def test_moderator_can_approve_submission(self, mod_page: Page, live_server, db):
"""Test that moderators CAN see and use approve button."""
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
@@ -240,11 +216,7 @@ class TestModeratorPermissions:
# Create a pending submission
user = User.objects.filter(username="testuser").first()
if not user:
user = User.objects.create_user(
username="testuser",
email="testuser@example.com",
password="testpass123"
)
user = User.objects.create_user(username="testuser", email="testuser@example.com", password="testpass123")
park = Park.objects.first()
if not park:
@@ -259,7 +231,7 @@ class TestModeratorPermissions:
submission_type="EDIT",
changes={"description": "Test change for moderator"},
reason="Moderator permission test",
status="PENDING"
status="PENDING",
)
try:
@@ -267,9 +239,7 @@ class TestModeratorPermissions:
mod_page.wait_for_load_state("networkidle")
# Moderator should be able to see the submission
submission_row = mod_page.locator(
f'[data-submission-id="{submission.pk}"]'
)
submission_row = mod_page.locator(f'[data-submission-id="{submission.pk}"]')
if submission_row.is_visible():
# Should see approve button
@@ -279,9 +249,7 @@ class TestModeratorPermissions:
finally:
submission.delete()
def test_moderator_can_change_park_status(
self, mod_page: Page, live_server, db
):
def test_moderator_can_change_park_status(self, mod_page: Page, live_server, db):
"""Test that moderators CAN see and use park status change buttons."""
from apps.parks.models import Park
@@ -293,18 +261,14 @@ class TestModeratorPermissions:
mod_page.wait_for_load_state("networkidle")
# Status action buttons SHOULD be visible to moderator
status_actions = mod_page.locator('[data-park-status-actions]')
status_actions = mod_page.locator("[data-park-status-actions]")
if status_actions.is_visible():
# Should see close temporarily button
close_temp_btn = status_actions.get_by_role(
"button", name="Close Temporarily"
)
close_temp_btn = status_actions.get_by_role("button", name="Close Temporarily")
expect(close_temp_btn).to_be_visible()
def test_moderator_cannot_access_admin_only_transitions(
self, mod_page: Page, live_server, db
):
def test_moderator_cannot_access_admin_only_transitions(self, mod_page: Page, live_server, db):
"""Test that moderators CANNOT access admin-only transitions."""
# This test verifies that certain transitions require admin privileges
# Specific transitions depend on the FSM configuration
@@ -327,22 +291,18 @@ class TestModeratorPermissions:
# Check for admin-only buttons (if any are configured)
# The specific buttons that should be hidden depend on the FSM configuration
status_actions = mod_page.locator('[data-park-status-actions]')
status_actions = mod_page.locator("[data-park-status-actions]")
# If there are admin-only transitions, verify they're hidden
# This is a placeholder - actual admin-only transitions depend on configuration
admin_only_btn = status_actions.get_by_role(
"button", name="Force Delete" # Example admin-only action
)
admin_only_btn = status_actions.get_by_role("button", name="Force Delete") # Example admin-only action
expect(admin_only_btn).not_to_be_visible()
class TestPermissionDeniedErrorHandling:
"""Tests for error handling when permission is denied."""
def test_permission_denied_shows_error_toast(
self, auth_page: Page, live_server, db
):
def test_permission_denied_shows_error_toast(self, auth_page: Page, live_server, db):
"""Test that permission denied errors show appropriate toast."""
from apps.parks.models import Park
@@ -355,9 +315,12 @@ class TestPermissionDeniedErrorHandling:
auth_page.wait_for_load_state("networkidle")
# Make the request programmatically with HTMX header
response = auth_page.evaluate("""
response = auth_page.evaluate(
"""
async () => {
const response = await fetch('/core/fsm/parks/park/""" + str(park.pk) + """/transition/transition_to_closed_temp/', {
const response = await fetch('/core/fsm/parks/park/"""
+ str(park.pk)
+ """/transition/transition_to_closed_temp/', {
method: 'POST',
headers: {
'HX-Request': 'true',
@@ -370,18 +333,17 @@ class TestPermissionDeniedErrorHandling:
hxTrigger: response.headers.get('HX-Trigger')
};
}
""")
"""
)
# Check if error toast was triggered
if response and response.get('status') in [400, 403]:
hx_trigger = response.get('hxTrigger')
if response and response.get("status") in [400, 403]:
hx_trigger = response.get("hxTrigger")
if hx_trigger:
assert 'showToast' in hx_trigger
assert 'error' in hx_trigger.lower() or 'denied' in hx_trigger.lower()
assert "showToast" in hx_trigger
assert "error" in hx_trigger.lower() or "denied" in hx_trigger.lower()
def test_database_state_unchanged_on_permission_denied(
self, auth_page: Page, live_server, db
):
def test_database_state_unchanged_on_permission_denied(self, auth_page: Page, live_server, db):
"""Test that database state is unchanged when permission is denied."""
from apps.parks.models import Park
@@ -395,9 +357,12 @@ class TestPermissionDeniedErrorHandling:
auth_page.goto(f"{live_server.url}/parks/{park.slug}/")
auth_page.wait_for_load_state("networkidle")
auth_page.evaluate("""
auth_page.evaluate(
"""
async () => {
await fetch('/core/fsm/parks/park/""" + str(park.pk) + """/transition/transition_to_closed_temp/', {
await fetch('/core/fsm/parks/park/"""
+ str(park.pk)
+ """/transition/transition_to_closed_temp/', {
method: 'POST',
headers: {
'HX-Request': 'true',
@@ -406,7 +371,8 @@ class TestPermissionDeniedErrorHandling:
credentials: 'include'
});
}
""")
"""
)
# Verify database state did NOT change
park.refresh_from_db()
@@ -416,9 +382,7 @@ class TestPermissionDeniedErrorHandling:
class TestTransitionButtonVisibility:
"""Tests for correct transition button visibility based on permissions and state."""
def test_transition_button_hidden_when_state_invalid(
self, mod_page: Page, live_server, db
):
def test_transition_button_hidden_when_state_invalid(self, mod_page: Page, live_server, db):
"""Test that transition buttons are hidden when the current state is invalid."""
from apps.parks.models import Park
@@ -430,7 +394,7 @@ class TestTransitionButtonVisibility:
mod_page.goto(f"{live_server.url}/parks/{park.slug}/")
mod_page.wait_for_load_state("networkidle")
status_actions = mod_page.locator('[data-park-status-actions]')
status_actions = mod_page.locator("[data-park-status-actions]")
# Reopen button should NOT be visible for operating park
# (can't reopen something that's already operating)
@@ -442,9 +406,7 @@ class TestTransitionButtonVisibility:
demolish_btn = status_actions.get_by_role("button", name="Mark as Demolished")
expect(demolish_btn).not_to_be_visible()
def test_correct_buttons_shown_for_closed_temp_state(
self, mod_page: Page, live_server, db
):
def test_correct_buttons_shown_for_closed_temp_state(self, mod_page: Page, live_server, db):
"""Test that correct buttons are shown for temporarily closed state."""
from apps.parks.models import Park
@@ -461,21 +423,17 @@ class TestTransitionButtonVisibility:
mod_page.goto(f"{live_server.url}/parks/{park.slug}/")
mod_page.wait_for_load_state("networkidle")
status_actions = mod_page.locator('[data-park-status-actions]')
status_actions = mod_page.locator("[data-park-status-actions]")
# Reopen button SHOULD be visible
reopen_btn = status_actions.get_by_role("button", name="Reopen")
expect(reopen_btn).to_be_visible()
# Close Temporarily should NOT be visible (already closed)
close_temp_btn = status_actions.get_by_role(
"button", name="Close Temporarily"
)
close_temp_btn = status_actions.get_by_role("button", name="Close Temporarily")
expect(close_temp_btn).not_to_be_visible()
def test_correct_buttons_shown_for_closed_perm_state(
self, mod_page: Page, live_server, db
):
def test_correct_buttons_shown_for_closed_perm_state(self, mod_page: Page, live_server, db):
"""Test that correct buttons are shown for permanently closed state."""
from apps.parks.models import Park
@@ -492,7 +450,7 @@ class TestTransitionButtonVisibility:
mod_page.goto(f"{live_server.url}/parks/{park.slug}/")
mod_page.wait_for_load_state("networkidle")
status_actions = mod_page.locator('[data-park-status-actions]')
status_actions = mod_page.locator("[data-park-status-actions]")
# Demolish/Relocate buttons SHOULD be visible
demolish_btn = status_actions.get_by_role("button", name="Mark as Demolished")