from datetime import datetime, timedelta from unittest.mock import patch import pytest from fastapi import HTTPException from jose import jwt from app.core.security import ( create_access_token, create_refresh_token, decode_token, get_password_hash, verify_password, ) class TestJWTTokens: """Test JWT token creation and validation""" def test_create_access_token_default_expiry(self): """Test creating access token with default expiry""" with patch('app.core.security.settings') as mock_settings: mock_settings.jwt_secret = "test_secret" mock_settings.jwt_alg = "HS256" mock_settings.jwt_access_ttl_min = 15 token = create_access_token("user123") # Decode to verify contents payload = jwt.decode(token, "test_secret", algorithms=["HS256"]) assert payload["sub"] == "user123" assert "exp" in payload assert "type" not in payload # Access tokens don't have type def test_create_access_token_custom_expiry(self): """Test creating access token with custom expiry""" with patch('app.core.security.settings') as mock_settings: mock_settings.jwt_secret = "test_secret" mock_settings.jwt_alg = "HS256" custom_delta = timedelta(minutes=30) token = create_access_token("user123", expires_delta=custom_delta) payload = jwt.decode(token, "test_secret", algorithms=["HS256"]) # Check expiry is approximately 30 minutes from now exp_time = datetime.utcfromtimestamp(payload["exp"]) expected_exp = datetime.utcnow() + custom_delta assert abs((exp_time - expected_exp).total_seconds()) < 5 # 5 second tolerance def test_create_refresh_token_default_expiry(self): """Test creating refresh token with default expiry""" with patch('app.core.security.settings') as mock_settings: mock_settings.jwt_secret = "test_secret" mock_settings.jwt_alg = "HS256" mock_settings.jwt_refresh_ttl_days = 7 token = create_refresh_token("user123") payload = jwt.decode(token, "test_secret", algorithms=["HS256"]) assert payload["sub"] == "user123" assert payload["type"] == "refresh" assert "exp" in payload def test_create_refresh_token_custom_expiry(self): """Test creating refresh token with custom expiry""" with patch('app.core.security.settings') as mock_settings: mock_settings.jwt_secret = "test_secret" mock_settings.jwt_alg = "HS256" custom_delta = timedelta(days=14) token = create_refresh_token("user123", expires_delta=custom_delta) payload = jwt.decode(token, "test_secret", algorithms=["HS256"]) assert payload["type"] == "refresh" # Check expiry is approximately 14 days from now exp_time = datetime.utcfromtimestamp(payload["exp"]) expected_exp = datetime.utcnow() + custom_delta assert abs((exp_time - expected_exp).total_seconds()) < 5 def test_decode_token_valid(self): """Test decoding valid JWT token""" with patch('app.core.security.settings') as mock_settings: mock_settings.jwt_secret = "test_secret" mock_settings.jwt_alg = "HS256" # Create a token first original_payload = {"sub": "user123", "exp": datetime.utcnow() + timedelta(minutes=15)} token = jwt.encode(original_payload, "test_secret", algorithm="HS256") # Decode it payload = decode_token(token) assert payload["sub"] == "user123" def test_decode_token_invalid(self): """Test decoding invalid JWT token""" with patch('app.core.security.settings') as mock_settings: mock_settings.jwt_secret = "test_secret" mock_settings.jwt_alg = "HS256" with pytest.raises(HTTPException) as exc_info: decode_token("invalid.jwt.token") assert exc_info.value.status_code == 401 assert "Could not validate credentials" in str(exc_info.value.detail) def test_decode_token_expired(self): """Test decoding expired JWT token""" with patch('app.core.security.settings') as mock_settings: mock_settings.jwt_secret = "test_secret" mock_settings.jwt_alg = "HS256" # Create expired token expired_payload = {"sub": "user123", "exp": datetime.utcnow() - timedelta(minutes=1)} expired_token = jwt.encode(expired_payload, "test_secret", algorithm="HS256") with pytest.raises(HTTPException) as exc_info: decode_token(expired_token) assert exc_info.value.status_code == 401 def test_decode_token_wrong_secret(self): """Test decoding token with wrong secret""" with patch('app.core.security.settings') as mock_settings: mock_settings.jwt_secret = "wrong_secret" mock_settings.jwt_alg = "HS256" # Create token with different secret payload = {"sub": "user123", "exp": datetime.utcnow() + timedelta(minutes=15)} token = jwt.encode(payload, "correct_secret", algorithm="HS256") with pytest.raises(HTTPException) as exc_info: decode_token(token) assert exc_info.value.status_code == 401 class TestPasswordHashing: """Test password hashing and verification""" def test_get_password_hash(self): """Test password hashing""" password = "test_password_123" hashed = get_password_hash(password) # Hash should be different from original password assert hashed != password # Hash should start with bcrypt identifier assert hashed.startswith("$2b$") def test_verify_password_correct(self): """Test password verification with correct password""" password = "test_password_123" hashed = get_password_hash(password) assert verify_password(password, hashed) is True def test_verify_password_incorrect(self): """Test password verification with incorrect password""" password = "test_password_123" wrong_password = "wrong_password" hashed = get_password_hash(password) assert verify_password(wrong_password, hashed) is False def test_password_hash_uniqueness(self): """Test that same password generates different hashes (due to salt)""" password = "test_password_123" hash1 = get_password_hash(password) hash2 = get_password_hash(password) # Hashes should be different due to salt assert hash1 != hash2 # But both should verify correctly assert verify_password(password, hash1) is True assert verify_password(password, hash2) is True def test_verify_empty_password(self): """Test password verification with empty password""" hashed = get_password_hash("real_password") assert verify_password("", hashed) is False def test_verify_empty_hash(self): """Test password verification with empty hash""" # This should handle gracefully without crashing assert verify_password("test_password", "") is False class TestTokenSecurity: """Test token security properties""" def test_token_contains_no_sensitive_data(self): """Test that tokens don't contain sensitive information""" with patch('app.core.security.settings') as mock_settings: mock_settings.jwt_secret = "test_secret" mock_settings.jwt_alg = "HS256" token = create_access_token("user123") payload = jwt.decode(token, "test_secret", algorithms=["HS256"]) # Token should only contain safe fields assert "password" not in payload assert "hashed_password" not in payload assert "email" not in payload assert payload["sub"] == "user123" def test_refresh_token_has_type_field(self): """Test that refresh tokens have type field""" with patch('app.core.security.settings') as mock_settings: mock_settings.jwt_secret = "test_secret" mock_settings.jwt_alg = "HS256" mock_settings.jwt_refresh_ttl_days = 7 token = create_refresh_token("user123") payload = jwt.decode(token, "test_secret", algorithms=["HS256"]) assert payload["type"] == "refresh" def test_access_token_has_no_type_field(self): """Test that access tokens don't have type field""" with patch('app.core.security.settings') as mock_settings: mock_settings.jwt_secret = "test_secret" mock_settings.jwt_alg = "HS256" mock_settings.jwt_access_ttl_min = 15 token = create_access_token("user123") payload = jwt.decode(token, "test_secret", algorithms=["HS256"]) assert "type" not in payload