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

@@ -21,18 +21,18 @@ dependencies are not installed or if running in CI without browser support.
import os
import unittest
from django.test import TestCase, LiveServerTestCase, override_settings
from django.urls import reverse
from django.contrib.auth import get_user_model
from django.test import LiveServerTestCase, TestCase, override_settings
from django.urls import reverse
# Check if selenium and axe are available
try:
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
HAS_SELENIUM = True
except ImportError:
HAS_SELENIUM = False
@@ -158,7 +158,7 @@ class WCAGComplianceTests(AccessibilityTestMixin, LiveServerTestCase):
cls.driver = webdriver.Chrome(options=chrome_options)
cls.driver.implicitly_wait(10)
except Exception as e:
raise unittest.SkipTest(f"Chrome WebDriver not available: {e}")
raise unittest.SkipTest(f"Chrome WebDriver not available: {e}") from None
@classmethod
def tearDownClass(cls):

View File

@@ -16,17 +16,11 @@ This module provides extensive test coverage for:
Test patterns follow Django styleguide conventions.
"""
import pytest
from unittest.mock import patch, MagicMock
from django.test import TestCase
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase, APIClient
from rest_framework.test import APIClient
from tests.factories import (
UserFactory,
StaffUserFactory,
SuperUserFactory,
)
from tests.test_utils import EnhancedAPITestCase

View File

@@ -6,8 +6,8 @@ with proper error codes, messages, and details.
"""
from django.test import TestCase
from rest_framework.test import APIClient
from rest_framework import status
from rest_framework.test import APIClient
class ErrorResponseFormatTestCase(TestCase):

View File

@@ -6,8 +6,8 @@ similar endpoints and behave as expected.
"""
from django.test import TestCase
from rest_framework.test import APIClient
from rest_framework import status
from rest_framework.test import APIClient
class FilterParameterNamingTestCase(TestCase):
@@ -137,7 +137,7 @@ class FilterMetadataTestCase(TestCase):
if response.status_code == status.HTTP_200_OK:
data = response.json()
if data.get("data") and data["data"].get("categorical"):
for field, options in data["data"]["categorical"].items():
for _field, options in data["data"]["categorical"].items():
if isinstance(options, list) and options:
option = options[0]
# Each option should have value and label

View File

@@ -6,8 +6,8 @@ metadata including count, next, previous, page_size, current_page, and total_pag
"""
from django.test import TestCase
from rest_framework.test import APIClient
from rest_framework import status
from rest_framework.test import APIClient
class PaginationMetadataTestCase(TestCase):

View File

@@ -13,20 +13,18 @@ Test patterns follow Django styleguide conventions with:
- Permission and authorization testing
"""
import pytest
from unittest.mock import patch, MagicMock
from django.test import TestCase
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase, APIClient
from unittest.mock import patch
from apps.parks.models import Park, ParkPhoto
from rest_framework import status
from rest_framework.test import APIClient
from apps.parks.models import Park
from tests.factories import (
UserFactory,
CompanyFactory,
ParkFactory,
StaffUserFactory,
SuperUserFactory,
ParkFactory,
CompanyFactory,
UserFactory,
)
from tests.test_utils import EnhancedAPITestCase
@@ -471,7 +469,7 @@ class TestParkAPIQueryOptimization(EnhancedAPITestCase):
def test__park_list__uses_select_related(self):
"""Test that park list uses select_related for optimization."""
# Create multiple parks
for i in range(5):
for _i in range(5):
ParkFactory(operator=self.operator)
url = '/api/v1/parks/hybrid/'

View File

@@ -5,10 +5,9 @@ These tests verify that all API endpoints return responses in the standardized
format with proper success/error indicators, data nesting, and error codes.
"""
import pytest
from django.test import TestCase
from rest_framework.test import APIClient
from rest_framework import status
from rest_framework.test import APIClient
class ResponseFormatTestCase(TestCase):

View File

