Update spec files to match current implementation state
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
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