feat: Implement MFA authentication, add ride statistics model, and update various services, APIs, and tests across the application.

This commit is contained in:
pacnpal
2025-12-28 17:32:53 -05:00
parent aa56c46c27
commit c95f99ca10
452 changed files with 7948 additions and 6073 deletions

View File

@@ -1,6 +1,8 @@
import pytest
import contextlib
import tempfile
from pathlib import Path
import pytest
from playwright.sync_api import Page
@@ -212,9 +214,10 @@ def admin_page(page: Page, live_server, setup_test_data):
def submission_pending(db):
"""Create a pending EditSubmission for FSM testing."""
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from apps.moderation.models import EditSubmission
from apps.parks.models import Park
from django.contrib.contenttypes.models import ContentType
User = get_user_model()
@@ -246,19 +249,18 @@ def submission_pending(db):
yield submission
# Cleanup
try:
with contextlib.suppress(Exception):
submission.delete()
except Exception:
pass
@pytest.fixture
def submission_approved(db):
"""Create an approved EditSubmission for FSM testing."""
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from apps.moderation.models import EditSubmission
from apps.parks.models import Park
from django.contrib.contenttypes.models import ContentType
User = get_user_model()
@@ -285,10 +287,8 @@ def submission_approved(db):
yield submission
try:
with contextlib.suppress(Exception):
submission.delete()
except Exception:
pass
@pytest.fixture
@@ -322,9 +322,10 @@ def park_closed_temp(db):
@pytest.fixture
def park_closed_perm(db):
"""Create a permanently closed Park for FSM testing."""
from tests.factories import ParkFactory
from datetime import date, timedelta
from tests.factories import ParkFactory
park = ParkFactory(
name="FSM Test Park Closed Perm",
slug="fsm-test-park-closed-perm",
@@ -368,9 +369,10 @@ def ride_sbno(db, park_operating):
@pytest.fixture
def ride_closed_perm(db, park_operating):
"""Create a permanently closed Ride for FSM testing."""
from tests.factories import RideFactory
from datetime import date, timedelta
from tests.factories import RideFactory
ride = RideFactory(
name="FSM Test Ride Closed Perm",
slug="fsm-test-ride-closed-perm",
@@ -386,6 +388,7 @@ def ride_closed_perm(db, park_operating):
def queue_item_pending(db):
"""Create a pending ModerationQueue item for FSM testing."""
from django.contrib.auth import get_user_model
from apps.moderation.models import ModerationQueue
User = get_user_model()
@@ -406,16 +409,15 @@ def queue_item_pending(db):
yield queue_item
try:
with contextlib.suppress(Exception):
queue_item.delete()
except Exception:
pass
@pytest.fixture
def bulk_operation_pending(db):
"""Create a pending BulkOperation for FSM testing."""
from django.contrib.auth import get_user_model
from apps.moderation.models import BulkOperation
User = get_user_model()
@@ -437,10 +439,8 @@ def bulk_operation_pending(db):
yield operation
try:
with contextlib.suppress(Exception):
operation.delete()
except Exception:
pass
# =============================================================================

View File

@@ -1,4 +1,4 @@
from playwright.sync_api import expect, Page
from playwright.sync_api import Page, expect
def test_login_page(page: Page):

View File

@@ -17,6 +17,8 @@ These tests verify:
- User-friendly error messages are displayed
"""
import re
import pytest
from playwright.sync_api import Page, expect
@@ -73,9 +75,10 @@ class TestInvalidTransitionErrors:
):
"""Test that trying to approve an already-approved submission shows error."""
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from apps.moderation.models import EditSubmission
from apps.parks.models import Park
from django.contrib.contenttypes.models import ContentType
User = get_user_model()
@@ -351,9 +354,10 @@ class TestConfirmationDialogs:
):
"""Test that confirmation dialog appears for reject transition."""
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from apps.moderation.models import EditSubmission
from apps.parks.models import Park
from django.contrib.contenttypes.models import ContentType
User = get_user_model()
@@ -414,9 +418,10 @@ class TestConfirmationDialogs:
):
"""Test that canceling the confirmation dialog prevents the transition."""
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from apps.moderation.models import EditSubmission
from apps.parks.models import Park
from django.contrib.contenttypes.models import ContentType
User = get_user_model()
@@ -472,9 +477,10 @@ class TestConfirmationDialogs:
):
"""Test that accepting the confirmation dialog executes the transition."""
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from apps.moderation.models import EditSubmission
from apps.parks.models import Park
from django.contrib.contenttypes.models import ContentType
User = get_user_model()
@@ -629,7 +635,7 @@ class TestToastNotificationBehavior:
expect(toast).to_be_visible(timeout=5000)
# Should have error/danger styling (red)
expect(toast).to_have_class(/error|danger|bg-red|text-red/)
expect(toast).to_have_class(re.compile(r"error|danger|bg-red|text-red"))
def test_success_toast_has_correct_styling(
self, mod_page: Page, live_server, db
@@ -665,4 +671,4 @@ class TestToastNotificationBehavior:
expect(toast).to_be_visible(timeout=5000)
# Should have success styling (green)
expect(toast).to_have_class(/success|bg-green|text-green/)
expect(toast).to_have_class(re.compile(r"success|bg-green|text-green"))

View File

@@ -89,9 +89,10 @@ class TestRegularUserPermissions:
):
"""Test that regular users cannot approve submissions."""
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from apps.moderation.models import EditSubmission
from apps.parks.models import Park
from django.contrib.contenttypes.models import ContentType
User = get_user_model()
@@ -229,9 +230,10 @@ class TestModeratorPermissions:
):
"""Test that moderators CAN see and use approve button."""
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from apps.moderation.models import EditSubmission
from apps.parks.models import Park
from django.contrib.contenttypes.models import ContentType
User = get_user_model()
@@ -501,5 +503,5 @@ class TestTransitionButtonVisibility:
assert visible, "Expected demolish or relocate button for CLOSED_PERM state"
# Reopen should still be visible to restore to operating
reopen_btn = status_actions.get_by_role("button", name="Reopen")
status_actions.get_by_role("button", name="Reopen")
# May or may not be visible depending on FSM configuration

View File

@@ -15,14 +15,15 @@ These tests verify:
"""
import pytest
from playwright.sync_api import Page, expect
from django.contrib.contenttypes.models import ContentType
from playwright.sync_api import Page, expect
@pytest.fixture
def pending_submission(db):
"""Create a pending EditSubmission for testing."""
from django.contrib.auth import get_user_model
from apps.moderation.models import EditSubmission
from apps.parks.models import Park
@@ -63,9 +64,10 @@ def pending_submission(db):
def pending_photo_submission(db):
"""Create a pending PhotoSubmission for testing."""
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from apps.moderation.models import PhotoSubmission
from apps.parks.models import Park
from django.contrib.contenttypes.models import ContentType
User = get_user_model()
@@ -146,7 +148,6 @@ class TestEditSubmissionTransitions:
expect(status_badge).to_contain_text("Approved")
# Verify database state
from apps.moderation.models import EditSubmission
pending_submission.refresh_from_db()
assert pending_submission.status == "APPROVED"
@@ -180,7 +181,6 @@ class TestEditSubmissionTransitions:
expect(status_badge).to_contain_text("Rejected")
# Verify database state
from apps.moderation.models import EditSubmission
pending_submission.refresh_from_db()
assert pending_submission.status == "REJECTED"
@@ -214,7 +214,6 @@ class TestEditSubmissionTransitions:
expect(status_badge).to_contain_text("Escalated")
# Verify database state
from apps.moderation.models import EditSubmission
pending_submission.refresh_from_db()
assert pending_submission.status == "ESCALATED"
@@ -254,7 +253,6 @@ class TestPhotoSubmissionTransitions:
expect(toast).to_contain_text("approved")
# Verify database state
from apps.moderation.models import PhotoSubmission
pending_photo_submission.refresh_from_db()
assert pending_photo_submission.status == "APPROVED"
@@ -290,7 +288,6 @@ class TestPhotoSubmissionTransitions:
expect(toast).to_contain_text("rejected")
# Verify database state
from apps.moderation.models import PhotoSubmission
pending_photo_submission.refresh_from_db()
assert pending_photo_submission.status == "REJECTED"
@@ -302,6 +299,7 @@ class TestModerationQueueTransitions:
def pending_queue_item(self, db):
"""Create a pending ModerationQueue item for testing."""
from django.contrib.auth import get_user_model
from apps.moderation.models import ModerationQueue
User = get_user_model()
@@ -346,7 +344,6 @@ class TestModerationQueueTransitions:
expect(status_badge).to_contain_text("In Progress", timeout=5000)
# Verify database state
from apps.moderation.models import ModerationQueue
pending_queue_item.refresh_from_db()
assert pending_queue_item.status == "IN_PROGRESS"
@@ -376,7 +373,6 @@ class TestModerationQueueTransitions:
toast = mod_page.locator('[data-toast]')
expect(toast).to_be_visible(timeout=5000)
from apps.moderation.models import ModerationQueue
pending_queue_item.refresh_from_db()
assert pending_queue_item.status == "COMPLETED"
@@ -388,6 +384,7 @@ class TestBulkOperationTransitions:
def pending_bulk_operation(self, db):
"""Create a pending BulkOperation for testing."""
from django.contrib.auth import get_user_model
from apps.moderation.models import BulkOperation
User = get_user_model()
@@ -438,7 +435,6 @@ class TestBulkOperationTransitions:
expect(toast).to_contain_text("cancel")
# Verify database state
from apps.moderation.models import BulkOperation
pending_bulk_operation.refresh_from_db()
assert pending_bulk_operation.status == "CANCELLED"
@@ -470,7 +466,7 @@ class TestTransitionLoadingStates:
# Check for htmx-indicator visibility (may be brief)
# The indicator should become visible during the request
loading_indicator = submission_row.locator('.htmx-indicator')
submission_row.locator('.htmx-indicator')
# Wait for transition to complete
toast = mod_page.locator('[data-toast]')

View File

@@ -83,7 +83,7 @@ class TestParkDetailPage:
page.goto(f"{live_server.url}/parks/{park.slug}/")
# Look for rides section/tab
rides_section = page.locator(
page.locator(
"[data-testid='rides-section'], #rides, [role='tabpanel']"
)
@@ -162,7 +162,7 @@ class TestParkNavigation:
# Click parks link in breadcrumb
breadcrumb.get_by_role("link", name="Parks").click()
expect(page).to_have_url(f"**/parks/**")
expect(page).to_have_url("**/parks/**")
def test__back_button__returns_to_previous_page(
self, page: Page, live_server, parks_data
@@ -179,4 +179,4 @@ class TestParkNavigation:
# Go back
page.go_back()
expect(page).to_have_url(f"**/parks/**")
expect(page).to_have_url("**/parks/**")

View File

@@ -13,15 +13,16 @@ These tests verify:
- StateLog entry created in database
"""
import re
from datetime import date, timedelta
import pytest
from playwright.sync_api import Page, expect
from datetime import date, timedelta
@pytest.fixture
def operating_park(db):
"""Create an operating Park for testing status transitions."""
from apps.parks.models import Park
from tests.factories import ParkFactory
# Use factory to create a complete park
@@ -93,7 +94,6 @@ class TestParkStatusTransitions:
expect(status_badge).to_contain_text("Temporarily Closed", timeout=5000)
# Verify database state
from apps.parks.models import Park
operating_park.refresh_from_db()
assert operating_park.status == "CLOSED_TEMP"
@@ -127,7 +127,6 @@ class TestParkStatusTransitions:
expect(status_badge).to_contain_text("Operating", timeout=5000)
# Verify database state
from apps.parks.models import Park
operating_park.refresh_from_db()
assert operating_park.status == "OPERATING"
@@ -165,7 +164,6 @@ class TestParkStatusTransitions:
expect(status_badge).to_contain_text("Permanently Closed", timeout=5000)
# Verify database state
from apps.parks.models import Park
operating_park.refresh_from_db()
assert operating_park.status == "CLOSED_PERM"
@@ -206,7 +204,6 @@ class TestParkStatusTransitions:
expect(status_badge).to_contain_text("Demolished", timeout=5000)
# Verify database state
from apps.parks.models import Park
operating_park.refresh_from_db()
assert operating_park.status == "DEMOLISHED"
@@ -274,7 +271,6 @@ class TestRideStatusTransitions:
expect(status_badge).to_contain_text("Temporarily Closed", timeout=5000)
# Verify database state
from apps.rides.models import Ride
operating_ride.refresh_from_db()
assert operating_ride.status == "CLOSED_TEMP"
@@ -310,7 +306,6 @@ class TestRideStatusTransitions:
expect(status_badge).to_contain_text("SBNO", timeout=5000)
# Verify database state
from apps.rides.models import Ride
operating_ride.refresh_from_db()
assert operating_ride.status == "SBNO"
@@ -344,7 +339,6 @@ class TestRideStatusTransitions:
expect(status_badge).to_contain_text("Operating", timeout=5000)
# Verify database state
from apps.rides.models import Ride
operating_ride.refresh_from_db()
assert operating_ride.status == "OPERATING"
@@ -384,7 +378,6 @@ class TestRideStatusTransitions:
expect(status_badge).to_contain_text("Permanently Closed", timeout=5000)
# Verify database state
from apps.rides.models import Ride
operating_ride.refresh_from_db()
assert operating_ride.status == "CLOSED_PERM"
@@ -429,7 +422,6 @@ class TestRideStatusTransitions:
expect(status_badge).to_contain_text("Demolished", timeout=5000)
# Verify database state
from apps.rides.models import Ride
operating_ride.refresh_from_db()
assert operating_ride.status == "DEMOLISHED"
@@ -474,7 +466,6 @@ class TestRideStatusTransitions:
expect(status_badge).to_contain_text("Relocated", timeout=5000)
# Verify database state
from apps.rides.models import Ride
operating_ride.refresh_from_db()
assert operating_ride.status == "RELOCATED"
@@ -507,7 +498,6 @@ class TestRideClosingWorkflow:
expect(status_badge).to_contain_text("Closing", timeout=5000)
# Verify database state
from apps.rides.models import Ride
operating_ride.refresh_from_db()
assert operating_ride.status == "CLOSING"
else:
@@ -549,7 +539,7 @@ class TestStatusBadgeStyling:
mod_page.wait_for_load_state("networkidle")
status_badge = mod_page.locator('[data-status-badge]')
expect(status_badge).to_have_class(/bg-green|text-green|success/)
expect(status_badge).to_have_class(re.compile(r"bg-green|text-green|success"))
def test_closed_temp_status_badge_style(
self, mod_page: Page, operating_park, live_server
@@ -562,7 +552,7 @@ class TestStatusBadgeStyling:
mod_page.wait_for_load_state("networkidle")
status_badge = mod_page.locator('[data-status-badge]')
expect(status_badge).to_have_class(/bg-yellow|text-yellow|warning/)
expect(status_badge).to_have_class(re.compile(r"bg-yellow|text-yellow|warning"))
def test_closed_perm_status_badge_style(
self, mod_page: Page, operating_park, live_server
@@ -575,7 +565,7 @@ class TestStatusBadgeStyling:
mod_page.wait_for_load_state("networkidle")
status_badge = mod_page.locator('[data-status-badge]')
expect(status_badge).to_have_class(/bg-red|text-red|danger/)
expect(status_badge).to_have_class(re.compile(r"bg-red|text-red|danger"))
def test_demolished_status_badge_style(
self, mod_page: Page, operating_park, live_server
@@ -588,4 +578,4 @@ class TestStatusBadgeStyling:
mod_page.wait_for_load_state("networkidle")
status_badge = mod_page.locator('[data-status-badge]')
expect(status_badge).to_have_class(/bg-gray|text-gray|muted/)
expect(status_badge).to_have_class(re.compile(r"bg-gray|text-gray|muted"))

View File

@@ -1,4 +1,4 @@
from playwright.sync_api import expect, Page
from playwright.sync_api import Page, expect
def test_parks_list_page(page: Page):

View File

@@ -1,4 +1,4 @@
from playwright.sync_api import expect, Page
from playwright.sync_api import Page, expect
def test_profile_page(page: Page):

View File

@@ -168,7 +168,6 @@ class TestReviewEditing:
def test__own_review__shows_edit_button(self, auth_page: Page, live_server, test_review):
"""Test user's own review shows edit button."""
# Navigate to reviews after creating one
park_url = auth_page.url
# Look for edit button on own review
edit_button = auth_page.locator(
@@ -324,7 +323,7 @@ class TestRideReviews:
intensity_field = auth_page.locator(
"select[name='intensity'], input[name='intensity']"
)
wait_time_field = auth_page.locator(
auth_page.locator(
"input[name='wait_time'], select[name='wait_time']"
)

View File

@@ -1,4 +1,4 @@
from playwright.sync_api import expect, Page
from playwright.sync_api import Page, expect
def test_reviews_list_page(page: Page):

View File

@@ -1,4 +1,4 @@
from playwright.sync_api import expect, Page
from playwright.sync_api import Page, expect
def test_rides_list_page(page: Page):