import pytest from rest_framework.test import APIClient pytestmark = pytest.mark.django_db VALID_USER = { 'name': 'Test User', 'email': 'test@example.com', 'password': 'TestPass123!', 'password_confirm': 'TestPass123!' } REGISTER_URL = '/api/auth/register/' LOGIN_URL = '/api/auth/login/' LOGOUT_URL = '/api/auth/logout/' ME_URL = '/api/auth/me/' @pytest.fixture def api_client(): return APIClient() @pytest.fixture def user_data(): return VALID_USER.copy() @pytest.fixture def registered_user(api_client, user_data): """Register a user and return the response data.""" response = api_client.post(REGISTER_URL, data=user_data, format='json') return response.data class TestRegistration: def test_valid_registration(self, api_client, user_data): """TC-01: Valid registration returns 201 with tokens.""" response = api_client.post(REGISTER_URL, data=user_data, format='json') assert response.status_code == 201 assert 'access' in response.data assert 'refresh' in response.data assert 'user' in response.data assert response.data['user']['email'] == user_data['email'] def test_duplicate_email(self, api_client, user_data): """TC-02: Duplicate email returns 400.""" api_client.post(REGISTER_URL, data=user_data, format='json') response = api_client.post(REGISTER_URL, data=user_data, format='json') assert response.status_code == 400 def test_password_mismatch(self, api_client, user_data): """Password mismatch returns 400.""" user_data['password_confirm'] = 'DifferentPassword!' response = api_client.post(REGISTER_URL, data=user_data, format='json') assert response.status_code == 400 def test_missing_fields(self, api_client): """Missing required fields returns 400.""" response = api_client.post(REGISTER_URL, data={'email': 'x@x.com'}, format='json') assert response.status_code == 400 class TestLogin: def test_valid_login(self, api_client, user_data, registered_user): """TC-03: Valid login returns 200 with tokens.""" login_data = {'email': user_data['email'], 'password': user_data['password']} response = api_client.post(LOGIN_URL, data=login_data, format='json') assert response.status_code == 200 assert 'access' in response.data assert 'refresh' in response.data def test_invalid_credentials(self, api_client, user_data, registered_user): """TC-04: Wrong password returns 400.""" login_data = {'email': user_data['email'], 'password': 'WrongPassword!'} response = api_client.post(LOGIN_URL, data=login_data, format='json') assert response.status_code == 400 class TestMe: def test_me_without_token(self, api_client): """GET /me/ without token returns 401.""" response = api_client.get(ME_URL) assert response.status_code == 401 def test_get_me(self, api_client, user_data, registered_user): """Authenticated GET /me/ returns user data.""" access_token = registered_user['access'] api_client.credentials(HTTP_AUTHORIZATION='Bearer ' + access_token) response = api_client.get(ME_URL) assert response.status_code == 200 assert response.data['email'] == user_data['email'] class TestLoginThrottle: def test_login_rate_limit_blocks_after_threshold( self, api_client, user_data, registered_user, ): """Login endpoint is throttled at 10/minute — verify the 11th attempt is rejected with 429, regardless of credentials.""" bad = {'email': user_data['email'], 'password': 'nope'} # Burn through the limit with invalid credentials (400 per attempt). for _ in range(10): r = api_client.post(LOGIN_URL, data=bad, format='json') assert r.status_code in (400, 401) # 11th must be throttled. r = api_client.post(LOGIN_URL, data=bad, format='json') assert r.status_code == 429 class TestLogout: def test_logout(self, api_client, user_data, registered_user): """Logout blacklists refresh token, returns 204.""" access_token = registered_user['access'] refresh_token = registered_user['refresh'] api_client.credentials(HTTP_AUTHORIZATION='Bearer ' + access_token) response = api_client.post( LOGOUT_URL, data={'refresh_token': refresh_token}, format='json' ) assert response.status_code == 204 class TestRegistrationRace: def test_concurrent_email_race_returns_400(self, api_client, user_data): """F2: a concurrent case-variant registration can bypass the case-insensitive validate_email check; the DB unique index then raises IntegrityError. The view must surface a clean 400 keyed on `email`, not an unhandled 500.""" from unittest.mock import patch from django.db import IntegrityError with patch( 'apps.accounts.serializers.RegisterSerializer.save', side_effect=IntegrityError, ): response = api_client.post(REGISTER_URL, data=user_data, format='json') assert response.status_code == 400 assert 'email' in response.data