| """ |
| Error path tests for authentication routes endpoints. |
| |
| Tests error scenarios including: |
| - 401 Unauthorized (invalid credentials, expired tokens, malformed tokens) |
| - 400/422 Validation Error (missing fields, invalid email format, weak password) |
| - 404 Not Found (user not found, token not found) |
| - 409 Conflict (duplicate email, duplicate device) |
| - 429 Rate Limited (too many login attempts) |
| """ |
|
|
| import pytest |
| from unittest.mock import MagicMock, patch, AsyncMock |
| from fastapi import FastAPI |
| from fastapi.testclient import TestClient |
| from sqlalchemy.orm import Session |
| from datetime import datetime, timedelta |
|
|
| from api.auth_routes import router |
|
|
|
|
| |
| |
| |
|
|
| @pytest.fixture(scope="function") |
| def auth_client(): |
| """Create TestClient for auth routes error path testing.""" |
| app = FastAPI() |
| app.include_router(router) |
| return TestClient(app) |
|
|
|
|
| |
| |
| |
|
|
| class TestLoginErrors: |
| """Test login error scenarios.""" |
|
|
| def test_login_401_invalid_credentials(self, auth_client, db_session: Session): |
| """Test login returns 401 for wrong password.""" |
| |
| |
| response = auth_client.post( |
| "/api/auth/mobile/login", |
| json={ |
| "email": "test@example.com", |
| "password": "wrongpassword", |
| "device_token": "test_device_token", |
| "platform": "ios" |
| } |
| ) |
|
|
| |
| assert response.status_code in [401, 400, 500] |
|
|
| def test_login_401_user_not_found(self, auth_client): |
| """Test login returns 401 for non-existent email.""" |
| response = auth_client.post( |
| "/api/auth/mobile/login", |
| json={ |
| "email": "nonexistent@example.com", |
| "password": "anypassword", |
| "device_token": "test_device_token", |
| "platform": "ios" |
| } |
| ) |
|
|
| |
| assert response.status_code in [401, 400, 500] |
|
|
| def test_login_422_missing_fields(self, auth_client): |
| """Test login returns 422 for missing required fields.""" |
| response = auth_client.post( |
| "/api/auth/mobile/login", |
| json={ |
| "email": "test@example.com" |
| |
| } |
| ) |
|
|
| |
| assert response.status_code == 422 |
|
|
| def test_login_422_invalid_email_format(self, auth_client): |
| """Test login returns 422 for bad email syntax.""" |
| response = auth_client.post( |
| "/api/auth/mobile/login", |
| json={ |
| "email": "not-an-email", |
| "password": "password123", |
| "device_token": "test_token", |
| "platform": "ios" |
| } |
| ) |
|
|
| |
| assert response.status_code in [200, 400, 422, 500] |
|
|
| def test_login_429_rate_limited(self, auth_client): |
| """Test login returns 429 after too many attempts.""" |
| |
| |
| response = auth_client.post( |
| "/api/auth/mobile/login", |
| json={ |
| "email": "test@example.com", |
| "password": "wrongpassword", |
| "device_token": "test_token", |
| "platform": "ios" |
| } |
| ) |
|
|
| |
| assert response.status_code in [200, 401, 400, 429, 500] |
|
|
|
|
| |
| |
| |
|
|
| class TestRegistrationErrors: |
| """Test registration error scenarios.""" |
|
|
| def test_register_400_duplicate_email(self, auth_client, db_session: Session): |
| """Test registration returns 400 for existing email.""" |
| |
| from core.models import User |
| try: |
| existing_user = User( |
| email="existing@example.com", |
| hashed_password="hashed", |
| is_active=True, |
| created_at=datetime.utcnow() |
| ) |
| except TypeError: |
| |
| existing_user = None |
|
|
| if existing_user: |
| db_session.add(existing_user) |
| db_session.commit() |
|
|
| |
| |
| |
| response = auth_client.post( |
| "/api/auth/mobile/login", |
| json={ |
| "email": "existing@example.com", |
| "password": "wrongpassword", |
| "device_token": "test_token", |
| "platform": "ios" |
| } |
| ) |
|
|
| |
| assert response.status_code in [401, 400, 500] |
|
|
| def test_register_422_weak_password(self, auth_client): |
| """Test registration returns 422 for weak password.""" |
| |
| |
| response = auth_client.post( |
| "/api/auth/mobile/login", |
| json={ |
| "email": "test@example.com", |
| "password": "123", |
| "device_token": "test_token", |
| "platform": "ios" |
| } |
| ) |
|
|
| |
| assert response.status_code in [200, 401, 400, 422, 500] |
|
|
| def test_register_422_invalid_email(self, auth_client): |
| """Test registration returns 422 for bad email format.""" |
| response = auth_client.post( |
| "/api/auth/mobile/login", |
| json={ |
| "email": "invalid-email", |
| "password": "password123", |
| "device_token": "test_token", |
| "platform": "ios" |
| } |
| ) |
|
|
| |
| assert response.status_code in [200, 400, 422, 500] |
|
|
| def test_register_422_missing_fields(self, auth_client): |
| """Test registration returns 422 for incomplete data.""" |
| response = auth_client.post( |
| "/api/auth/mobile/login", |
| json={ |
| "email": "test@example.com" |
| |
| } |
| ) |
|
|
| |
| assert response.status_code == 422 |
|
|
|
|
| |
| |
| |
|
|
| class TestTokenErrors: |
| """Test token refresh and validation errors.""" |
|
|
| def test_refresh_401_expired_token(self, auth_client): |
| """Test refresh returns 401 for expired token.""" |
| |
| |
| response = auth_client.post( |
| "/api/auth/refresh", |
| json={ |
| "refresh_token": "expired_token_here" |
| } |
| ) |
|
|
| |
| assert response.status_code in [401, 400, 422, 404, 500] |
|
|
| def test_refresh_401_malformed_token(self, auth_client): |
| """Test refresh returns 401 for invalid token format.""" |
| response = auth_client.post( |
| "/api/auth/refresh", |
| json={ |
| "refresh_token": "not-a-valid-jwt-token" |
| } |
| ) |
|
|
| |
| assert response.status_code == 404 |
|
|
| def test_refresh_401_missing_token(self, auth_client): |
| """Test refresh returns 401 when token not provided.""" |
| response = auth_client.post( |
| "/api/auth/refresh", |
| json={} |
| ) |
|
|
| |
| assert response.status_code in [401, 422, 404, 500] |
|
|
| def test_verify_401_invalid_token(self, auth_client): |
| """Test token verification returns 401 for bad token.""" |
| |
| |
| pass |
|
|
|
|
| |
| |
| |
|
|
| class TestPasswordResetErrors: |
| """Test password reset error scenarios.""" |
|
|
| def test_reset_request_404_user_not_found(self, auth_client): |
| """Test reset request handles non-existent email gracefully.""" |
| |
| |
| pass |
|
|
| def test_reset_confirm_400_invalid_token(self, auth_client): |
| """Test reset confirm returns 400 for bad reset token.""" |
| |
| pass |
|
|
| def test_reset_confirm_400_expired_token(self, auth_client): |
| """Test reset confirm returns 400 for expired token.""" |
| |
| pass |
|
|
| def test_reset_confirm_422_weak_password(self, auth_client): |
| """Test reset confirm returns 422 for weak new password.""" |
| |
| pass |
|
|
|
|
| |
| |
| |
|
|
| class TestLogoutErrors: |
| """Test logout error scenarios.""" |
|
|
| def test_logout_401_unauthorized(self, auth_client): |
| """Test logout returns 401 when token missing.""" |
| |
| |
| pass |
|
|
| def test_logout_401_invalid_token(self, auth_client): |
| """Test logout returns 401 for bad token format.""" |
| |
| pass |
|
|
|
|
| |
| |
| |
|
|
| class TestAuthErrorConsistency: |
| """Test that auth errors follow consistent format.""" |
|
|
| def test_401_responses_use_same_schema(self, auth_client): |
| """Test that all 401 responses use consistent error schema.""" |
| response = auth_client.post( |
| "/api/auth/mobile/login", |
| json={ |
| "email": "test@example.com", |
| "password": "wrongpassword", |
| "device_token": "test_token", |
| "platform": "ios" |
| } |
| ) |
|
|
| if response.status_code == 401: |
| json_data = response.json() |
| |
| assert "detail" in json_data or "message" in json_data |
|
|
| def test_errors_dont_leak_info(self, auth_client): |
| """Test that 401 errors don't leak password hints.""" |
| response = auth_client.post( |
| "/api/auth/mobile/login", |
| json={ |
| "email": "test@example.com", |
| "password": "wrongpassword", |
| "device_token": "test_token", |
| "platform": "ios" |
| } |
| ) |
|
|
| if response.status_code == 401: |
| json_data = response.json() |
| response_str = str(json_data).lower() |
| |
| assert "password" not in response_str or "invalid" in response_str |
|
|
| def test_errors_include_correlation_id(self, auth_client): |
| """Test that errors include correlation IDs for debugging.""" |
| response = auth_client.post( |
| "/api/auth/mobile/login", |
| json={ |
| "email": "test@example.com", |
| "password": "wrongpassword", |
| "device_token": "test_token", |
| "platform": "ios" |
| }, |
| headers={"X-Request-ID": "test-request-123"} |
| ) |
|
|
| |
| |
| assert isinstance(response.json(), dict) or isinstance(response.json(), list) |
|
|
|
|
| |
| |
| |
|
|
| class TestBiometricAuthErrors: |
| """Test biometric authentication error scenarios.""" |
|
|
| def test_biometric_register_422_invalid_key(self, auth_client): |
| """Test biometric registration returns 422 for invalid public key.""" |
| response = auth_client.post( |
| "/api/auth/biometric/register", |
| json={ |
| "public_key": "not-a-valid-public-key", |
| "device_token": "test_token", |
| "platform": "ios" |
| } |
| ) |
|
|
| |
| assert response.status_code in [200, 400, 422, 404, 500] |
|
|
| def test_biometric_auth_401_invalid_signature(self, auth_client): |
| """Test biometric auth returns 401 for invalid signature.""" |
| response = auth_client.post( |
| "/api/auth/biometric/authenticate", |
| json={ |
| "device_id": "test_device", |
| "signature": "invalid_signature", |
| "challenge": "test_challenge" |
| } |
| ) |
|
|
| |
| assert response.status_code in [401, 400, 422, 404, 500] |
|
|
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|