import os from unittest.mock import AsyncMock, MagicMock, patch import pytest from backend.app.services.garmin import GarminService, GarminAuthError, GarminAPIError from backend.app.models.garmin_sync_log import GarminSyncStatus from datetime import datetime, timedelta import garth # Import garth for type hinting @pytest.fixture def mock_env_vars(): with patch.dict(os.environ, {"GARMIN_USERNAME": "test_user", "GARMIN_PASSWORD": "test_password"}): yield def create_garth_client_mock(): mock_client_instance = MagicMock(spec=garth.Client) mock_client_instance.login = AsyncMock(return_value=True) mock_client_instance.get_activities = AsyncMock(return_value=[]) mock_client_instance.get_activity = AsyncMock(return_value={}) mock_client_instance.load = AsyncMock(side_effect=FileNotFoundError) mock_client_instance.save = AsyncMock() return mock_client_instance @pytest.mark.asyncio async def test_garmin_authentication_success(db_session, mock_env_vars): """Test successful Garmin Connect authentication""" with patch('backend.app.services.garmin.garth.Client') as mock_client_class: mock_instance = mock_client_class.return_value mock_instance.load.side_effect = FileNotFoundError service = GarminService(db_session) result = await service.authenticate() assert result is True mock_instance.login.assert_called_once_with(os.getenv("GARMIN_USERNAME"), os.getenv("GARMIN_PASSWORD")) mock_instance.save.assert_called_once_with(service.session_dir) @pytest.mark.asyncio async def test_garmin_authentication_failure(db_session, mock_env_vars): """Test authentication failure handling""" with patch('backend.app.services.garmin.garth.Client') as mock_client_class: mock_instance = mock_client_class.return_value mock_instance.load.side_effect = FileNotFoundError mock_instance.login.side_effect = Exception("Invalid credentials") service = GarminService(db_session) with pytest.raises(GarminAuthError): await service.authenticate() mock_instance.login.assert_called_once_with(os.getenv("GARMIN_USERNAME"), os.getenv("GARMIN_PASSWORD")) mock_instance.save.assert_not_called() @pytest.mark.asyncio async def test_garmin_authentication_load_session_success(db_session, mock_env_vars): """Test successful loading of existing Garmin session""" with patch('backend.app.services.garmin.garth.Client') as mock_client_class: mock_instance = mock_client_class.return_value mock_instance.load.side_effect = None service = GarminService(db_session) result = await service.authenticate() assert result is True mock_instance.load.assert_called_once_with(service.session_dir) mock_instance.login.assert_not_called() mock_instance.save.assert_not_called() @pytest.mark.asyncio async def test_garmin_authentication_missing_credentials(db_session): """Test authentication failure when credentials are missing""" with patch.dict(os.environ, {"GARMIN_USERNAME": "", "GARMIN_PASSWORD": ""}): with patch('backend.app.services.garmin.garth.Client') as mock_client_class: mock_instance = mock_client_class.return_value mock_instance.load.side_effect = FileNotFoundError service = GarminService(db_session) with pytest.raises(GarminAuthError, match="Garmin username or password not configured."): await service.authenticate() mock_instance.login.assert_not_called() mock_instance.save.assert_not_called() @pytest.mark.asyncio async def test_activity_sync(db_session, mock_env_vars): """Test successful activity synchronization""" with patch('backend.app.services.garmin.garth.Client', new_callable=create_garth_client_mock) as mock_client_class: mock_instance = mock_client_class.return_value mock_instance.get_activities.return_value = [ {"activityId": 123, "startTime": "2024-01-01T08:00:00"} ] service = GarminService(db_session) service.client = mock_instance activities = await service.get_activities() assert len(activities) == 1 assert activities[0]["activityId"] == 123 mock_instance.get_activities.assert_called_once() @pytest.mark.asyncio async def test_rate_limiting_handling(db_session, mock_env_vars): """Test API rate limit error handling""" with patch('backend.app.services.garmin.garth.Client', new_callable=create_garth_client_mock) as mock_client_class: mock_instance = mock_client_class.return_value mock_instance.get_activities.side_effect = Exception("Rate limit exceeded") service = GarminService(db_session) service.client = mock_instance with pytest.raises(GarminAPIError): await service.get_activities() mock_instance.get_activities.assert_called_once() @pytest.mark.asyncio async def test_get_activity_details_success(db_session, mock_env_vars): """Test successful retrieval of activity details.""" with patch('backend.app.services.garmin.garth.Client', new_callable=create_garth_client_mock) as mock_client_class: mock_instance = mock_client_class.return_value mock_instance.get_activity.return_value = {"activityId": 123, "details": "data"} service = GarminService(db_session) service.client = mock_instance details = await service.get_activity_details("123") assert details["activityId"] == 123 mock_instance.get_activity.assert_called_once_with("123") @pytest.mark.asyncio async def test_get_activity_details_failure(db_session, mock_env_vars): """Test failure in retrieving activity details.""" with patch('backend.app.services.garmin.garth.Client', new_callable=create_garth_client_mock) as mock_client_class: mock_instance = mock_client_class.return_value mock_instance.get_activity.side_effect = Exception("Activity not found") service = GarminService(db_session) service.client = mock_instance with pytest.raises(GarminAPIError, match="Failed to fetch activity details"): await service.get_activity_details("123") mock_instance.get_activity.assert_called_once_with("123") @pytest.mark.asyncio async def test_is_authenticated(db_session): """Test is_authenticated method""" service = GarminService(db_session) assert service.is_authenticated() is False service.client = MagicMock() assert service.is_authenticated() is True