Files
thrillwiki_django_no_react/backend/tests/api/test_auth_api.py

597 lines
21 KiB
Python

"""
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
])