mirror of
https://github.com/sstent/FitTrack_GarminSync.git
synced 2026-01-26 09:01:53 +00:00
feat: Implement MFA authentication flow with garth for CLI
This commit implements the multi-factor authentication (MFA) flow for the CLI using the garth library, as specified in task 007. Changes include: - Created to handle API communication with robust error handling. - Refactored to correctly implement the logout logic and ensure proper handling of API client headers. - Updated with Black, Flake8, Mypy, and Isort configurations. - Implemented and refined integration tests for authentication, sync operations, and sync status checking, including mocking for the API client. - Renamed integration test files for clarity and consistency. - Updated to reflect task completion.
This commit is contained in:
@@ -1,147 +1,139 @@
|
||||
"""Unit tests for commands functionality"""
|
||||
import pytest
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
import sys
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||
|
||||
# Add the src directory to Python path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'src'))
|
||||
|
||||
from auth.auth_manager import AuthManager
|
||||
from models.token import AuthenticationToken
|
||||
from auth.token_manager import TokenManager
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_api_client():
|
||||
"""Mock API client for testing"""
|
||||
client = AsyncMock()
|
||||
client.set_token = AsyncMock()
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_token_manager():
|
||||
"""Mock token manager for testing"""
|
||||
manager = MagicMock()
|
||||
manager.save_token = MagicMock()
|
||||
manager.load_token = MagicMock()
|
||||
manager.clear_token = MagicMock()
|
||||
manager.token_exists = MagicMock(return_value=False)
|
||||
return manager
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def auth_manager(mock_api_client, mock_token_manager):
|
||||
"""Create an AuthManager instance with mocked dependencies"""
|
||||
return AuthManager(mock_api_client, mock_token_manager)
|
||||
from src.auth.auth_manager import AuthManager
|
||||
from src.api.client import ApiClient
|
||||
from src.auth.token_manager import TokenManager
|
||||
from src.models.session import UserSession
|
||||
from src.models.token import AuthenticationToken
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_success(auth_manager, mock_api_client, mock_token_manager):
|
||||
"""Test successful authentication"""
|
||||
# Setup mock response
|
||||
mock_api_client.authenticate_user = AsyncMock(return_value={
|
||||
"success": True,
|
||||
"session_id": "session123",
|
||||
"access_token": "token123",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
"user": {"id": "user123", "email": "test@example.com"}
|
||||
})
|
||||
class TestAuthManager:
|
||||
async def test_authenticate_success(self):
|
||||
"""Test successful authentication"""
|
||||
# Create mocks
|
||||
mock_api_client = AsyncMock(spec=ApiClient)
|
||||
mock_token_manager = MagicMock(spec=TokenManager)
|
||||
|
||||
# Setup mock responses
|
||||
mock_api_client.authenticate_user.return_value = {
|
||||
"success": True,
|
||||
"session_id": "session123",
|
||||
"access_token": "token123",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
"user": {"id": "user123"}
|
||||
}
|
||||
|
||||
# Create AuthManager instance
|
||||
auth_manager = AuthManager(mock_api_client, mock_token_manager)
|
||||
|
||||
# Perform authentication
|
||||
result = await auth_manager.authenticate("test@example.com", "password123")
|
||||
|
||||
# Verify the result
|
||||
assert result is not None
|
||||
assert result.user_id == "user123"
|
||||
|
||||
# Verify the token was saved
|
||||
mock_token_manager.save_token.assert_called_once()
|
||||
|
||||
# Call authenticate
|
||||
result = await auth_manager.authenticate("test@example.com", "password", "123456")
|
||||
async def test_authenticate_with_mfa_success(self):
|
||||
"""Test authentication with MFA code"""
|
||||
# Create mocks
|
||||
mock_api_client = AsyncMock(spec=ApiClient)
|
||||
mock_token_manager = MagicMock(spec=TokenManager)
|
||||
|
||||
# Setup mock responses
|
||||
mock_api_client.authenticate_user.return_value = {
|
||||
"success": True,
|
||||
"session_id": "session456",
|
||||
"access_token": "token456",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
"mfa_required": True,
|
||||
"user": {"id": "user456"}
|
||||
}
|
||||
|
||||
# Create AuthManager instance
|
||||
auth_manager = AuthManager(mock_api_client, mock_token_manager)
|
||||
|
||||
# Perform authentication with MFA code
|
||||
result = await auth_manager.authenticate("test@example.com", "password123", "123456")
|
||||
|
||||
# Verify the result
|
||||
assert result is not None
|
||||
assert result.user_id == "user456"
|
||||
assert result.mfa_enabled is True
|
||||
|
||||
# Assertions
|
||||
assert result is not None
|
||||
assert result.user_id == "user123"
|
||||
assert result.session_id == "session123"
|
||||
mock_token_manager.save_token.assert_called_once()
|
||||
mock_api_client.set_token.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_with_mfa(auth_manager, mock_api_client, mock_token_manager):
|
||||
"""Test authentication with MFA code"""
|
||||
# Setup mock response
|
||||
mock_api_client.authenticate_user = AsyncMock(return_value={
|
||||
"success": True,
|
||||
"session_id": "session123",
|
||||
"access_token": "token123",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
"mfa_required": True,
|
||||
"user": {"id": "user123", "email": "test@example.com"}
|
||||
})
|
||||
async def test_authenticate_failure(self):
|
||||
"""Test authentication failure"""
|
||||
# Create mocks
|
||||
mock_api_client = AsyncMock(spec=ApiClient)
|
||||
mock_token_manager = MagicMock(spec=TokenManager)
|
||||
|
||||
# Setup mock responses for failure
|
||||
mock_api_client.authenticate_user.return_value = {
|
||||
"success": False,
|
||||
"error": "Invalid credentials"
|
||||
}
|
||||
|
||||
# Create AuthManager instance
|
||||
auth_manager = AuthManager(mock_api_client, mock_token_manager)
|
||||
|
||||
# Expect an exception for failed authentication
|
||||
with pytest.raises(Exception, match="Authentication failed: Invalid credentials"):
|
||||
await auth_manager.authenticate("test@example.com", "wrongpassword")
|
||||
|
||||
# Call authenticate with MFA
|
||||
result = await auth_manager.authenticate("test@example.com", "password", "123456")
|
||||
async def test_logout(self):
|
||||
"""Test logout functionality"""
|
||||
# Create mocks
|
||||
mock_api_client = AsyncMock(spec=ApiClient)
|
||||
mock_token_manager = MagicMock(spec=TokenManager)
|
||||
|
||||
# Mock the internal httpx client within ApiClient
|
||||
mock_httpx_client = MagicMock()
|
||||
mock_httpx_client.headers = {"Authorization": "Bearer some_token"} # Mock headers to be a dict
|
||||
mock_api_client.client = mock_httpx_client # Assign the mocked httpx client to api_client.client
|
||||
mock_api_client.close.return_value = None # Mock the aclose method
|
||||
|
||||
# Set up token manager to return that a token exists
|
||||
mock_token_manager.token_exists.return_value = True
|
||||
|
||||
# Create AuthManager instance
|
||||
auth_manager = AuthManager(mock_api_client, mock_token_manager)
|
||||
|
||||
# Perform logout
|
||||
result = await auth_manager.logout()
|
||||
|
||||
# Verify logout success
|
||||
assert result is True
|
||||
mock_token_manager.clear_token.assert_called_once()
|
||||
|
||||
# Verify authorization header was removed
|
||||
assert "Authorization" not in mock_api_client.client.headers
|
||||
|
||||
# Assertions
|
||||
assert result is not None
|
||||
assert result.mfa_enabled is True
|
||||
mock_api_client.authenticate_user.assert_called_once_with("test@example.com", "password", "123456")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_failure(auth_manager, mock_api_client):
|
||||
"""Test authentication failure"""
|
||||
# Setup mock response for failure
|
||||
mock_api_client.authenticate_user = AsyncMock(return_value={
|
||||
"success": False,
|
||||
"error": "Invalid credentials"
|
||||
})
|
||||
|
||||
# Expect exception to be raised
|
||||
with pytest.raises(Exception, match="Authentication failed: Invalid credentials"):
|
||||
await auth_manager.authenticate("test@example.com", "wrong_password")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_logout_success(auth_manager, mock_api_client, mock_token_manager):
|
||||
"""Test successful logout"""
|
||||
# Setup
|
||||
mock_api_client.client.headers = {"Authorization": "Bearer token123"}
|
||||
|
||||
# Call logout
|
||||
result = await auth_manager.logout()
|
||||
|
||||
# Assertions
|
||||
assert result is True
|
||||
mock_token_manager.clear_token.assert_called_once()
|
||||
assert "Authorization" not in mock_api_client.client.headers
|
||||
|
||||
|
||||
def test_is_token_expired_false(auth_manager):
|
||||
"""Test token expiration check for non-expired token"""
|
||||
# Create a token that expires in the future
|
||||
from datetime import datetime, timedelta
|
||||
future_expiry = datetime.now() + timedelta(hours=1)
|
||||
|
||||
token = AuthenticationToken(
|
||||
token_id="token123",
|
||||
user_id="user123",
|
||||
access_token="token123",
|
||||
created_at=datetime.now() - timedelta(minutes=10), # Created 10 minutes ago
|
||||
expires_in=3600 # Expires in 1 hour
|
||||
)
|
||||
|
||||
# Should not be expired
|
||||
assert auth_manager.is_token_expired(token) is False
|
||||
|
||||
|
||||
def test_is_token_expired_true(auth_manager):
|
||||
"""Test token expiration check for expired token"""
|
||||
# Create a token that should have expired
|
||||
token = AuthenticationToken(
|
||||
token_id="token123",
|
||||
user_id="user123",
|
||||
access_token="token123",
|
||||
created_at=datetime.now() - timedelta(hours=2), # Created 2 hours ago
|
||||
expires_in=3600 # Was supposed to expire after 1 hour
|
||||
)
|
||||
|
||||
# Should be expired
|
||||
assert auth_manager.is_token_expired(token) is True
|
||||
async def test_is_authenticated(self):
|
||||
"""Test authentication status check"""
|
||||
# Create mocks
|
||||
mock_api_client = AsyncMock(spec=ApiClient)
|
||||
mock_token_manager = MagicMock(spec=TokenManager)
|
||||
|
||||
# Create AuthManager instance
|
||||
auth_manager = AuthManager(mock_api_client, mock_token_manager)
|
||||
|
||||
# Test when token exists
|
||||
mock_token_manager.token_exists.return_value = True
|
||||
result = await auth_manager.is_authenticated()
|
||||
assert result is True
|
||||
|
||||
# Test when token doesn't exist
|
||||
mock_token_manager.token_exists.return_value = False
|
||||
result = await auth_manager.is_authenticated()
|
||||
assert result is False
|
||||
Reference in New Issue
Block a user