mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-24 15:51:09 -05:00
- Introduced a comprehensive Secret Management Guide detailing best practices, secret classification, development setup, production management, rotation procedures, and emergency protocols. - Implemented a client-side performance monitoring script to track various metrics including page load performance, paint metrics, layout shifts, and memory usage. - Enhanced search accessibility with keyboard navigation support for search results, ensuring compliance with WCAG standards and improving user experience.
535 lines
16 KiB
Python
535 lines
16 KiB
Python
import pytest
|
|
import tempfile
|
|
from pathlib import Path
|
|
from playwright.sync_api import Page
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def setup_test_data(django_db_setup, django_db_blocker):
|
|
"""
|
|
Setup test data before the test session using factories.
|
|
|
|
This fixture:
|
|
- Uses factories instead of shelling out to management commands
|
|
- Is scoped to session (not autouse per test) to reduce overhead
|
|
- Uses django_db_blocker to allow database access in session-scoped fixture
|
|
"""
|
|
with django_db_blocker.unblock():
|
|
from django.contrib.auth import get_user_model
|
|
|
|
User = get_user_model()
|
|
|
|
# Create test users if they don't exist
|
|
test_users = [
|
|
{"username": "testuser", "email": "testuser@example.com", "password": "testpass123"},
|
|
{"username": "moderator", "email": "moderator@example.com", "password": "modpass123", "is_staff": True},
|
|
{"username": "admin", "email": "admin@example.com", "password": "adminpass123", "is_staff": True, "is_superuser": True},
|
|
]
|
|
|
|
for user_data in test_users:
|
|
password = user_data.pop("password")
|
|
user, created = User.objects.get_or_create(
|
|
username=user_data["username"],
|
|
defaults=user_data
|
|
)
|
|
if created:
|
|
user.set_password(password)
|
|
user.save()
|
|
|
|
yield
|
|
|
|
# Cleanup is handled automatically by pytest-django's transactional database
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def setup_page(page: Page):
|
|
"""Configure page for tests"""
|
|
# Set viewport size
|
|
page.set_viewport_size({"width": 1280, "height": 720})
|
|
|
|
# Set default navigation timeout
|
|
page.set_default_timeout(5000)
|
|
|
|
# Listen for console errors
|
|
page.on(
|
|
"console",
|
|
lambda msg: (
|
|
print(f"Browser console {msg.type}: {msg.text}")
|
|
if msg.type == "error"
|
|
else None
|
|
),
|
|
)
|
|
|
|
yield page
|
|
|
|
|
|
@pytest.fixture
|
|
def auth_page(page: Page, live_server, setup_test_data):
|
|
"""Fixture for authenticated page"""
|
|
# Login using live_server URL
|
|
page.goto(f"{live_server.url}/accounts/login/")
|
|
page.get_by_label("Username").fill("testuser")
|
|
page.get_by_label("Password").fill("testpass123")
|
|
page.get_by_role("button", name="Sign In").click()
|
|
|
|
yield page
|
|
|
|
|
|
@pytest.fixture
|
|
def mod_page(page: Page, live_server, setup_test_data):
|
|
"""Fixture for moderator page"""
|
|
# Login as moderator using live_server URL
|
|
page.goto(f"{live_server.url}/accounts/login/")
|
|
page.get_by_label("Username").fill("moderator")
|
|
page.get_by_label("Password").fill("modpass123")
|
|
page.get_by_role("button", name="Sign In").click()
|
|
|
|
yield page
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def test_images():
|
|
"""Generate temporary test images for upload tests.
|
|
|
|
Creates simple valid JPEG images using raw bytes.
|
|
Images are cleaned up after the test session.
|
|
"""
|
|
# Minimal valid JPEG image (1x1 red pixel)
|
|
# This is a valid JPEG that any image library will accept
|
|
jpeg_bytes = bytes([
|
|
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01,
|
|
0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43,
|
|
0x00, 0x08, 0x06, 0x06, 0x07, 0x06, 0x05, 0x08, 0x07, 0x07, 0x07, 0x09,
|
|
0x09, 0x08, 0x0A, 0x0C, 0x14, 0x0D, 0x0C, 0x0B, 0x0B, 0x0C, 0x19, 0x12,
|
|
0x13, 0x0F, 0x14, 0x1D, 0x1A, 0x1F, 0x1E, 0x1D, 0x1A, 0x1C, 0x1C, 0x20,
|
|
0x24, 0x2E, 0x27, 0x20, 0x22, 0x2C, 0x23, 0x1C, 0x1C, 0x28, 0x37, 0x29,
|
|
0x2C, 0x30, 0x31, 0x34, 0x34, 0x34, 0x1F, 0x27, 0x39, 0x3D, 0x38, 0x32,
|
|
0x3C, 0x2E, 0x33, 0x34, 0x32, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x00, 0x01,
|
|
0x00, 0x01, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, 0x00, 0x1F, 0x00, 0x00,
|
|
0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
|
|
0x09, 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x10, 0x00, 0x02, 0x01, 0x03,
|
|
0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7D,
|
|
0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06,
|
|
0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08,
|
|
0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62, 0x72,
|
|
0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28,
|
|
0x29, 0x2A, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45,
|
|
0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59,
|
|
0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75,
|
|
0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89,
|
|
0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3,
|
|
0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6,
|
|
0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9,
|
|
0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2,
|
|
0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4,
|
|
0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01,
|
|
0x00, 0x00, 0x3F, 0x00, 0xFB, 0xD5, 0xDB, 0x20, 0xA8, 0xBA, 0xAE, 0xAF,
|
|
0xDA, 0xAD, 0x28, 0xA6, 0x02, 0x8A, 0x28, 0x03, 0xFF, 0xD9
|
|
])
|
|
|
|
temp_dir = tempfile.mkdtemp(prefix="thrillwiki_test_")
|
|
temp_path = Path(temp_dir)
|
|
|
|
test_photo_path = temp_path / "test_photo.jpg"
|
|
test_avatar_path = temp_path / "test_avatar.jpg"
|
|
|
|
test_photo_path.write_bytes(jpeg_bytes)
|
|
test_avatar_path.write_bytes(jpeg_bytes)
|
|
|
|
yield {
|
|
"test_photo": str(test_photo_path),
|
|
"test_avatar": str(test_avatar_path),
|
|
}
|
|
|
|
# Cleanup
|
|
import shutil
|
|
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
|
|
|
|
@pytest.fixture
|
|
def test_park(auth_page: Page, live_server, test_images):
|
|
"""Fixture for test park"""
|
|
# Create test park using live_server URL
|
|
auth_page.goto(f"{live_server.url}/parks/create/")
|
|
auth_page.get_by_label("Name").fill("Test Park")
|
|
auth_page.get_by_label("Location").fill("Orlando, FL")
|
|
auth_page.get_by_label("Description").fill("A test theme park")
|
|
auth_page.get_by_label("Photo").set_input_files(test_images["test_photo"])
|
|
auth_page.get_by_role("button", name="Create Park").click()
|
|
|
|
yield auth_page
|
|
|
|
|
|
@pytest.fixture
|
|
def test_ride(test_park: Page, live_server, test_images):
|
|
"""Fixture for test ride"""
|
|
# Create test ride using live_server URL
|
|
test_park.goto(f"{live_server.url}/rides/create/")
|
|
test_park.get_by_label("Name").fill("Test Ride")
|
|
test_park.get_by_label("Park").select_option("Test Park")
|
|
test_park.get_by_label("Type").select_option("Roller Coaster")
|
|
test_park.get_by_label("Description").fill("A test ride")
|
|
test_park.get_by_label("Photo").set_input_files(test_images["test_photo"])
|
|
test_park.get_by_role("button", name="Create Ride").click()
|
|
|
|
yield test_park
|
|
|
|
|
|
@pytest.fixture
|
|
def test_review(test_park: Page, live_server):
|
|
"""Fixture for test review"""
|
|
# Create test review using live_server URL
|
|
test_park.goto(f"{live_server.url}/parks/test-park/")
|
|
test_park.get_by_role("tab", name="Reviews").click()
|
|
test_park.get_by_role("button", name="Write Review").click()
|
|
test_park.get_by_label("Rating").select_option("5")
|
|
test_park.get_by_label("Title").fill("Test Review")
|
|
test_park.get_by_label("Review").fill("This is a test review")
|
|
test_park.get_by_role("button", name="Submit Review").click()
|
|
|
|
yield test_park
|
|
|
|
|
|
# =============================================================================
|
|
# FSM Testing Fixtures
|
|
# =============================================================================
|
|
|
|
|
|
@pytest.fixture
|
|
def admin_page(page: Page, live_server, setup_test_data):
|
|
"""Fixture for admin/superuser page"""
|
|
# Login as admin using live_server URL
|
|
page.goto(f"{live_server.url}/accounts/login/")
|
|
page.get_by_label("Username").fill("admin")
|
|
page.get_by_label("Password").fill("adminpass123")
|
|
page.get_by_role("button", name="Sign In").click()
|
|
|
|
yield page
|
|
|
|
|
|
@pytest.fixture
|
|
def submission_pending(db):
|
|
"""Create a pending EditSubmission for FSM testing."""
|
|
from django.contrib.auth import get_user_model
|
|
from apps.moderation.models import EditSubmission
|
|
from apps.parks.models import Park
|
|
from django.contrib.contenttypes.models import ContentType
|
|
|
|
User = get_user_model()
|
|
|
|
# Get or create test user
|
|
user, _ = User.objects.get_or_create(
|
|
username="fsm_test_submitter",
|
|
defaults={"email": "fsm_test@example.com"}
|
|
)
|
|
user.set_password("testpass123")
|
|
user.save()
|
|
|
|
# Get a park
|
|
park = Park.objects.first()
|
|
if not park:
|
|
pytest.skip("No parks available for testing")
|
|
|
|
content_type = ContentType.objects.get_for_model(Park)
|
|
|
|
submission = EditSubmission.objects.create(
|
|
user=user,
|
|
content_type=content_type,
|
|
object_id=park.pk,
|
|
submission_type="EDIT",
|
|
changes={"description": "FSM test submission"},
|
|
reason="FSM e2e test",
|
|
status="PENDING"
|
|
)
|
|
|
|
yield submission
|
|
|
|
# Cleanup
|
|
try:
|
|
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 apps.moderation.models import EditSubmission
|
|
from apps.parks.models import Park
|
|
from django.contrib.contenttypes.models import ContentType
|
|
|
|
User = get_user_model()
|
|
|
|
user, _ = User.objects.get_or_create(
|
|
username="fsm_test_submitter_approved",
|
|
defaults={"email": "fsm_approved@example.com"}
|
|
)
|
|
|
|
park = Park.objects.first()
|
|
if not park:
|
|
pytest.skip("No parks available for testing")
|
|
|
|
content_type = ContentType.objects.get_for_model(Park)
|
|
|
|
submission = EditSubmission.objects.create(
|
|
user=user,
|
|
content_type=content_type,
|
|
object_id=park.pk,
|
|
submission_type="EDIT",
|
|
changes={"description": "Already approved"},
|
|
reason="FSM approved test",
|
|
status="APPROVED"
|
|
)
|
|
|
|
yield submission
|
|
|
|
try:
|
|
submission.delete()
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
@pytest.fixture
|
|
def park_operating(db):
|
|
"""Create an operating Park for FSM testing."""
|
|
from tests.factories import ParkFactory
|
|
|
|
park = ParkFactory(
|
|
name="FSM Test Park Operating",
|
|
slug="fsm-test-park-operating",
|
|
status="OPERATING"
|
|
)
|
|
|
|
yield park
|
|
|
|
|
|
@pytest.fixture
|
|
def park_closed_temp(db):
|
|
"""Create a temporarily closed Park for FSM testing."""
|
|
from tests.factories import ParkFactory
|
|
|
|
park = ParkFactory(
|
|
name="FSM Test Park Closed Temp",
|
|
slug="fsm-test-park-closed-temp",
|
|
status="CLOSED_TEMP"
|
|
)
|
|
|
|
yield park
|
|
|
|
|
|
@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
|
|
|
|
park = ParkFactory(
|
|
name="FSM Test Park Closed Perm",
|
|
slug="fsm-test-park-closed-perm",
|
|
status="CLOSED_PERM",
|
|
closing_date=date.today() - timedelta(days=365)
|
|
)
|
|
|
|
yield park
|
|
|
|
|
|
@pytest.fixture
|
|
def ride_operating(db, park_operating):
|
|
"""Create an operating Ride for FSM testing."""
|
|
from tests.factories import RideFactory
|
|
|
|
ride = RideFactory(
|
|
name="FSM Test Ride Operating",
|
|
slug="fsm-test-ride-operating",
|
|
park=park_operating,
|
|
status="OPERATING"
|
|
)
|
|
|
|
yield ride
|
|
|
|
|
|
@pytest.fixture
|
|
def ride_sbno(db, park_operating):
|
|
"""Create an SBNO Ride for FSM testing."""
|
|
from tests.factories import RideFactory
|
|
|
|
ride = RideFactory(
|
|
name="FSM Test Ride SBNO",
|
|
slug="fsm-test-ride-sbno",
|
|
park=park_operating,
|
|
status="SBNO"
|
|
)
|
|
|
|
yield ride
|
|
|
|
|
|
@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
|
|
|
|
ride = RideFactory(
|
|
name="FSM Test Ride Closed Perm",
|
|
slug="fsm-test-ride-closed-perm",
|
|
park=park_operating,
|
|
status="CLOSED_PERM",
|
|
closing_date=date.today() - timedelta(days=365)
|
|
)
|
|
|
|
yield ride
|
|
|
|
|
|
@pytest.fixture
|
|
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()
|
|
|
|
user, _ = User.objects.get_or_create(
|
|
username="fsm_queue_flagger",
|
|
defaults={"email": "fsm_queue@example.com"}
|
|
)
|
|
|
|
queue_item = ModerationQueue.objects.create(
|
|
item_type="CONTENT_REVIEW",
|
|
status="PENDING",
|
|
priority="MEDIUM",
|
|
title="FSM Test Queue Item",
|
|
description="Queue item for FSM e2e testing",
|
|
flagged_by=user
|
|
)
|
|
|
|
yield queue_item
|
|
|
|
try:
|
|
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()
|
|
|
|
user, _ = User.objects.get_or_create(
|
|
username="fsm_bulk_creator",
|
|
defaults={"email": "fsm_bulk@example.com", "is_staff": True}
|
|
)
|
|
|
|
operation = BulkOperation.objects.create(
|
|
operation_type="IMPORT",
|
|
status="PENDING",
|
|
priority="MEDIUM",
|
|
description="FSM Test Bulk Operation",
|
|
parameters={"test": True},
|
|
created_by=user,
|
|
total_items=10
|
|
)
|
|
|
|
yield operation
|
|
|
|
try:
|
|
operation.delete()
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
# =============================================================================
|
|
# Helper Fixtures
|
|
# =============================================================================
|
|
|
|
|
|
@pytest.fixture
|
|
def live_server(live_server_url):
|
|
"""Provide the live server URL for tests.
|
|
|
|
Note: This fixture is provided by pytest-django. The live_server_url
|
|
fixture provides the URL as a string.
|
|
"""
|
|
class LiveServer:
|
|
url = live_server_url
|
|
|
|
return LiveServer()
|
|
|
|
|
|
@pytest.fixture
|
|
def moderator_user(db):
|
|
"""Get or create a moderator user for testing."""
|
|
from django.contrib.auth import get_user_model
|
|
|
|
User = get_user_model()
|
|
|
|
user, _ = User.objects.get_or_create(
|
|
username="moderator",
|
|
defaults={
|
|
"email": "moderator@example.com",
|
|
"is_staff": True
|
|
}
|
|
)
|
|
user.set_password("modpass123")
|
|
user.save()
|
|
|
|
return user
|
|
|
|
|
|
@pytest.fixture
|
|
def regular_user(db):
|
|
"""Get or create a regular user for testing."""
|
|
from django.contrib.auth import get_user_model
|
|
|
|
User = get_user_model()
|
|
|
|
user, _ = User.objects.get_or_create(
|
|
username="testuser",
|
|
defaults={"email": "testuser@example.com"}
|
|
)
|
|
user.set_password("testpass123")
|
|
user.save()
|
|
|
|
return user
|
|
|
|
|
|
@pytest.fixture
|
|
def parks_data(db):
|
|
"""Create test parks for E2E testing."""
|
|
from tests.factories import ParkFactory
|
|
|
|
parks = [
|
|
ParkFactory(
|
|
name=f"E2E Test Park {i}",
|
|
slug=f"e2e-test-park-{i}",
|
|
status="OPERATING"
|
|
)
|
|
for i in range(3)
|
|
]
|
|
|
|
return parks
|
|
|
|
|
|
@pytest.fixture
|
|
def rides_data(db, parks_data):
|
|
"""Create test rides for E2E testing."""
|
|
from tests.factories import RideFactory
|
|
|
|
rides = []
|
|
for park in parks_data:
|
|
for i in range(2):
|
|
ride = RideFactory(
|
|
name=f"E2E Test Ride {park.name} {i}",
|
|
slug=f"e2e-test-ride-{park.slug}-{i}",
|
|
park=park,
|
|
status="OPERATING"
|
|
)
|
|
rides.append(ride)
|
|
|
|
return rides
|