@@ -15,24 +15,18 @@ This module provides extensive test coverage for:
Test patterns follow Django styleguide conventions.
"""
import pytest
from unittest.mock import patch, MagicMock
from django.test import TestCase
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase, APIClient
from rest_framework.test import APIClient
from tests.factories import (
UserFactory,
StaffUserFactory,
SuperUserFactory,
CoasterFactory,
DesignerCompanyFactory,
ManufacturerCompanyFactory,
ParkFactory,
RideFactory,
CoasterFactory,
CompanyFactory,
ManufacturerCompanyFactory,
DesignerCompanyFactory,
RideModelFactory,
StaffUserFactory,
UserFactory,
)
from tests.test_utils import EnhancedAPITestCase
@@ -752,7 +746,7 @@ class TestRideAPIQueryOptimization(EnhancedAPITestCase):
def test__ride_list__uses_select_related(self):
"""Test that ride list uses select_related for optimization."""
# Create multiple rides
for i in range(5):
for _i in range(5):
RideFactory(park=self.park)
response = self.client.get('/api/v1/rides/')

View File

@@ -9,7 +9,6 @@ import os
import django
import pytest
from django.conf import settings
# Configure Django settings before any tests run
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.django.test")

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):

View File

@@ -4,11 +4,11 @@ Following Django styleguide pattern for test data creation using factory_boy.
"""
import factory
from factory import fuzzy
from factory.django import DjangoModelFactory
from django.contrib.auth import get_user_model
from django.contrib.gis.geos import Point
from django.utils.text import slugify
from factory import fuzzy
from factory.django import DjangoModelFactory
User = get_user_model()

View File

@@ -4,21 +4,18 @@ Tests for Park forms.
Following Django styleguide pattern: test__<context>__<action>__<expected_outcome>
"""
import pytest
from decimal import Decimal
from unittest.mock import Mock, patch, MagicMock
from django.test import TestCase
from apps.parks.forms import (
ParkAutocomplete,
ParkForm,
ParkSearchForm,
ParkAutocomplete,
)
from tests.factories import (
ParkFactory,
OperatorCompanyFactory,
LocationFactory,
ParkFactory,
)

View File

@@ -4,21 +4,20 @@ Tests for Ride forms.
Following Django styleguide pattern: test__<context>__<action>__<expected_outcome>
"""
import pytest
from unittest.mock import Mock, patch, MagicMock
from django.test import TestCase
from apps.rides.forms import (
RideForm,
RideSearchForm,
)
from tests.factories import (
DesignerCompanyFactory,
ManufacturerCompanyFactory,
ParkAreaFactory,
ParkFactory,
RideFactory,
ParkAreaFactory,
ManufacturerCompanyFactory,
DesignerCompanyFactory,
RideModelFactory,
)

View File

@@ -12,11 +12,11 @@ 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
from django.test import Client, TestCase
from django.urls import reverse
User = get_user_model()
@@ -730,9 +730,10 @@ class TestFSMTransitionViewStateLog(TestCase):
def test_transition_creates_state_log(self):
"""Test that FSM transition creates a StateLog entry."""
from django_fsm_log.models import StateLog
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:

View File

