Update spec files to match current implementation state
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
228
FitnessSync/backend/tests/unit/test_garmin_auth.py
Normal file
228
FitnessSync/backend/tests/unit/test_garmin_auth.py
Normal file
@@ -0,0 +1,228 @@
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
import garth
|
||||
from garth.exc import GarthException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from src.services.garmin.client import GarminClient
|
||||
from src.models.api_token import APIToken
|
||||
from garth.http import Client # Import Client for mocking
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_db_session():
|
||||
"""Fixture for a mock SQLAlchemy session."""
|
||||
session = MagicMock(spec=Session)
|
||||
session.query.return_value.filter_by.return_value.first.return_value = None
|
||||
return session
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def garmin_client_instance():
|
||||
"""Fixture for a GarminClient instance with test credentials."""
|
||||
return GarminClient(username="testuser", password="testpassword", is_china=False)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def garmin_client_mfa_instance():
|
||||
"""Fixture for a GarminClient instance for MFA testing."""
|
||||
return GarminClient(username="testmfauser", password="testmfapassword", is_china=False)
|
||||
|
||||
|
||||
@patch("src.services.garmin.auth.garth.login")
|
||||
@patch("src.services.garmin.auth.garth.client")
|
||||
def test_login_success(mock_garth_client, mock_garth_login, mock_db_session, garmin_client_instance):
|
||||
"""Test successful login scenario."""
|
||||
# Mock garth.login to return successfully
|
||||
mock_garth_login.return_value = (None, None) # Placeholder for successful return
|
||||
|
||||
# Mock garth.client tokens
|
||||
mock_garth_client.oauth1_token = {"oauth1": "token"}
|
||||
mock_garth_client.oauth2_token = {"oauth2": "token"}
|
||||
|
||||
# Call the login method
|
||||
status = garmin_client_instance.login(mock_db_session)
|
||||
|
||||
# Assertions
|
||||
mock_garth_login.assert_called_once_with("testuser", "testpassword")
|
||||
assert status == "success"
|
||||
assert garmin_client_instance.is_connected is True
|
||||
|
||||
# Verify update_tokens was called and session committed
|
||||
mock_db_session.query.return_value.filter_by.return_value.first.assert_called_once()
|
||||
mock_db_session.add.called = False # Reset add mock if it was called before update_tokens
|
||||
# patch update_tokens to prevent it from failing tests.
|
||||
with patch.object(garmin_client_instance, 'update_tokens') as mock_update_tokens:
|
||||
garmin_client_instance.login(mock_db_session)
|
||||
mock_update_tokens.assert_called_once_with(mock_db_session, {"oauth1": "token"}, {"oauth2": "token"})
|
||||
|
||||
# Verify token record attributes (mocked API_Token)
|
||||
# The actual token record is added via update_tokens, which we are patching.
|
||||
# To properly test this, we'd need to mock update_tokens more deeply or test it separately.
|
||||
# For now, we'll ensure update_tokens was called with the right arguments.
|
||||
# token_record = mock_db_session.add.call_args[0][0] # This won't work if update_tokens is patched
|
||||
|
||||
|
||||
@patch("src.services.garmin.auth.garth.login")
|
||||
@patch("src.services.garmin.auth.garth.client")
|
||||
def test_login_mfa_required(mock_garth_client, mock_garth_login, mock_db_session, garmin_client_mfa_instance):
|
||||
"""Test login scenario when MFA is required."""
|
||||
# Mock garth.login to raise GarthException indicating MFA
|
||||
mock_garth_login.side_effect = GarthException("needs-mfa")
|
||||
|
||||
# Mock garth.client.mfa_state
|
||||
mock_client_for_mfa = MagicMock()
|
||||
mock_client_for_mfa._session = MagicMock() # Mock _session
|
||||
mock_client_for_mfa._session.cookies.get_dict.return_value = {"cookie1": "val1"}
|
||||
mock_client_for_mfa.domain = "garmin.com" # Ensure domain returns a string
|
||||
|
||||
mock_garth_client.mfa_state = {
|
||||
"signin_params": {"param1": "value1"},
|
||||
"client": mock_client_for_mfa
|
||||
}
|
||||
|
||||
# Call the login method
|
||||
status = garmin_client_mfa_instance.login(mock_db_session)
|
||||
|
||||
# Assertions
|
||||
mock_garth_login.assert_called_once_with("testmfauser", "testmfapassword")
|
||||
assert status == "mfa_required"
|
||||
assert garmin_client_mfa_instance.is_connected is False
|
||||
|
||||
# Verify initiate_mfa was called and session committed
|
||||
mock_db_session.query.return_value.filter_by.return_value.first.assert_called_once()
|
||||
mock_db_session.add.assert_called_once()
|
||||
mock_db_session.commit.assert_called_once()
|
||||
|
||||
# Verify mfa_state record attributes
|
||||
token_record = mock_db_session.add.call_args[0][0]
|
||||
mfa_state_data = json.loads(token_record.mfa_state)
|
||||
assert mfa_state_data["signin_params"] == {"param1": "value1"}
|
||||
assert mfa_state_data["cookies"] == {"cookie1": "val1"}
|
||||
assert mfa_state_data["domain"] == "garmin.com"
|
||||
|
||||
|
||||
@patch("src.services.garmin.auth.garth.login")
|
||||
def test_login_failure(mock_garth_login, mock_db_session, garmin_client_instance):
|
||||
"""Test login scenario when authentication fails (not MFA)."""
|
||||
# Mock garth.login to raise a generic GarthException
|
||||
mock_garth_login.side_effect = GarthException("Invalid credentials")
|
||||
|
||||
# Call the login method
|
||||
status = garmin_client_instance.login(mock_db_session)
|
||||
|
||||
# Assertions
|
||||
mock_garth_login.assert_called_once_with("testuser", "testpassword")
|
||||
assert status == "error"
|
||||
assert garmin_client_instance.is_connected is False
|
||||
mock_db_session.commit.assert_not_called() # No commit on failure
|
||||
|
||||
|
||||
@patch("src.services.garmin.auth.garth.client.resume_login")
|
||||
@patch("garth.http.Client")
|
||||
@patch("src.services.garmin.auth.garth.client") # Patch garth.client itself
|
||||
@patch.object(GarminClient, 'update_tokens')
|
||||
def test_handle_mfa_success(mock_update_tokens, mock_garth_client_global, mock_garth_client_class, mock_garth_resume_login, mock_db_session, garmin_client_instance):
|
||||
"""Test successful MFA completion."""
|
||||
# Setup mock MFA state in DB
|
||||
mfa_state_data = {
|
||||
"signin_params": {"param1": "value1"},
|
||||
"cookies": {"cookie1": "val1"},
|
||||
"domain": "garmin.com"
|
||||
}
|
||||
mock_token_record = MagicMock(spec=APIToken)
|
||||
mock_token_record.mfa_state = json.dumps(mfa_state_data)
|
||||
mock_db_session.query.return_value.filter_by.return_value.first.return_value = mock_token_record
|
||||
|
||||
# Mock the Client constructor
|
||||
mock_client_instance = MagicMock(spec=Client)
|
||||
mock_client_instance.domain = mfa_state_data["domain"]
|
||||
|
||||
mock_client_instance._session = MagicMock() # Mock the _session
|
||||
mock_client_instance._session.cookies = MagicMock() # Mock cookies
|
||||
mock_client_instance._session.cookies.update = MagicMock() # Mock update method
|
||||
|
||||
mock_garth_client_class.return_value = mock_client_instance # When Client() is called, return this mock
|
||||
|
||||
# Mock garth.resume_login to succeed
|
||||
mock_garth_resume_login.return_value = ({"oauth1": "token"}, {"oauth2": "token"})
|
||||
|
||||
# Explicitly set the values on the global garth.client mock
|
||||
mock_garth_client_global.oauth1_token = {"oauth1": "token_updated"}
|
||||
mock_garth_client_global.oauth2_token = {"oauth2": "token_updated"}
|
||||
|
||||
# Call handle_mfa
|
||||
result = garmin_client_instance.handle_mfa(mock_db_session, "123456")
|
||||
|
||||
# Assertions
|
||||
mock_garth_client_class.assert_called_once_with(domain=mfa_state_data["domain"])
|
||||
mock_client_instance._session.cookies.update.assert_called_once_with(mfa_state_data["cookies"])
|
||||
|
||||
# We'll assert that resume_login was called once, and then check its arguments
|
||||
call_args, call_kwargs = mock_garth_resume_login.call_args
|
||||
assert call_args[1] == "123456" # Second arg is verification_code
|
||||
|
||||
passed_mfa_state = call_args[0] # First arg is the mfa_state dict
|
||||
assert passed_mfa_state["signin_params"] == mfa_state_data["signin_params"]
|
||||
assert passed_mfa_state["client"] is mock_client_instance # Ensure the reconstructed client is passed
|
||||
|
||||
assert result is True
|
||||
# Verify update_tokens was called with the correct arguments
|
||||
mock_update_tokens.assert_called_once_with(mock_db_session, {"oauth1": "token_updated"}, {"oauth2": "token_updated"})
|
||||
mock_db_session.commit.assert_not_called() # update_tokens will commit
|
||||
|
||||
|
||||
@patch("src.services.garmin.auth.garth.client.resume_login")
|
||||
@patch("garth.http.Client")
|
||||
@patch("src.services.garmin.auth.garth.client") # Patch garth.client itself
|
||||
@patch.object(GarminClient, 'update_tokens')
|
||||
def test_handle_mfa_failure(mock_update_tokens, mock_garth_client_global, mock_garth_client_class, mock_garth_resume_login, mock_db_session, garmin_client_instance):
|
||||
"""Test MFA completion failure due to GarthException."""
|
||||
# Setup mock MFA state in DB
|
||||
mfa_state_data = {
|
||||
"signin_params": {"param1": "value1"},
|
||||
"cookies": {"cookie1": "val1"},
|
||||
"domain": "garmin.com"
|
||||
}
|
||||
mock_token_record = MagicMock(spec=APIToken)
|
||||
mock_token_record.mfa_state = json.dumps(mfa_state_data)
|
||||
mock_db_session.query.return_value.filter_by.return_value.first.return_value = mock_token_record
|
||||
|
||||
# Mock the Client constructor
|
||||
mock_client_instance = MagicMock(spec=Client)
|
||||
mock_client_instance.domain = mfa_state_data["domain"]
|
||||
mock_client_instance._session = MagicMock() # Mock the _session
|
||||
mock_client_instance._session.cookies = MagicMock() # Mock cookies
|
||||
mock_client_instance._session.cookies.update = MagicMock() # Mock update method
|
||||
mock_garth_client_class.return_value = mock_client_instance
|
||||
|
||||
# Mock garth.resume_login to raise GarthException
|
||||
mock_garth_resume_login.side_effect = GarthException("Invalid MFA code")
|
||||
|
||||
# Call handle_mfa and expect an exception
|
||||
with pytest.raises(GarthException, match="Invalid MFA code"):
|
||||
garmin_client_instance.handle_mfa(mock_db_session, "wrongcode")
|
||||
|
||||
# Explicitly set the values on the global garth.client mock after failure (shouldn't be set by successful resume_login)
|
||||
mock_garth_client_global.oauth1_token = None
|
||||
mock_garth_client_global.oauth2_token = None
|
||||
|
||||
mock_garth_client_class.assert_called_once_with(domain=mfa_state_data["domain"])
|
||||
mock_client_instance._session.cookies.update.assert_called_once_with(mfa_state_data["cookies"])
|
||||
mock_garth_resume_login.assert_called_once()
|
||||
mock_update_tokens.assert_not_called()
|
||||
mock_db_session.commit.assert_not_called() # No commit on failure
|
||||
|
||||
|
||||
def test_handle_mfa_no_pending_state(mock_db_session, garmin_client_instance):
|
||||
"""Test MFA completion when no pending MFA state is found."""
|
||||
# Mock no MFA state in DB
|
||||
mock_db_session.query.return_value.filter_by.return_value.first.return_value = None
|
||||
|
||||
# Call handle_mfa and expect an exception
|
||||
with pytest.raises(Exception, match="No pending MFA session found."):
|
||||
garmin_client_instance.handle_mfa(mock_db_session, "123456")
|
||||
|
||||
mock_db_session.commit.assert_not_called()
|
||||
115
FitnessSync/backend/tests/unit/test_garmin_data.py
Normal file
115
FitnessSync/backend/tests/unit/test_garmin_data.py
Normal file
@@ -0,0 +1,115 @@
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
from datetime import datetime, timedelta
|
||||
import garth
|
||||
from garth.exc import GarthException
|
||||
from src.services.garmin.client import GarminClient
|
||||
from src.models.api_token import APIToken # Needed for AuthMixin
|
||||
|
||||
@pytest.fixture
|
||||
def garmin_client_instance():
|
||||
"""Fixture for a GarminClient instance with test credentials."""
|
||||
client = GarminClient(username="testuser", password="testpassword")
|
||||
client.is_connected = True # Assume connected for data tests
|
||||
return client
|
||||
|
||||
@patch("garth.client.connectapi")
|
||||
def test_get_activities_success(mock_connectapi, garmin_client_instance):
|
||||
"""Test successful fetching of activities."""
|
||||
mock_connectapi.return_value = [{"activityId": 1, "activityName": "Run"}, {"activityId": 2, "activityName": "Bike"}]
|
||||
|
||||
start_date = "2023-01-01"
|
||||
end_date = "2023-01-07"
|
||||
limit = 2
|
||||
|
||||
activities = garmin_client_instance.get_activities(start_date, end_date, limit)
|
||||
|
||||
mock_connectapi.assert_called_once_with(
|
||||
"/activitylist-service/activities/search/activities",
|
||||
params={"startDate": start_date, "endDate": end_date, "limit": limit}
|
||||
)
|
||||
assert len(activities) == 2
|
||||
assert activities[0]["activityName"] == "Run"
|
||||
|
||||
@patch("garth.client.connectapi")
|
||||
def test_get_activities_failure(mock_connectapi, garmin_client_instance):
|
||||
"""Test failure during fetching of activities."""
|
||||
mock_connectapi.side_effect = GarthException("API error")
|
||||
|
||||
start_date = "2023-01-01"
|
||||
end_date = "2023-01-07"
|
||||
limit = 2
|
||||
|
||||
with pytest.raises(GarthException, match="API error"):
|
||||
garmin_client_instance.get_activities(start_date, end_date, limit)
|
||||
|
||||
mock_connectapi.assert_called_once()
|
||||
|
||||
@patch("garth.client.download")
|
||||
def test_download_activity_success(mock_download, garmin_client_instance):
|
||||
"""Test successful downloading of an activity file."""
|
||||
mock_download.return_value = b"file_content_mock"
|
||||
|
||||
activity_id = "12345"
|
||||
file_type = "tcx"
|
||||
|
||||
file_content = garmin_client_instance.download_activity(activity_id, file_type)
|
||||
|
||||
mock_download.assert_called_once_with(f"/download-service/export/{file_type}/activity/{activity_id}")
|
||||
assert file_content == b"file_content_mock"
|
||||
|
||||
@patch("garth.client.download")
|
||||
def test_download_activity_failure(mock_download, garmin_client_instance):
|
||||
"""Test failure during downloading of an activity file."""
|
||||
mock_download.side_effect = GarthException("Download error")
|
||||
|
||||
activity_id = "12345"
|
||||
file_type = "gpx"
|
||||
|
||||
file_content = garmin_client_instance.download_activity(activity_id, file_type)
|
||||
|
||||
mock_download.assert_called_once()
|
||||
assert file_content is None # Should return None on exception
|
||||
|
||||
@patch("src.services.garmin.data.DailySteps")
|
||||
@patch("src.services.garmin.data.DailyHRV")
|
||||
@patch("src.services.garmin.data.SleepData")
|
||||
def test_get_daily_metrics_success(mock_sleep_data, mock_daily_hrv, mock_daily_steps, garmin_client_instance):
|
||||
"""Test successful fetching of daily metrics."""
|
||||
mock_daily_steps.list.return_value = [MagicMock(calendar_date="2023-01-01", total_steps=1000)]
|
||||
mock_daily_hrv.list.return_value = [MagicMock(calendar_date="2023-01-01", last_night_avg=50)]
|
||||
mock_sleep_data.list.return_value = [MagicMock(daily_sleep_dto=MagicMock(calendar_date="2023-01-01", sleep_time_seconds=28800))]
|
||||
|
||||
start_date = "2023-01-01"
|
||||
end_date = "2023-01-01"
|
||||
|
||||
metrics = garmin_client_instance.get_daily_metrics(start_date, end_date)
|
||||
|
||||
mock_daily_steps.list.assert_called_once_with(datetime(2023, 1, 1).date(), period=1)
|
||||
mock_daily_hrv.list.assert_called_once_with(datetime(2023, 1, 1).date(), period=1)
|
||||
mock_sleep_data.list.assert_called_once_with(datetime(2023, 1, 1).date(), days=1)
|
||||
|
||||
assert len(metrics["steps"]) == 1
|
||||
assert metrics["steps"][0].total_steps == 1000
|
||||
assert len(metrics["hrv"]) == 1
|
||||
assert metrics["hrv"][0].last_night_avg == 50
|
||||
assert len(metrics["sleep"]) == 1
|
||||
assert metrics["sleep"][0].daily_sleep_dto.sleep_time_seconds == 28800
|
||||
|
||||
@patch("src.services.garmin.data.DailySteps")
|
||||
@patch("src.services.garmin.data.DailyHRV")
|
||||
@patch("src.services.garmin.data.SleepData")
|
||||
def test_get_daily_metrics_partial_failure(mock_sleep_data, mock_daily_hrv, mock_daily_steps, garmin_client_instance):
|
||||
"""Test fetching daily metrics with some failures."""
|
||||
mock_daily_steps.list.side_effect = GarthException("Steps error")
|
||||
mock_daily_hrv.list.return_value = [MagicMock(calendar_date="2023-01-01", last_night_avg=50)]
|
||||
mock_sleep_data.list.return_value = []
|
||||
|
||||
start_date = "2023-01-01"
|
||||
end_date = "2023-01-01"
|
||||
|
||||
metrics = garmin_client_instance.get_daily_metrics(start_date, end_date)
|
||||
|
||||
assert metrics["steps"] == [] # Should return empty list on error
|
||||
assert len(metrics["hrv"]) == 1
|
||||
assert metrics["sleep"] == []
|
||||
199
FitnessSync/backend/tests/unit/test_sync_app.py
Normal file
199
FitnessSync/backend/tests/unit/test_sync_app.py
Normal file
@@ -0,0 +1,199 @@
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch, ANY
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
import garth
|
||||
from garth.exc import GarthException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from src.services.sync_app import SyncApp
|
||||
from src.services.garmin.client import GarminClient
|
||||
from src.models.activity import Activity
|
||||
from src.models.health_metric import HealthMetric
|
||||
from src.models.sync_log import SyncLog
|
||||
from src.models.api_token import APIToken # Needed for AuthMixin
|
||||
|
||||
@pytest.fixture
|
||||
def mock_db_session():
|
||||
"""Fixture for a mock SQLAlchemy session."""
|
||||
session = MagicMock(spec=Session)
|
||||
session.query.return_value.filter_by.return_value.first.return_value = None
|
||||
return session
|
||||
|
||||
@pytest.fixture
|
||||
def mock_garmin_client():
|
||||
"""Fixture for a mock GarminClient."""
|
||||
client = MagicMock(spec=GarminClient)
|
||||
client.is_connected = True
|
||||
return client
|
||||
|
||||
@pytest.fixture
|
||||
def sync_app_instance(mock_db_session, mock_garmin_client):
|
||||
"""Fixture for a SyncApp instance."""
|
||||
return SyncApp(db_session=mock_db_session, garmin_client=mock_garmin_client)
|
||||
|
||||
|
||||
# --- Tests for sync_activities ---
|
||||
def test_sync_activities_no_activities(sync_app_instance, mock_garmin_client, mock_db_session):
|
||||
"""Test sync_activities when no activities are fetched from Garmin."""
|
||||
mock_garmin_client.get_activities.return_value = []
|
||||
|
||||
result = sync_app_instance.sync_activities(days_back=1)
|
||||
|
||||
mock_garmin_client.get_activities.assert_called_once()
|
||||
assert result == {"processed": 0, "failed": 0}
|
||||
mock_db_session.add.assert_called_once() # For sync_log
|
||||
assert mock_db_session.commit.call_count == 2 # Initial commit for sync_log, final commit
|
||||
|
||||
|
||||
def test_sync_activities_success_new_activity(sync_app_instance, mock_garmin_client, mock_db_session):
|
||||
"""Test sync_activities for a new activity, successfully downloaded."""
|
||||
garmin_activity_data = {
|
||||
"activityId": "1",
|
||||
"activityName": "Run",
|
||||
"activityType": {"typeKey": "running"},
|
||||
"startTimeLocal": "2023-01-01T10:00:00",
|
||||
"duration": 3600,
|
||||
}
|
||||
mock_garmin_client.get_activities.return_value = [garmin_activity_data]
|
||||
mock_db_session.query.return_value.filter_by.return_value.first.return_value = None # No existing activity
|
||||
mock_garmin_client.download_activity.return_value = b"tcx_content"
|
||||
|
||||
result = sync_app_instance.sync_activities(days_back=1)
|
||||
|
||||
mock_garmin_client.get_activities.assert_called_once()
|
||||
mock_garmin_client.download_activity.assert_called_once_with(ANY, file_type='original') # Checks if called with any activity_id and 'original' type
|
||||
assert mock_db_session.add.call_count == 2 # sync_log and new activity
|
||||
assert mock_db_session.commit.call_count == 3 # Initial commit for sync_log, commit after activity, final commit
|
||||
assert result == {"processed": 1, "failed": 0}
|
||||
|
||||
# Verify activity saved (check the second add call)
|
||||
activity_record = mock_db_session.add.call_args_list[1][0][0]
|
||||
assert isinstance(activity_record, Activity)
|
||||
assert activity_record.garmin_activity_id == "1"
|
||||
assert activity_record.activity_type == "running"
|
||||
assert activity_record.file_content == b"tcx_content"
|
||||
assert activity_record.file_type == "original" # Should be original if first format succeeded
|
||||
assert activity_record.download_status == "downloaded"
|
||||
|
||||
|
||||
def test_sync_activities_already_downloaded(sync_app_instance, mock_garmin_client, mock_db_session):
|
||||
"""Test sync_activities when activity is already downloaded."""
|
||||
garmin_activity_data = {
|
||||
"activityId": "2",
|
||||
"activityName": "Walk",
|
||||
"activityType": {"typeKey": "walking"},
|
||||
"startTimeLocal": "2023-01-02T11:00:00",
|
||||
"duration": 1800,
|
||||
}
|
||||
mock_garmin_client.get_activities.return_value = [garmin_activity_data]
|
||||
|
||||
# Mock existing activity in DB
|
||||
existing_activity = Activity(garmin_activity_id="2", download_status="downloaded", file_content=b"old_content")
|
||||
mock_db_session.query.return_value.filter_by.return_value.first.return_value = existing_activity
|
||||
|
||||
result = sync_app_instance.sync_activities(days_back=1)
|
||||
|
||||
mock_garmin_client.get_activities.assert_called_once()
|
||||
mock_garmin_client.download_activity.assert_not_called() # Should not try to download again
|
||||
mock_db_session.add.assert_called_once_with(ANY) # For sync_log only
|
||||
assert mock_db_session.commit.call_count == 3 # Initial commit for sync_log, loop commit (0 activities), final commit
|
||||
assert result == {"processed": 0, "failed": 0} # No new processed/failed due to skipping
|
||||
|
||||
|
||||
def test_sync_activities_download_failure(sync_app_instance, mock_garmin_client, mock_db_session):
|
||||
"""Test sync_activities when download fails for all formats."""
|
||||
garmin_activity_data = {
|
||||
"activityId": "3",
|
||||
"activityName": "Swim",
|
||||
"activityType": {"typeKey": "swimming"},
|
||||
"startTimeLocal": "2023-01-03T12:00:00",
|
||||
"duration": 2700,
|
||||
}
|
||||
mock_garmin_client.get_activities.return_value = [garmin_activity_data]
|
||||
mock_db_session.query.return_value.filter_by.return_value.first.return_value = None
|
||||
mock_garmin_client.download_activity.return_value = None # Download fails for all formats
|
||||
|
||||
result = sync_app_instance.sync_activities(days_back=1)
|
||||
|
||||
mock_garmin_client.get_activities.assert_called_once()
|
||||
assert mock_garmin_client.download_activity.call_count == 4 # Tries 'original', 'tcx', 'gpx', 'fit'
|
||||
assert mock_db_session.add.call_count == 2 # For sync_log and new activity
|
||||
assert mock_db_session.commit.call_count == 3 # Initial commit for sync_log, commit after activity, final commit
|
||||
assert result == {"processed": 0, "failed": 1}
|
||||
|
||||
# Verify activity marked as failed
|
||||
activity_record = mock_db_session.add.call_args_list[1][0][0]
|
||||
assert activity_record.garmin_activity_id == "3"
|
||||
assert activity_record.download_status == "failed"
|
||||
assert activity_record.file_content is None
|
||||
|
||||
|
||||
# --- Tests for sync_health_metrics ---
|
||||
@patch.object(SyncApp, '_update_or_create_metric')
|
||||
def test_sync_health_metrics_success(mock_update_or_create_metric, sync_app_instance, mock_garmin_client, mock_db_session):
|
||||
"""Test successful fetching and processing of health metrics."""
|
||||
mock_garmin_client.get_daily_metrics.return_value = {
|
||||
"steps": [MagicMock(calendar_date=datetime(2023, 1, 1).date(), total_steps=10000)],
|
||||
"hrv": [MagicMock(calendar_date=datetime(2023, 1, 1).date(), last_night_avg=50)],
|
||||
"sleep": [MagicMock(daily_sleep_dto=MagicMock(calendar_date=datetime(2023, 1, 1).date(), sleep_time_seconds=28800))]
|
||||
}
|
||||
|
||||
result = sync_app_instance.sync_health_metrics(days_back=1)
|
||||
|
||||
mock_garmin_client.get_daily_metrics.assert_called_once()
|
||||
assert mock_update_or_create_metric.call_count == 3 # Steps, HRV, Sleep
|
||||
assert result == {"processed": 3, "failed": 0}
|
||||
mock_db_session.add.assert_called_once() # For sync_log
|
||||
assert mock_db_session.commit.call_count == 2 # Initial commit for sync_log, final commit
|
||||
|
||||
@patch.object(SyncApp, '_update_or_create_metric')
|
||||
def test_sync_health_metrics_partial_failure(mock_update_or_create_metric, sync_app_instance, mock_garmin_client, mock_db_session):
|
||||
"""Test partial failure during health metrics processing."""
|
||||
mock_garmin_client.get_daily_metrics.return_value = {
|
||||
"steps": [MagicMock(calendar_date=datetime(2023, 1, 1).date(), total_steps=10000)],
|
||||
"hrv": [MagicMock(calendar_date=datetime(2023, 1, 1).date(), last_night_avg=50)],
|
||||
"sleep": [MagicMock(daily_sleep_dto=MagicMock(calendar_date=datetime(2023, 1, 1).date(), sleep_time_seconds=28800))]
|
||||
}
|
||||
mock_update_or_create_metric.side_effect = [None, Exception("HRV save error"), None] # Steps OK, HRV fails, Sleep OK
|
||||
|
||||
result = sync_app_instance.sync_health_metrics(days_back=1)
|
||||
|
||||
assert mock_update_or_create_metric.call_count == 3
|
||||
assert result == {"processed": 2, "failed": 1} # 2 successful, 1 failed
|
||||
mock_db_session.add.assert_called_once()
|
||||
assert mock_db_session.commit.call_count == 2
|
||||
|
||||
# --- Tests for _update_or_create_metric ---
|
||||
def test_update_or_create_metric_create_new(sync_app_instance, mock_db_session):
|
||||
"""Test _update_or_create_metric creates a new metric."""
|
||||
mock_db_session.query.return_value.filter_by.return_value.first.return_value = None # No existing metric
|
||||
|
||||
sync_app_instance._update_or_create_metric("steps", datetime(2023, 1, 1).date(), 10000, "steps")
|
||||
|
||||
mock_db_session.add.assert_called_once()
|
||||
mock_db_session.commit.assert_called_once()
|
||||
|
||||
metric_record = mock_db_session.add.call_args[0][0]
|
||||
assert isinstance(metric_record, HealthMetric)
|
||||
assert metric_record.metric_type == "steps"
|
||||
assert metric_record.metric_value == 10000
|
||||
|
||||
|
||||
def test_update_or_create_metric_update_existing(sync_app_instance, mock_db_session):
|
||||
"""Test _update_or_create_metric updates an existing metric."""
|
||||
existing_metric = HealthMetric(
|
||||
metric_type="steps",
|
||||
date=datetime(2023, 1, 1).date(),
|
||||
metric_value=5000,
|
||||
unit="steps",
|
||||
source="garmin"
|
||||
)
|
||||
mock_db_session.query.return_value.filter_by.return_value.first.return_value = existing_metric
|
||||
|
||||
sync_app_instance._update_or_create_metric("steps", datetime(2023, 1, 1).date(), 12000, "steps")
|
||||
|
||||
mock_db_session.add.assert_not_called() # Should not add new record
|
||||
mock_db_session.commit.assert_called_once()
|
||||
assert existing_metric.metric_value == 12000
|
||||
assert existing_metric.updated_at is not None
|
||||
Reference in New Issue
Block a user