mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-23 14:51:08 -05:00
Add standardized HTMX conventions, interaction patterns, and migration guide for ThrillWiki UX
This commit is contained in:
6
backend/tests/api/__init__.py
Normal file
6
backend/tests/api/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""
|
||||
API consistency tests.
|
||||
|
||||
This module contains tests to verify API response format consistency,
|
||||
pagination, filtering, and error handling across all endpoints.
|
||||
"""
|
||||
596
backend/tests/api/test_auth_api.py
Normal file
596
backend/tests/api/test_auth_api.py
Normal file
@@ -0,0 +1,596 @@
|
||||
"""
|
||||
Comprehensive tests for Auth API endpoints.
|
||||
|
||||
This module provides extensive test coverage for:
|
||||
- LoginAPIView: User login with JWT tokens
|
||||
- SignupAPIView: User registration with email verification
|
||||
- LogoutAPIView: User logout with token blacklisting
|
||||
- CurrentUserAPIView: Get current user info
|
||||
- PasswordResetAPIView: Password reset request
|
||||
- PasswordChangeAPIView: Password change for authenticated users
|
||||
- SocialProvidersAPIView: Available social providers
|
||||
- AuthStatusAPIView: Check authentication status
|
||||
- EmailVerificationAPIView: Email verification
|
||||
- ResendVerificationAPIView: Resend verification email
|
||||
|
||||
Test patterns follow Django styleguide conventions.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase, APIClient
|
||||
|
||||
from tests.factories import (
|
||||
UserFactory,
|
||||
StaffUserFactory,
|
||||
SuperUserFactory,
|
||||
)
|
||||
from tests.test_utils import EnhancedAPITestCase
|
||||
|
||||
|
||||
class TestLoginAPIView(EnhancedAPITestCase):
|
||||
"""Test cases for LoginAPIView."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.user = UserFactory()
|
||||
self.user.set_password('testpass123')
|
||||
self.user.save()
|
||||
self.url = '/api/v1/auth/login/'
|
||||
|
||||
def test__login__with_valid_credentials__returns_tokens(self):
|
||||
"""Test successful login returns JWT tokens."""
|
||||
response = self.client.post(self.url, {
|
||||
'username': self.user.username,
|
||||
'password': 'testpass123'
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIn('access', response.data)
|
||||
self.assertIn('refresh', response.data)
|
||||
self.assertIn('user', response.data)
|
||||
|
||||
def test__login__with_email__returns_tokens(self):
|
||||
"""Test login with email instead of username."""
|
||||
response = self.client.post(self.url, {
|
||||
'username': self.user.email,
|
||||
'password': 'testpass123'
|
||||
})
|
||||
|
||||
self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_400_BAD_REQUEST])
|
||||
|
||||
def test__login__with_invalid_password__returns_400(self):
|
||||
"""Test login with wrong password returns error."""
|
||||
response = self.client.post(self.url, {
|
||||
'username': self.user.username,
|
||||
'password': 'wrongpassword'
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn('error', response.data)
|
||||
|
||||
def test__login__with_nonexistent_user__returns_400(self):
|
||||
"""Test login with nonexistent username returns error."""
|
||||
response = self.client.post(self.url, {
|
||||
'username': 'nonexistentuser',
|
||||
'password': 'testpass123'
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test__login__with_missing_username__returns_400(self):
|
||||
"""Test login without username returns error."""
|
||||
response = self.client.post(self.url, {
|
||||
'password': 'testpass123'
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test__login__with_missing_password__returns_400(self):
|
||||
"""Test login without password returns error."""
|
||||
response = self.client.post(self.url, {
|
||||
'username': self.user.username
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test__login__with_empty_credentials__returns_400(self):
|
||||
"""Test login with empty credentials returns error."""
|
||||
response = self.client.post(self.url, {})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test__login__inactive_user__returns_error(self):
|
||||
"""Test login with inactive user returns appropriate error."""
|
||||
self.user.is_active = False
|
||||
self.user.save()
|
||||
|
||||
response = self.client.post(self.url, {
|
||||
'username': self.user.username,
|
||||
'password': 'testpass123'
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class TestSignupAPIView(EnhancedAPITestCase):
|
||||
"""Test cases for SignupAPIView."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.url = '/api/v1/auth/signup/'
|
||||
self.valid_data = {
|
||||
'username': 'newuser',
|
||||
'email': 'newuser@example.com',
|
||||
'password1': 'ComplexPass123!',
|
||||
'password2': 'ComplexPass123!'
|
||||
}
|
||||
|
||||
def test__signup__with_valid_data__creates_user(self):
|
||||
"""Test successful signup creates user."""
|
||||
response = self.client.post(self.url, self.valid_data)
|
||||
|
||||
self.assertIn(response.status_code, [status.HTTP_201_CREATED, status.HTTP_400_BAD_REQUEST])
|
||||
|
||||
def test__signup__with_existing_username__returns_400(self):
|
||||
"""Test signup with existing username returns error."""
|
||||
UserFactory(username='existinguser')
|
||||
|
||||
data = self.valid_data.copy()
|
||||
data['username'] = 'existinguser'
|
||||
|
||||
response = self.client.post(self.url, data)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test__signup__with_existing_email__returns_400(self):
|
||||
"""Test signup with existing email returns error."""
|
||||
UserFactory(email='existing@example.com')
|
||||
|
||||
data = self.valid_data.copy()
|
||||
data['email'] = 'existing@example.com'
|
||||
|
||||
response = self.client.post(self.url, data)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test__signup__with_password_mismatch__returns_400(self):
|
||||
"""Test signup with mismatched passwords returns error."""
|
||||
data = self.valid_data.copy()
|
||||
data['password2'] = 'DifferentPass123!'
|
||||
|
||||
response = self.client.post(self.url, data)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test__signup__with_weak_password__returns_400(self):
|
||||
"""Test signup with weak password returns error."""
|
||||
data = self.valid_data.copy()
|
||||
data['password1'] = '123'
|
||||
data['password2'] = '123'
|
||||
|
||||
response = self.client.post(self.url, data)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test__signup__with_invalid_email__returns_400(self):
|
||||
"""Test signup with invalid email returns error."""
|
||||
data = self.valid_data.copy()
|
||||
data['email'] = 'notanemail'
|
||||
|
||||
response = self.client.post(self.url, data)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test__signup__with_missing_fields__returns_400(self):
|
||||
"""Test signup with missing required fields returns error."""
|
||||
response = self.client.post(self.url, {'username': 'onlyusername'})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class TestLogoutAPIView(EnhancedAPITestCase):
|
||||
"""Test cases for LogoutAPIView."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.user = UserFactory()
|
||||
self.url = '/api/v1/auth/logout/'
|
||||
|
||||
def test__logout__authenticated_user__returns_success(self):
|
||||
"""Test successful logout for authenticated user."""
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
response = self.client.post(self.url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIn('message', response.data)
|
||||
|
||||
def test__logout__unauthenticated_user__returns_401(self):
|
||||
"""Test logout without authentication returns 401."""
|
||||
response = self.client.post(self.url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
def test__logout__with_refresh_token__blacklists_token(self):
|
||||
"""Test logout with refresh token blacklists the token."""
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
# Simulate providing a refresh token
|
||||
response = self.client.post(self.url, {'refresh': 'dummy-token'})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
|
||||
class TestCurrentUserAPIView(EnhancedAPITestCase):
|
||||
"""Test cases for CurrentUserAPIView."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.user = UserFactory()
|
||||
self.url = '/api/v1/auth/user/'
|
||||
|
||||
def test__current_user__authenticated__returns_user_data(self):
|
||||
"""Test getting current user data when authenticated."""
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
response = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['username'], self.user.username)
|
||||
|
||||
def test__current_user__unauthenticated__returns_401(self):
|
||||
"""Test getting current user without auth returns 401."""
|
||||
response = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
|
||||
class TestPasswordResetAPIView(EnhancedAPITestCase):
|
||||
"""Test cases for PasswordResetAPIView."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.user = UserFactory()
|
||||
self.url = '/api/v1/auth/password/reset/'
|
||||
|
||||
def test__password_reset__with_valid_email__returns_success(self):
|
||||
"""Test password reset request with valid email."""
|
||||
response = self.client.post(self.url, {'email': self.user.email})
|
||||
|
||||
# Should return success (don't reveal if email exists)
|
||||
self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_400_BAD_REQUEST])
|
||||
|
||||
def test__password_reset__with_nonexistent_email__returns_success(self):
|
||||
"""Test password reset with nonexistent email returns success (security)."""
|
||||
response = self.client.post(self.url, {'email': 'nonexistent@example.com'})
|
||||
|
||||
# Should return success to not reveal email existence
|
||||
self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_400_BAD_REQUEST])
|
||||
|
||||
def test__password_reset__with_missing_email__returns_400(self):
|
||||
"""Test password reset without email returns error."""
|
||||
response = self.client.post(self.url, {})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test__password_reset__with_invalid_email_format__returns_400(self):
|
||||
"""Test password reset with invalid email format returns error."""
|
||||
response = self.client.post(self.url, {'email': 'notanemail'})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class TestPasswordChangeAPIView(EnhancedAPITestCase):
|
||||
"""Test cases for PasswordChangeAPIView."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.user = UserFactory()
|
||||
self.user.set_password('oldpassword123')
|
||||
self.user.save()
|
||||
self.url = '/api/v1/auth/password/change/'
|
||||
|
||||
def test__password_change__with_valid_data__changes_password(self):
|
||||
"""Test password change with valid data."""
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
response = self.client.post(self.url, {
|
||||
'old_password': 'oldpassword123',
|
||||
'new_password1': 'NewComplexPass123!',
|
||||
'new_password2': 'NewComplexPass123!'
|
||||
})
|
||||
|
||||
self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_400_BAD_REQUEST])
|
||||
|
||||
def test__password_change__with_wrong_old_password__returns_400(self):
|
||||
"""Test password change with wrong old password."""
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
response = self.client.post(self.url, {
|
||||
'old_password': 'wrongpassword',
|
||||
'new_password1': 'NewComplexPass123!',
|
||||
'new_password2': 'NewComplexPass123!'
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test__password_change__unauthenticated__returns_401(self):
|
||||
"""Test password change without authentication."""
|
||||
response = self.client.post(self.url, {
|
||||
'old_password': 'oldpassword123',
|
||||
'new_password1': 'NewComplexPass123!',
|
||||
'new_password2': 'NewComplexPass123!'
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
|
||||
class TestSocialProvidersAPIView(EnhancedAPITestCase):
|
||||
"""Test cases for SocialProvidersAPIView."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.url = '/api/v1/auth/social/providers/'
|
||||
|
||||
def test__social_providers__returns_list(self):
|
||||
"""Test getting list of social providers."""
|
||||
response = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIsInstance(response.data, list)
|
||||
|
||||
|
||||
class TestAuthStatusAPIView(EnhancedAPITestCase):
|
||||
"""Test cases for AuthStatusAPIView."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.user = UserFactory()
|
||||
self.url = '/api/v1/auth/status/'
|
||||
|
||||
def test__auth_status__authenticated__returns_authenticated_true(self):
|
||||
"""Test auth status for authenticated user."""
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
response = self.client.post(self.url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertTrue(response.data.get('authenticated'))
|
||||
self.assertIsNotNone(response.data.get('user'))
|
||||
|
||||
def test__auth_status__unauthenticated__returns_authenticated_false(self):
|
||||
"""Test auth status for unauthenticated user."""
|
||||
response = self.client.post(self.url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertFalse(response.data.get('authenticated'))
|
||||
|
||||
|
||||
class TestAvailableProvidersAPIView(EnhancedAPITestCase):
|
||||
"""Test cases for AvailableProvidersAPIView."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.url = '/api/v1/auth/social/available/'
|
||||
|
||||
def test__available_providers__returns_provider_list(self):
|
||||
"""Test getting available social providers."""
|
||||
response = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIsInstance(response.data, list)
|
||||
|
||||
|
||||
class TestConnectedProvidersAPIView(EnhancedAPITestCase):
|
||||
"""Test cases for ConnectedProvidersAPIView."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.user = UserFactory()
|
||||
self.url = '/api/v1/auth/social/connected/'
|
||||
|
||||
def test__connected_providers__authenticated__returns_list(self):
|
||||
"""Test getting connected providers for authenticated user."""
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
response = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIsInstance(response.data, list)
|
||||
|
||||
def test__connected_providers__unauthenticated__returns_401(self):
|
||||
"""Test getting connected providers without auth."""
|
||||
response = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
|
||||
class TestConnectProviderAPIView(EnhancedAPITestCase):
|
||||
"""Test cases for ConnectProviderAPIView."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.user = UserFactory()
|
||||
|
||||
def test__connect_provider__unauthenticated__returns_401(self):
|
||||
"""Test connecting provider without auth."""
|
||||
response = self.client.post('/api/v1/auth/social/connect/google/', {
|
||||
'access_token': 'dummy-token'
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
def test__connect_provider__invalid_provider__returns_400(self):
|
||||
"""Test connecting invalid provider."""
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
response = self.client.post('/api/v1/auth/social/connect/invalid/', {
|
||||
'access_token': 'dummy-token'
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test__connect_provider__missing_token__returns_400(self):
|
||||
"""Test connecting provider without token."""
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
response = self.client.post('/api/v1/auth/social/connect/google/', {})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class TestDisconnectProviderAPIView(EnhancedAPITestCase):
|
||||
"""Test cases for DisconnectProviderAPIView."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.user = UserFactory()
|
||||
|
||||
def test__disconnect_provider__unauthenticated__returns_401(self):
|
||||
"""Test disconnecting provider without auth."""
|
||||
response = self.client.post('/api/v1/auth/social/disconnect/google/')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
def test__disconnect_provider__invalid_provider__returns_400(self):
|
||||
"""Test disconnecting invalid provider."""
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
response = self.client.post('/api/v1/auth/social/disconnect/invalid/')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class TestSocialAuthStatusAPIView(EnhancedAPITestCase):
|
||||
"""Test cases for SocialAuthStatusAPIView."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.user = UserFactory()
|
||||
self.url = '/api/v1/auth/social/status/'
|
||||
|
||||
def test__social_auth_status__authenticated__returns_status(self):
|
||||
"""Test getting social auth status."""
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
response = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__social_auth_status__unauthenticated__returns_401(self):
|
||||
"""Test getting social auth status without auth."""
|
||||
response = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
|
||||
class TestEmailVerificationAPIView(EnhancedAPITestCase):
|
||||
"""Test cases for EmailVerificationAPIView."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
|
||||
def test__email_verification__invalid_token__returns_404(self):
|
||||
"""Test email verification with invalid token."""
|
||||
response = self.client.get('/api/v1/auth/verify-email/invalid-token/')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
||||
class TestResendVerificationAPIView(EnhancedAPITestCase):
|
||||
"""Test cases for ResendVerificationAPIView."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.user = UserFactory(is_active=False)
|
||||
self.url = '/api/v1/auth/resend-verification/'
|
||||
|
||||
def test__resend_verification__missing_email__returns_400(self):
|
||||
"""Test resend verification without email."""
|
||||
response = self.client.post(self.url, {})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test__resend_verification__already_verified__returns_400(self):
|
||||
"""Test resend verification for already verified user."""
|
||||
active_user = UserFactory(is_active=True)
|
||||
|
||||
response = self.client.post(self.url, {'email': active_user.email})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test__resend_verification__nonexistent_email__returns_success(self):
|
||||
"""Test resend verification for nonexistent email (security)."""
|
||||
response = self.client.post(self.url, {'email': 'nonexistent@example.com'})
|
||||
|
||||
# Should return success to not reveal email existence
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
|
||||
class TestAuthAPIEdgeCases(EnhancedAPITestCase):
|
||||
"""Test cases for edge cases in auth APIs."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
|
||||
def test__login__with_special_characters_in_username__handled_safely(self):
|
||||
"""Test login with special characters in username."""
|
||||
special_usernames = [
|
||||
"user<script>alert(1)</script>",
|
||||
"user'; DROP TABLE users;--",
|
||||
"user&password=hacked",
|
||||
]
|
||||
|
||||
for username in special_usernames:
|
||||
response = self.client.post('/api/v1/auth/login/', {
|
||||
'username': username,
|
||||
'password': 'testpass123'
|
||||
})
|
||||
# Should not crash, return appropriate error
|
||||
self.assertIn(response.status_code, [
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
status.HTTP_401_UNAUTHORIZED
|
||||
])
|
||||
|
||||
def test__signup__with_very_long_username__handled_safely(self):
|
||||
"""Test signup with very long username."""
|
||||
response = self.client.post('/api/v1/auth/signup/', {
|
||||
'username': 'a' * 1000,
|
||||
'email': 'test@example.com',
|
||||
'password1': 'ComplexPass123!',
|
||||
'password2': 'ComplexPass123!'
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test__login__with_unicode_characters__handled_safely(self):
|
||||
"""Test login with unicode characters."""
|
||||
response = self.client.post('/api/v1/auth/login/', {
|
||||
'username': 'user\u202e',
|
||||
'password': 'pass\u202e'
|
||||
})
|
||||
|
||||
self.assertIn(response.status_code, [
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
status.HTTP_401_UNAUTHORIZED
|
||||
])
|
||||
120
backend/tests/api/test_error_handling.py
Normal file
120
backend/tests/api/test_error_handling.py
Normal file
@@ -0,0 +1,120 @@
|
||||
"""
|
||||
Tests for API error handling consistency.
|
||||
|
||||
These tests verify that all error responses follow the standardized format
|
||||
with proper error codes, messages, and details.
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
from rest_framework.test import APIClient
|
||||
from rest_framework import status
|
||||
|
||||
|
||||
class ErrorResponseFormatTestCase(TestCase):
|
||||
"""Tests for standardized error response format."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test client."""
|
||||
self.client = APIClient()
|
||||
|
||||
def test_404_error_format(self):
|
||||
"""Test that 404 errors follow standardized format."""
|
||||
response = self.client.get("/api/v1/parks/nonexistent-slug/")
|
||||
|
||||
if response.status_code == status.HTTP_404_NOT_FOUND:
|
||||
data = response.json()
|
||||
# Should have error information
|
||||
self.assertTrue(
|
||||
"error" in data or "detail" in data or "status" in data,
|
||||
"404 response should contain error information"
|
||||
)
|
||||
|
||||
def test_400_error_format(self):
|
||||
"""Test that 400 validation errors follow standardized format."""
|
||||
response = self.client.get(
|
||||
"/api/v1/rides/hybrid/",
|
||||
{"offset": "invalid"}
|
||||
)
|
||||
|
||||
if response.status_code == status.HTTP_400_BAD_REQUEST:
|
||||
data = response.json()
|
||||
# Should have error information
|
||||
self.assertTrue(
|
||||
"error" in data or "status" in data or "detail" in data,
|
||||
"400 response should contain error information"
|
||||
)
|
||||
|
||||
def test_500_error_format(self):
|
||||
"""Test that 500 errors follow standardized format."""
|
||||
# This is harder to test directly, but we can verify the handler exists
|
||||
pass
|
||||
|
||||
|
||||
class ErrorCodeConsistencyTestCase(TestCase):
|
||||
"""Tests for consistent error codes."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test client."""
|
||||
self.client = APIClient()
|
||||
|
||||
def test_validation_error_code(self):
|
||||
"""Test that validation errors use consistent error codes."""
|
||||
response = self.client.get(
|
||||
"/api/v1/rides/hybrid/",
|
||||
{"offset": "invalid"}
|
||||
)
|
||||
|
||||
if response.status_code == status.HTTP_400_BAD_REQUEST:
|
||||
data = response.json()
|
||||
if "error" in data and isinstance(data["error"], dict):
|
||||
self.assertIn("code", data["error"])
|
||||
self.assertEqual(data["error"]["code"], "VALIDATION_ERROR")
|
||||
|
||||
|
||||
class AuthenticationErrorTestCase(TestCase):
|
||||
"""Tests for authentication error handling."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test client."""
|
||||
self.client = APIClient()
|
||||
|
||||
def test_unauthorized_error_format(self):
|
||||
"""Test that unauthorized errors are properly formatted."""
|
||||
# Try to access protected endpoint without auth
|
||||
response = self.client.get("/api/v1/accounts/profile/")
|
||||
|
||||
if response.status_code == status.HTTP_401_UNAUTHORIZED:
|
||||
data = response.json()
|
||||
# Should have error information
|
||||
self.assertTrue(
|
||||
"error" in data or "detail" in data,
|
||||
"401 response should contain error information"
|
||||
)
|
||||
|
||||
def test_forbidden_error_format(self):
|
||||
"""Test that forbidden errors are properly formatted."""
|
||||
# This would need authentication to test properly
|
||||
pass
|
||||
|
||||
|
||||
class ExceptionHandlerTestCase(TestCase):
|
||||
"""Tests for the custom exception handler."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test client."""
|
||||
self.client = APIClient()
|
||||
|
||||
def test_custom_exception_handler_is_configured(self):
|
||||
"""Test that custom exception handler is configured."""
|
||||
from django.conf import settings
|
||||
|
||||
exception_handler = settings.REST_FRAMEWORK.get("EXCEPTION_HANDLER")
|
||||
self.assertEqual(
|
||||
exception_handler,
|
||||
"apps.core.api.exceptions.custom_exception_handler"
|
||||
)
|
||||
|
||||
def test_throttled_error_format(self):
|
||||
"""Test that throttled errors are properly formatted."""
|
||||
# This would need many rapid requests to trigger throttling
|
||||
pass
|
||||
146
backend/tests/api/test_filters.py
Normal file
146
backend/tests/api/test_filters.py
Normal file
@@ -0,0 +1,146 @@
|
||||
"""
|
||||
Tests for API filter and search parameter consistency.
|
||||
|
||||
These tests verify that filter parameters are named consistently across
|
||||
similar endpoints and behave as expected.
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
from rest_framework.test import APIClient
|
||||
from rest_framework import status
|
||||
|
||||
|
||||
class FilterParameterNamingTestCase(TestCase):
|
||||
"""Tests for consistent filter parameter naming."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test client."""
|
||||
self.client = APIClient()
|
||||
|
||||
def test_range_filter_naming_convention(self):
|
||||
"""Test that range filters use {field}_min/{field}_max naming."""
|
||||
# Test parks rating range filter
|
||||
response = self.client.get(
|
||||
"/api/v1/parks/hybrid/",
|
||||
{"rating_min": 3.0, "rating_max": 5.0}
|
||||
)
|
||||
# Should not return error for valid filter names
|
||||
self.assertIn(
|
||||
response.status_code,
|
||||
[status.HTTP_200_OK, status.HTTP_400_BAD_REQUEST]
|
||||
)
|
||||
|
||||
def test_search_parameter_naming(self):
|
||||
"""Test that search parameter is named consistently."""
|
||||
response = self.client.get("/api/v1/parks/hybrid/", {"search": "cedar"})
|
||||
self.assertIn(
|
||||
response.status_code,
|
||||
[status.HTTP_200_OK, status.HTTP_400_BAD_REQUEST]
|
||||
)
|
||||
|
||||
def test_ordering_parameter_naming(self):
|
||||
"""Test that ordering parameter is named consistently."""
|
||||
response = self.client.get("/api/v1/parks/hybrid/", {"ordering": "name"})
|
||||
self.assertIn(
|
||||
response.status_code,
|
||||
[status.HTTP_200_OK, status.HTTP_400_BAD_REQUEST]
|
||||
)
|
||||
|
||||
def test_ordering_descending_prefix(self):
|
||||
"""Test that descending ordering uses - prefix."""
|
||||
response = self.client.get("/api/v1/parks/hybrid/", {"ordering": "-name"})
|
||||
self.assertIn(
|
||||
response.status_code,
|
||||
[status.HTTP_200_OK, status.HTTP_400_BAD_REQUEST]
|
||||
)
|
||||
|
||||
|
||||
class FilterBehaviorTestCase(TestCase):
|
||||
"""Tests for consistent filter behavior."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test client."""
|
||||
self.client = APIClient()
|
||||
|
||||
def test_filter_combination_and_logic(self):
|
||||
"""Test that multiple different filters use AND logic."""
|
||||
response = self.client.get(
|
||||
"/api/v1/parks/hybrid/",
|
||||
{"rating_min": 4.0, "country": "us"}
|
||||
)
|
||||
if response.status_code == status.HTTP_200_OK:
|
||||
data = response.json()
|
||||
# Results should match both criteria
|
||||
self.assertIn("success", data)
|
||||
|
||||
def test_multi_select_filter_or_logic(self):
|
||||
"""Test that multi-select filters within same field use OR logic."""
|
||||
response = self.client.get(
|
||||
"/api/v1/rides/hybrid/",
|
||||
{"ride_type": "Coaster,Dark Ride"}
|
||||
)
|
||||
if response.status_code == status.HTTP_200_OK:
|
||||
data = response.json()
|
||||
self.assertIn("success", data)
|
||||
|
||||
def test_invalid_filter_value_returns_error(self):
|
||||
"""Test that invalid filter values return appropriate error."""
|
||||
response = self.client.get(
|
||||
"/api/v1/parks/hybrid/",
|
||||
{"rating_min": "not_a_number"}
|
||||
)
|
||||
# Could be 200 (ignored) or 400 (validation error)
|
||||
self.assertIn(
|
||||
response.status_code,
|
||||
[status.HTTP_200_OK, status.HTTP_400_BAD_REQUEST]
|
||||
)
|
||||
|
||||
|
||||
class FilterMetadataTestCase(TestCase):
|
||||
"""Tests for filter metadata endpoint consistency."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test client."""
|
||||
self.client = APIClient()
|
||||
|
||||
def test_parks_filter_metadata_structure(self):
|
||||
"""Test parks filter metadata has expected structure."""
|
||||
response = self.client.get("/api/v1/parks/filter-metadata/")
|
||||
|
||||
if response.status_code == status.HTTP_200_OK:
|
||||
data = response.json()
|
||||
self.assertIn("success", data)
|
||||
self.assertIn("data", data)
|
||||
|
||||
if data.get("data"):
|
||||
metadata = data["data"]
|
||||
# Should have categorical and/or ranges
|
||||
self.assertTrue(
|
||||
"categorical" in metadata or "ranges" in metadata or
|
||||
"total_count" in metadata or "ordering_options" in metadata,
|
||||
"Filter metadata should contain filter options"
|
||||
)
|
||||
|
||||
def test_rides_filter_metadata_structure(self):
|
||||
"""Test rides filter metadata has expected structure."""
|
||||
response = self.client.get("/api/v1/rides/filter-metadata/")
|
||||
|
||||
if response.status_code == status.HTTP_200_OK:
|
||||
data = response.json()
|
||||
self.assertIn("success", data)
|
||||
self.assertIn("data", data)
|
||||
|
||||
def test_filter_option_format(self):
|
||||
"""Test that filter options have consistent format."""
|
||||
response = self.client.get("/api/v1/parks/filter-metadata/")
|
||||
|
||||
if response.status_code == status.HTTP_200_OK:
|
||||
data = response.json()
|
||||
if data.get("data") and data["data"].get("categorical"):
|
||||
for field, options in data["data"]["categorical"].items():
|
||||
if isinstance(options, list) and options:
|
||||
option = options[0]
|
||||
# Each option should have value and label
|
||||
if isinstance(option, dict):
|
||||
self.assertIn("value", option)
|
||||
self.assertIn("label", option)
|
||||
118
backend/tests/api/test_pagination.py
Normal file
118
backend/tests/api/test_pagination.py
Normal file
@@ -0,0 +1,118 @@
|
||||
"""
|
||||
Tests for API pagination consistency.
|
||||
|
||||
These tests verify that all paginated endpoints return consistent pagination
|
||||
metadata including count, next, previous, page_size, current_page, and total_pages.
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
from rest_framework.test import APIClient
|
||||
from rest_framework import status
|
||||
|
||||
|
||||
class PaginationMetadataTestCase(TestCase):
|
||||
"""Tests for standardized pagination metadata."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test client."""
|
||||
self.client = APIClient()
|
||||
|
||||
def test_pagination_metadata_fields(self):
|
||||
"""Test that paginated responses include standard metadata fields."""
|
||||
response = self.client.get("/api/v1/parks/")
|
||||
|
||||
if response.status_code == status.HTTP_200_OK:
|
||||
data = response.json()
|
||||
# Check for pagination metadata in either root or nested format
|
||||
if "count" in data:
|
||||
# Standard DRF pagination format
|
||||
self.assertIn("count", data)
|
||||
self.assertIn("results", data)
|
||||
elif "data" in data and isinstance(data["data"], dict):
|
||||
# Check nested format for hybrid endpoints
|
||||
result = data["data"]
|
||||
if "total_count" in result:
|
||||
self.assertIn("total_count", result)
|
||||
|
||||
def test_page_size_limits(self):
|
||||
"""Test that page_size parameter is respected."""
|
||||
response = self.client.get("/api/v1/parks/", {"page_size": 5})
|
||||
|
||||
if response.status_code == status.HTTP_200_OK:
|
||||
data = response.json()
|
||||
if "results" in data:
|
||||
self.assertLessEqual(len(data["results"]), 5)
|
||||
|
||||
def test_max_page_size_limit(self):
|
||||
"""Test that maximum page size limit is enforced."""
|
||||
# Request more than max (100 items)
|
||||
response = self.client.get("/api/v1/parks/", {"page_size": 200})
|
||||
|
||||
if response.status_code == status.HTTP_200_OK:
|
||||
data = response.json()
|
||||
if "results" in data:
|
||||
# Should be capped at 100
|
||||
self.assertLessEqual(len(data["results"]), 100)
|
||||
|
||||
def test_page_navigation(self):
|
||||
"""Test that next and previous URLs are correctly generated."""
|
||||
response = self.client.get("/api/v1/parks/", {"page": 1, "page_size": 10})
|
||||
|
||||
if response.status_code == status.HTTP_200_OK:
|
||||
data = response.json()
|
||||
if "count" in data and data["count"] > 10:
|
||||
# Should have a next URL
|
||||
self.assertIsNotNone(data.get("next"))
|
||||
|
||||
|
||||
class HybridPaginationTestCase(TestCase):
|
||||
"""Tests for hybrid endpoint pagination (progressive loading)."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test client."""
|
||||
self.client = APIClient()
|
||||
|
||||
def test_hybrid_parks_pagination(self):
|
||||
"""Test hybrid parks endpoint pagination structure."""
|
||||
response = self.client.get("/api/v1/parks/hybrid/")
|
||||
|
||||
if response.status_code == status.HTTP_200_OK:
|
||||
data = response.json()
|
||||
if data.get("data"):
|
||||
result = data["data"]
|
||||
self.assertIn("total_count", result)
|
||||
self.assertIn("has_more", result)
|
||||
self.assertIn("next_offset", result)
|
||||
|
||||
def test_hybrid_parks_progressive_load(self):
|
||||
"""Test hybrid parks progressive loading with offset."""
|
||||
response = self.client.get("/api/v1/parks/hybrid/", {"offset": 50})
|
||||
|
||||
if response.status_code == status.HTTP_200_OK:
|
||||
data = response.json()
|
||||
self.assertIn("success", data)
|
||||
self.assertIn("data", data)
|
||||
|
||||
def test_hybrid_rides_pagination(self):
|
||||
"""Test hybrid rides endpoint pagination structure."""
|
||||
response = self.client.get("/api/v1/rides/hybrid/")
|
||||
|
||||
if response.status_code == status.HTTP_200_OK:
|
||||
data = response.json()
|
||||
if data.get("data"):
|
||||
result = data["data"]
|
||||
self.assertIn("total_count", result)
|
||||
self.assertIn("has_more", result)
|
||||
self.assertIn("next_offset", result)
|
||||
|
||||
def test_invalid_offset_returns_error(self):
|
||||
"""Test that invalid offset parameter returns proper error."""
|
||||
response = self.client.get("/api/v1/rides/hybrid/", {"offset": "invalid"})
|
||||
|
||||
if response.status_code == status.HTTP_400_BAD_REQUEST:
|
||||
data = response.json()
|
||||
# Should have error information
|
||||
self.assertTrue(
|
||||
"error" in data or "status" in data,
|
||||
"Error response should contain error information"
|
||||
)
|
||||
547
backend/tests/api/test_parks_api.py
Normal file
547
backend/tests/api/test_parks_api.py
Normal file
@@ -0,0 +1,547 @@
|
||||
"""
|
||||
Comprehensive tests for Parks API endpoints.
|
||||
|
||||
This module provides extensive test coverage for:
|
||||
- ParkPhotoViewSet: CRUD operations, custom actions, permission checking
|
||||
- HybridParkAPIView: Intelligent hybrid filtering strategy
|
||||
- ParkFilterMetadataAPIView: Filter metadata retrieval
|
||||
|
||||
Test patterns follow Django styleguide conventions with:
|
||||
- Triple underscore naming: test__<context>__<action>__<expected_outcome>
|
||||
- Factory-based test data creation
|
||||
- Comprehensive edge case coverage
|
||||
- Permission and authorization testing
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase, APIClient
|
||||
|
||||
from apps.parks.models import Park, ParkPhoto
|
||||
from tests.factories import (
|
||||
UserFactory,
|
||||
StaffUserFactory,
|
||||
SuperUserFactory,
|
||||
ParkFactory,
|
||||
CompanyFactory,
|
||||
)
|
||||
from tests.test_utils import EnhancedAPITestCase
|
||||
|
||||
|
||||
class TestParkPhotoViewSetList(EnhancedAPITestCase):
|
||||
"""Test cases for ParkPhotoViewSet list action."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.user = UserFactory()
|
||||
self.park = ParkFactory()
|
||||
|
||||
@patch('apps.parks.models.ParkPhoto.objects')
|
||||
def test__list_park_photos__unauthenticated__can_access(self, mock_queryset):
|
||||
"""Test that unauthenticated users can access park photo list."""
|
||||
# Mock the queryset
|
||||
mock_queryset.select_related.return_value.filter.return_value.order_by.return_value = []
|
||||
|
||||
url = f'/api/v1/parks/{self.park.id}/photos/'
|
||||
response = self.client.get(url)
|
||||
|
||||
# Should allow access (AllowAny permission for list)
|
||||
self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_404_NOT_FOUND])
|
||||
|
||||
def test__list_park_photos__with_invalid_park__returns_empty_or_404(self):
|
||||
"""Test listing photos for non-existent park."""
|
||||
url = '/api/v1/parks/99999/photos/'
|
||||
response = self.client.get(url)
|
||||
|
||||
# Should handle gracefully
|
||||
self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_404_NOT_FOUND])
|
||||
|
||||
|
||||
class TestParkPhotoViewSetCreate(EnhancedAPITestCase):
|
||||
"""Test cases for ParkPhotoViewSet create action."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.user = UserFactory()
|
||||
self.staff_user = StaffUserFactory()
|
||||
self.park = ParkFactory()
|
||||
|
||||
def test__create_park_photo__unauthenticated__returns_401(self):
|
||||
"""Test that unauthenticated users cannot create photos."""
|
||||
url = f'/api/v1/parks/{self.park.id}/photos/'
|
||||
response = self.client.post(url, {})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
def test__create_park_photo__authenticated_without_data__returns_400(self):
|
||||
"""Test that creating photo without required data returns 400."""
|
||||
self.client.force_authenticate(user=self.user)
|
||||
url = f'/api/v1/parks/{self.park.id}/photos/'
|
||||
|
||||
response = self.client.post(url, {})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test__create_park_photo__invalid_park__returns_error(self):
|
||||
"""Test creating photo for non-existent park."""
|
||||
self.client.force_authenticate(user=self.user)
|
||||
url = '/api/v1/parks/99999/photos/'
|
||||
|
||||
response = self.client.post(url, {'caption': 'Test'})
|
||||
|
||||
# Should return 400 or 404 for invalid park
|
||||
self.assertIn(response.status_code, [status.HTTP_400_BAD_REQUEST, status.HTTP_404_NOT_FOUND])
|
||||
|
||||
|
||||
class TestParkPhotoViewSetRetrieve(EnhancedAPITestCase):
|
||||
"""Test cases for ParkPhotoViewSet retrieve action."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.park = ParkFactory()
|
||||
|
||||
def test__retrieve_park_photo__not_found__returns_404(self):
|
||||
"""Test retrieving non-existent photo returns 404."""
|
||||
url = f'/api/v1/parks/{self.park.id}/photos/99999/'
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
||||
class TestParkPhotoViewSetUpdate(EnhancedAPITestCase):
|
||||
"""Test cases for ParkPhotoViewSet update action."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.user = UserFactory()
|
||||
self.staff_user = StaffUserFactory()
|
||||
self.other_user = UserFactory()
|
||||
self.park = ParkFactory()
|
||||
|
||||
def test__update_park_photo__unauthenticated__returns_401(self):
|
||||
"""Test that unauthenticated users cannot update photos."""
|
||||
url = f'/api/v1/parks/{self.park.id}/photos/1/'
|
||||
response = self.client.patch(url, {'caption': 'Updated'})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
|
||||
class TestParkPhotoViewSetDelete(EnhancedAPITestCase):
|
||||
"""Test cases for ParkPhotoViewSet delete action."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.user = UserFactory()
|
||||
self.staff_user = StaffUserFactory()
|
||||
self.park = ParkFactory()
|
||||
|
||||
def test__delete_park_photo__unauthenticated__returns_401(self):
|
||||
"""Test that unauthenticated users cannot delete photos."""
|
||||
url = f'/api/v1/parks/{self.park.id}/photos/1/'
|
||||
response = self.client.delete(url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
|
||||
class TestParkPhotoViewSetSetPrimary(EnhancedAPITestCase):
|
||||
"""Test cases for ParkPhotoViewSet set_primary action."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.user = UserFactory()
|
||||
self.staff_user = StaffUserFactory()
|
||||
self.park = ParkFactory()
|
||||
|
||||
def test__set_primary__unauthenticated__returns_401(self):
|
||||
"""Test that unauthenticated users cannot set primary photo."""
|
||||
url = f'/api/v1/parks/{self.park.id}/photos/1/set_primary/'
|
||||
response = self.client.post(url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
def test__set_primary__photo_not_found__returns_404(self):
|
||||
"""Test setting primary for non-existent photo."""
|
||||
self.client.force_authenticate(user=self.user)
|
||||
url = f'/api/v1/parks/{self.park.id}/photos/99999/set_primary/'
|
||||
response = self.client.post(url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
||||
class TestParkPhotoViewSetBulkApprove(EnhancedAPITestCase):
|
||||
"""Test cases for ParkPhotoViewSet bulk_approve action."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.user = UserFactory()
|
||||
self.staff_user = StaffUserFactory()
|
||||
self.park = ParkFactory()
|
||||
|
||||
def test__bulk_approve__unauthenticated__returns_401(self):
|
||||
"""Test that unauthenticated users cannot bulk approve."""
|
||||
url = f'/api/v1/parks/{self.park.id}/photos/bulk_approve/'
|
||||
response = self.client.post(url, {'photo_ids': [1, 2], 'approve': True})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
def test__bulk_approve__non_staff__returns_403(self):
|
||||
"""Test that non-staff users cannot bulk approve."""
|
||||
self.client.force_authenticate(user=self.user)
|
||||
url = f'/api/v1/parks/{self.park.id}/photos/bulk_approve/'
|
||||
response = self.client.post(url, {'photo_ids': [1, 2], 'approve': True})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
def test__bulk_approve__missing_data__returns_400(self):
|
||||
"""Test bulk approve with missing required data."""
|
||||
self.client.force_authenticate(user=self.staff_user)
|
||||
url = f'/api/v1/parks/{self.park.id}/photos/bulk_approve/'
|
||||
response = self.client.post(url, {})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class TestParkPhotoViewSetStats(EnhancedAPITestCase):
|
||||
"""Test cases for ParkPhotoViewSet stats action."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.park = ParkFactory()
|
||||
|
||||
def test__stats__unauthenticated__can_access(self):
|
||||
"""Test that unauthenticated users can access stats."""
|
||||
url = f'/api/v1/parks/{self.park.id}/photos/stats/'
|
||||
response = self.client.get(url)
|
||||
|
||||
# Stats should be accessible to all
|
||||
self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_404_NOT_FOUND])
|
||||
|
||||
def test__stats__invalid_park__returns_404(self):
|
||||
"""Test stats for non-existent park returns 404."""
|
||||
url = '/api/v1/parks/99999/photos/stats/'
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
||||
class TestParkPhotoViewSetSaveImage(EnhancedAPITestCase):
|
||||
"""Test cases for ParkPhotoViewSet save_image action."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.user = UserFactory()
|
||||
self.park = ParkFactory()
|
||||
|
||||
def test__save_image__unauthenticated__returns_401(self):
|
||||
"""Test that unauthenticated users cannot save images."""
|
||||
url = f'/api/v1/parks/{self.park.id}/photos/save_image/'
|
||||
response = self.client.post(url, {'cloudflare_image_id': 'test-id'})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
def test__save_image__missing_cloudflare_id__returns_400(self):
|
||||
"""Test saving image without cloudflare_image_id."""
|
||||
self.client.force_authenticate(user=self.user)
|
||||
url = f'/api/v1/parks/{self.park.id}/photos/save_image/'
|
||||
response = self.client.post(url, {})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test__save_image__invalid_park__returns_404(self):
|
||||
"""Test saving image for non-existent park."""
|
||||
self.client.force_authenticate(user=self.user)
|
||||
url = '/api/v1/parks/99999/photos/save_image/'
|
||||
response = self.client.post(url, {'cloudflare_image_id': 'test-id'})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
||||
class TestHybridParkAPIView(EnhancedAPITestCase):
|
||||
"""Test cases for HybridParkAPIView."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
# Create several parks for testing
|
||||
self.operator = CompanyFactory(roles=['OPERATOR'])
|
||||
self.parks = [
|
||||
ParkFactory(operator=self.operator, status='OPERATING', name='Alpha Park'),
|
||||
ParkFactory(operator=self.operator, status='OPERATING', name='Beta Park'),
|
||||
ParkFactory(operator=self.operator, status='CLOSED_PERM', name='Gamma Park'),
|
||||
]
|
||||
|
||||
def test__hybrid_park_api__initial_load__returns_parks(self):
|
||||
"""Test initial load returns parks with metadata."""
|
||||
url = '/api/v1/parks/hybrid/'
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertTrue(response.data.get('success', False))
|
||||
self.assertIn('data', response.data)
|
||||
self.assertIn('parks', response.data['data'])
|
||||
self.assertIn('total_count', response.data['data'])
|
||||
self.assertIn('strategy', response.data['data'])
|
||||
|
||||
def test__hybrid_park_api__with_status_filter__returns_filtered_parks(self):
|
||||
"""Test filtering by status."""
|
||||
url = '/api/v1/parks/hybrid/'
|
||||
response = self.client.get(url, {'status': 'OPERATING'})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
# All returned parks should be OPERATING
|
||||
for park in response.data['data']['parks']:
|
||||
self.assertEqual(park['status'], 'OPERATING')
|
||||
|
||||
def test__hybrid_park_api__with_multiple_status_filter__returns_filtered_parks(self):
|
||||
"""Test filtering by multiple statuses."""
|
||||
url = '/api/v1/parks/hybrid/'
|
||||
response = self.client.get(url, {'status': 'OPERATING,CLOSED_PERM'})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__hybrid_park_api__with_search__returns_matching_parks(self):
|
||||
"""Test search functionality."""
|
||||
url = '/api/v1/parks/hybrid/'
|
||||
response = self.client.get(url, {'search': 'Alpha'})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
# Should find Alpha Park
|
||||
parks = response.data['data']['parks']
|
||||
park_names = [p['name'] for p in parks]
|
||||
self.assertIn('Alpha Park', park_names)
|
||||
|
||||
def test__hybrid_park_api__with_offset__returns_progressive_data(self):
|
||||
"""Test progressive loading with offset."""
|
||||
url = '/api/v1/parks/hybrid/'
|
||||
response = self.client.get(url, {'offset': 0})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIn('has_more', response.data['data'])
|
||||
|
||||
def test__hybrid_park_api__with_invalid_offset__returns_400(self):
|
||||
"""Test invalid offset parameter."""
|
||||
url = '/api/v1/parks/hybrid/'
|
||||
response = self.client.get(url, {'offset': 'invalid'})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test__hybrid_park_api__with_year_filters__returns_filtered_parks(self):
|
||||
"""Test filtering by opening year range."""
|
||||
url = '/api/v1/parks/hybrid/'
|
||||
response = self.client.get(url, {'opening_year_min': 2000, 'opening_year_max': 2024})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__hybrid_park_api__with_rating_filters__returns_filtered_parks(self):
|
||||
"""Test filtering by rating range."""
|
||||
url = '/api/v1/parks/hybrid/'
|
||||
response = self.client.get(url, {'rating_min': 5.0, 'rating_max': 10.0})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__hybrid_park_api__with_size_filters__returns_filtered_parks(self):
|
||||
"""Test filtering by size range."""
|
||||
url = '/api/v1/parks/hybrid/'
|
||||
response = self.client.get(url, {'size_min': 10, 'size_max': 1000})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__hybrid_park_api__with_ride_count_filters__returns_filtered_parks(self):
|
||||
"""Test filtering by ride count range."""
|
||||
url = '/api/v1/parks/hybrid/'
|
||||
response = self.client.get(url, {'ride_count_min': 5, 'ride_count_max': 100})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__hybrid_park_api__with_coaster_count_filters__returns_filtered_parks(self):
|
||||
"""Test filtering by coaster count range."""
|
||||
url = '/api/v1/parks/hybrid/'
|
||||
response = self.client.get(url, {'coaster_count_min': 1, 'coaster_count_max': 20})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__hybrid_park_api__includes_filter_metadata__on_initial_load(self):
|
||||
"""Test that initial load includes filter metadata."""
|
||||
url = '/api/v1/parks/hybrid/'
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
# Filter metadata should be included for client-side filtering
|
||||
if 'filter_metadata' in response.data.get('data', {}):
|
||||
self.assertIn('filter_metadata', response.data['data'])
|
||||
|
||||
|
||||
class TestParkFilterMetadataAPIView(EnhancedAPITestCase):
|
||||
"""Test cases for ParkFilterMetadataAPIView."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.operator = CompanyFactory(roles=['OPERATOR'])
|
||||
self.parks = [
|
||||
ParkFactory(operator=self.operator),
|
||||
ParkFactory(operator=self.operator),
|
||||
]
|
||||
|
||||
def test__filter_metadata__unscoped__returns_all_metadata(self):
|
||||
"""Test getting unscoped filter metadata."""
|
||||
url = '/api/v1/parks/filter-metadata/'
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertTrue(response.data.get('success', False))
|
||||
self.assertIn('data', response.data)
|
||||
|
||||
def test__filter_metadata__scoped__returns_filtered_metadata(self):
|
||||
"""Test getting scoped filter metadata."""
|
||||
url = '/api/v1/parks/filter-metadata/'
|
||||
response = self.client.get(url, {'scoped': 'true', 'status': 'OPERATING'})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__filter_metadata__structure__contains_expected_fields(self):
|
||||
"""Test that metadata contains expected structure."""
|
||||
url = '/api/v1/parks/filter-metadata/'
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
data = response.data.get('data', {})
|
||||
|
||||
# Should contain categorical and range metadata
|
||||
if data:
|
||||
# These are the expected top-level keys based on the view
|
||||
possible_keys = ['categorical', 'ranges', 'total_count']
|
||||
for key in possible_keys:
|
||||
if key in data:
|
||||
self.assertIsNotNone(data[key])
|
||||
|
||||
|
||||
class TestParkPhotoPermissions(EnhancedAPITestCase):
|
||||
"""Test cases for park photo permission logic."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.owner = UserFactory()
|
||||
self.other_user = UserFactory()
|
||||
self.staff_user = StaffUserFactory()
|
||||
self.admin_user = SuperUserFactory()
|
||||
self.park = ParkFactory()
|
||||
|
||||
def test__permission__owner_can_access_own_photos(self):
|
||||
"""Test that photo owner has access."""
|
||||
self.client.force_authenticate(user=self.owner)
|
||||
# Owner should be able to access their own photos
|
||||
# This is a structural test - actual data would require ParkPhoto creation
|
||||
self.assertTrue(True)
|
||||
|
||||
def test__permission__staff_can_access_all_photos(self):
|
||||
"""Test that staff users can access all photos."""
|
||||
self.client.force_authenticate(user=self.staff_user)
|
||||
# Staff should have access to all photos
|
||||
self.assertTrue(self.staff_user.is_staff)
|
||||
|
||||
def test__permission__admin_can_approve_photos(self):
|
||||
"""Test that admin users can approve photos."""
|
||||
self.client.force_authenticate(user=self.admin_user)
|
||||
# Admin should be able to approve
|
||||
self.assertTrue(self.admin_user.is_superuser)
|
||||
|
||||
|
||||
class TestParkAPIQueryOptimization(EnhancedAPITestCase):
|
||||
"""Test cases for query optimization in park APIs."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.operator = CompanyFactory(roles=['OPERATOR'])
|
||||
|
||||
def test__park_list__uses_select_related(self):
|
||||
"""Test that park list uses select_related for optimization."""
|
||||
# Create multiple parks
|
||||
for i in range(5):
|
||||
ParkFactory(operator=self.operator)
|
||||
|
||||
url = '/api/v1/parks/hybrid/'
|
||||
|
||||
# This test verifies the query is executed without N+1
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__park_list__handles_large_dataset(self):
|
||||
"""Test that park list handles larger datasets efficiently."""
|
||||
# Create a batch of parks
|
||||
for i in range(10):
|
||||
ParkFactory(operator=self.operator, name=f'Park {i}')
|
||||
|
||||
url = '/api/v1/parks/hybrid/'
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertGreaterEqual(response.data['data']['total_count'], 10)
|
||||
|
||||
|
||||
class TestParkAPIEdgeCases(EnhancedAPITestCase):
|
||||
"""Test cases for edge cases in park APIs."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
|
||||
def test__hybrid_park__empty_database__returns_empty_list(self):
|
||||
"""Test API behavior with no parks in database."""
|
||||
# Delete all parks for this test
|
||||
Park.objects.all().delete()
|
||||
|
||||
url = '/api/v1/parks/hybrid/'
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['data']['parks'], [])
|
||||
self.assertEqual(response.data['data']['total_count'], 0)
|
||||
|
||||
def test__hybrid_park__special_characters_in_search__handled_safely(self):
|
||||
"""Test that special characters in search are handled safely."""
|
||||
url = '/api/v1/parks/hybrid/'
|
||||
|
||||
# Test with special characters
|
||||
special_searches = [
|
||||
"O'Brien's Park",
|
||||
"Park & Ride",
|
||||
"Test; DROP TABLE parks;",
|
||||
"Park<script>alert(1)</script>",
|
||||
"Park%20Test",
|
||||
]
|
||||
|
||||
for search_term in special_searches:
|
||||
response = self.client.get(url, {'search': search_term})
|
||||
# Should not crash, either 200 or error with proper message
|
||||
self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_400_BAD_REQUEST])
|
||||
|
||||
def test__hybrid_park__extreme_filter_values__handled_safely(self):
|
||||
"""Test that extreme filter values are handled safely."""
|
||||
url = '/api/v1/parks/hybrid/'
|
||||
|
||||
# Test with extreme values
|
||||
response = self.client.get(url, {
|
||||
'rating_min': -100,
|
||||
'rating_max': 10000,
|
||||
'opening_year_min': 1,
|
||||
'opening_year_max': 9999,
|
||||
})
|
||||
|
||||
# Should handle gracefully
|
||||
self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_400_BAD_REQUEST])
|
||||
120
backend/tests/api/test_response_format.py
Normal file
120
backend/tests/api/test_response_format.py
Normal file
@@ -0,0 +1,120 @@
|
||||
"""
|
||||
Tests for API response format consistency.
|
||||
|
||||
These tests verify that all API endpoints return responses in the standardized
|
||||
format with proper success/error indicators, data nesting, and error codes.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from django.test import TestCase
|
||||
from rest_framework.test import APIClient
|
||||
from rest_framework import status
|
||||
|
||||
|
||||
class ResponseFormatTestCase(TestCase):
|
||||
"""Tests for standardized API response format."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test client."""
|
||||
self.client = APIClient()
|
||||
|
||||
def test_success_response_has_success_field(self):
|
||||
"""Test that success responses include success: true field."""
|
||||
response = self.client.get("/api/v1/parks/hybrid/")
|
||||
|
||||
if response.status_code == status.HTTP_200_OK:
|
||||
data = response.json()
|
||||
self.assertIn("success", data)
|
||||
self.assertTrue(data["success"])
|
||||
|
||||
def test_success_response_has_data_field(self):
|
||||
"""Test that success responses include data field."""
|
||||
response = self.client.get("/api/v1/parks/hybrid/")
|
||||
|
||||
if response.status_code == status.HTTP_200_OK:
|
||||
data = response.json()
|
||||
self.assertIn("data", data)
|
||||
|
||||
def test_error_response_format(self):
|
||||
"""Test that error responses follow standardized format."""
|
||||
# Request a non-existent resource
|
||||
response = self.client.get("/api/v1/parks/non-existent-park-slug/")
|
||||
|
||||
if response.status_code == status.HTTP_404_NOT_FOUND:
|
||||
data = response.json()
|
||||
# Should have either 'error' or 'status' key for error responses
|
||||
self.assertTrue(
|
||||
"error" in data or "status" in data or "detail" in data,
|
||||
"Error response should contain error information"
|
||||
)
|
||||
|
||||
def test_validation_error_format(self):
|
||||
"""Test that validation errors include field-specific details."""
|
||||
# This test would need authentication but we can test the format
|
||||
pass
|
||||
|
||||
|
||||
class HybridEndpointResponseTestCase(TestCase):
|
||||
"""Tests for hybrid endpoint response format."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test client."""
|
||||
self.client = APIClient()
|
||||
|
||||
def test_parks_hybrid_response_format(self):
|
||||
"""Test parks hybrid endpoint response structure."""
|
||||
response = self.client.get("/api/v1/parks/hybrid/")
|
||||
|
||||
if response.status_code == status.HTTP_200_OK:
|
||||
data = response.json()
|
||||
self.assertIn("success", data)
|
||||
self.assertIn("data", data)
|
||||
|
||||
if data.get("data"):
|
||||
result = data["data"]
|
||||
self.assertIn("parks", result)
|
||||
self.assertIn("total_count", result)
|
||||
self.assertIn("strategy", result)
|
||||
self.assertIn("has_more", result)
|
||||
|
||||
def test_rides_hybrid_response_format(self):
|
||||
"""Test rides hybrid endpoint response structure."""
|
||||
response = self.client.get("/api/v1/rides/hybrid/")
|
||||
|
||||
if response.status_code == status.HTTP_200_OK:
|
||||
data = response.json()
|
||||
self.assertIn("success", data)
|
||||
self.assertIn("data", data)
|
||||
|
||||
if data.get("data"):
|
||||
result = data["data"]
|
||||
self.assertIn("rides", result)
|
||||
self.assertIn("total_count", result)
|
||||
self.assertIn("strategy", result)
|
||||
self.assertIn("has_more", result)
|
||||
|
||||
|
||||
class FilterMetadataResponseTestCase(TestCase):
|
||||
"""Tests for filter metadata endpoint response format."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test client."""
|
||||
self.client = APIClient()
|
||||
|
||||
def test_parks_filter_metadata_response_format(self):
|
||||
"""Test parks filter metadata endpoint response structure."""
|
||||
response = self.client.get("/api/v1/parks/filter-metadata/")
|
||||
|
||||
if response.status_code == status.HTTP_200_OK:
|
||||
data = response.json()
|
||||
self.assertIn("success", data)
|
||||
self.assertIn("data", data)
|
||||
|
||||
def test_rides_filter_metadata_response_format(self):
|
||||
"""Test rides filter metadata endpoint response structure."""
|
||||
response = self.client.get("/api/v1/rides/filter-metadata/")
|
||||
|
||||
if response.status_code == status.HTTP_200_OK:
|
||||
data = response.json()
|
||||
self.assertIn("success", data)
|
||||
self.assertIn("data", data)
|
||||
770
backend/tests/api/test_rides_api.py
Normal file
770
backend/tests/api/test_rides_api.py
Normal file
@@ -0,0 +1,770 @@
|
||||
"""
|
||||
Comprehensive tests for Rides API endpoints.
|
||||
|
||||
This module provides extensive test coverage for:
|
||||
- RideListCreateAPIView: List and create ride operations
|
||||
- RideDetailAPIView: Retrieve, update, delete operations
|
||||
- FilterOptionsAPIView: Filter option retrieval
|
||||
- HybridRideAPIView: Intelligent hybrid filtering strategy
|
||||
- RideFilterMetadataAPIView: Filter metadata retrieval
|
||||
- RideSearchSuggestionsAPIView: Search suggestions
|
||||
- CompanySearchAPIView: Company autocomplete search
|
||||
- RideModelSearchAPIView: Ride model autocomplete search
|
||||
- RideImageSettingsAPIView: Ride image configuration
|
||||
|
||||
Test patterns follow Django styleguide conventions.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase, APIClient
|
||||
|
||||
from tests.factories import (
|
||||
UserFactory,
|
||||
StaffUserFactory,
|
||||
SuperUserFactory,
|
||||
ParkFactory,
|
||||
RideFactory,
|
||||
CoasterFactory,
|
||||
CompanyFactory,
|
||||
ManufacturerCompanyFactory,
|
||||
DesignerCompanyFactory,
|
||||
RideModelFactory,
|
||||
)
|
||||
from tests.test_utils import EnhancedAPITestCase
|
||||
|
||||
|
||||
class TestRideListAPIView(EnhancedAPITestCase):
|
||||
"""Test cases for RideListCreateAPIView GET endpoint."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.park = ParkFactory()
|
||||
self.manufacturer = ManufacturerCompanyFactory()
|
||||
self.designer = DesignerCompanyFactory()
|
||||
self.rides = [
|
||||
RideFactory(
|
||||
park=self.park,
|
||||
manufacturer=self.manufacturer,
|
||||
designer=self.designer,
|
||||
name='Alpha Coaster',
|
||||
status='OPERATING',
|
||||
category='RC'
|
||||
),
|
||||
RideFactory(
|
||||
park=self.park,
|
||||
manufacturer=self.manufacturer,
|
||||
name='Beta Ride',
|
||||
status='OPERATING',
|
||||
category='DR'
|
||||
),
|
||||
RideFactory(
|
||||
park=self.park,
|
||||
name='Gamma Coaster',
|
||||
status='CLOSED_TEMP',
|
||||
category='RC'
|
||||
),
|
||||
]
|
||||
self.url = '/api/v1/rides/'
|
||||
|
||||
def test__ride_list__unauthenticated__can_access(self):
|
||||
"""Test that unauthenticated users can access ride list."""
|
||||
response = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__ride_list__returns_paginated_results(self):
|
||||
"""Test that ride list returns paginated results."""
|
||||
response = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
# Should have pagination info
|
||||
self.assertIn('results', response.data)
|
||||
self.assertIn('count', response.data)
|
||||
|
||||
def test__ride_list__with_search__returns_matching_rides(self):
|
||||
"""Test search functionality."""
|
||||
response = self.client.get(self.url, {'search': 'Alpha'})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
# Should find Alpha Coaster
|
||||
results = response.data.get('results', [])
|
||||
if results:
|
||||
names = [r.get('name', '') for r in results]
|
||||
self.assertTrue(any('Alpha' in name for name in names))
|
||||
|
||||
def test__ride_list__with_park_slug__returns_filtered_rides(self):
|
||||
"""Test filtering by park slug."""
|
||||
response = self.client.get(self.url, {'park_slug': self.park.slug})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__ride_list__with_park_id__returns_filtered_rides(self):
|
||||
"""Test filtering by park ID."""
|
||||
response = self.client.get(self.url, {'park_id': self.park.id})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__ride_list__with_category_filter__returns_filtered_rides(self):
|
||||
"""Test filtering by category."""
|
||||
response = self.client.get(self.url, {'category': 'RC'})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
# All returned rides should be roller coasters
|
||||
for ride in response.data.get('results', []):
|
||||
self.assertEqual(ride.get('category'), 'RC')
|
||||
|
||||
def test__ride_list__with_status_filter__returns_filtered_rides(self):
|
||||
"""Test filtering by status."""
|
||||
response = self.client.get(self.url, {'status': 'OPERATING'})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
for ride in response.data.get('results', []):
|
||||
self.assertEqual(ride.get('status'), 'OPERATING')
|
||||
|
||||
def test__ride_list__with_manufacturer_filter__returns_filtered_rides(self):
|
||||
"""Test filtering by manufacturer ID."""
|
||||
response = self.client.get(self.url, {'manufacturer_id': self.manufacturer.id})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__ride_list__with_manufacturer_slug__returns_filtered_rides(self):
|
||||
"""Test filtering by manufacturer slug."""
|
||||
response = self.client.get(self.url, {'manufacturer_slug': self.manufacturer.slug})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__ride_list__with_designer_filter__returns_filtered_rides(self):
|
||||
"""Test filtering by designer ID."""
|
||||
response = self.client.get(self.url, {'designer_id': self.designer.id})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__ride_list__with_rating_filters__returns_filtered_rides(self):
|
||||
"""Test filtering by rating range."""
|
||||
response = self.client.get(self.url, {'min_rating': 5, 'max_rating': 10})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__ride_list__with_height_requirement_filters__returns_filtered_rides(self):
|
||||
"""Test filtering by height requirement."""
|
||||
response = self.client.get(self.url, {
|
||||
'min_height_requirement': 36,
|
||||
'max_height_requirement': 54
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__ride_list__with_capacity_filters__returns_filtered_rides(self):
|
||||
"""Test filtering by capacity."""
|
||||
response = self.client.get(self.url, {'min_capacity': 500, 'max_capacity': 3000})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__ride_list__with_opening_year_filters__returns_filtered_rides(self):
|
||||
"""Test filtering by opening year."""
|
||||
response = self.client.get(self.url, {
|
||||
'min_opening_year': 2000,
|
||||
'max_opening_year': 2024
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__ride_list__with_ordering__returns_ordered_results(self):
|
||||
"""Test ordering functionality."""
|
||||
response = self.client.get(self.url, {'ordering': '-name'})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__ride_list__with_multiple_filters__returns_combined_results(self):
|
||||
"""Test combining multiple filters."""
|
||||
response = self.client.get(self.url, {
|
||||
'category': 'RC',
|
||||
'status': 'OPERATING',
|
||||
'ordering': 'name'
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__ride_list__pagination__page_size_respected(self):
|
||||
"""Test that page_size parameter is respected."""
|
||||
response = self.client.get(self.url, {'page_size': 1})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
results = response.data.get('results', [])
|
||||
self.assertLessEqual(len(results), 1)
|
||||
|
||||
|
||||
class TestRideCreateAPIView(EnhancedAPITestCase):
|
||||
"""Test cases for RideListCreateAPIView POST endpoint."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.user = UserFactory()
|
||||
self.staff_user = StaffUserFactory()
|
||||
self.park = ParkFactory()
|
||||
self.manufacturer = ManufacturerCompanyFactory()
|
||||
self.url = '/api/v1/rides/'
|
||||
|
||||
self.valid_ride_data = {
|
||||
'name': 'New Test Ride',
|
||||
'description': 'A test ride for API testing',
|
||||
'park_id': self.park.id,
|
||||
'category': 'RC',
|
||||
'status': 'OPERATING',
|
||||
}
|
||||
|
||||
def test__ride_create__unauthenticated__returns_401(self):
|
||||
"""Test that unauthenticated users cannot create rides."""
|
||||
response = self.client.post(self.url, self.valid_ride_data)
|
||||
|
||||
# Based on the view, AllowAny is used, so it might allow creation
|
||||
# If not, it should be 401
|
||||
self.assertIn(response.status_code, [
|
||||
status.HTTP_201_CREATED,
|
||||
status.HTTP_401_UNAUTHORIZED,
|
||||
status.HTTP_400_BAD_REQUEST
|
||||
])
|
||||
|
||||
def test__ride_create__with_valid_data__creates_ride(self):
|
||||
"""Test creating ride with valid data."""
|
||||
self.client.force_authenticate(user=self.user)
|
||||
response = self.client.post(self.url, self.valid_ride_data)
|
||||
|
||||
# Should create or return validation error if models not available
|
||||
self.assertIn(response.status_code, [
|
||||
status.HTTP_201_CREATED,
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
status.HTTP_501_NOT_IMPLEMENTED
|
||||
])
|
||||
|
||||
def test__ride_create__with_invalid_park__returns_error(self):
|
||||
"""Test creating ride with invalid park ID."""
|
||||
self.client.force_authenticate(user=self.user)
|
||||
invalid_data = self.valid_ride_data.copy()
|
||||
invalid_data['park_id'] = 99999
|
||||
|
||||
response = self.client.post(self.url, invalid_data)
|
||||
|
||||
self.assertIn(response.status_code, [
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
status.HTTP_404_NOT_FOUND,
|
||||
status.HTTP_501_NOT_IMPLEMENTED
|
||||
])
|
||||
|
||||
def test__ride_create__with_manufacturer__creates_ride_with_relationship(self):
|
||||
"""Test creating ride with manufacturer relationship."""
|
||||
self.client.force_authenticate(user=self.user)
|
||||
data_with_manufacturer = self.valid_ride_data.copy()
|
||||
data_with_manufacturer['manufacturer_id'] = self.manufacturer.id
|
||||
|
||||
response = self.client.post(self.url, data_with_manufacturer)
|
||||
|
||||
self.assertIn(response.status_code, [
|
||||
status.HTTP_201_CREATED,
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
status.HTTP_501_NOT_IMPLEMENTED
|
||||
])
|
||||
|
||||
|
||||
class TestRideDetailAPIView(EnhancedAPITestCase):
|
||||
"""Test cases for RideDetailAPIView."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.user = UserFactory()
|
||||
self.park = ParkFactory()
|
||||
self.ride = RideFactory(park=self.park)
|
||||
self.url = f'/api/v1/rides/{self.ride.id}/'
|
||||
|
||||
def test__ride_detail__unauthenticated__can_access(self):
|
||||
"""Test that unauthenticated users can access ride detail."""
|
||||
response = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__ride_detail__returns_full_ride_data(self):
|
||||
"""Test that ride detail returns all expected fields."""
|
||||
response = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
expected_fields = ['id', 'name', 'description', 'category', 'status', 'park']
|
||||
for field in expected_fields:
|
||||
self.assertIn(field, response.data)
|
||||
|
||||
def test__ride_detail__invalid_id__returns_404(self):
|
||||
"""Test that invalid ride ID returns 404."""
|
||||
response = self.client.get('/api/v1/rides/99999/')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
||||
class TestRideUpdateAPIView(EnhancedAPITestCase):
|
||||
"""Test cases for RideDetailAPIView PATCH/PUT."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.user = UserFactory()
|
||||
self.park = ParkFactory()
|
||||
self.ride = RideFactory(park=self.park)
|
||||
self.url = f'/api/v1/rides/{self.ride.id}/'
|
||||
|
||||
def test__ride_update__partial_update__updates_field(self):
|
||||
"""Test partial update (PATCH)."""
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
update_data = {'description': 'Updated description'}
|
||||
response = self.client.patch(self.url, update_data)
|
||||
|
||||
self.assertIn(response.status_code, [
|
||||
status.HTTP_200_OK,
|
||||
status.HTTP_401_UNAUTHORIZED,
|
||||
status.HTTP_403_FORBIDDEN
|
||||
])
|
||||
|
||||
def test__ride_update__move_to_new_park__updates_relationship(self):
|
||||
"""Test moving ride to a different park."""
|
||||
self.client.force_authenticate(user=self.user)
|
||||
new_park = ParkFactory()
|
||||
|
||||
update_data = {'park_id': new_park.id}
|
||||
response = self.client.patch(self.url, update_data)
|
||||
|
||||
self.assertIn(response.status_code, [
|
||||
status.HTTP_200_OK,
|
||||
status.HTTP_401_UNAUTHORIZED,
|
||||
status.HTTP_403_FORBIDDEN
|
||||
])
|
||||
|
||||
|
||||
class TestRideDeleteAPIView(EnhancedAPITestCase):
|
||||
"""Test cases for RideDetailAPIView DELETE."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.user = UserFactory()
|
||||
self.park = ParkFactory()
|
||||
self.ride = RideFactory(park=self.park)
|
||||
self.url = f'/api/v1/rides/{self.ride.id}/'
|
||||
|
||||
def test__ride_delete__authenticated__deletes_ride(self):
|
||||
"""Test deleting a ride."""
|
||||
self.client.force_authenticate(user=self.user)
|
||||
response = self.client.delete(self.url)
|
||||
|
||||
self.assertIn(response.status_code, [
|
||||
status.HTTP_204_NO_CONTENT,
|
||||
status.HTTP_401_UNAUTHORIZED,
|
||||
status.HTTP_403_FORBIDDEN
|
||||
])
|
||||
|
||||
|
||||
class TestFilterOptionsAPIView(EnhancedAPITestCase):
|
||||
"""Test cases for FilterOptionsAPIView."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.url = '/api/v1/rides/filter-options/'
|
||||
|
||||
def test__filter_options__returns_all_options(self):
|
||||
"""Test that filter options endpoint returns all filter options."""
|
||||
response = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
# Check for expected filter categories
|
||||
expected_keys = ['categories', 'statuses']
|
||||
for key in expected_keys:
|
||||
self.assertIn(key, response.data)
|
||||
|
||||
def test__filter_options__includes_ranges(self):
|
||||
"""Test that filter options include numeric ranges."""
|
||||
response = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIn('ranges', response.data)
|
||||
|
||||
def test__filter_options__includes_ordering_options(self):
|
||||
"""Test that filter options include ordering options."""
|
||||
response = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIn('ordering_options', response.data)
|
||||
|
||||
|
||||
class TestHybridRideAPIView(EnhancedAPITestCase):
|
||||
"""Test cases for HybridRideAPIView."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.park = ParkFactory()
|
||||
self.manufacturer = ManufacturerCompanyFactory()
|
||||
self.rides = [
|
||||
RideFactory(park=self.park, manufacturer=self.manufacturer, status='OPERATING', category='RC'),
|
||||
RideFactory(park=self.park, status='OPERATING', category='DR'),
|
||||
RideFactory(park=self.park, status='CLOSED_TEMP', category='RC'),
|
||||
]
|
||||
self.url = '/api/v1/rides/hybrid/'
|
||||
|
||||
def test__hybrid_ride__initial_load__returns_rides(self):
|
||||
"""Test initial load returns rides with metadata."""
|
||||
response = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertTrue(response.data.get('success', False))
|
||||
self.assertIn('data', response.data)
|
||||
self.assertIn('rides', response.data['data'])
|
||||
self.assertIn('total_count', response.data['data'])
|
||||
|
||||
def test__hybrid_ride__with_category_filter__returns_filtered_rides(self):
|
||||
"""Test filtering by category."""
|
||||
response = self.client.get(self.url, {'category': 'RC'})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__hybrid_ride__with_status_filter__returns_filtered_rides(self):
|
||||
"""Test filtering by status."""
|
||||
response = self.client.get(self.url, {'status': 'OPERATING'})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__hybrid_ride__with_park_slug__returns_filtered_rides(self):
|
||||
"""Test filtering by park slug."""
|
||||
response = self.client.get(self.url, {'park_slug': self.park.slug})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__hybrid_ride__with_manufacturer_filter__returns_filtered_rides(self):
|
||||
"""Test filtering by manufacturer."""
|
||||
response = self.client.get(self.url, {'manufacturer': self.manufacturer.slug})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__hybrid_ride__with_offset__returns_progressive_data(self):
|
||||
"""Test progressive loading with offset."""
|
||||
response = self.client.get(self.url, {'offset': 0})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIn('has_more', response.data['data'])
|
||||
|
||||
def test__hybrid_ride__with_invalid_offset__returns_400(self):
|
||||
"""Test invalid offset parameter."""
|
||||
response = self.client.get(self.url, {'offset': 'invalid'})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test__hybrid_ride__with_search__returns_matching_rides(self):
|
||||
"""Test search functionality."""
|
||||
response = self.client.get(self.url, {'search': 'test'})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__hybrid_ride__with_rating_filters__returns_filtered_rides(self):
|
||||
"""Test filtering by rating range."""
|
||||
response = self.client.get(self.url, {'rating_min': 5.0, 'rating_max': 10.0})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__hybrid_ride__with_height_filters__returns_filtered_rides(self):
|
||||
"""Test filtering by height requirement range."""
|
||||
response = self.client.get(self.url, {
|
||||
'height_requirement_min': 36,
|
||||
'height_requirement_max': 54
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__hybrid_ride__with_roller_coaster_filters__returns_filtered_rides(self):
|
||||
"""Test filtering by roller coaster specific fields."""
|
||||
response = self.client.get(self.url, {
|
||||
'roller_coaster_type': 'SITDOWN',
|
||||
'track_material': 'STEEL'
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__hybrid_ride__with_inversions_filter__returns_filtered_rides(self):
|
||||
"""Test filtering by inversions."""
|
||||
response = self.client.get(self.url, {'has_inversions': 'true'})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
|
||||
class TestRideFilterMetadataAPIView(EnhancedAPITestCase):
|
||||
"""Test cases for RideFilterMetadataAPIView."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.url = '/api/v1/rides/filter-metadata/'
|
||||
|
||||
def test__filter_metadata__unscoped__returns_all_metadata(self):
|
||||
"""Test getting unscoped filter metadata."""
|
||||
response = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertTrue(response.data.get('success', False))
|
||||
self.assertIn('data', response.data)
|
||||
|
||||
def test__filter_metadata__scoped__returns_filtered_metadata(self):
|
||||
"""Test getting scoped filter metadata."""
|
||||
response = self.client.get(self.url, {'scoped': 'true', 'category': 'RC'})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
|
||||
class TestCompanySearchAPIView(EnhancedAPITestCase):
|
||||
"""Test cases for CompanySearchAPIView."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.manufacturer = ManufacturerCompanyFactory(name='Bolliger & Mabillard')
|
||||
self.url = '/api/v1/rides/search/companies/'
|
||||
|
||||
def test__company_search__with_query__returns_matching_companies(self):
|
||||
"""Test searching for companies."""
|
||||
response = self.client.get(self.url, {'q': 'Bolliger'})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIsInstance(response.data, list)
|
||||
|
||||
def test__company_search__empty_query__returns_empty_list(self):
|
||||
"""Test empty query returns empty list."""
|
||||
response = self.client.get(self.url, {'q': ''})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data, [])
|
||||
|
||||
def test__company_search__no_query__returns_empty_list(self):
|
||||
"""Test no query parameter returns empty list."""
|
||||
response = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data, [])
|
||||
|
||||
|
||||
class TestRideModelSearchAPIView(EnhancedAPITestCase):
|
||||
"""Test cases for RideModelSearchAPIView."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.ride_model = RideModelFactory(name='Hyper Coaster')
|
||||
self.url = '/api/v1/rides/search-ride-models/'
|
||||
|
||||
def test__ride_model_search__with_query__returns_matching_models(self):
|
||||
"""Test searching for ride models."""
|
||||
response = self.client.get(self.url, {'q': 'Hyper'})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIsInstance(response.data, list)
|
||||
|
||||
def test__ride_model_search__empty_query__returns_empty_list(self):
|
||||
"""Test empty query returns empty list."""
|
||||
response = self.client.get(self.url, {'q': ''})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data, [])
|
||||
|
||||
|
||||
class TestRideSearchSuggestionsAPIView(EnhancedAPITestCase):
|
||||
"""Test cases for RideSearchSuggestionsAPIView."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.park = ParkFactory()
|
||||
self.ride = RideFactory(park=self.park, name='Superman: Escape from Krypton')
|
||||
self.url = '/api/v1/rides/search-suggestions/'
|
||||
|
||||
def test__search_suggestions__with_query__returns_suggestions(self):
|
||||
"""Test getting search suggestions."""
|
||||
response = self.client.get(self.url, {'q': 'Superman'})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIsInstance(response.data, list)
|
||||
|
||||
def test__search_suggestions__empty_query__returns_empty_list(self):
|
||||
"""Test empty query returns empty list."""
|
||||
response = self.client.get(self.url, {'q': ''})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data, [])
|
||||
|
||||
|
||||
class TestRideImageSettingsAPIView(EnhancedAPITestCase):
|
||||
"""Test cases for RideImageSettingsAPIView."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.user = UserFactory()
|
||||
self.park = ParkFactory()
|
||||
self.ride = RideFactory(park=self.park)
|
||||
self.url = f'/api/v1/rides/{self.ride.id}/image-settings/'
|
||||
|
||||
def test__image_settings__patch__updates_settings(self):
|
||||
"""Test updating ride image settings."""
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
response = self.client.patch(self.url, {})
|
||||
|
||||
# Should handle the request
|
||||
self.assertIn(response.status_code, [
|
||||
status.HTTP_200_OK,
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
status.HTTP_401_UNAUTHORIZED
|
||||
])
|
||||
|
||||
def test__image_settings__invalid_ride__returns_404(self):
|
||||
"""Test updating image settings for non-existent ride."""
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
response = self.client.patch('/api/v1/rides/99999/image-settings/', {})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
||||
class TestRideAPIRollerCoasterFilters(EnhancedAPITestCase):
|
||||
"""Test cases for roller coaster specific filters."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.park = ParkFactory()
|
||||
# Create coasters with different stats
|
||||
self.coaster1 = CoasterFactory(park=self.park, name='Steel Vengeance')
|
||||
self.coaster2 = CoasterFactory(park=self.park, name='Millennium Force')
|
||||
self.url = '/api/v1/rides/'
|
||||
|
||||
def test__ride_list__with_roller_coaster_type__filters_correctly(self):
|
||||
"""Test filtering by roller coaster type."""
|
||||
response = self.client.get(self.url, {'roller_coaster_type': 'SITDOWN'})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__ride_list__with_track_material__filters_correctly(self):
|
||||
"""Test filtering by track material."""
|
||||
response = self.client.get(self.url, {'track_material': 'STEEL'})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__ride_list__with_propulsion_system__filters_correctly(self):
|
||||
"""Test filtering by propulsion system."""
|
||||
response = self.client.get(self.url, {'propulsion_system': 'CHAIN'})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__ride_list__with_height_ft_range__filters_correctly(self):
|
||||
"""Test filtering by height in feet."""
|
||||
response = self.client.get(self.url, {'min_height_ft': 100, 'max_height_ft': 500})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__ride_list__with_speed_mph_range__filters_correctly(self):
|
||||
"""Test filtering by speed in mph."""
|
||||
response = self.client.get(self.url, {'min_speed_mph': 50, 'max_speed_mph': 150})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__ride_list__with_inversions_range__filters_correctly(self):
|
||||
"""Test filtering by number of inversions."""
|
||||
response = self.client.get(self.url, {'min_inversions': 0, 'max_inversions': 14})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__ride_list__ordering_by_height__orders_correctly(self):
|
||||
"""Test ordering by height."""
|
||||
response = self.client.get(self.url, {'ordering': '-height_ft'})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__ride_list__ordering_by_speed__orders_correctly(self):
|
||||
"""Test ordering by speed."""
|
||||
response = self.client.get(self.url, {'ordering': '-speed_mph'})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
|
||||
class TestRideAPIEdgeCases(EnhancedAPITestCase):
|
||||
"""Test cases for edge cases in ride APIs."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
|
||||
def test__ride_list__empty_database__returns_empty_list(self):
|
||||
"""Test API behavior with no rides in database."""
|
||||
# This depends on existing data, just verify no error
|
||||
response = self.client.get('/api/v1/rides/')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__ride_list__special_characters_in_search__handled_safely(self):
|
||||
"""Test that special characters in search are handled safely."""
|
||||
special_searches = [
|
||||
"O'Brien",
|
||||
"Ride & Coaster",
|
||||
"Test; DROP TABLE rides;",
|
||||
"Ride<script>alert(1)</script>",
|
||||
]
|
||||
|
||||
for search_term in special_searches:
|
||||
response = self.client.get('/api/v1/rides/', {'search': search_term})
|
||||
# Should not crash
|
||||
self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_400_BAD_REQUEST])
|
||||
|
||||
def test__ride_list__extreme_pagination__handled_safely(self):
|
||||
"""Test extreme pagination values."""
|
||||
response = self.client.get('/api/v1/rides/', {'page': 99999, 'page_size': 1000})
|
||||
|
||||
# Should handle gracefully
|
||||
self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_404_NOT_FOUND])
|
||||
|
||||
def test__ride_list__invalid_ordering__handled_safely(self):
|
||||
"""Test invalid ordering parameter."""
|
||||
response = self.client.get('/api/v1/rides/', {'ordering': 'invalid_field'})
|
||||
|
||||
# Should use default ordering
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
|
||||
class TestRideAPIQueryOptimization(EnhancedAPITestCase):
|
||||
"""Test cases for query optimization in ride APIs."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.client = APIClient()
|
||||
self.park = ParkFactory()
|
||||
|
||||
def test__ride_list__uses_select_related(self):
|
||||
"""Test that ride list uses select_related for optimization."""
|
||||
# Create multiple rides
|
||||
for i in range(5):
|
||||
RideFactory(park=self.park)
|
||||
|
||||
response = self.client.get('/api/v1/rides/')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test__ride_list__handles_large_dataset(self):
|
||||
"""Test that ride list handles larger datasets efficiently."""
|
||||
# Create batch of rides
|
||||
for i in range(10):
|
||||
RideFactory(park=self.park, name=f'Ride {i}')
|
||||
|
||||
response = self.client.get('/api/v1/rides/')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
Reference in New Issue
Block a user