- Add complete agent harness structure with 8 roles (leader, triager, architect, implementer, reviewer, security, qa, documenter) - Implement strict workflow with 9 stages and mandatory gates - Add comprehensive verification script and runtime status tracking - Create artifact-based evidence system with contracts and schemas - Add agent policy matrix with permissions and anti-cheat rules - Include test suite (44 tests passing) and CI-ready structure - Add documentation: README, HOWTO, CHECKPOINTS, templates - Configure model routing policies and token-aware task assignment - Add BDD/SDD specification guides and feature templates - Include starter pack for quick project onboarding All verification checks pass. Framework ready for production use.
262 lines
10 KiB
Python
262 lines
10 KiB
Python
"""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() |