Spaces:
Sleeping
Sleeping
| 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/' | |
| def api_client(): | |
| return APIClient() | |
| def user_data(): | |
| return VALID_USER.copy() | |
| 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 | |