"""Unit tests for authentication service.""" import unittest from unittest.mock import MagicMock, patch from src.models.auth import LoginRequest, TokenPayload from src.services.auth_service import ( AuthService, auth_service, InvalidCredentialsError, AccountLockedError, InvalidTokenError ) from src.services.token_service import TokenService from src.services.session_store import SessionStore class TestAuthService(unittest.TestCase): """Test cases for AuthService.""" def setUp(self): """Set up test fixtures.""" self.mock_token_service = MagicMock(spec=TokenService) self.mock_session_store = MagicMock(spec=SessionStore) self.auth_service = AuthService( token_svc=self.mock_token_service, session_store=self.mock_session_store ) # Default mock returns self.mock_token_service.create_access_token.return_value = ("access_token_123", "token_id_123") self.mock_token_service.create_refresh_token.return_value = "refresh_token_456" def test_login_success(self): """Test successful login.""" request = LoginRequest(email="alice@example.com", password="SecurePass123!") with patch.object(self.auth_service, '_get_user_by_email') as mock_get_user: mock_get_user.return_value = { "id": "user-001", "email": "alice@example.com", "password_hash": "hashed_password", "role": "user", "active": True } with patch.object(self.auth_service, '_verify_password', return_value=True): result = self.auth_service.login(request, "127.0.0.1") self.assertTrue(result.success) self.assertEqual(result.access_token, "access_token_123") self.assertEqual(result.refresh_token, "refresh_token_456") self.mock_session_store.create_session.assert_called_once() def test_login_invalid_credentials(self): """Test login with invalid credentials.""" request = LoginRequest(email="alice@example.com", password="WrongPassword!") with patch.object(self.auth_service, '_get_user_by_email') as mock_get_user: mock_get_user.return_value = { "id": "user-001", "email": "alice@example.com", "password_hash": "hashed_password", "role": "user", "active": True } with patch.object(self.auth_service, '_verify_password', return_value=False): with self.assertRaises(InvalidCredentialsError): self.auth_service.login(request, "127.0.0.1") def test_login_nonexistent_user(self): """Test login with nonexistent user.""" request = LoginRequest(email="nonexistent@test.com", password="AnyPassword123!") with patch.object(self.auth_service, '_get_user_by_email', return_value=None): with self.assertRaises(InvalidCredentialsError): self.auth_service.login(request, "127.0.0.1") def test_login_inactive_account(self): """Test login with inactive account.""" request = LoginRequest(email="alice@example.com", password="SecurePass123!") with patch.object(self.auth_service, '_get_user_by_email') as mock_get_user: mock_get_user.return_value = { "id": "user-001", "email": "alice@example.com", "password_hash": "hashed_password", "role": "user", "active": False } with self.assertRaises(InvalidCredentialsError) as ctx: self.auth_service.login(request, "127.0.0.1") self.assertIn("desactivada", str(ctx.exception)) def test_logout_success(self): """Test successful logout.""" result = self.auth_service.logout("token_id_123", "user-001") self.assertTrue(result) self.mock_session_store.revoke_session.assert_called_once_with("token_id_123") def test_logout_all_sessions(self): """Test logout all sessions.""" self.mock_session_store.revoke_all_user_sessions.return_value = 3 result = self.auth_service.logout_all("user-001") self.assertEqual(result, 3) self.mock_session_store.revoke_all_user_sessions.assert_called_once_with("user-001") def test_refresh_success(self): """Test successful token refresh.""" mock_payload = TokenPayload( sub="user-001", email="alice@example.com", role="user", iat=1234567890, exp=1234567890 + 900, jti="token_id_123", type="refresh" ) self.mock_token_service.verify_token.return_value = mock_payload self.mock_session_store.is_session_valid.return_value = True self.mock_token_service.create_access_token.return_value = ("new_access_token", "new_token_id") result = self.auth_service.refresh("valid_refresh_token") self.assertTrue(result.success) self.assertEqual(result.access_token, "new_access_token") def test_refresh_invalid_token(self): """Test refresh with invalid token.""" self.mock_token_service.verify_token.side_effect = InvalidTokenError("Invalid token") with self.assertRaises(InvalidTokenError): self.auth_service.refresh("invalid_token") def test_refresh_revoked_session(self): """Test refresh with revoked session.""" mock_payload = TokenPayload( sub="user-001", email="alice@example.com", role="user", iat=1234567890, exp=1234567890 + 900, jti="token_id_123", type="refresh" ) self.mock_token_service.verify_token.return_value = mock_payload self.mock_session_store.is_session_valid.return_value = False with self.assertRaises(InvalidTokenError): self.auth_service.refresh("valid_refresh_token") def test_validate_token_valid(self): """Test token validation with valid token.""" mock_payload = TokenPayload( sub="user-001", email="alice@example.com", role="user", iat=1234567890, exp=1234567890 + 900, jti="token_id_123" ) self.mock_token_service.verify_token.return_value = mock_payload self.mock_session_store.is_session_valid.return_value = True is_valid, payload, error = self.auth_service.validate_token("valid_token") self.assertTrue(is_valid) self.assertEqual(payload.sub, "user-001") self.assertIsNone(error) def test_validate_token_revoked(self): """Test token validation with revoked session.""" mock_payload = TokenPayload( sub="user-001", email="alice@example.com", role="user", iat=1234567890, exp=1234567890 + 900, jti="token_id_123" ) self.mock_token_service.verify_token.return_value = mock_payload self.mock_session_store.is_session_valid.return_value = False is_valid, payload, error = self.auth_service.validate_token("valid_token") self.assertFalse(is_valid) self.assertEqual(error, "Session revoked") def test_validate_token_expired(self): """Test token validation with expired token.""" from jwt.exceptions import ExpiredSignatureError self.mock_token_service.verify_token.side_effect = ExpiredSignatureError("Token expired") is_valid, payload, error = self.auth_service.validate_token("expired_token") self.assertFalse(is_valid) self.assertIn("expired", error.lower()) class TestRateLimiting(unittest.TestCase): """Test rate limiting functionality.""" def setUp(self): self.auth_service = AuthService() def test_rate_limit_allows_first_attempts(self): """Test rate limit allows initial attempts.""" import time ip = "10.0.0.1" # Clear any existing rate limit data self.auth_service._rate_limit_store = {ip: []} request = LoginRequest(email="test@test.com", password="wrong") with patch.object(self.auth_service, '_get_user_by_email', return_value=None): # First 9 attempts should raise InvalidCredentialsError (not locked) for i in range(9): try: self.auth_service.login(request, ip) except InvalidCredentialsError: pass # Expected - user doesn't exist except AccountLockedError: self.fail("Should not be locked after 9 attempts") # 10th attempt should still raise InvalidCredentialsError (not locked yet) try: self.auth_service.login(request, ip) except InvalidCredentialsError: pass # Still not locked except AccountLockedError: self.fail("Should not be locked after 10 attempts") # Verify rate limit counter is at 10 self.assertEqual(len(self.auth_service._rate_limit_store[ip]), 10) def test_rate_limit_blocks_after_threshold(self): """Test rate limit blocks after threshold.""" import time ip = "10.0.0.2" # Pre-fill rate limit now = time.time() self.auth_service._rate_limit_store[ip] = [now - 50] * 10 request = LoginRequest(email="test@test.com", password="wrong") with patch.object(self.auth_service, '_get_user_by_email', return_value=None): with self.assertRaises(AccountLockedError): self.auth_service.login(request, ip) if __name__ == "__main__": unittest.main()