@@ -5,19 +5,16 @@ These tests verify the complete state transition workflows for
Parks and Rides using the FSM implementation.
"""
import pytest
from datetime import date, timedelta
from django.test import TestCase
from django.core.exceptions import ValidationError
from apps.parks.models import Park
from apps.rides.models import Ride
import pytest
from django.test import TestCase
from tests.factories import (
ParkAreaFactory,
ParkFactory,
RideFactory,
UserFactory,
ParkAreaFactory,
)

View File

@@ -6,18 +6,15 @@ validation, location creation, and related operations.
"""
import pytest
from django.test import TestCase, TransactionTestCase
from django.db import transaction
from django.test import TestCase
from apps.parks.models import Park, ParkArea, ParkReview
from apps.parks.forms import ParkForm
from tests.factories import (
ParkFactory,
ParkAreaFactory,
OperatorCompanyFactory,
UserFactory,
ParkAreaFactory,
ParkFactory,
RideFactory,
UserFactory,
)
@@ -61,9 +58,9 @@ class TestParkCreationWorkflow(TestCase):
park = ParkFactory()
# Add areas
area1 = ParkAreaFactory(park=park, name="Main Entrance")
area2 = ParkAreaFactory(park=park, name="Thrill Zone")
area3 = ParkAreaFactory(park=park, name="Kids Area")
ParkAreaFactory(park=park, name="Main Entrance")
ParkAreaFactory(park=park, name="Thrill Zone")
ParkAreaFactory(park=park, name="Kids Area")
# Verify structure
assert park.areas.count() == 3
@@ -156,7 +153,7 @@ class TestParkReviewWorkflow(TestCase):
from tests.factories import ParkReviewFactory
review1 = ParkReviewFactory(park=park, user=user1, rating=10, is_published=True)
ParkReviewFactory(park=park, user=user1, rating=10, is_published=True)
review2 = ParkReviewFactory(park=park, user=user2, rating=2, is_published=True)
# Unpublish the low rating

View File

@@ -5,22 +5,21 @@ These tests verify the complete workflow of photo uploads including
validation, processing, and moderation.
"""
import pytest
from unittest.mock import Mock, patch
from django.test import TestCase
import pytest
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase
from apps.parks.models import ParkPhoto
from apps.rides.models import RidePhoto
from apps.parks.services.media_service import ParkMediaService
from tests.factories import (
ParkFactory,
RideFactory,
ParkPhotoFactory,
RideFactory,
RidePhotoFactory,
UserFactory,
StaffUserFactory,
UserFactory,
)
@@ -215,9 +214,9 @@ class TestRidePhotoWorkflow(TestCase):
"""Test ride photos can have different types."""
ride = RideFactory()
exterior = RidePhotoFactory(ride=ride, photo_type="exterior")
queue = RidePhotoFactory(ride=ride, photo_type="queue")
onride = RidePhotoFactory(ride=ride, photo_type="onride")
RidePhotoFactory(ride=ride, photo_type="exterior")
RidePhotoFactory(ride=ride, photo_type="queue")
RidePhotoFactory(ride=ride, photo_type="onride")
assert ride.photos.filter(photo_type="exterior").count() == 1
assert ride.photos.filter(photo_type="queue").count() == 1

View File

@@ -4,31 +4,13 @@ Tests for Core managers and querysets.
Following Django styleguide pattern: test__<context>__<action>__<expected_outcome>
"""
import pytest
from django.test import TestCase
from django.utils import timezone
from datetime import timedelta
from unittest.mock import Mock, patch
from apps.core.managers import (
BaseQuerySet,
BaseManager,
LocationQuerySet,
LocationManager,
ReviewableQuerySet,
ReviewableManager,
HierarchicalQuerySet,
HierarchicalManager,
TimestampedQuerySet,
TimestampedManager,
StatusQuerySet,
StatusManager,
)
from tests.factories import (
ParkFactory,
ParkReviewFactory,
RideFactory,
UserFactory,
)

View File

