feat: complete monorepo structure with frontend and shared resources

- Add complete backend/ directory with full Django application
- Add frontend/ directory with Vite + TypeScript setup ready for Next.js
- Add comprehensive shared/ directory with:
  - Complete documentation and memory-bank archives
  - Media files and avatars (letters, park/ride images)
  - Deployment scripts and automation tools
  - Shared types and utilities
- Add architecture/ directory with migration guides
- Configure pnpm workspace for monorepo development
- Update .gitignore to exclude .django_tailwind_cli/ build artifacts
- Preserve all historical documentation in shared/docs/memory-bank/
- Set up proper structure for full-stack development with shared resources
This commit is contained in:
pacnpal
2025-08-23 18:40:07 -04:00
parent b0e0678590
commit d504d41de2
762 changed files with 142636 additions and 0 deletions

0
backend/tests/.gitkeep Normal file
View File

84
backend/tests/README.md Normal file
View File

@@ -0,0 +1,84 @@
# ThrillWiki Test Suite
This directory contains the comprehensive test suite for ThrillWiki, including unit tests and integration tests for all major components of the system.
## Running Tests
To run the complete test suite with coverage reporting:
```bash
python tests/test_runner.py
```
This will:
1. Run all tests across all apps
2. Generate a coverage report in the terminal
3. Create a detailed HTML coverage report in `tests/coverage_html/`
## Viewing Coverage Reports
There are two ways to view the coverage reports:
1. Terminal Report: Shows a quick overview of test coverage directly in your terminal after running the tests.
2. HTML Report: A detailed, interactive report showing line-by-line coverage that can be accessed in two ways:
- Directly open `tests/coverage_html/index.html` in your browser
- Visit `http://localhost:8000/coverage/` when running the development server (only available in DEBUG mode)
The HTML report provides:
- Line-by-line coverage analysis
- Branch coverage information
- Missing lines highlighting
- Interactive file browser
- Detailed statistics per module
## Test Structure
The test suite is organized by app, with each app having its own test file:
- `parks/tests.py`: Tests for park-related functionality
- `companies/tests.py`: Tests for company and manufacturer models
- `location/tests.py`: Tests for location functionality and GeoDjango features
- Additional test files in other app directories
## Writing New Tests
When adding new features or modifying existing ones, please ensure:
1. All new code is covered by tests
2. Tests follow the existing pattern in related test files
3. Both positive and negative test cases are included
4. Edge cases are considered and tested
## Test Categories
The test suite includes:
- Model Tests: Verify model creation, validation, and methods
- View Tests: Test view responses and template rendering
- Form Tests: Validate form processing and validation
- Integration Tests: Test interactions between components
## Continuous Integration
These tests are run automatically on:
- Pull request creation
- Merges to main branch
- Release tagging
## Troubleshooting
If tests fail:
1. Check the error message and stack trace
2. Verify test database settings
3. Ensure all required dependencies are installed
4. Check for any pending migrations
For any issues, please create a ticket in the issue tracker.
## Development Tips
- Run the development server with `python manage.py runserver` to access the coverage reports at `http://localhost:8000/coverage/`
- Coverage reports are only served in development mode (when DEBUG=True)
- The coverage directory is automatically created when running tests
- Reports are updated each time you run the test suite

View File

@@ -0,0 +1,3 @@
# ThrillWiki Test Package
# This file makes the tests directory a Python package for proper module
# discovery

116
backend/tests/e2e/README.md Normal file
View File

@@ -0,0 +1,116 @@
# ThrillWiki E2E Tests
This directory contains end-to-end tests for ThrillWiki using Playwright and pytest.
## Setup
1. Install dependencies:
```bash
uv pip install -r requirements.txt
```
2. Install Playwright browsers:
```bash
playwright install
```
3. Create test fixtures:
```bash
mkdir -p tests/fixtures
```
4. Add test assets:
Place the following files in `tests/fixtures/`:
- `test_photo.jpg` - A sample photo for testing uploads
- `test_avatar.jpg` - A sample avatar image for profile tests
## Running Tests
### Run all tests:
```bash
pytest tests/e2e/
```
### Run specific test files:
```bash
pytest tests/e2e/test_auth.py
pytest tests/e2e/test_parks.py
pytest tests/e2e/test_rides.py
pytest tests/e2e/test_reviews.py
pytest tests/e2e/test_profiles.py
```
### Run tests by marker:
```bash
pytest -m auth
pytest -m parks
pytest -m rides
pytest -m reviews
pytest -m profiles
```
### Run tests with different browsers:
```bash
pytest --browser chromium
pytest --browser firefox
pytest --browser webkit
```
### Run tests headlessly:
```bash
pytest --headless
```
## Test Structure
- `test_auth.py` - Authentication tests (login, signup, logout)
- `test_parks.py` - Theme park tests (listing, details, reviews, photos)
- `test_rides.py` - Ride tests (listing, details, reviews, photos)
- `test_reviews.py` - Review tests (creation, editing, moderation)
- `test_profiles.py` - User profile tests (settings, preferences)
## Test Data
The tests expect the following test users to exist in the database:
1. Regular User:
- Username: testuser
- Password: testpass123
2. Moderator:
- Username: moderator
- Password: modpass123
You can create these users using Django management commands:
```bash
python manage.py create_test_users
```
## Test Environment
Tests expect:
1. Django development server running on http://localhost:8000
2. Database with test data loaded
3. Media handling configured for test uploads
## Debugging
1. Use `--headed` flag to see browser during test execution
2. Use `--slowmo 1000` to slow down test execution
3. Use `--debug` for detailed logging
4. Screenshots are saved in `test-results/` on failure
## Common Issues
1. **Connection Refused**: Ensure Django server is running
2. **Element Not Found**: Check selectors and page load timing
3. **Upload Failures**: Verify test fixtures exist
4. **Authentication Errors**: Verify test users exist in database
## Contributing
1. Add new tests in appropriate test files
2. Follow existing test patterns
3. Include comments explaining test scenarios
4. Update README for new test categories
5. Add new fixtures as needed

25
backend/tests/e2e/cleanup.sh Executable file
View File

@@ -0,0 +1,25 @@
#!/bin/bash
set -e # Exit on error
echo "Cleaning up test data..."
# Run Django cleanup command
uv run manage.py cleanup_test_data
# Clean up test fixtures
echo "Cleaning up test fixtures..."
rm -f tests/fixtures/test_photo.jpg
rm -f tests/fixtures/test_avatar.jpg
# Clean up Playwright artifacts
echo "Cleaning up Playwright artifacts..."
rm -rf test-results/
rm -rf playwright-report/
# Clean up pytest cache
echo "Cleaning up pytest cache..."
rm -rf .pytest_cache/
rm -rf tests/e2e/__pycache__/
find . -type d -name "__pycache__" -exec rm -r {} + 2>/dev/null || true
echo "Cleanup complete!"

View File

