mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 09:31:09 -05:00
feat: major project restructure - move Django to backend dir and fix critical imports
- Restructure project: moved Django backend to backend/ directory - Add frontend/ directory for future Next.js application - Add shared/ directory for common resources - Fix critical Django import errors: - Add missing sys.path modification for apps directory - Fix undefined CATEGORY_CHOICES imports in rides module - Fix media migration undefined references - Remove unused imports and f-strings without placeholders - Install missing django-environ dependency - Django server now runs without ModuleNotFoundError - Update .gitignore and README for new structure - Add pnpm workspace configuration for monorepo setup
This commit is contained in:
@@ -1,84 +0,0 @@
|
||||
# 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
|
||||
@@ -1,3 +0,0 @@
|
||||
# ThrillWiki Test Package
|
||||
# This file makes the tests directory a Python package for proper module
|
||||
# discovery
|
||||
@@ -1,116 +0,0 @@
|
||||
# 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
|
||||
@@ -1,25 +0,0 @@
|
||||
#!/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!"
|
||||
@@ -1,101 +0,0 @@
|
||||
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
|
||||
@@ -1,28 +0,0 @@
|
||||
[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
|
||||
@@ -1,55 +0,0 @@
|
||||
#!/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"
|
||||
@@ -1,91 +0,0 @@
|
||||
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()
|
||||
@@ -1,130 +0,0 @@
|
||||
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)
|
||||
@@ -1,171 +0,0 @@
|
||||
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()
|
||||
@@ -1,160 +0,0 @@
|
||||
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()
|
||||
@@ -1,165 +0,0 @@
|
||||
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()
|
||||
@@ -1,362 +0,0 @@
|
||||
"""
|
||||
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
tests/fixtures/README.md
vendored
33
tests/fixtures/README.md
vendored
@@ -1,33 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,244 +0,0 @@
|
||||
"""
|
||||
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)
|
||||
@@ -1,382 +0,0 @@
|
||||
"""
|
||||
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 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)
|
||||
@@ -1,249 +0,0 @@
|
||||
"""
|
||||
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 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)
|
||||
@@ -1,126 +0,0 @@
|
||||
#!/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))
|
||||
@@ -1,342 +0,0 @@
|
||||
"""
|
||||
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."""
|
||||
Reference in New Issue
Block a user