mirror of
https://github.com/sstent/AICyclingCoach.git
synced 2026-02-07 23:12:06 +00:00
sync
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, patch, MagicMock
|
||||
from unittest.mock import patch, MagicMock
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
||||
from sqlalchemy.pool import StaticPool
|
||||
from sqlalchemy import select
|
||||
@@ -12,6 +12,7 @@ from datetime import datetime, timedelta
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from backend.app.config import Settings
|
||||
import garminconnect
|
||||
|
||||
# --- Completely Rewritten Fixtures ---
|
||||
|
||||
@@ -51,11 +52,12 @@ async def db_session(test_engine, setup_database):
|
||||
|
||||
@pytest.fixture
|
||||
def mock_garmin_service():
|
||||
"""Mock the GarminService for testing."""
|
||||
"""Mock the GarminConnectService for testing."""
|
||||
mock_service = MagicMock(spec=GarminService)
|
||||
mock_service.authenticate = AsyncMock(return_value=True)
|
||||
mock_service.get_activities = AsyncMock(return_value=[])
|
||||
mock_service.get_activity_details = AsyncMock(return_value={})
|
||||
# python-garminconnect.Garmin.login is not async
|
||||
mock_service.authenticate = MagicMock(return_value=True)
|
||||
mock_service.get_activities = MagicMock(return_value=[])
|
||||
mock_service.get_activity_details = MagicMock(return_value={})
|
||||
return mock_service
|
||||
|
||||
@pytest.fixture
|
||||
@@ -86,18 +88,18 @@ async def test_successful_sync_functional(db_session: AsyncSession, mock_garmin_
|
||||
{
|
||||
'activityId': '1001',
|
||||
'activityType': {'typeKey': 'cycling'},
|
||||
'startTimeLocal': (datetime.now() - timedelta(days=1)).isoformat(),
|
||||
'startTimeLocal': (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d %H:%M:%S"),
|
||||
'duration': 3600,
|
||||
'distance': 50000,
|
||||
'averageHR': 150,
|
||||
'maxHR': 180,
|
||||
'avgPower': 200,
|
||||
'elevationGain': 500
|
||||
'averageHeartRateInBeatsPerMinute': 150, # Adjusted for python-garminconnect
|
||||
'maxHeartRateInBeatsPerMinute': 180, # Adjusted for python-garminconnect
|
||||
'averagePower': 200, # Adjusted for python-garminconnect
|
||||
'totalElevationGain': 500 # Adjusted for python-garminconnect
|
||||
}
|
||||
]
|
||||
mock_garmin_service.get_activity_details.return_value = {
|
||||
'avgPower': 200,
|
||||
'elevationGain': 500,
|
||||
'averagePower': 200,
|
||||
'totalElevationGain': 500,
|
||||
'temperature': 25
|
||||
}
|
||||
|
||||
@@ -159,7 +161,7 @@ async def test_sync_with_authentication_error(db_session: AsyncSession, mock_gar
|
||||
service.garmin_service = mock_garmin_service
|
||||
|
||||
# Arrange
|
||||
mock_garmin_service.get_activities.side_effect = GarminAuthError("Invalid credentials")
|
||||
mock_garmin_service.authenticate.side_effect = GarminAuthError("Invalid credentials")
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(GarminAuthError):
|
||||
@@ -181,7 +183,7 @@ async def test_sync_with_api_error(db_session: AsyncSession, mock_garmin_service
|
||||
service.garmin_service = mock_garmin_service
|
||||
|
||||
# Arrange
|
||||
mock_garmin_service.get_activities.side_effect = GarminAPIError("Garmin service unavailable")
|
||||
mock_garmin_service.get_activities.side_effect = garminconnect.GarminConnectException("Garmin service unavailable")
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(GarminAPIError):
|
||||
@@ -207,22 +209,22 @@ async def test_sync_with_activity_details_retry_success(db_session: AsyncSession
|
||||
{
|
||||
'activityId': '1002',
|
||||
'activityType': {'typeKey': 'running'},
|
||||
'startTimeLocal': (datetime.now() - timedelta(days=2)).isoformat(),
|
||||
'startTimeLocal': (datetime.now() - timedelta(days=2)).strftime("%Y-%m-%d %H:%M:%S"),
|
||||
'duration': 3000,
|
||||
'distance': 10000
|
||||
}
|
||||
]
|
||||
# First call to get_activity_details fails, second succeeds
|
||||
mock_garmin_service.get_activity_details.side_effect = [
|
||||
GarminAPIError("Temporary network issue"),
|
||||
{'averageHR': 160, 'maxHR': 190}
|
||||
garminconnect.GarminConnectException("Temporary network issue"),
|
||||
{'averageHeartRateInBeatsPerMinute': 160, 'maxHeartRateInBeatsPerMinute': 190}
|
||||
]
|
||||
|
||||
# Act
|
||||
# Mock asyncio.sleep to avoid actual delays during tests
|
||||
with patch('asyncio.sleep', new_callable=AsyncMock) as mock_sleep:
|
||||
with patch('asyncio.sleep', new_callable=MagicMock) as mock_sleep:
|
||||
synced_count = await service.sync_recent_activities(days_back=7)
|
||||
mock_sleep.assert_awaited_with(1) # First retry delay
|
||||
mock_sleep.assert_called_with(1) # First retry delay
|
||||
|
||||
# Assert
|
||||
assert synced_count == 1
|
||||
@@ -251,24 +253,24 @@ async def test_sync_with_activity_details_retry_failure(db_session: AsyncSession
|
||||
{
|
||||
'activityId': '1003',
|
||||
'activityType': {'typeKey': 'swimming'},
|
||||
'startTimeLocal': (datetime.now() - timedelta(days=3)).isoformat(),
|
||||
'startTimeLocal': (datetime.now() - timedelta(days=3)).strftime("%Y-%m-%d %H:%M:%S"),
|
||||
'duration': 2000,
|
||||
'distance': 2000
|
||||
}
|
||||
]
|
||||
# All calls to get_activity_details fail
|
||||
mock_garmin_service.get_activity_details.side_effect = [
|
||||
GarminAPIError("Service unavailable"),
|
||||
GarminAPIError("Service unavailable"),
|
||||
GarminAPIError("Service unavailable")
|
||||
garminconnect.GarminConnectException("Service unavailable"),
|
||||
garminconnect.GarminConnectException("Service unavailable"),
|
||||
garminconnect.GarminConnectException("Service unavailable")
|
||||
]
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(GarminAPIError), \
|
||||
patch('asyncio.sleep', new_callable=AsyncMock) as mock_sleep:
|
||||
patch('asyncio.sleep', new_callable=MagicMock) as mock_sleep:
|
||||
await service.sync_recent_activities(days_back=7)
|
||||
assert mock_garmin_service.get_activity_details.call_count == 3
|
||||
mock_sleep.assert_awaited_with(4) # Last retry delay (2**(3-1))
|
||||
mock_sleep.assert_called_with(4) # Last retry delay (2**(3-1))
|
||||
|
||||
# Verify sync log in DB
|
||||
result = await db_session.execute(select(GarminSyncLog))
|
||||
@@ -296,12 +298,12 @@ async def test_sync_with_duplicate_activities_in_garmin_feed(db_session: AsyncSe
|
||||
{
|
||||
'activityId': '1004',
|
||||
'activityType': {'typeKey': 'cycling'},
|
||||
'startTimeLocal': (datetime.now() - timedelta(days=4)).isoformat(),
|
||||
'startTimeLocal': (datetime.now() - timedelta(days=4)).strftime("%Y-%m-%d %H:%M:%S"),
|
||||
'duration': 4000,
|
||||
'distance': 60000
|
||||
}
|
||||
]
|
||||
mock_garmin_service.get_activity_details.return_value = {'averageHR': 140}
|
||||
mock_garmin_service.get_activity_details.return_value = {'averageHeartRateInBeatsPerMinute': 140}
|
||||
await service.sync_recent_activities(days_back=7)
|
||||
|
||||
# Second sync: activity 1004 is present again, plus a new activity 1005
|
||||
@@ -309,19 +311,19 @@ async def test_sync_with_duplicate_activities_in_garmin_feed(db_session: AsyncSe
|
||||
{
|
||||
'activityId': '1004',
|
||||
'activityType': {'typeKey': 'cycling'},
|
||||
'startTimeLocal': (datetime.now() - timedelta(days=4)).isoformat(),
|
||||
'startTimeLocal': (datetime.now() - timedelta(days=4)).strftime("%Y-%m-%d %H:%M:%S"),
|
||||
'duration': 4000,
|
||||
'distance': 60000
|
||||
},
|
||||
{
|
||||
'activityId': '1005',
|
||||
'activityType': {'typeKey': 'running'},
|
||||
'startTimeLocal': (datetime.now() - timedelta(days=5)).isoformat(),
|
||||
'startTimeLocal': (datetime.now() - timedelta(days=5)).strftime("%Y-%m-%d %H:%M:%S"),
|
||||
'duration': 2500,
|
||||
'distance': 5000
|
||||
}
|
||||
]
|
||||
mock_garmin_service.get_activity_details.return_value = {'averageHR': 130} # for activity 1005
|
||||
mock_garmin_service.get_activity_details.return_value = {'averageHeartRateInBeatsPerMinute': 130} # for activity 1005
|
||||
|
||||
# Act
|
||||
synced_count = await service.sync_recent_activities(days_back=7)
|
||||
@@ -349,6 +351,8 @@ async def test_garmin_sync_with_real_creds(db_session: AsyncSession, real_garmin
|
||||
# Arrange
|
||||
service = WorkoutSyncService(db=db_session)
|
||||
service.garmin_service = real_garmin_service
|
||||
# Ensure garmin_service is authenticated for real tests
|
||||
await real_garmin_service.authenticate()
|
||||
|
||||
# Act
|
||||
# We sync the last 1 day to keep the test fast
|
||||
|
||||
Reference in New Issue
Block a user