@@ -4,32 +4,23 @@ Tests for Park managers and querysets.
Following Django styleguide pattern: test__<context>__<action>__<expected_outcome>
"""
import pytest
from django.test import TestCase
from django.utils import timezone
from datetime import timedelta
from apps.parks.models import Park, ParkArea, ParkReview, Company
from apps.parks.managers import (
ParkQuerySet,
ParkManager,
ParkAreaQuerySet,
ParkAreaManager,
ParkReviewQuerySet,
ParkReviewManager,
CompanyQuerySet,
CompanyManager,
)
from apps.parks.models import Company, Park, ParkArea, ParkReview
from tests.factories import (
ParkFactory,
CoasterFactory,
ManufacturerCompanyFactory,
OperatorCompanyFactory,
ParkAreaFactory,
ParkFactory,
ParkReviewFactory,
RideFactory,
CoasterFactory,
UserFactory,
OperatorCompanyFactory,
ManufacturerCompanyFactory,
)
@@ -290,7 +281,7 @@ class TestParkReviewQuerySet(TestCase):
"""Test moderation_required filters reviews needing moderation."""
user1 = UserFactory()
user2 = UserFactory()
published = ParkReviewFactory(is_published=True, user=user1)
ParkReviewFactory(is_published=True, user=user1)
unpublished = ParkReviewFactory(is_published=False, user=user2)
result = ParkReview.objects.moderation_required()

View File

@@ -6,29 +6,22 @@ Following Django styleguide pattern: test__<context>__<action>__<expected_outcom
import pytest
from django.test import TestCase
from django.utils import timezone
from apps.rides.models import Ride, RideModel, RideReview
from apps.rides.managers import (
RideQuerySet,
RideManager,
RideModelQuerySet,
RideModelManager,
RideQuerySet,
RideReviewQuerySet,
RideReviewManager,
RollerCoasterStatsQuerySet,
RollerCoasterStatsManager,
)
from apps.rides.models import Ride, RideModel, RideReview
from tests.factories import (
RideFactory,
CoasterFactory,
DesignerCompanyFactory,
ManufacturerCompanyFactory,
ParkFactory,
RideFactory,
RideModelFactory,
RideReviewFactory,
UserFactory,
ManufacturerCompanyFactory,
DesignerCompanyFactory,
)

View File

@@ -4,11 +4,10 @@ Tests for ContractValidationMiddleware.
Following Django styleguide pattern: test__<context>__<action>__<expected_outcome>
"""
import pytest
import json
from unittest.mock import Mock, patch, MagicMock
from django.test import TestCase, RequestFactory, override_settings
from django.http import JsonResponse, HttpResponse
from unittest.mock import Mock, patch
from django.http import HttpResponse, JsonResponse
from django.test import RequestFactory, TestCase, override_settings
from apps.api.v1.middleware import (
ContractValidationMiddleware,

View File

@@ -4,29 +4,26 @@ Tests for Account serializers.
Following Django styleguide pattern: test__<context>__<action>__<expected_outcome>
"""
from unittest.mock import Mock, patch
import pytest
from unittest.mock import Mock, patch, MagicMock
from django.test import TestCase, RequestFactory
from django.test import RequestFactory, TestCase
from apps.accounts.serializers import (
UserSerializer,
LoginSerializer,
SignupSerializer,
PasswordResetSerializer,
PasswordChangeSerializer,
PasswordResetSerializer,
SignupSerializer,
SocialProviderSerializer,
UserSerializer,
)
from apps.api.v1.accounts.serializers import (
UserProfileCreateInputSerializer,
UserProfileUpdateInputSerializer,
UserProfileOutputSerializer,
UserProfileUpdateInputSerializer,
)
from tests.factories import (
UserFactory,
StaffUserFactory,
)
@@ -169,7 +166,7 @@ class TestSignupSerializer(TestCase):
def test__validate_email__duplicate_email__returns_error(self):
"""Test validation fails with duplicate email."""
existing_user = UserFactory(email="existing@example.com")
UserFactory(email="existing@example.com")
data = {
"username": "newuser",
"email": "existing@example.com",
@@ -185,7 +182,7 @@ class TestSignupSerializer(TestCase):
def test__validate_email__case_insensitive__returns_error(self):
"""Test email validation is case insensitive."""
existing_user = UserFactory(email="existing@example.com")
UserFactory(email="existing@example.com")
data = {
"username": "newuser",
"email": "EXISTING@EXAMPLE.COM",
@@ -201,7 +198,7 @@ class TestSignupSerializer(TestCase):
def test__validate_username__duplicate_username__returns_error(self):
"""Test validation fails with duplicate username."""
existing_user = UserFactory(username="existinguser")
UserFactory(username="existinguser")
data = {
"username": "existinguser",
"email": "new@example.com",
@@ -262,7 +259,7 @@ class TestPasswordResetSerializer(TestCase):
def test__validate__valid_email__returns_normalized_email(self):
"""Test validation normalizes email."""
user = UserFactory(email="test@example.com")
UserFactory(email="test@example.com")
data = {"email": " TEST@EXAMPLE.COM "}
serializer = PasswordResetSerializer(data=data)
@@ -302,7 +299,7 @@ class TestPasswordResetSerializer(TestCase):
@patch("apps.accounts.serializers.EmailService.send_email")
def test__save__existing_user__sends_email(self, mock_send_email):
"""Test save sends email for existing user."""
user = UserFactory(email="reset@example.com")
UserFactory(email="reset@example.com")
data = {"email": "reset@example.com"}
factory = RequestFactory()

View File

@@ -4,27 +4,27 @@ Tests for Park serializers.
Following Django styleguide pattern: test__<context>__<action>__<expected_outcome>
"""
from unittest.mock import Mock
import pytest
from unittest.mock import Mock, MagicMock
from django.test import TestCase
from apps.api.v1.parks.serializers import (
ParkPhotoOutputSerializer,
ParkPhotoCreateInputSerializer,
ParkPhotoUpdateInputSerializer,
ParkPhotoListOutputSerializer,
ParkPhotoApprovalInputSerializer,
ParkPhotoStatsOutputSerializer,
ParkPhotoSerializer,
HybridParkSerializer,
ParkPhotoApprovalInputSerializer,
ParkPhotoCreateInputSerializer,
ParkPhotoListOutputSerializer,
ParkPhotoOutputSerializer,
ParkPhotoSerializer,
ParkPhotoStatsOutputSerializer,
ParkPhotoUpdateInputSerializer,
ParkSerializer,
)
from tests.factories import (
CloudflareImageFactory,
ParkFactory,
ParkPhotoFactory,
UserFactory,
CloudflareImageFactory,
)

View File

@@ -4,31 +4,30 @@ Tests for Ride serializers.
Following Django styleguide pattern: test__<context>__<action>__<expected_outcome>
"""
from unittest.mock import Mock
import pytest
from unittest.mock import Mock, MagicMock
from django.test import TestCase
from apps.api.v1.rides.serializers import (
RidePhotoOutputSerializer,
RidePhotoCreateInputSerializer,
RidePhotoUpdateInputSerializer,
RidePhotoListOutputSerializer,
HybridRideSerializer,
RidePhotoApprovalInputSerializer,
RidePhotoCreateInputSerializer,
RidePhotoListOutputSerializer,
RidePhotoOutputSerializer,
RidePhotoSerializer,
RidePhotoStatsOutputSerializer,
RidePhotoTypeFilterSerializer,
RidePhotoSerializer,
HybridRideSerializer,
RidePhotoUpdateInputSerializer,
RideSerializer,
)
from tests.factories import (
CloudflareImageFactory,
DesignerCompanyFactory,
ManufacturerCompanyFactory,
RideFactory,
RidePhotoFactory,
ParkFactory,
UserFactory,
CloudflareImageFactory,
ManufacturerCompanyFactory,
DesignerCompanyFactory,
)

View File

@@ -4,20 +4,19 @@ Tests for ParkMediaService.
Following Django styleguide pattern: test__<context>__<action>__<expected_outcome>
"""
from unittest.mock import Mock, patch
import pytest
from unittest.mock import Mock, patch, MagicMock
from django.test import TestCase
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase
from apps.parks.services.media_service import ParkMediaService
from apps.parks.models import ParkPhoto
from apps.parks.services.media_service import ParkMediaService
from tests.factories import (
ParkFactory,
ParkPhotoFactory,
UserFactory,
StaffUserFactory,
CloudflareImageFactory,
UserFactory,
)
@@ -114,7 +113,7 @@ class TestParkMediaServiceGetParkPhotos(TestCase):
def test__get_park_photos__primary_first__orders_primary_first(self):
"""Test get_park_photos with primary_first orders primary photos first."""
park = ParkFactory()
non_primary = ParkPhotoFactory(park=park, is_primary=False, is_approved=True)
ParkPhotoFactory(park=park, is_primary=False, is_approved=True)
primary = ParkPhotoFactory(park=park, is_primary=True, is_approved=True)
result = ParkMediaService.get_park_photos(park, primary_first=True)

View File

@@ -4,22 +4,21 @@ Tests for RideService.
Following Django styleguide pattern: test__<context>__<action>__<expected_outcome>
"""
from unittest.mock import Mock, patch
import pytest
from unittest.mock import Mock, patch, MagicMock
from django.test import TestCase
from django.core.exceptions import ValidationError
from apps.rides.services import RideService
from apps.rides.models import Ride
from apps.rides.services import RideService
from tests.factories import (
DesignerCompanyFactory,
ManufacturerCompanyFactory,
ParkAreaFactory,
ParkFactory,
RideFactory,
RideModelFactory,
ParkAreaFactory,
UserFactory,
ManufacturerCompanyFactory,
DesignerCompanyFactory,
)

View File

@@ -4,22 +4,20 @@ Tests for UserDeletionService and AccountService.
Following Django styleguide pattern: test__<context>__<action>__<expected_outcome>
"""
from unittest.mock import patch
import pytest
from unittest.mock import Mock, patch, MagicMock
from django.test import TestCase, RequestFactory
from django.utils import timezone
from django.test import RequestFactory, TestCase
from apps.accounts.services import UserDeletionService, AccountService
from apps.accounts.models import User
from apps.accounts.services import AccountService, UserDeletionService
from tests.factories import (
UserFactory,
StaffUserFactory,
SuperUserFactory,
ParkReviewFactory,
RideReviewFactory,
ParkFactory,
ParkReviewFactory,
RideFactory,
RideReviewFactory,
SuperUserFactory,
UserFactory,
)
@@ -92,7 +90,7 @@ class TestUserDeletionServiceDeleteUserPreserveSubmissions(TestCase):
user_pk = user.pk
result = UserDeletionService.delete_user_preserve_submissions(user)
UserDeletionService.delete_user_preserve_submissions(user)
# User should be deleted
assert not User.objects.filter(pk=user_pk).exists()
@@ -270,7 +268,7 @@ class TestAccountServiceInitiateEmailChange(TestCase):
def test__initiate_email_change__duplicate_email__returns_error(self):
"""Test initiate_email_change returns error for duplicate email."""
existing_user = UserFactory(email="existing@example.com")
UserFactory(email="existing@example.com")
user = UserFactory()
factory = RequestFactory()

View File

@@ -3,17 +3,17 @@ Test cases demonstrating the factory pattern usage.
Following Django styleguide pattern for test data creation.
"""
from django.test import TestCase
from django.contrib.auth import get_user_model
from django.test import TestCase
from .factories import (
UserFactory,
ParkFactory,
RideFactory,
ParkReviewFactory,
RideReviewFactory,
CompanyFactory,
ParkFactory,
ParkReviewFactory,
RideFactory,
RideReviewFactory,
Traits,
UserFactory,
)
User = get_user_model()

View File

@@ -4,15 +4,15 @@ Comprehensive API endpoint testing with proper naming conventions.
"""
from django.urls import reverse
from rest_framework.test import APITestCase, APIClient
from rest_framework import status
from rest_framework.test import APIClient, APITestCase
from apps.parks.models import Park
from tests.factories import (
UserFactory,
StaffUserFactory,
CompanyFactory,
ParkFactory,
StaffUserFactory,
UserFactory,
)

View File

@@ -4,18 +4,19 @@ Uses proper naming conventions and comprehensive coverage.
"""
from datetime import date, timedelta
from django.test import TestCase
from django.db import IntegrityError
from django.test import TestCase
from django.utils import timezone
from apps.parks.models import Park, Company
from apps.parks.models import Company, Park
from tests.factories import (
UserFactory,
CompanyFactory,
ParkFactory,
ParkAreaFactory,
ParkFactory,
ParkReviewFactory,
TestScenarios,
UserFactory,
)

View File

@@ -1,9 +1,10 @@
#!/usr/bin/env python
import os
import sys
import coverage # type: ignore
import django
from django.test.runner import DiscoverRunner
import coverage # type: ignore
def setup_django():

View File

@@ -3,12 +3,13 @@ Test utilities and helpers following Django styleguide patterns.
Provides reusable testing patterns and assertion helpers.
"""
from typing import Dict, Any, Optional, List
from datetime import date, datetime
from django.test import TestCase
from typing import Any
from django.contrib.auth import get_user_model
from rest_framework.test import APITestCase
from django.test import TestCase
from rest_framework import status
from rest_framework.test import APITestCase
User = get_user_model()
@@ -22,8 +23,8 @@ class ApiTestMixin:
*,
status_code: int = status.HTTP_200_OK,
response_status: str = "success",
data_type: Optional[type] = None,
contains_fields: Optional[List[str]] = None,
data_type: type | None = None,
contains_fields: list[str] | None = None,
):
"""
Assert API response has correct structure and content.
@@ -68,8 +69,8 @@ class ApiTestMixin:
response,
*,
status_code: int,
error_code: Optional[str] = None,
message_contains: Optional[str] = None,
error_code: str | None = None,
message_contains: str | None = None,
):
"""
Assert API response is an error with specific characteristics.
@@ -94,9 +95,9 @@ class ApiTestMixin:
self,
response,
*,
expected_count: Optional[int] = None,
has_next: Optional[bool] = None,
has_previous: Optional[bool] = None,
expected_count: int | None = None,
has_next: bool | None = None,
has_previous: bool | None = None,
):
"""
Assert API response has correct pagination structure.
@@ -136,7 +137,7 @@ class ApiTestMixin:
class ModelTestMixin:
"""Mixin providing common model testing utilities."""
def assertModelFields(self, instance, expected_fields: Dict[str, Any]):
def assertModelFields(self, instance, expected_fields: dict[str, Any]):
"""
Assert model instance has expected field values.
@@ -155,8 +156,8 @@ class ModelTestMixin:
def assertModelValidation(
self,
model_class,
invalid_data: Dict[str, Any],
expected_errors: List[str],
invalid_data: dict[str, Any],
expected_errors: list[str],
):
"""
Assert model validation catches expected errors.
@@ -175,7 +176,7 @@ class ModelTestMixin:
for expected_error in expected_errors:
self.assertIn(expected_error, exception_str)
def assertDatabaseConstraint(self, model_factory, invalid_data: Dict[str, Any]):
def assertDatabaseConstraint(self, model_factory, invalid_data: dict[str, Any]):
"""
Assert database constraint is enforced.
@@ -299,7 +300,7 @@ class GeographyTestMixin:
point2: (latitude, longitude) tuple
max_distance_km: Maximum allowed distance in kilometers
"""
from math import radians, cos, sin, asin, sqrt
from math import asin, cos, radians, sin, sqrt
lat1, lon1 = point1
lat2, lon2 = point2

View File

@@ -6,14 +6,14 @@ for testing FSM transitions, HTMX interactions, and other common scenarios.
"""
from .fsm_test_helpers import (
create_test_submission,
assert_state_log_created,
assert_status_changed,
assert_toast_triggered,
create_test_park,
create_test_ride,
assert_status_changed,
assert_state_log_created,
assert_toast_triggered,
wait_for_htmx_swap,
create_test_submission,
verify_transition_buttons_visible,
wait_for_htmx_swap,
)
__all__ = [

View File

@@ -9,10 +9,11 @@ Reusable utility functions for testing FSM transitions:
"""
import json
from typing import Any, Dict, List, Optional, Type
from django.db.models import Model
from typing import Any
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.db.models import Model
from django.http import HttpResponse
User = get_user_model()
@@ -25,10 +26,10 @@ User = get_user_model()
def create_test_submission(
status: str = "PENDING",
user: Optional[User] = None,
park: Optional[Model] = None,
user: User | None = None,
park: Model | None = None,
submission_type: str = "EDIT",
changes: Optional[Dict[str, Any]] = None,
changes: dict[str, Any] | None = None,
reason: str = "Test submission",
**kwargs
) -> "EditSubmission":
@@ -87,8 +88,8 @@ def create_test_submission(
def create_test_park(
status: str = "OPERATING",
name: Optional[str] = None,
slug: Optional[str] = None,
name: str | None = None,
slug: str | None = None,
**kwargs
) -> "Park":
"""
@@ -125,9 +126,9 @@ def create_test_park(
def create_test_ride(
status: str = "OPERATING",
name: Optional[str] = None,
slug: Optional[str] = None,
park: Optional[Model] = None,
name: str | None = None,
slug: str | None = None,
park: Model | None = None,
**kwargs
) -> "Ride":
"""
@@ -170,8 +171,8 @@ def create_test_ride(
def create_test_photo_submission(
status: str = "PENDING",
user: Optional[User] = None,
park: Optional[Model] = None,
user: User | None = None,
park: Model | None = None,
**kwargs
) -> "PhotoSubmission":
"""
@@ -254,8 +255,8 @@ def assert_status_changed(obj: Model, expected_status: str) -> None:
def assert_state_log_created(
obj: Model,
transition_name: str,
user: Optional[User] = None,
expected_state: Optional[str] = None
user: User | None = None,
expected_state: str | None = None
) -> None:
"""
Assert that a StateLog entry was created for a transition.
@@ -295,7 +296,7 @@ def assert_state_log_created(
def assert_toast_triggered(
response: HttpResponse,
message: Optional[str] = None,
message: str | None = None,
toast_type: str = "success"
) -> None:
"""
@@ -375,9 +376,9 @@ def wait_for_htmx_swap(
def verify_transition_buttons_visible(
page,
transitions: List[str],
transitions: list[str],
container_selector: str = "[data-status-actions]"
) -> Dict[str, bool]:
) -> dict[str, bool]:
"""
Verify which transition buttons are visible on the page.
@@ -487,7 +488,7 @@ def click_and_confirm(page, button_locator, accept: bool = True) -> None:
# =============================================================================
def make_htmx_post(client, url: str, data: Optional[Dict] = None) -> HttpResponse:
def make_htmx_post(client, url: str, data: dict | None = None) -> HttpResponse:
"""
Make a POST request with HTMX headers.
@@ -529,7 +530,7 @@ def get_fsm_transition_url(
pk: int,
transition_name: str,
use_slug: bool = False,
slug: Optional[str] = None
slug: str | None = None
) -> str:
"""
Generate the URL for an FSM transition.

View File

@@ -5,9 +5,7 @@ These tests verify that the breadcrumb system generates
correct navigation structures and Schema.org markup.
"""
import pytest
from django.test import RequestFactory
from django.urls import reverse
from apps.core.utils.breadcrumbs import (
Breadcrumb,

View File

@@ -7,7 +7,6 @@ with various parameter combinations.
import pytest
from django.template import Context, Template
from django.test import RequestFactory, override_settings
@pytest.mark.django_db

View File

@@ -8,7 +8,6 @@ correct responses with proper headers and content.
import json
import pytest
from django.http import HttpRequest
from django.test import RequestFactory
from apps.core.htmx_utils import (

View File

@@ -5,7 +5,6 @@ These tests verify that message helper functions generate
consistent, user-friendly messages.
"""
import pytest
from apps.core.utils.messages import (
confirm_delete,

View File

@@ -5,7 +5,6 @@ These tests verify that meta tag helpers generate
correct SEO and social sharing metadata.
"""
import pytest
from django.test import RequestFactory
from apps.core.utils.meta import (