"""Tests for authentication utilities.""" from datetime import UTC, datetime, timedelta import pytest from fastapi import HTTPException from app.utils.auth import JWTUtils, PasswordUtils, TokenUtils PASSWORD_HASH_LENGTH_GT = 50 TOKEN_LENGTH_GT = 20 TOKEN_DOTS_COUNT = 2 STATUS_CODE_UNAUTHORIZED = 401 class TestPasswordUtils: """Test password utility functions.""" def test_hash_password(self) -> None: """Test password hashing.""" password = "testpassword123" hashed = PasswordUtils.hash_password(password) # Hash should be different from original password assert hashed != password # Hash should be a string assert isinstance(hashed, str) # Hash should have reasonable length (bcrypt produces ~60 chars) assert len(hashed) > PASSWORD_HASH_LENGTH_GT def test_hash_password_different_salts(self) -> None: """Test that same password produces different hashes (different salts).""" password = "testpassword123" hash1 = PasswordUtils.hash_password(password) hash2 = PasswordUtils.hash_password(password) # Same password should produce different hashes due to different salts assert hash1 != hash2 def test_verify_password_correct(self) -> None: """Test password verification with correct password.""" password = "testpassword123" hashed = PasswordUtils.hash_password(password) # Correct password should verify assert PasswordUtils.verify_password(password, hashed) is True def test_verify_password_incorrect(self) -> None: """Test password verification with incorrect password.""" password = "testpassword123" wrong_password = "wrongpassword" hashed = PasswordUtils.hash_password(password) # Wrong password should not verify assert PasswordUtils.verify_password(wrong_password, hashed) is False def test_verify_password_empty(self) -> None: """Test password verification with empty password.""" password = "testpassword123" hashed = PasswordUtils.hash_password(password) # Empty password should not verify assert PasswordUtils.verify_password("", hashed) is False class TestJWTUtils: """Test JWT utility functions.""" def test_create_access_token(self) -> None: """Test JWT token creation.""" data = {"sub": "123", "email": "test@example.com"} token = JWTUtils.create_access_token(data) # Token should be a string assert isinstance(token, str) # Token should have reasonable length assert len(token) > PASSWORD_HASH_LENGTH_GT # Token should contain dots (JWT format) assert token.count(".") == TOKEN_DOTS_COUNT def test_create_access_token_with_expiry(self) -> None: """Test JWT token creation with custom expiry.""" data = {"sub": "123", "email": "test@example.com"} expires_delta = timedelta(minutes=5) token = JWTUtils.create_access_token(data, expires_delta) # Should create a valid token assert isinstance(token, str) assert len(token) > PASSWORD_HASH_LENGTH_GT def test_decode_access_token(self) -> None: """Test JWT token decoding.""" original_data = {"sub": "123", "email": "test@example.com", "role": "user"} token = JWTUtils.create_access_token(original_data) decoded_data = JWTUtils.decode_access_token(token) # Should decode to original data (plus exp) assert decoded_data["sub"] == original_data["sub"] assert decoded_data["email"] == original_data["email"] assert decoded_data["role"] == original_data["role"] assert "exp" in decoded_data def test_decode_invalid_token(self) -> None: """Test decoding invalid JWT token.""" invalid_token = "invalid.token.here" with pytest.raises(HTTPException) as exc_info: JWTUtils.decode_access_token(invalid_token) assert exc_info.value.status_code == STATUS_CODE_UNAUTHORIZED assert "Could not validate credentials" in exc_info.value.detail def test_decode_expired_token(self) -> None: """Test decoding expired JWT token.""" data = {"sub": "123", "email": "test@example.com"} # Create token that expires immediately expires_delta = timedelta(seconds=-1) token = JWTUtils.create_access_token(data, expires_delta) with pytest.raises(HTTPException) as exc_info: JWTUtils.decode_access_token(token) assert exc_info.value.status_code == STATUS_CODE_UNAUTHORIZED assert "Token has expired" in exc_info.value.detail def test_decode_empty_token(self) -> None: """Test decoding empty token.""" with pytest.raises(HTTPException) as exc_info: JWTUtils.decode_access_token("") assert exc_info.value.status_code == STATUS_CODE_UNAUTHORIZED class TestTokenUtils: """Test token utility functions.""" def test_generate_api_token(self) -> None: """Test API token generation.""" token = TokenUtils.generate_api_token() # Token should be a string assert isinstance(token, str) # Token should have reasonable length assert len(token) > TOKEN_LENGTH_GT # Token should be URL-safe assert all(c.isalnum() or c in "-_" for c in token) def test_generate_api_token_unique(self) -> None: """Test that API tokens are unique.""" token1 = TokenUtils.generate_api_token() token2 = TokenUtils.generate_api_token() # Tokens should be different assert token1 != token2 def test_is_token_expired_none(self) -> None: """Test token expiry check with None.""" # None expiry should not be expired assert TokenUtils.is_token_expired(None) is False def test_is_token_expired_future(self) -> None: """Test token expiry check with future date.""" future_date = datetime.now(UTC) + timedelta(hours=1) # Future date should not be expired assert TokenUtils.is_token_expired(future_date) is False def test_is_token_expired_past(self) -> None: """Test token expiry check with past date.""" past_date = datetime.now(UTC) - timedelta(hours=1) # Past date should be expired assert TokenUtils.is_token_expired(past_date) is True def test_is_token_expired_naive_datetime(self) -> None: """Test token expiry check with naive datetime.""" # Create a past naive datetime (using UTC time to be consistent) past_date = datetime.now(UTC) - timedelta(hours=1) # Should handle naive datetime (treat as UTC) assert TokenUtils.is_token_expired(past_date) is True