# Unit tests for authentication service import pytest from unittest.mock import AsyncMock, patch from datetime import datetime, timedelta from app.core.security import ( create_access_token, verify_token, hash_password, verify_password, get_current_user ) from app.schemas.user import UserCreate from app.crud.user import create_user, authenticate_user class TestPasswordHashing: """Test password hashing functionality.""" def test_hash_password_creates_hash(self): """Test that password hashing creates a hash.""" password = "testpassword123" hashed = hash_password(password) assert hashed != password assert len(hashed) > 50 # bcrypt hashes are long assert hashed.startswith("$2b$") def test_verify_password_correct(self): """Test password verification with correct password.""" password = "testpassword123" hashed = hash_password(password) assert verify_password(password, hashed) is True def test_verify_password_incorrect(self): """Test password verification with incorrect password.""" password = "testpassword123" wrong_password = "wrongpassword" hashed = hash_password(password) assert verify_password(wrong_password, hashed) is False def test_hash_same_password_different_hashes(self): """Test that hashing the same password twice gives different hashes.""" password = "testpassword123" hash1 = hash_password(password) hash2 = hash_password(password) assert hash1 != hash2 assert verify_password(password, hash1) is True assert verify_password(password, hash2) is True class TestJWTTokens: """Test JWT token functionality.""" def test_create_access_token(self): """Test creating access token.""" data = {"sub": "user123", "email": "test@example.com"} token = create_access_token(data=data) assert isinstance(token, str) assert len(token) > 100 # JWT tokens are long assert "." in token # JWT format has dots def test_create_token_with_expiry(self): """Test creating token with custom expiry.""" data = {"sub": "user123"} expires_delta = timedelta(minutes=30) token = create_access_token(data=data, expires_delta=expires_delta) # Verify token was created assert isinstance(token, str) assert len(token) > 100 def test_verify_valid_token(self): """Test verifying a valid token.""" data = {"sub": "user123", "email": "test@example.com"} token = create_access_token(data=data) payload = verify_token(token) assert payload["sub"] == "user123" assert payload["email"] == "test@example.com" assert "exp" in payload def test_verify_invalid_token(self): """Test verifying an invalid token.""" invalid_token = "invalid.token.here" with pytest.raises(Exception): # Should raise an exception verify_token(invalid_token) def test_verify_expired_token(self): """Test verifying an expired token.""" data = {"sub": "user123"} # Create token that expires immediately expires_delta = timedelta(seconds=-1) token = create_access_token(data=data, expires_delta=expires_delta) with pytest.raises(Exception): # Should raise an exception verify_token(token) class TestUserCRUD: """Test user CRUD operations.""" @pytest.mark.asyncio async def test_create_user_success(self, test_db): """Test successful user creation.""" user_data = UserCreate( email="newuser@test.com", password="securepassword123", first_name="New", last_name="User" ) user = await create_user(test_db, user_data) assert user.email == "newuser@test.com" assert user.first_name == "New" assert user.last_name == "User" assert user.password_hash != "securepassword123" # Should be hashed assert user.id is not None assert user.created_at is not None @pytest.mark.asyncio async def test_create_user_duplicate_email(self, test_db): """Test creating user with duplicate email.""" # Create first user user_data1 = UserCreate( email="duplicate@test.com", password="password123", first_name="First", last_name="User" ) await create_user(test_db, user_data1) await test_db.commit() # Try to create second user with same email user_data2 = UserCreate( email="duplicate@test.com", password="password456", first_name="Second", last_name="User" ) with pytest.raises(Exception): # Should raise integrity error await create_user(test_db, user_data2) await test_db.commit() @pytest.mark.asyncio async def test_authenticate_user_success(self, test_db): """Test successful user authentication.""" # Create user user_data = UserCreate( email="auth@test.com", password="testpassword123", first_name="Auth", last_name="User" ) user = await create_user(test_db, user_data) await test_db.commit() # Authenticate user authenticated_user = await authenticate_user( test_db, "auth@test.com", "testpassword123" ) assert authenticated_user is not None assert authenticated_user.email == "auth@test.com" assert authenticated_user.id == user.id @pytest.mark.asyncio async def test_authenticate_user_wrong_password(self, test_db): """Test authentication with wrong password.""" # Create user user_data = UserCreate( email="wrongpass@test.com", password="correctpassword", first_name="Test", last_name="User" ) await create_user(test_db, user_data) await test_db.commit() # Try to authenticate with wrong password authenticated_user = await authenticate_user( test_db, "wrongpass@test.com", "wrongpassword" ) assert authenticated_user is None @pytest.mark.asyncio async def test_authenticate_user_nonexistent(self, test_db): """Test authentication with non-existent user.""" authenticated_user = await authenticate_user( test_db, "nonexistent@test.com", "password123" ) assert authenticated_user is None class TestAuthenticationIntegration: """Test authentication integration with FastAPI.""" @pytest.mark.asyncio async def test_get_current_user_valid_token(self, test_db, test_user): """Test getting current user with valid token.""" # Create token for test user token_data = {"sub": str(test_user.id), "email": test_user.email} token = create_access_token(data=token_data) # Mock the database dependency with patch('app.core.security.get_db') as mock_get_db: mock_get_db.return_value.__aenter__.return_value = test_db current_user = await get_current_user(token, test_db) assert current_user.id == test_user.id assert current_user.email == test_user.email @pytest.mark.asyncio async def test_get_current_user_invalid_token(self, test_db): """Test getting current user with invalid token.""" invalid_token = "invalid.token.here" with pytest.raises(Exception): await get_current_user(invalid_token, test_db) @pytest.mark.asyncio async def test_get_current_user_nonexistent_user(self, test_db): """Test getting current user for non-existent user.""" # Create token for non-existent user token_data = {"sub": "non-existent-id", "email": "fake@test.com"} token = create_access_token(data=token_data) with pytest.raises(Exception): await get_current_user(token, test_db) class TestSecurityValidation: """Test security validation functions.""" def test_password_strength_validation(self): """Test password strength requirements.""" # This would test password strength if implemented weak_passwords = [ "123", "password", "abc", "12345678" ] strong_passwords = [ "SecurePassword123!", "MyStr0ngP@ssw0rd", "C0mpl3xP@ssw0rd!" ] # Note: Implement password strength validation if needed assert True # Placeholder def test_email_validation(self): """Test email format validation.""" valid_emails = [ "test@example.com", "user.name@domain.co.uk", "user+tag@example.org" ] invalid_emails = [ "invalid-email", "user@", "@domain.com", "user@domain" ] # Note: Email validation is typically handled by Pydantic assert True # Placeholder @pytest.mark.asyncio async def test_rate_limiting_simulation(self): """Test rate limiting for authentication attempts.""" # This would test rate limiting if implemented # Simulate multiple failed login attempts failed_attempts = [] for i in range(5): # Mock failed authentication attempt failed_attempts.append(f"attempt_{i}") assert len(failed_attempts) == 5 # In real implementation, would test that after X failed attempts, # further attempts are rate limited class TestTokenSecurity: """Test token security features.""" def test_token_contains_required_claims(self): """Test that tokens contain required claims.""" data = {"sub": "user123", "email": "test@example.com"} token = create_access_token(data=data) payload = verify_token(token) # Check required claims assert "sub" in payload assert "exp" in payload assert "iat" in payload assert payload["sub"] == "user123" def test_token_expiry_time(self): """Test token expiry time is set correctly.""" data = {"sub": "user123"} expires_delta = timedelta(minutes=30) token = create_access_token(data=data, expires_delta=expires_delta) payload = verify_token(token) # Check expiry is approximately correct (within 1 minute tolerance) exp_time = datetime.fromtimestamp(payload["exp"]) expected_exp = datetime.utcnow() + expires_delta time_diff = abs((exp_time - expected_exp).total_seconds()) assert time_diff < 60 # Within 1 minute tolerance def test_token_uniqueness(self): """Test that different tokens are generated for same data.""" data = {"sub": "user123", "email": "test@example.com"} token1 = create_access_token(data=data) token2 = create_access_token(data=data) # Tokens should be different due to different iat (issued at) times assert token1 != token2 # But both should decode to similar payload (except iat and exp) payload1 = verify_token(token1) payload2 = verify_token(token2) assert payload1["sub"] == payload2["sub"] assert payload1["email"] == payload2["email"]