Files
FitTrack2/FitnessSync/backend/tests/unit/test_sync_app.py

200 lines
9.3 KiB
Python

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