@@ -0,0 +1,101 @@
import pytest
from playwright.sync_api import Page
import subprocess
@pytest.fixture(autouse=True)
def setup_test_data():
"""Setup test data before each test session"""
subprocess.run(["uv", "run", "manage.py", "create_test_users"], check=True)
yield
subprocess.run(["uv", "run", "manage.py", "cleanup_test_data"], check=True)
@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):
"""Fixture for authenticated page"""
# Login
page.goto("http://localhost:8000/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):
"""Fixture for moderator page"""
# Login as moderator
page.goto("http://localhost:8000/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
def test_park(auth_page: Page):
"""Fixture for test park"""
# Create test park
auth_page.goto("http://localhost:8000/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("tests/fixtures/test_photo.jpg")
auth_page.get_by_role("button", name="Create Park").click()
yield auth_page
@pytest.fixture
def test_ride(test_park: Page):
"""Fixture for test ride"""
# Create test ride
test_park.goto("http://localhost:8000/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("tests/fixtures/test_photo.jpg")
test_park.get_by_role("button", name="Create Ride").click()
yield test_park
@pytest.fixture
def test_review(test_park: Page):
"""Fixture for test review"""
# Create test review
test_park.goto("http://localhost:8000/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

View File

@@ -0,0 +1,28 @@
[pytest]
# Base URL for tests
base_url = http://localhost:8000
# Test file patterns
python_files = test_*.py
python_classes = Test*
python_functions = test_*
# Playwright specific settings
addopts = --headed --browser chromium
# Markers
markers =
auth: authentication related tests
parks: theme park related tests
rides: ride related tests
reviews: review related tests
profiles: user profile related tests
# Test timeout
timeout = 30
# Logging settings
log_cli = true
log_cli_level = INFO
log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)
log_cli_date_format = %Y-%m-%d %H:%M:%S

55
backend/tests/e2e/setup.sh Executable file
View File

@@ -0,0 +1,55 @@
#!/bin/bash
set -e # Exit on error
# Function to check command exists
check_command() {
if ! command -v $1 &> /dev/null; then
echo "Error: $1 is required but not installed."
exit 1
fi
}
# Check required commands
check_command uv
check_command curl
check_command playwright
# Clean up any existing test data
echo "Cleaning up any existing test data..."
uv run manage.py cleanup_test_data || true
# Install Python dependencies
echo "Installing Python dependencies..."
uv pip install -r requirements.txt
# Install Playwright browsers
echo "Installing Playwright browsers..."
playwright install chromium firefox webkit
# Create test fixtures directory
echo "Setting up test fixtures..."
mkdir -p tests/fixtures
# Download test images
echo "Downloading test images..."
curl -L "https://picsum.photos/1920/1080" -o tests/fixtures/test_photo.jpg
curl -L "https://picsum.photos/500/500" -o tests/fixtures/test_avatar.jpg
# Verify images were downloaded
if [ ! -f tests/fixtures/test_photo.jpg ] || [ ! -f tests/fixtures/test_avatar.jpg ]; then
echo "Error: Failed to download test images"
exit 1
fi
# Create test users
echo "Creating test users..."
uv run manage.py create_test_users
# Make cleanup script executable
chmod +x tests/e2e/cleanup.sh
echo "Setup complete! You can now:"
echo "1. Run all tests: pytest tests/e2e/"
echo "2. Run specific tests: pytest tests/e2e/test_auth.py"
echo "3. Run with specific browser: pytest --browser firefox tests/e2e/"
echo "4. Clean up test data: ./tests/e2e/cleanup.sh"

View File

@@ -0,0 +1,91 @@
from playwright.sync_api import expect, Page
def test_login_page(page: Page):
# Navigate to login page
page.goto("http://localhost:8000/accounts/login/")
# Check login form elements
expect(page.get_by_label("Username")).to_be_visible()
expect(page.get_by_label("Password")).to_be_visible()
expect(page.get_by_role("button", name="Sign In")).to_be_visible()
# Check social login buttons
expect(page.get_by_role("link", name="Sign in with Google")).to_be_visible()
expect(page.get_by_role("link", name="Sign in with Discord")).to_be_visible()
def test_successful_login(page: Page):
# Navigate to login page
page.goto("http://localhost:8000/accounts/login/")
# Fill in login form
page.get_by_label("Username").fill("testuser")
page.get_by_label("Password").fill("testpass123")
# Click login button
page.get_by_role("button", name="Sign In").click()
# Verify redirect to home page
expect(page).to_have_url("http://localhost:8000/")
expect(page.get_by_role("link", name="Profile")).to_be_visible()
def test_failed_login(page: Page):
# Navigate to login page
page.goto("http://localhost:8000/accounts/login/")
# Fill in incorrect credentials
page.get_by_label("Username").fill("wronguser")
page.get_by_label("Password").fill("wrongpass")
# Click login button
page.get_by_role("button", name="Sign In").click()
# Verify error message
expect(page.get_by_text("Invalid username or password")).to_be_visible()
def test_signup_page(page: Page):
# Navigate to signup page
page.goto("http://localhost:8000/accounts/signup/")
# Check signup form elements
expect(page.get_by_label("Username")).to_be_visible()
expect(page.get_by_label("Email")).to_be_visible()
expect(page.get_by_label("Password")).to_be_visible()
expect(page.get_by_label("Password (again)")).to_be_visible()
expect(page.get_by_role("button", name="Sign Up")).to_be_visible()
def test_successful_signup(page: Page):
# Navigate to signup page
page.goto("http://localhost:8000/accounts/signup/")
# Fill in signup form
page.get_by_label("Username").fill("newuser")
page.get_by_label("Email").fill("newuser@example.com")
page.get_by_label("Password").fill("newpass123")
page.get_by_label("Password (again)").fill("newpass123")
# Click signup button
page.get_by_role("button", name="Sign Up").click()
# Verify redirect to home page
expect(page).to_have_url("http://localhost:8000/")
expect(page.get_by_role("link", name="Profile")).to_be_visible()
def test_logout(page: Page):
# First login
page.goto("http://localhost:8000/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()
# Click logout
page.get_by_role("link", name="Logout").click()
# Verify redirect to home page and logged out state
expect(page).to_have_url("http://localhost:8000/")
expect(page.get_by_role("link", name="Login")).to_be_visible()

View File

@@ -0,0 +1,130 @@
from playwright.sync_api import expect, Page
def test_parks_list_page(page: Page):
# Navigate to parks page
page.goto("http://localhost:8000/parks/")
# Check parks list elements
expect(page.get_by_role("heading", name="Theme Parks")).to_be_visible()
expect(page.get_by_role("searchbox", name="Search parks")).to_be_visible()
# Check filter options
expect(page.get_by_role("combobox", name="Country")).to_be_visible()
expect(page.get_by_role("combobox", name="State/Region")).to_be_visible()
def test_park_search(page: Page):
# Navigate to parks page
page.goto("http://localhost:8000/parks/")
# Search for a park
search_box = page.get_by_role("searchbox", name="Search parks")
search_box.fill("Universal")
search_box.press("Enter")
# Verify search results
expect(page.get_by_text("Universal Studios")).to_be_visible()
expect(page.get_by_text("Universal's Islands of Adventure")).to_be_visible()
def test_park_filters(page: Page):
# Navigate to parks page
page.goto("http://localhost:8000/parks/")
# Select country filter
page.get_by_role("combobox", name="Country").select_option("United States")
# Select state filter
page.get_by_role("combobox", name="State/Region").select_option("Florida")
# Verify filtered results
expect(page.get_by_text("Walt Disney World")).to_be_visible()
expect(page.get_by_text("Universal Orlando Resort")).to_be_visible()
def test_park_detail_page(page: Page):
# Navigate to a specific park page
page.goto("http://localhost:8000/parks/walt-disney-world/")
# Check park details
expect(page.get_by_role("heading", name="Walt Disney World")).to_be_visible()
expect(page.get_by_text("Location:")).to_be_visible()
expect(page.get_by_text("Orlando, Florida")).to_be_visible()
# Check park sections
expect(page.get_by_role("tab", name="Overview")).to_be_visible()
expect(page.get_by_role("tab", name="Rides")).to_be_visible()
expect(page.get_by_role("tab", name="Reviews")).to_be_visible()
expect(page.get_by_role("tab", name="Photos")).to_be_visible()
def test_add_park_review(page: Page):
# First login
page.goto("http://localhost:8000/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()
# Navigate to park page
page.goto("http://localhost:8000/parks/walt-disney-world/")
# Click on Reviews tab
page.get_by_role("tab", name="Reviews").click()
# Click Write Review button
page.get_by_role("button", name="Write Review").click()
# Fill review form
page.get_by_label("Rating").select_option("5")
page.get_by_label("Title").fill("Amazing Experience")
page.get_by_label("Review").fill("Had a fantastic time at the park!")
# Submit review
page.get_by_role("button", name="Submit Review").click()
# Verify review appears
expect(page.get_by_text("Amazing Experience")).to_be_visible()
expect(page.get_by_text("Had a fantastic time at the park!")).to_be_visible()
def test_add_park_photo(page: Page):
# First login
page.goto("http://localhost:8000/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()
# Navigate to park page
page.goto("http://localhost:8000/parks/walt-disney-world/")
# Click on Photos tab
page.get_by_role("tab", name="Photos").click()
# Click Add Photo button
page.get_by_role("button", name="Add Photo").click()
# Upload photo
page.get_by_label("Photo").set_input_files("tests/fixtures/test_photo.jpg")
page.get_by_label("Caption").fill("Beautiful castle at sunset")
# Submit photo
page.get_by_role("button", name="Upload Photo").click()
# Verify photo appears
expect(page.get_by_text("Beautiful castle at sunset")).to_be_visible()
def test_park_map(page: Page):
# Navigate to park page
page.goto("http://localhost:8000/parks/walt-disney-world/")
# Check map exists
expect(page.locator("#park-map")).to_be_visible()
# Check map controls
expect(page.get_by_role("button", name="Zoom in")).to_be_visible()
expect(page.get_by_role("button", name="Zoom out")).to_be_visible()
# Verify map markers
expect(page.locator(".map-marker")).to_have_count.greater_than(0)

View File

@@ -0,0 +1,171 @@
from playwright.sync_api import expect, Page
def test_profile_page(page: Page):
# First login
page.goto("http://localhost:8000/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()
# Navigate to profile
page.get_by_role("link", name="Profile").click()
# Check profile sections
expect(page.get_by_role("heading", name="Profile")).to_be_visible()
expect(page.get_by_role("tab", name="Overview")).to_be_visible()
expect(page.get_by_role("tab", name="Reviews")).to_be_visible()
expect(page.get_by_role("tab", name="Photos")).to_be_visible()
expect(page.get_by_role("tab", name="Settings")).to_be_visible()
def test_edit_profile(page: Page):
# First login
page.goto("http://localhost:8000/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()
# Navigate to profile settings
page.get_by_role("link", name="Profile").click()
page.get_by_role("tab", name="Settings").click()
# Edit profile information
page.get_by_label("Display Name").fill("Test User")
page.get_by_label("Bio").fill("Theme park enthusiast")
page.get_by_label("Location").fill("Orlando, FL")
# Upload avatar
page.get_by_label("Avatar").set_input_files("tests/fixtures/test_avatar.jpg")
# Save changes
page.get_by_role("button", name="Save Changes").click()
# Verify updates
expect(page.get_by_text("Profile updated successfully")).to_be_visible()
expect(page.get_by_text("Test User")).to_be_visible()
expect(page.get_by_text("Theme park enthusiast")).to_be_visible()
expect(page.get_by_text("Orlando, FL")).to_be_visible()
def test_change_password(page: Page):
# First login
page.goto("http://localhost:8000/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()
# Navigate to profile settings
page.get_by_role("link", name="Profile").click()
page.get_by_role("tab", name="Settings").click()
# Click change password
page.get_by_role("link", name="Change Password").click()
# Fill password form
page.get_by_label("Current Password").fill("testpass123")
page.get_by_label("New Password").fill("newpass123")
page.get_by_label("Confirm New Password").fill("newpass123")
# Submit form
page.get_by_role("button", name="Change Password").click()
# Verify password changed
expect(page.get_by_text("Password changed successfully")).to_be_visible()
def test_email_preferences(page: Page):
# First login
page.goto("http://localhost:8000/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()
# Navigate to profile settings
page.get_by_role("link", name="Profile").click()
page.get_by_role("tab", name="Settings").click()
# Click email preferences
page.get_by_role("link", name="Email Preferences").click()
# Update preferences
page.get_by_label("Newsletter").check()
page.get_by_label("Review Notifications").check()
page.get_by_label("Photo Comments").uncheck()
# Save changes
page.get_by_role("button", name="Save Preferences").click()
# Verify updates
expect(page.get_by_text("Email preferences updated")).to_be_visible()
def test_privacy_settings(page: Page):
# First login
page.goto("http://localhost:8000/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()
# Navigate to profile settings
page.get_by_role("link", name="Profile").click()
page.get_by_role("tab", name="Settings").click()
# Click privacy settings
page.get_by_role("link", name="Privacy Settings").click()
# Update settings
page.get_by_label("Show Email").uncheck()
page.get_by_label("Show Location").check()
page.get_by_label("Public Profile").check()
# Save changes
page.get_by_role("button", name="Save Privacy Settings").click()
# Verify updates
expect(page.get_by_text("Privacy settings updated")).to_be_visible()
def test_connected_accounts(page: Page):
# First login
page.goto("http://localhost:8000/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()
# Navigate to profile settings
page.get_by_role("link", name="Profile").click()
page.get_by_role("tab", name="Settings").click()
# Click connected accounts
page.get_by_role("link", name="Connected Accounts").click()
# Check available connections
expect(page.get_by_role("link", name="Connect Google")).to_be_visible()
expect(page.get_by_role("link", name="Connect Discord")).to_be_visible()
def test_delete_account(page: Page):
# First login
page.goto("http://localhost:8000/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()
# Navigate to profile settings
page.get_by_role("link", name="Profile").click()
page.get_by_role("tab", name="Settings").click()
# Click delete account
page.get_by_role("link", name="Delete Account").click()
# Confirm deletion
page.get_by_label("Password").fill("testpass123")
page.get_by_label("Confirm").check()
# Click delete button
page.get_by_role("button", name="Delete Account").click()
# Verify redirect to home and logged out
expect(page).to_have_url("http://localhost:8000/")
expect(page.get_by_role("link", name="Login")).to_be_visible()

View File

@@ -0,0 +1,160 @@
from playwright.sync_api import expect, Page
def test_reviews_list_page(page: Page):
# Navigate to reviews page
page.goto("http://localhost:8000/reviews/")
# Check reviews list elements
expect(page.get_by_role("heading", name="Latest Reviews")).to_be_visible()
expect(page.get_by_role("searchbox", name="Search reviews")).to_be_visible()
# Check filter options
expect(page.get_by_role("combobox", name="Type")).to_be_visible()
expect(page.get_by_role("combobox", name="Rating")).to_be_visible()
expect(page.get_by_role("combobox", name="Sort By")).to_be_visible()
def test_review_search(page: Page):
# Navigate to reviews page
page.goto("http://localhost:8000/reviews/")
# Search for reviews
search_box = page.get_by_role("searchbox", name="Search reviews")
search_box.fill("great experience")
search_box.press("Enter")
# Verify search results
expect(page.get_by_text("great experience", exact=False)).to_be_visible()
def test_review_filters(page: Page):
# Navigate to reviews page
page.goto("http://localhost:8000/reviews/")
# Select type filter
page.get_by_role("combobox", name="Type").select_option("Parks")
# Select rating filter
page.get_by_role("combobox", name="Rating").select_option("5 Stars")
# Select sort order
page.get_by_role("combobox", name="Sort By").select_option("Most Recent")
# Verify filtered results
expect(page.get_by_text("Park Review")).to_be_visible()
expect(page.locator(".five-star-rating")).to_be_visible()
def test_review_detail_page(page: Page):
# Navigate to a specific review
page.goto("http://localhost:8000/reviews/1/")
# Check review details
expect(page.get_by_role("heading", name="Review")).to_be_visible()
expect(page.locator(".review-rating")).to_be_visible()
expect(page.locator(".review-date")).to_be_visible()
expect(page.locator(".review-author")).to_be_visible()
expect(page.locator(".review-content")).to_be_visible()
def test_review_voting(page: Page):
# First login
page.goto("http://localhost:8000/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()
# Navigate to review
page.goto("http://localhost:8000/reviews/1/")
# Click helpful button
page.get_by_role("button", name="Helpful").click()
# Verify vote registered
expect(page.get_by_text("You found this review helpful")).to_be_visible()
expect(page.locator(".helpful-count")).to_contain_text("1")
def test_review_comments(page: Page):
# First login
page.goto("http://localhost:8000/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()
# Navigate to review
page.goto("http://localhost:8000/reviews/1/")
# Add comment
page.get_by_label("Comment").fill("Great review! Very helpful.")
page.get_by_role("button", name="Post Comment").click()
# Verify comment appears
expect(page.get_by_text("Great review! Very helpful.")).to_be_visible()
def test_review_moderation(page: Page):
# First login as moderator
page.goto("http://localhost:8000/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()
# Navigate to review
page.goto("http://localhost:8000/reviews/1/")
# Check moderation options
expect(page.get_by_role("button", name="Edit Review")).to_be_visible()
expect(page.get_by_role("button", name="Delete Review")).to_be_visible()
expect(page.get_by_role("button", name="Flag Review")).to_be_visible()
def test_review_reporting(page: Page):
# First login
page.goto("http://localhost:8000/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()
# Navigate to review
page.goto("http://localhost:8000/reviews/1/")
# Click report button
page.get_by_role("button", name="Report Review").click()
# Fill report form
page.get_by_label("Reason").select_option("inappropriate")
page.get_by_label("Details").fill("This review contains inappropriate content")
# Submit report
page.get_by_role("button", name="Submit Report").click()
# Verify report confirmation
expect(page.get_by_text("Review reported successfully")).to_be_visible()
def test_review_editing(page: Page):
# First login as review author
page.goto("http://localhost:8000/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()
# Navigate to own review
page.goto("http://localhost:8000/reviews/1/")
# Click edit button
page.get_by_role("button", name="Edit Review").click()
# Modify review
page.get_by_label("Title").fill("Updated Review Title")
page.get_by_label("Review").fill("Updated review content")
# Save changes
page.get_by_role("button", name="Save Changes").click()
# Verify updates
expect(page.get_by_text("Updated Review Title")).to_be_visible()
expect(page.get_by_text("Updated review content")).to_be_visible()
expect(page.get_by_text("(edited)")).to_be_visible()

View File

@@ -0,0 +1,165 @@
from playwright.sync_api import expect, Page
def test_rides_list_page(page: Page):
# Navigate to rides page
page.goto("http://localhost:8000/rides/")
# Check rides list elements
expect(page.get_by_role("heading", name="Rides & Attractions")).to_be_visible()
expect(page.get_by_role("searchbox", name="Search rides")).to_be_visible()
# Check filter options
expect(page.get_by_role("combobox", name="Park")).to_be_visible()
expect(page.get_by_role("combobox", name="Type")).to_be_visible()
expect(page.get_by_role("combobox", name="Manufacturer")).to_be_visible()
expect(page.get_by_role("combobox", name="Status")).to_be_visible()
def test_ride_search(page: Page):
# Navigate to rides page
page.goto("http://localhost:8000/rides/")
# Search for a ride
search_box = page.get_by_role("searchbox", name="Search rides")
search_box.fill("Space Mountain")
search_box.press("Enter")
# Verify search results
expect(page.get_by_text("Space Mountain")).to_be_visible()
expect(page.get_by_text("Walt Disney World")).to_be_visible()
def test_ride_filters(page: Page):
# Navigate to rides page
page.goto("http://localhost:8000/rides/")
# Select park filter
page.get_by_role("combobox", name="Park").select_option("Walt Disney World")
# Select type filter
page.get_by_role("combobox", name="Type").select_option("Roller Coaster")
# Select manufacturer filter
page.get_by_role("combobox", name="Manufacturer").select_option("Vekoma")
# Verify filtered results
expect(page.get_by_text("Space Mountain")).to_be_visible()
expect(page.get_by_text("Roller Coaster")).to_be_visible()
expect(page.get_by_text("Vekoma")).to_be_visible()
def test_ride_detail_page(page: Page):
# Navigate to a specific ride page
page.goto("http://localhost:8000/rides/space-mountain-magic-kingdom/")
# Check ride details
expect(page.get_by_role("heading", name="Space Mountain")).to_be_visible()
expect(page.get_by_text("Park:")).to_be_visible()
expect(page.get_by_text("Walt Disney World")).to_be_visible()
expect(page.get_by_text("Type:")).to_be_visible()
expect(page.get_by_text("Roller Coaster")).to_be_visible()
expect(page.get_by_text("Manufacturer:")).to_be_visible()
expect(page.get_by_text("Vekoma")).to_be_visible()
# Check ride sections
expect(page.get_by_role("tab", name="Overview")).to_be_visible()
expect(page.get_by_role("tab", name="Stats")).to_be_visible()
expect(page.get_by_role("tab", name="Reviews")).to_be_visible()
expect(page.get_by_role("tab", name="Photos")).to_be_visible()
def test_ride_stats(page: Page):
# Navigate to ride page
page.goto("http://localhost:8000/rides/space-mountain-magic-kingdom/")
# Click on Stats tab
page.get_by_role("tab", name="Stats").click()
# Check stats are visible
expect(page.get_by_text("Height:")).to_be_visible()
expect(page.get_by_text("Length:")).to_be_visible()
expect(page.get_by_text("Speed:")).to_be_visible()
expect(page.get_by_text("Duration:")).to_be_visible()
expect(page.get_by_text("Height Requirement:")).to_be_visible()
def test_add_ride_review(page: Page):
# First login
page.goto("http://localhost:8000/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()
# Navigate to ride page
page.goto("http://localhost:8000/rides/space-mountain-magic-kingdom/")
# Click on Reviews tab
page.get_by_role("tab", name="Reviews").click()
# Click Write Review button
page.get_by_role("button", name="Write Review").click()
# Fill review form
page.get_by_label("Rating").select_option("5")
page.get_by_label("Title").fill("Best Coaster Ever")
page.get_by_label("Review").fill("Such a thrilling experience in the dark!")
# Submit review
page.get_by_role("button", name="Submit Review").click()
# Verify review appears
expect(page.get_by_text("Best Coaster Ever")).to_be_visible()
expect(page.get_by_text("Such a thrilling experience in the dark!")).to_be_visible()
def test_add_ride_photo(page: Page):
# First login
page.goto("http://localhost:8000/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()
# Navigate to ride page
page.goto("http://localhost:8000/rides/space-mountain-magic-kingdom/")
# Click on Photos tab
page.get_by_role("tab", name="Photos").click()
# Click Add Photo button
page.get_by_role("button", name="Add Photo").click()
# Upload photo
page.get_by_label("Photo").set_input_files("tests/fixtures/test_photo.jpg")
page.get_by_label("Caption").fill("Awesome ride entrance")
# Submit photo
page.get_by_role("button", name="Upload Photo").click()
# Verify photo appears
expect(page.get_by_text("Awesome ride entrance")).to_be_visible()
def test_ride_wait_times(page: Page):
# Navigate to ride page
page.goto("http://localhost:8000/rides/space-mountain-magic-kingdom/")
# Check wait time section
expect(page.get_by_text("Current Wait Time:")).to_be_visible()
expect(page.get_by_text("minutes")).to_be_visible()
# Check historical wait times
expect(page.get_by_text("Average Wait Times")).to_be_visible()
expect(page.locator("#wait-time-chart")).to_be_visible()
def test_ride_location(page: Page):
# Navigate to ride page
page.goto("http://localhost:8000/rides/space-mountain-magic-kingdom/")
# Check location section
expect(page.get_by_text("Location in Park")).to_be_visible()
expect(page.locator("#ride-location-map")).to_be_visible()
# Verify map marker
expect(page.locator(".ride-marker")).to_be_visible()

362
backend/tests/factories.py Normal file
View File

@@ -0,0 +1,362 @@
"""
Test factories for ThrillWiki models.
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
User = get_user_model()
class UserFactory(DjangoModelFactory):
"""Factory for creating User instances."""
class Meta:
model = User
django_get_or_create = ("username",)
username = factory.Sequence(lambda n: f"testuser{n}")
email = factory.LazyAttribute(lambda obj: f"{obj.username}@example.com")
first_name = factory.Faker("first_name")
last_name = factory.Faker("last_name")
is_active = True
is_staff = False
is_superuser = False
@factory.post_generation
def set_password(obj, create, extracted, **kwargs):
if create:
password = extracted or "testpass123"
obj.set_password(password)
obj.save()
class StaffUserFactory(UserFactory):
"""Factory for creating staff User instances."""
is_staff = True
class SuperUserFactory(UserFactory):
"""Factory for creating superuser instances."""
is_staff = True
is_superuser = True
class CompanyFactory(DjangoModelFactory):
"""Factory for creating Company instances."""
class Meta:
model = "parks.Company"
django_get_or_create = ("name",)
name = factory.Faker("company")
slug = factory.LazyAttribute(lambda obj: slugify(obj.name))
description = factory.Faker("text", max_nb_chars=500)
website = factory.Faker("url")
founded_year = fuzzy.FuzzyInteger(1800, 2024)
roles = factory.LazyFunction(lambda: ["OPERATOR"])
@factory.post_generation
def multiple_roles(obj, create, extracted, **kwargs):
"""Optionally add multiple roles."""
if create and extracted:
obj.roles = extracted
obj.save()
class OperatorCompanyFactory(CompanyFactory):
"""Factory for companies that operate parks."""
roles = factory.LazyFunction(lambda: ["OPERATOR"])
class ManufacturerCompanyFactory(CompanyFactory):
"""Factory for companies that manufacture rides."""
roles = factory.LazyFunction(lambda: ["MANUFACTURER"])
class DesignerCompanyFactory(CompanyFactory):
"""Factory for companies that design rides."""
roles = factory.LazyFunction(lambda: ["DESIGNER"])
class LocationFactory(DjangoModelFactory):
"""Factory for creating Location instances."""
class Meta:
model = "location.Location"
name = factory.Faker("city")
location_type = "park"
latitude = fuzzy.FuzzyFloat(-90, 90)
longitude = fuzzy.FuzzyFloat(-180, 180)
street_address = factory.Faker("street_address")
city = factory.Faker("city")
state = factory.Faker("state")
country = factory.Faker("country")
postal_code = factory.Faker("postcode")
@factory.lazy_attribute
def point(self):
return Point(float(self.longitude), float(self.latitude))
class ParkFactory(DjangoModelFactory):
"""Factory for creating Park instances."""
class Meta:
model = "parks.Park"
django_get_or_create = ("slug",)
name = factory.Sequence(lambda n: f"Test Park {n}")
slug = factory.LazyAttribute(lambda obj: slugify(obj.name))
description = factory.Faker("text", max_nb_chars=1000)
status = "OPERATING"
opening_date = factory.Faker("date_between", start_date="-50y", end_date="today")
closing_date = None
operating_season = factory.Faker("sentence", nb_words=4)
size_acres = fuzzy.FuzzyDecimal(1, 1000, precision=2)
website = factory.Faker("url")
average_rating = fuzzy.FuzzyDecimal(1, 10, precision=2)
ride_count = fuzzy.FuzzyInteger(5, 100)
coaster_count = fuzzy.FuzzyInteger(1, 20)
# Relationships
operator = factory.SubFactory(OperatorCompanyFactory)
property_owner = factory.SubFactory(OperatorCompanyFactory)
@factory.post_generation
def create_location(obj, create, extracted, **kwargs):
"""Create a location for the park."""
if create:
LocationFactory(content_object=obj, name=obj.name, location_type="park")
class ClosedParkFactory(ParkFactory):
"""Factory for creating closed parks."""
status = "CLOSED_PERM"
closing_date = factory.Faker("date_between", start_date="-10y", end_date="today")
class ParkAreaFactory(DjangoModelFactory):
"""Factory for creating ParkArea instances."""
class Meta:
model = "parks.ParkArea"
django_get_or_create = ("park", "slug")
name = factory.Faker("word")
slug = factory.LazyAttribute(lambda obj: slugify(obj.name))
description = factory.Faker("text", max_nb_chars=500)
# Relationships
park = factory.SubFactory(ParkFactory)
class RideModelFactory(DjangoModelFactory):
"""Factory for creating RideModel instances."""
class Meta:
model = "rides.RideModel"
django_get_or_create = ("name", "manufacturer")
name = factory.Faker("word")
description = factory.Faker("text", max_nb_chars=500)
# Relationships
manufacturer = factory.SubFactory(ManufacturerCompanyFactory)
class RideFactory(DjangoModelFactory):
"""Factory for creating Ride instances."""
class Meta:
model = "rides.Ride"
django_get_or_create = ("park", "slug")
name = factory.Sequence(lambda n: f"Test Ride {n}")
slug = factory.LazyAttribute(lambda obj: slugify(obj.name))
description = factory.Faker("text", max_nb_chars=1000)
category = fuzzy.FuzzyChoice(["RC", "WC", "TR", "WR", "DR", "CR", "FR", "SP"])
status = "OPERATING"
opening_date = factory.Faker("date_between", start_date="-30y", end_date="today")
closing_date = None
min_height_in = fuzzy.FuzzyInteger(36, 48)
max_height_in = None
capacity_per_hour = fuzzy.FuzzyInteger(500, 3000)
ride_duration_seconds = fuzzy.FuzzyInteger(60, 300)
average_rating = fuzzy.FuzzyDecimal(1, 10, precision=2)
# Relationships
park = factory.SubFactory(ParkFactory)
manufacturer = factory.SubFactory(ManufacturerCompanyFactory)
designer = factory.SubFactory(DesignerCompanyFactory)
ride_model = factory.SubFactory(RideModelFactory)
park_area = factory.SubFactory(
ParkAreaFactory, park=factory.SelfAttribute("..park")
)
@factory.post_generation
def create_location(obj, create, extracted, **kwargs):
"""Create a location for the ride."""
if create:
LocationFactory(content_object=obj, name=obj.name, location_type="ride")
class CoasterFactory(RideFactory):
"""Factory for creating roller coaster rides."""
category = fuzzy.FuzzyChoice(["RC", "WC"])
min_height_in = fuzzy.FuzzyInteger(42, 54)
ride_duration_seconds = fuzzy.FuzzyInteger(90, 240)
class ParkReviewFactory(DjangoModelFactory):
"""Factory for creating ParkReview instances."""
class Meta:
model = "parks.ParkReview"
django_get_or_create = ("park", "user")
rating = fuzzy.FuzzyInteger(1, 10)
title = factory.Faker("sentence", nb_words=6)
content = factory.Faker("text", max_nb_chars=2000)
visit_date = factory.Faker("date_between", start_date="-2y", end_date="today")
is_published = True
moderation_notes = ""
# Relationships
park = factory.SubFactory(ParkFactory)
user = factory.SubFactory(UserFactory)
class RideReviewFactory(DjangoModelFactory):
"""Factory for creating RideReview instances."""
class Meta:
model = "rides.RideReview"
django_get_or_create = ("ride", "user")
rating = fuzzy.FuzzyInteger(1, 10)
title = factory.Faker("sentence", nb_words=6)
content = factory.Faker("text", max_nb_chars=2000)
visit_date = factory.Faker("date_between", start_date="-2y", end_date="today")
is_published = True
moderation_notes = ""
# Relationships
ride = factory.SubFactory(RideFactory)
user = factory.SubFactory(UserFactory)
class ModeratedReviewFactory(ParkReviewFactory):
"""Factory for creating moderated reviews."""
moderation_notes = factory.Faker("sentence")
moderated_by = factory.SubFactory(StaffUserFactory)
moderated_at = factory.Faker("date_time_between", start_date="-1y", end_date="now")
class EditSubmissionFactory(DjangoModelFactory):
"""Factory for creating EditSubmission instances."""
class Meta:
model = "moderation.EditSubmission"
submission_type = "UPDATE"
changes = factory.LazyFunction(lambda: {"name": "Updated Name"})
status = "PENDING"
notes = factory.Faker("sentence")
# Relationships
submitted_by = factory.SubFactory(UserFactory)
content_object = factory.SubFactory(ParkFactory)
# Trait mixins for common scenarios
class Traits:
"""Common trait mixins for factories."""
@staticmethod
def operating_park():
"""Trait for operating parks."""
return {"status": "OPERATING", "closing_date": None}
@staticmethod
def closed_park():
"""Trait for closed parks."""
return {
"status": "CLOSED_PERM",
"closing_date": factory.Faker(
"date_between", start_date="-10y", end_date="today"
),
}
@staticmethod
def high_rated():
"""Trait for highly rated items."""
return {"average_rating": fuzzy.FuzzyDecimal(8, 10, precision=2)}
@staticmethod
def recent_submission():
"""Trait for recent submissions."""
return {
"submitted_at": factory.Faker(
"date_time_between", start_date="-7d", end_date="now"
)
}
# Specialized factories for testing scenarios
class TestScenarios:
"""Pre-configured factory combinations for common test scenarios."""
@staticmethod
def complete_park_with_rides(num_rides=5):
"""Create a complete park with rides and reviews."""
park = ParkFactory()
rides = [RideFactory(park=park) for _ in range(num_rides)]
park_review = ParkReviewFactory(park=park)
ride_reviews = [RideReviewFactory(ride=ride) for ride in rides[:2]]
return {
"park": park,
"rides": rides,
"park_review": park_review,
"ride_reviews": ride_reviews,
}
@staticmethod
def moderation_workflow():
"""Create a complete moderation workflow scenario."""
user = UserFactory()
moderator = StaffUserFactory()
park = ParkFactory()
submission = EditSubmissionFactory(submitted_by=user, content_object=park)
return {
"user": user,
"moderator": moderator,
"park": park,
"submission": submission,
}
@staticmethod
def review_scenario():
"""Create a scenario with multiple reviews and ratings."""
park = ParkFactory()
users = [UserFactory() for _ in range(5)]
reviews = [ParkReviewFactory(park=park, user=user) for user in users]
return {"park": park, "users": users, "reviews": reviews}

33
backend/tests/fixtures/README.md vendored Normal file
View File

@@ -0,0 +1,33 @@
# Test Fixtures
This directory contains test assets used by the e2e tests.
## Required Files
1. `test_photo.jpg`
- Used for testing photo uploads for parks and rides
- Recommended size: 1920x1080
- Max size: 5MB
2. `test_avatar.jpg`
- Used for testing profile avatar uploads
- Recommended size: 500x500
- Max size: 2MB
## Generating Test Images
You can create these test images using any image editing software, or download placeholder images from:
- https://picsum.photos/1920/1080 (for test_photo.jpg)
- https://picsum.photos/500/500 (for test_avatar.jpg)
Save the downloaded images with the correct filenames in this directory.
## Usage
The test files reference these images using relative paths:
```python
page.get_by_label("Photo").set_input_files("tests/fixtures/test_photo.jpg")
page.get_by_label("Avatar").set_input_files("tests/fixtures/test_avatar.jpg")
```
Make sure these files exist before running the tests.

View File

@@ -0,0 +1,244 @@
"""
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 .factories import (
UserFactory,
ParkFactory,
RideFactory,
ParkReviewFactory,
RideReviewFactory,
CompanyFactory,
Traits,
)
User = get_user_model()
class FactoryTestCase(TestCase):
"""Test case demonstrating factory usage patterns."""
def test_user_factory_basic(self):
"""Test basic user factory functionality."""
# Build without saving to database
user = UserFactory.build()
self.assertIsInstance(user, User)
self.assertTrue(user.username.startswith("testuser"))
self.assertIn("@example.com", user.email)
# Create and save to database
user = UserFactory()
self.assertTrue(user.pk)
self.assertTrue(user.check_password("testpass123"))
def test_user_factory_with_custom_password(self):
"""Test user factory with custom password."""
user = UserFactory(set_password__password="custompass")
self.assertTrue(user.check_password("custompass"))
def test_staff_user_factory(self):
"""Test staff user factory."""
from .factories import StaffUserFactory
staff = StaffUserFactory()
self.assertTrue(staff.is_staff)
self.assertFalse(staff.is_superuser)
def test_company_factory_with_roles(self):
"""Test company factory with different roles."""
# Operator company
operator = CompanyFactory(roles=["OPERATOR"])
self.assertEqual(operator.roles, ["OPERATOR"])
# Manufacturer company
manufacturer = CompanyFactory(roles=["MANUFACTURER"])
self.assertEqual(manufacturer.roles, ["MANUFACTURER"])
# Multi-role company
multi_role = CompanyFactory(roles=["OPERATOR", "MANUFACTURER"])
self.assertEqual(set(multi_role.roles), {"OPERATOR", "MANUFACTURER"})
def test_park_factory_basic(self):
"""Test basic park factory functionality."""
park = ParkFactory.build()
self.assertTrue(park.name.startswith("Test Park"))
self.assertEqual(park.status, "OPERATING")
self.assertIsNotNone(park.operator)
# Test that constraints are respected
self.assertGreaterEqual(park.average_rating, 1)
self.assertLessEqual(park.average_rating, 10)
self.assertGreaterEqual(park.ride_count, 0)
self.assertGreaterEqual(park.coaster_count, 0)
def test_park_factory_with_traits(self):
"""Test park factory with traits."""
# Closed park
closed_park = ParkFactory.build(**Traits.closed_park())
self.assertEqual(closed_park.status, "CLOSED_PERM")
self.assertIsNotNone(closed_park.closing_date)
# High rated park
high_rated = ParkFactory.build(**Traits.high_rated())
self.assertGreaterEqual(high_rated.average_rating, 8)
def test_ride_factory_basic(self):
"""Test basic ride factory functionality."""
ride = RideFactory.build()
self.assertTrue(ride.name.startswith("Test Ride"))
self.assertEqual(ride.status, "OPERATING")
self.assertIsNotNone(ride.park)
self.assertIsNotNone(ride.manufacturer)
# Test constraints
if ride.min_height_in and ride.max_height_in:
self.assertLessEqual(ride.min_height_in, ride.max_height_in)
self.assertGreaterEqual(ride.average_rating, 1)
self.assertLessEqual(ride.average_rating, 10)
def test_coaster_factory(self):
"""Test roller coaster specific factory."""
from .factories import CoasterFactory
coaster = CoasterFactory.build()
self.assertIn(coaster.category, ["RC", "WC"])
self.assertGreaterEqual(coaster.min_height_in, 42)
self.assertLessEqual(coaster.min_height_in, 54)
def test_review_factories(self):
"""Test review factory functionality."""
park_review = ParkReviewFactory.build()
self.assertGreaterEqual(park_review.rating, 1)
self.assertLessEqual(park_review.rating, 10)
self.assertTrue(park_review.is_published)
ride_review = RideReviewFactory.build()
self.assertGreaterEqual(ride_review.rating, 1)
self.assertLessEqual(ride_review.rating, 10)
def test_sequence_functionality(self):
"""Test that sequences work correctly."""
users = [UserFactory.build() for _ in range(3)]
usernames = [user.username for user in users]
# Should have unique usernames
self.assertEqual(len(set(usernames)), 3)
self.assertTrue(all("testuser" in username for username in usernames))
def test_lazy_attributes(self):
"""Test lazy attribute functionality."""
park = ParkFactory.build(name="Custom Park Name")
self.assertEqual(park.slug, "custom-park-name")
def test_fuzzy_fields(self):
"""Test fuzzy field generation."""
parks = [ParkFactory.build() for _ in range(10)]
# Should have varied values
ratings = [p.average_rating for p in parks if p.average_rating]
# Should have different ratings
self.assertGreater(len(set(ratings)), 1)
ride_counts = [p.ride_count for p in parks if p.ride_count]
# Should have different counts
self.assertGreater(len(set(ride_counts)), 1)
class TestScenariosTestCase(TestCase):
"""Test case for pre-configured test scenarios."""
def test_build_only_scenario(self):
"""Test scenarios using build() to avoid database operations."""
# Create minimal scenario data using build()
park = ParkFactory.build()
rides = [RideFactory.build(park=park) for _ in range(3)]
# Verify the scenario
self.assertEqual(len(rides), 3)
for ride in rides:
self.assertEqual(ride.park, park)
def test_review_scenario_build(self):
"""Test review scenario using build()."""
park = ParkFactory.build()
users = [UserFactory.build() for _ in range(3)]
reviews = [ParkReviewFactory.build(park=park, user=user) for user in users]
# Verify scenario
self.assertEqual(len(reviews), 3)
for review in reviews:
self.assertEqual(review.park, park)
self.assertIn(review.user, users)
class FactoryValidationTestCase(TestCase):
"""Test that factories respect model validation."""
def test_rating_constraints(self):
"""Test that rating constraints are respected."""
# Valid ratings
valid_review = ParkReviewFactory.build(rating=5)
self.assertEqual(valid_review.rating, 5)
# Edge cases
min_review = ParkReviewFactory.build(rating=1)
self.assertEqual(min_review.rating, 1)
max_review = ParkReviewFactory.build(rating=10)
self.assertEqual(max_review.rating, 10)
def test_date_constraints(self):
"""Test that date constraints are logical."""
from datetime import date
# Valid dates
park = ParkFactory.build(
opening_date=date(2020, 1, 1), closing_date=date(2023, 12, 31)
)
# Verify opening is before closing
if park.opening_date and park.closing_date:
self.assertLessEqual(park.opening_date, park.closing_date)
def test_height_requirements(self):
"""Test that height requirements are logical."""
ride = RideFactory.build(min_height_in=48, max_height_in=72)
if ride.min_height_in and ride.max_height_in:
self.assertLessEqual(ride.min_height_in, ride.max_height_in)
class FactoryPerformanceTestCase(TestCase):
"""Test factory performance and bulk operations."""
def test_bulk_creation_build(self):
"""Test bulk creation using build() for performance."""
import time
start_time = time.time()
users = [UserFactory.build() for _ in range(100)]
build_time = time.time() - start_time
self.assertEqual(len(users), 100)
self.assertLess(build_time, 1.0) # Should be fast with build()
# Verify uniqueness
usernames = [user.username for user in users]
self.assertEqual(len(set(usernames)), 100)
def test_related_object_creation(self):
"""Test creation of objects with relationships."""
# Build park with relationships
park = ParkFactory.build()
# Verify relationships exist
self.assertIsNotNone(park.operator)
self.assertIsNotNone(park.property_owner)
# Build ride with park relationship
ride = RideFactory.build(park=park)
self.assertEqual(ride.park, park)

View File

@@ -0,0 +1,382 @@
"""
Test cases for Parks API following Django styleguide patterns.
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 apps.parks.models import Park
from tests.factories import (
UserFactory,
StaffUserFactory,
CompanyFactory,
ParkFactory,
)
class TestParkListApi(APITestCase):
"""Test cases for Park list API endpoint."""
def setUp(self):
"""Set up test data."""
self.client = APIClient()
self.user = UserFactory()
self.company = CompanyFactory(roles=["OPERATOR"])
self.parks = [
ParkFactory(operator=self.company, name="Park A"),
ParkFactory(operator=self.company, name="Park B", status="CLOSED_TEMP"),
ParkFactory(operator=self.company, name="Park C"),
]
self.url = reverse("parks_api:park-list")
def test__park_list_api__unauthenticated_user__can_access(self):
"""Test that unauthenticated users can access park list."""
response = self.client.get(self.url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["status"], "success")
self.assertIsInstance(response.data["data"], list)
def test__park_list_api__returns_all_parks__in_correct_format(self):
"""Test that park list returns all parks in correct format."""
response = self.client.get(self.url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data["data"]), 3)
# Check response structure
park_data = response.data["data"][0]
expected_fields = [
"id",
"name",
"slug",
"status",
"description",
"average_rating",
"coaster_count",
"ride_count",
"location",
"operator",
"created_at",
"updated_at",
]
for field in expected_fields:
self.assertIn(field, park_data)
def test__park_list_api__with_status_filter__returns_filtered_results(
self,
):
"""Test that status filter works correctly."""
response = self.client.get(self.url, {"status": "OPERATING"})
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Should return only operating parks (2 out of 3)
operating_parks = [
p for p in response.data["data"] if p["status"] == "OPERATING"
]
self.assertEqual(len(operating_parks), 2)
def test__park_list_api__with_search_query__returns_matching_results(self):
"""Test that search functionality works correctly."""
response = self.client.get(self.url, {"search": "Park A"})
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data["data"]), 1)
self.assertEqual(response.data["data"][0]["name"], "Park A")
def test__park_list_api__with_ordering__returns_ordered_results(self):
"""Test that ordering functionality works correctly."""
response = self.client.get(self.url, {"ordering": "-name"})
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Should be ordered by name descending (C, B, A)
names = [park["name"] for park in response.data["data"]]
self.assertEqual(names, ["Park C", "Park B", "Park A"])
class TestParkDetailApi(APITestCase):
"""Test cases for Park detail API endpoint."""
def setUp(self):
"""Set up test data."""
self.client = APIClient()
self.company = CompanyFactory(roles=["OPERATOR"])
self.park = ParkFactory(operator=self.company)
self.url = reverse("parks_api:park-detail", kwargs={"slug": self.park.slug})
def test__park_detail_api__with_valid_slug__returns_park_details(self):
"""Test that park detail API returns correct park information."""
response = self.client.get(self.url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["status"], "success")
park_data = response.data["data"]
self.assertEqual(park_data["id"], self.park.id)
self.assertEqual(park_data["name"], self.park.name)
self.assertEqual(park_data["slug"], self.park.slug)
# Check that detailed fields are included
detailed_fields = [
"opening_date",
"closing_date",
"operating_season",
"size_acres",
"website",
"areas",
"operator",
"property_owner",
]
for field in detailed_fields:
self.assertIn(field, park_data)
def test__park_detail_api__with_invalid_slug__returns_404(self):
"""Test that invalid slug returns 404 error."""
invalid_url = reverse("parks_api:park-detail", kwargs={"slug": "nonexistent"})
response = self.client.get(invalid_url)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertEqual(response.data["status"], "error")
self.assertEqual(response.data["error"]["code"], "NOT_FOUND")
class TestParkCreateApi(APITestCase):
"""Test cases for Park creation API endpoint."""
def setUp(self):
"""Set up test data."""
self.client = APIClient()
self.user = UserFactory()
self.staff_user = StaffUserFactory()
self.company = CompanyFactory(roles=["OPERATOR"])
self.url = reverse("parks_api:park-list") # POST to list endpoint
self.valid_park_data = {
"name": "New Test Park",
"description": "A test park for API testing",
"operator_id": self.company.id,
"status": "OPERATING",
}
def test__park_create_api__unauthenticated_user__returns_401(self):
"""Test that unauthenticated users cannot create parks."""
response = self.client.post(self.url, self.valid_park_data)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test__park_create_api__authenticated_user__can_create_park(self):
"""Test that authenticated users can create parks."""
self.client.force_authenticate(user=self.user)
response = self.client.post(self.url, self.valid_park_data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data["status"], "success")
# Verify park was created
park_data = response.data["data"]
self.assertEqual(park_data["name"], "New Test Park")
self.assertTrue(Park.objects.filter(name="New Test Park").exists())
def test__park_create_api__with_invalid_data__returns_validation_errors(
self,
):
"""Test that invalid data returns proper validation errors."""
self.client.force_authenticate(user=self.user)
invalid_data = self.valid_park_data.copy()
invalid_data["name"] = "" # Empty name should be invalid
response = self.client.post(self.url, invalid_data)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data["status"], "error")
self.assertIn("name", response.data["error"]["details"])
def test__park_create_api__with_invalid_date_range__returns_validation_error(
self,
):
"""Test that invalid date ranges are caught by validation."""
self.client.force_authenticate(user=self.user)
invalid_data = self.valid_park_data.copy()
invalid_data.update(
{
"opening_date": "2020-06-01",
"closing_date": "2020-05-01", # Before opening date
}
)
response = self.client.post(self.url, invalid_data)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn("Closing date cannot be before opening date", str(response.data))
class TestParkUpdateApi(APITestCase):
"""Test cases for Park update API endpoint."""
def setUp(self):
"""Set up test data."""
self.client = APIClient()
self.user = UserFactory()
self.company = CompanyFactory(roles=["OPERATOR"])
self.park = ParkFactory(operator=self.company)
self.url = reverse("parks_api:park-detail", kwargs={"slug": self.park.slug})
def test__park_update_api__authenticated_user__can_update_park(self):
"""Test that authenticated users can update parks."""
self.client.force_authenticate(user=self.user)
update_data = {
"name": "Updated Park Name",
"description": "Updated description",
}
response = self.client.patch(self.url, update_data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["status"], "success")
# Verify park was updated
self.park.refresh_from_db()
self.assertEqual(self.park.name, "Updated Park Name")
self.assertEqual(self.park.description, "Updated description")
def test__park_update_api__with_invalid_data__returns_validation_errors(
self,
):
"""Test that invalid update data returns validation errors."""
self.client.force_authenticate(user=self.user)
invalid_data = {
"opening_date": "2020-06-01",
"closing_date": "2020-05-01", # Invalid date range
}
response = self.client.patch(self.url, invalid_data)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
class TestParkStatsApi(APITestCase):
"""Test cases for Park statistics API endpoint."""
def setUp(self):
"""Set up test data."""
self.client = APIClient()
self.company = CompanyFactory(roles=["OPERATOR"])
# Create parks with different statuses
ParkFactory(operator=self.company, status="OPERATING")
ParkFactory(operator=self.company, status="OPERATING")
ParkFactory(operator=self.company, status="CLOSED_TEMP")
self.url = reverse("parks_api:park-stats")
def test__park_stats_api__returns_correct_statistics(self):
"""Test that park statistics API returns correct data."""
response = self.client.get(self.url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["status"], "success")
stats = response.data["data"]
expected_fields = [
"total_parks",
"operating_parks",
"closed_parks",
"under_construction",
"average_rating",
"recently_added_count",
]
for field in expected_fields:
self.assertIn(field, stats)
# Verify counts are correct
self.assertEqual(stats["total_parks"], 3)
self.assertEqual(stats["operating_parks"], 2)
class TestParkApiErrorHandling(APITestCase):
"""Test cases for Park API error handling."""
def setUp(self):
"""Set up test data."""
self.client = APIClient()
def test__park_api__with_malformed_json__returns_parse_error(self):
"""Test that malformed JSON returns proper error."""
url = reverse("parks_api:park-list")
response = self.client.post(
url, data='{"invalid": json}', content_type="application/json"
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data["status"], "error")
def test__park_api__with_unsupported_method__returns_405(self):
"""Test that unsupported HTTP methods return 405."""
park = ParkFactory()
url = reverse("parks_api:park-detail", kwargs={"slug": park.slug})
response = self.client.head(url) # HEAD not supported
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
class TestParkApiIntegration(APITestCase):
"""Integration tests for Park API with complete scenarios."""
def test__complete_park_workflow__create_update_retrieve_delete(self):
"""Test complete CRUD workflow for parks."""
user = UserFactory()
company = CompanyFactory(roles=["OPERATOR"])
self.client.force_authenticate(user=user)
# 1. Create park
create_data = {
"name": "Integration Test Park",
"description": "A park for integration testing",
"operator_id": company.id,
}
create_response = self.client.post(reverse("parks_api:park-list"), create_data)
self.assertEqual(create_response.status_code, status.HTTP_201_CREATED)
park_slug = create_response.data["data"]["slug"]
# 2. Retrieve park
detail_url = reverse("parks_api:park-detail", kwargs={"slug": park_slug})
retrieve_response = self.client.get(detail_url)
self.assertEqual(retrieve_response.status_code, status.HTTP_200_OK)
self.assertEqual(
retrieve_response.data["data"]["name"], "Integration Test Park"
)
# 3. Update park
update_data = {"description": "Updated integration test description"}
update_response = self.client.patch(detail_url, update_data)
self.assertEqual(update_response.status_code, status.HTTP_200_OK)
self.assertEqual(
update_response.data["data"]["description"],
"Updated integration test description",
)
# 4. Delete park
delete_response = self.client.delete(detail_url)
self.assertEqual(delete_response.status_code, status.HTTP_204_NO_CONTENT)
# 5. Verify park is deleted
verify_response = self.client.get(detail_url)
self.assertEqual(verify_response.status_code, status.HTTP_404_NOT_FOUND)

View File

@@ -0,0 +1,249 @@
"""
Test cases for Parks models following Django styleguide patterns.
Uses proper naming conventions and comprehensive coverage.
"""
from datetime import date, timedelta
from django.test import TestCase
from django.db import IntegrityError
from django.utils import timezone
from apps.parks.models import Park, Company
from tests.factories import (
UserFactory,
CompanyFactory,
ParkFactory,
ParkAreaFactory,
ParkReviewFactory,
TestScenarios,
)
class TestParkModel(TestCase):
"""Test cases for the Park model."""
def setUp(self):
"""Set up test data."""
self.company = CompanyFactory(roles=["OPERATOR"])
self.user = UserFactory()
def test__park_creation__with_valid_data__succeeds(self):
"""Test that park can be created with valid data."""
park = ParkFactory(operator=self.company)
self.assertIsInstance(park.id, int)
self.assertEqual(park.operator, self.company)
self.assertEqual(park.status, "OPERATING")
self.assertIsNotNone(park.created_at)
def test__park_str_representation__returns_park_name(self):
"""Test that park string representation returns the name."""
park = ParkFactory(name="Test Park", operator=self.company)
self.assertEqual(str(park), "Test Park")
def test__park_slug__is_automatically_generated(self):
"""Test that park slug is generated from name."""
park = ParkFactory(name="Amazing Theme Park", operator=self.company)
self.assertEqual(park.slug, "amazing-theme-park")
def test__park_constraints__closing_date_after_opening__is_enforced(self):
"""Test that closing date must be after opening date."""
with self.assertRaises(IntegrityError):
ParkFactory(
operator=self.company,
opening_date=date(2020, 6, 1),
closing_date=date(2020, 5, 1), # Before opening
)
def test__park_constraints__positive_size__is_enforced(self):
"""Test that park size must be positive."""
with self.assertRaises(IntegrityError):
ParkFactory(operator=self.company, size_acres=-10)
def test__park_constraints__rating_range__is_enforced(self):
"""Test that rating must be within valid range."""
# Test upper bound
with self.assertRaises(IntegrityError):
ParkFactory(operator=self.company, average_rating=11)
# Test lower bound
with self.assertRaises(IntegrityError):
ParkFactory(operator=self.company, average_rating=0)
def test__park_constraints__coaster_count_lte_ride_count__is_enforced(
self,
):
"""Test that coaster count cannot exceed ride count."""
with self.assertRaises(IntegrityError):
ParkFactory(
operator=self.company,
ride_count=5,
coaster_count=10, # More coasters than total rides
)
def test__park_relationships__operator_is_required(self):
"""Test that park must have an operator."""
with self.assertRaises(IntegrityError):
Park.objects.create(
name="Test Park",
slug="test-park",
# Missing operator
)
def test__park_relationships__property_owner_is_optional(self):
"""Test that property owner is optional."""
park = ParkFactory(operator=self.company, property_owner=None)
self.assertIsNone(park.property_owner)
self.assertEqual(park.operator, self.company)
class TestParkModelManagers(TestCase):
"""Test cases for Park model custom managers."""
def setUp(self):
"""Set up test data."""
self.company = CompanyFactory(roles=["OPERATOR"])
self.operating_park = ParkFactory(operator=self.company, status="OPERATING")
self.closed_park = ParkFactory(operator=self.company, status="CLOSED_TEMP")
def test__park_manager__operating_filter__returns_only_operating_parks(
self,
):
"""Test that operating() filter returns only operating parks."""
operating_parks = Park.objects.operating()
self.assertEqual(operating_parks.count(), 1)
self.assertEqual(operating_parks.first(), self.operating_park)
def test__park_manager__closed_filter__returns_only_closed_parks(self):
"""Test that closed() filter returns only closed parks."""
closed_parks = Park.objects.closed()
self.assertEqual(closed_parks.count(), 1)
self.assertEqual(closed_parks.first(), self.closed_park)
def test__park_manager__optimized_for_list__includes_stats(self):
"""Test that optimized_for_list includes statistical annotations."""
parks = Park.objects.optimized_for_list()
park = parks.first()
# Check that statistical fields are available
self.assertTrue(hasattr(park, "ride_count_calculated"))
self.assertTrue(hasattr(park, "coaster_count_calculated"))
self.assertTrue(hasattr(park, "area_count"))
class TestParkAreaModel(TestCase):
"""Test cases for the ParkArea model."""
def setUp(self):
"""Set up test data."""
self.company = CompanyFactory(roles=["OPERATOR"])
self.park = ParkFactory(operator=self.company)
def test__park_area_creation__with_valid_data__succeeds(self):
"""Test that park area can be created with valid data."""
area = ParkAreaFactory(park=self.park)
self.assertIsInstance(area.id, int)
self.assertEqual(area.park, self.park)
self.assertIsNotNone(area.created_at)
def test__park_area_unique_constraint__park_and_slug__is_enforced(self):
"""Test that park+slug combination must be unique."""
ParkAreaFactory(park=self.park, slug="test-area")
with self.assertRaises(IntegrityError):
ParkAreaFactory(park=self.park, slug="test-area") # Duplicate
class TestCompanyModel(TestCase):
"""Test cases for the Company model."""
def test__company_creation__with_valid_data__succeeds(self):
"""Test that company can be created with valid data."""
company = CompanyFactory()
self.assertIsInstance(company.id, int)
self.assertIsInstance(company.roles, list)
self.assertIsNotNone(company.created_at)
def test__company_manager__operators_filter__returns_only_operators(self):
"""Test that operators() filter works correctly."""
operator = CompanyFactory(roles=["OPERATOR"])
manufacturer = CompanyFactory(roles=["MANUFACTURER"])
operators = Company.objects.operators()
self.assertIn(operator, operators)
self.assertNotIn(manufacturer, operators)
class TestParkReviewModel(TestCase):
"""Test cases for the ParkReview model."""
def setUp(self):
"""Set up test data."""
self.company = CompanyFactory(roles=["OPERATOR"])
self.park = ParkFactory(operator=self.company)
self.user = UserFactory()
def test__park_review_creation__with_valid_data__succeeds(self):
"""Test that park review can be created with valid data."""
review = ParkReviewFactory(park=self.park, user=self.user)
self.assertIsInstance(review.id, int)
self.assertEqual(review.park, self.park)
self.assertEqual(review.user, self.user)
self.assertTrue(1 <= review.rating <= 10)
def test__park_review_constraints__rating_range__is_enforced(self):
"""Test that review rating must be within valid range."""
with self.assertRaises(IntegrityError):
ParkReviewFactory(park=self.park, user=self.user, rating=11)
def test__park_review_constraints__visit_date_not_future__is_enforced(
self,
):
"""Test that visit date cannot be in the future."""
future_date = timezone.now().date() + timedelta(days=1)
with self.assertRaises(IntegrityError):
ParkReviewFactory(park=self.park, user=self.user, visit_date=future_date)
def test__park_review_unique_constraint__park_and_user__is_enforced(self):
"""Test that user can only review each park once."""
ParkReviewFactory(park=self.park, user=self.user)
with self.assertRaises(IntegrityError):
ParkReviewFactory(park=self.park, user=self.user) # Duplicate
class TestParkModelIntegration(TestCase):
"""Integration tests for Park model with related models."""
def test__complete_park_scenario__with_all_relationships__works_correctly(
self,
):
"""Test complete park creation with all relationships."""
scenario = TestScenarios.complete_park_with_rides(num_rides=3)
park = scenario["park"]
scenario["rides"]
areas = scenario["areas"]
reviews = scenario["reviews"]
# Verify all relationships are properly created
self.assertEqual(park.rides.count(), 3)
self.assertEqual(park.areas.count(), len(areas))
self.assertEqual(park.reviews.filter(is_published=True).count(), len(reviews))
# Test that park statistics are calculated correctly
parks_with_stats = Park.objects.with_complete_stats()
park_with_stats = parks_with_stats.get(id=park.id)
self.assertEqual(park_with_stats.ride_count_calculated, 3)
self.assertIsNotNone(park_with_stats.average_rating_calculated)

View File

@@ -0,0 +1,126 @@
#!/usr/bin/env python
import os
import sys
import django
from django.conf import settings
from django.test.runner import DiscoverRunner
import coverage # type: ignore
def setup_django():
"""Set up Django test environment"""
# Add the project root directory to Python path
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, project_root)
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.django.test")
django.setup()
# Use PostGIS for GeoDjango support
settings.DATABASES = {
"default": {
"ENGINE": "django.contrib.gis.db.backends.postgis",
"NAME": "test_thrillwiki",
"USER": "postgres",
"PASSWORD": "postgres",
"HOST": "localhost",
"PORT": "5432",
"TEST": {
"NAME": "test_thrillwiki",
},
}
}
settings.DEBUG = False
# Skip problematic migrations during tests
settings.MIGRATION_MODULES = {
"parks": None,
"operators": None,
"property_owners": None,
"location": None,
"rides": None,
"reviews": None,
}
class CustomTestRunner(DiscoverRunner):
def __init__(self, *args, **kwargs):
self.cov = coverage.Coverage(
source=[
"parks",
"operators",
"property_owners",
"location",
"rides",
"reviews",
],
omit=[
"*/migrations/*",
"*/management/*",
"*/admin.py",
"*/apps.py",
"manage.py",
],
)
self.cov.start()
super().__init__(*args, **kwargs)
def setup_databases(self, **kwargs):
"""Set up databases and ensure content types are created"""
old_config = super().setup_databases(**kwargs)
# Create necessary content types
from django.contrib.contenttypes.models import ContentType
ContentType.objects.get_or_create(app_label="parks", model="park")
return old_config
def run_suite(self, suite, **kwargs):
results = super().run_suite(suite, **kwargs)
self.cov.stop()
self.cov.save()
# Print coverage report
print("\nCoverage Report:")
self.cov.report()
# Generate HTML coverage report
html_dir = os.path.join("tests", "coverage_html")
self.cov.html_report(directory=html_dir)
print(f"\nDetailed HTML coverage report generated in: {html_dir}")
return results
def run_tests():
# Set up Django
setup_django()
# Initialize test runner
test_runner = CustomTestRunner(verbosity=2, interactive=True, keepdb=True)
# Define test labels for discovery
test_labels = [
"parks.tests",
"operators.tests",
"property_owners.tests",
"location.tests",
"rides.tests",
"reviews.tests",
]
# Run tests and collect results
failures = test_runner.run_tests(test_labels)
return failures
if __name__ == "__main__":
# Create tests directory if it doesn't exist
os.makedirs("tests", exist_ok=True)
os.makedirs(os.path.join("tests", "coverage_html"), exist_ok=True)
# Run tests and exit with appropriate status code
failures = run_tests()
sys.exit(bool(failures))

342
backend/tests/test_utils.py Normal file
View File

@@ -0,0 +1,342 @@
"""
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 django.contrib.auth import get_user_model
from rest_framework.test import APITestCase
from rest_framework import status
User = get_user_model()
class ApiTestMixin:
"""Mixin providing common API testing utilities."""
def assertApiResponse(
self,
response,
*,
status_code: int = status.HTTP_200_OK,
response_status: str = "success",
data_type: Optional[type] = None,
contains_fields: Optional[List[str]] = None,
):
"""
Assert API response has correct structure and content.
Args:
response: DRF Response object
status_code: Expected HTTP status code
response_status: Expected response status ('success' or 'error')
data_type: Expected type of response data (list, dict, etc.)
contains_fields: List of fields that should be in response data
"""
self.assertEqual(response.status_code, status_code)
self.assertEqual(response.data["status"], response_status)
if response_status == "success":
self.assertIn("data", response.data)
if data_type:
self.assertIsInstance(response.data["data"], data_type)
if contains_fields and response.data["data"]:
data = response.data["data"]
# Handle both single objects and lists
if isinstance(data, list) and data:
data = data[0]
if isinstance(data, dict):
for field in contains_fields:
self.assertIn(
field,
data,
f"Field '{field}' missing from response data",
)
elif response_status == "error":
self.assertIn("error", response.data)
self.assertIn("code", response.data["error"])
self.assertIn("message", response.data["error"])
def assertApiError(
self,
response,
*,
status_code: int,
error_code: Optional[str] = None,
message_contains: Optional[str] = None,
):
"""
Assert API response is an error with specific characteristics.
Args:
response: DRF Response object
status_code: Expected HTTP status code
error_code: Expected error code in response
message_contains: String that should be in error message
"""
self.assertApiResponse(
response, status_code=status_code, response_status="error"
)
if error_code:
self.assertEqual(response.data["error"]["code"], error_code)
if message_contains:
self.assertIn(message_contains, response.data["error"]["message"])
def assertPaginatedResponse(
self,
response,
*,
expected_count: Optional[int] = None,
has_next: Optional[bool] = None,
has_previous: Optional[bool] = None,
):
"""
Assert API response has correct pagination structure.
Args:
response: DRF Response object
expected_count: Expected number of items in current page
has_next: Whether pagination should have next page
has_previous: Whether pagination should have previous page
"""
self.assertApiResponse(response, data_type=list)
self.assertIn("pagination", response.data)
pagination = response.data["pagination"]
required_fields = [
"page",
"page_size",
"total_pages",
"total_count",
"has_next",
"has_previous",
]
for field in required_fields:
self.assertIn(field, pagination)
if expected_count is not None:
self.assertEqual(len(response.data["data"]), expected_count)
if has_next is not None:
self.assertEqual(pagination["has_next"], has_next)
if has_previous is not None:
self.assertEqual(pagination["has_previous"], has_previous)
class ModelTestMixin:
"""Mixin providing common model testing utilities."""
def assertModelFields(self, instance, expected_fields: Dict[str, Any]):
"""
Assert model instance has expected field values.
Args:
instance: Model instance
expected_fields: Dict of field_name: expected_value
"""
for field_name, expected_value in expected_fields.items():
actual_value = getattr(instance, field_name)
self.assertEqual(
actual_value,
expected_value,
f"Field '{field_name}' expected {expected_value}, got {actual_value}",
)
def assertModelValidation(
self,
model_class,
invalid_data: Dict[str, Any],
expected_errors: List[str],
):
"""
Assert model validation catches expected errors.
Args:
model_class: Model class to test
invalid_data: Data that should cause validation errors
expected_errors: List of error messages that should be raised
"""
instance = model_class(**invalid_data)
with self.assertRaises(Exception) as context:
instance.full_clean()
exception_str = str(context.exception)
for expected_error in expected_errors:
self.assertIn(expected_error, exception_str)
def assertDatabaseConstraint(self, model_factory, invalid_data: Dict[str, Any]):
"""
Assert database constraint is enforced.
Args:
model_factory: Factory class for creating model instances
invalid_data: Data that should violate database constraints
"""
from django.db import IntegrityError
with self.assertRaises(IntegrityError):
model_factory(**invalid_data)
class FactoryTestMixin:
"""Mixin providing factory testing utilities."""
def assertFactoryCreatesValidInstance(self, factory_class, **kwargs):
"""
Assert factory creates valid model instance.
Args:
factory_class: Factory class to test
**kwargs: Additional factory parameters
"""
instance = factory_class(**kwargs)
# Basic assertions
self.assertIsNotNone(instance.id)
self.assertIsNotNone(instance.created_at)
# Run full_clean to ensure validity
instance.full_clean()
return instance
def assertFactoryBatchCreation(self, factory_class, count: int = 5, **kwargs):
"""
Assert factory can create multiple valid instances.
Args:
factory_class: Factory class to test
count: Number of instances to create
**kwargs: Additional factory parameters
"""
instances = factory_class.create_batch(count, **kwargs)
self.assertEqual(len(instances), count)
for instance in instances:
self.assertIsNotNone(instance.id)
instance.full_clean()
return instances
class TimestampTestMixin:
"""Mixin for testing timestamp-related functionality."""
def assertRecentTimestamp(self, timestamp, tolerance_seconds: int = 5):
"""
Assert timestamp is recent (within tolerance).
Args:
timestamp: Timestamp to check
tolerance_seconds: Allowed difference in seconds
"""
from django.utils import timezone
now = timezone.now()
if isinstance(timestamp, date) and not isinstance(timestamp, datetime):
# Convert date to datetime for comparison
timestamp = datetime.combine(timestamp, datetime.min.time())
timestamp = timezone.make_aware(timestamp)
time_diff = abs((now - timestamp).total_seconds())
self.assertLessEqual(
time_diff,
tolerance_seconds,
f"Timestamp {timestamp} is not recent (diff: {time_diff}s)",
)
def assertTimestampOrder(self, earlier_timestamp, later_timestamp):
"""
Assert timestamps are in correct order.
Args:
earlier_timestamp: Should be before later_timestamp
later_timestamp: Should be after earlier_timestamp
"""
self.assertLess(
earlier_timestamp,
later_timestamp,
f"Timestamps not in order: {earlier_timestamp} should be before {later_timestamp}",
)
class GeographyTestMixin:
"""Mixin for testing geography-related functionality."""
def assertValidCoordinates(self, latitude: float, longitude: float):
"""
Assert coordinates are within valid ranges.
Args:
latitude: Latitude value
longitude: Longitude value
"""
self.assertGreaterEqual(latitude, -90, "Latitude below valid range")
self.assertLessEqual(latitude, 90, "Latitude above valid range")
self.assertGreaterEqual(longitude, -180, "Longitude below valid range")
self.assertLessEqual(longitude, 180, "Longitude above valid range")
def assertCoordinateDistance(
self, point1: tuple, point2: tuple, max_distance_km: float
):
"""
Assert two geographic points are within specified distance.
Args:
point1: (latitude, longitude) tuple
point2: (latitude, longitude) tuple
max_distance_km: Maximum allowed distance in kilometers
"""
from math import radians, cos, sin, asin, sqrt
lat1, lon1 = point1
lat2, lon2 = point2
# Haversine formula for great circle distance
lat1, lon1, lat2, lon2 = map(radians, [lat1, lon1, lat2, lon2])
dlat = lat2 - lat1
dlon = lon2 - lon1
a = sin(dlat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(dlon / 2) ** 2
c = 2 * asin(sqrt(a))
distance_km = 6371 * c # Earth's radius in km
self.assertLessEqual(
distance_km,
max_distance_km,
f"Points are {
distance_km:.2f}km apart, exceeds {max_distance_km}km",
)
class EnhancedTestCase(
ApiTestMixin,
ModelTestMixin,
FactoryTestMixin,
TimestampTestMixin,
GeographyTestMixin,
TestCase,
):
"""Enhanced TestCase with all testing mixins."""
class EnhancedAPITestCase(
ApiTestMixin,
ModelTestMixin,
FactoryTestMixin,
TimestampTestMixin,
GeographyTestMixin,
APITestCase,
):
"""Enhanced APITestCase with all testing mixins."""