"""Tests for missing download functionality with --missing and --dry-run options.""" import pytest import tempfile import shutil from pathlib import Path from unittest.mock import patch, MagicMock, call from clients.garmin_client import GarminClient from db.session import SessionLocal from db.models import ActivityDownload, Base from config.settings import DATA_DIR, DB_PATH class TestDownloadMissing: """Test missing download functionality with --missing and --dry-run options.""" @pytest.fixture(autouse=True) def setup_and_teardown(self): """Set up test database and clean up after tests.""" # Create a temporary directory for test data self.test_data_dir = Path(tempfile.mkdtemp()) # Patch settings to use test paths and in-memory database with patch('config.settings.DATA_DIR', self.test_data_dir), \ patch('config.settings.DB_PATH', "sqlite:///:memory:"): # Recreate engine with the patched DB_PATH from sqlalchemy import create_engine from db.session import Base self.engine = create_engine("sqlite:///:memory:") Base.metadata.create_all(bind=self.engine) # Patch the engine in the session module with patch('db.session.engine', self.engine): yield # Clean up if self.test_data_dir.exists(): shutil.rmtree(self.test_data_dir) def _clean_database(self): """Clean the database between tests to avoid conflicts.""" from db.session import Base Base.metadata.drop_all(bind=self.engine) Base.metadata.create_all(bind=self.engine) def test_get_downloaded_activity_ids_from_db(self): """Test getting downloaded activity IDs from database.""" self._clean_database() client = GarminClient() # Create test records activity_ids = [11111, 22222, 33333] with SessionLocal() as db: for activity_id in activity_ids: record = ActivityDownload( activity_id=activity_id, source="garmin-connect", file_path=str(self.test_data_dir / f"activity_{activity_id}.fit"), file_format="fit", status="success", size_bytes=100, checksum_sha256=f"checksum_{activity_id}" ) db.add(record) db.commit() # Test getting downloaded IDs with SessionLocal() as db: downloaded_ids = client.get_downloaded_activity_ids(db) assert set(downloaded_ids) == set(activity_ids) def test_get_downloaded_activity_ids_empty_db(self): """Test getting downloaded activity IDs from empty database.""" client = GarminClient() with SessionLocal() as db: downloaded_ids = client.get_downloaded_activity_ids(db) assert downloaded_ids == [] def test_get_downloaded_activity_ids_mixed_status(self): """Test getting downloaded activity IDs with mixed statuses.""" client = GarminClient() # Create test records with different statuses with SessionLocal() as db: # Success records success_ids = [11111, 22222] for activity_id in success_ids: record = ActivityDownload( activity_id=activity_id, source="garmin-connect", file_path=str(self.test_data_dir / f"activity_{activity_id}.fit"), file_format="fit", status="success", size_bytes=100, checksum_sha256=f"checksum_{activity_id}" ) db.add(record) # Failed record (should not be included) failed_record = ActivityDownload( activity_id=33333, source="garmin-connect", file_path=str(self.test_data_dir / "activity_33333.fit"), file_format="fit", status="failed", error_message="Download failed", size_bytes=0, checksum_sha256=None ) db.add(failed_record) db.commit() # Test getting downloaded IDs (only success status) with SessionLocal() as db: downloaded_ids = client.get_downloaded_activity_ids(db) assert set(downloaded_ids) == set(success_ids) assert 33333 not in downloaded_ids @patch('clients.garmin_client.GarminClient.get_all_activities') def test_find_missing_activities(self, mock_get_all_activities): """Test finding missing activities.""" # Mock all activities from Garmin all_activities = [ {"activityId": 11111, "activityName": "Ride 1"}, {"activityId": 22222, "activityName": "Ride 2"}, {"activityId": 33333, "activityName": "Ride 3"}, {"activityId": 44444, "activityName": "Ride 4"} ] mock_get_all_activities.return_value = all_activities # Create some downloaded records downloaded_ids = [11111, 33333] # Missing: 22222, 44444 with SessionLocal() as db: for activity_id in downloaded_ids: record = ActivityDownload( activity_id=activity_id, source="garmin-connect", file_path=str(self.test_data_dir / f"activity_{activity_id}.fit"), file_format="fit", status="success", size_bytes=100, checksum_sha256=f"checksum_{activity_id}" ) db.add(record) db.commit() # Test finding missing activities client = GarminClient() missing_activities = client.find_missing_activities(db) expected_missing = [ {"activityId": 22222, "activityName": "Ride 2"}, {"activityId": 44444, "activityName": "Ride 4"} ] assert len(missing_activities) == 2 assert missing_activities == expected_missing @patch('clients.garmin_client.GarminClient.get_all_activities') def test_find_missing_activities_none_missing(self, mock_get_all_activities): """Test finding missing activities when none are missing.""" # Mock all activities from Garmin all_activities = [ {"activityId": 11111, "activityName": "Ride 1"}, {"activityId": 22222, "activityName": "Ride 2"} ] mock_get_all_activities.return_value = all_activities # Create downloaded records for all activities downloaded_ids = [11111, 22222] with SessionLocal() as db: for activity_id in downloaded_ids: record = ActivityDownload( activity_id=activity_id, source="garmin-connect", file_path=str(self.test_data_dir / f"activity_{activity_id}.fit"), file_format="fit", status="success", size_bytes=100, checksum_sha256=f"checksum_{activity_id}" ) db.add(record) db.commit() # Test finding missing activities client = GarminClient() missing_activities = client.find_missing_activities(db) assert len(missing_activities) == 0 assert missing_activities == [] @patch('clients.garmin_client.GarminClient.get_all_activities') def test_find_missing_activities_limit(self, mock_get_all_activities): """Test finding missing activities with limit.""" # Mock many activities from Garmin all_activities = [ {"activityId": i, "activityName": f"Ride {i}"} for i in range(1, 11) ] mock_get_all_activities.return_value = all_activities # Create some downloaded records downloaded_ids = [1, 3, 5, 7, 9] # Missing: 2, 4, 6, 8, 10 with SessionLocal() as db: for activity_id in downloaded_ids: record = ActivityDownload( activity_id=activity_id, source="garmin-connect", file_path=str(self.test_data_dir / f"activity_{activity_id}.fit"), file_format="fit", status="success", size_bytes=100, checksum_sha256=f"checksum_{activity_id}" ) db.add(record) db.commit() # Test finding missing activities with limit client = GarminClient() missing_activities = client.find_missing_activities(db, limit=3) assert len(missing_activities) == 3 # Should return the first 3 missing activities (2, 4, 6) @patch('clients.garmin_client.GarminClient.download_activity_original') @patch('clients.garmin_client.GarminClient.get_all_activities') def test_download_missing_activities_dry_run(self, mock_get_all_activities, mock_download): """Test downloading missing activities in dry-run mode.""" # Mock all activities from Garmin all_activities = [ {"activityId": 11111, "activityName": "Ride 1"}, {"activityId": 22222, "activityName": "Ride 2"}, {"activityId": 33333, "activityName": "Ride 3"} ] mock_get_all_activities.return_value = all_activities # Create some downloaded records downloaded_ids = [11111] # Missing: 22222, 33333 with SessionLocal() as db: for activity_id in downloaded_ids: record = ActivityDownload( activity_id=activity_id, source="garmin-connect", file_path=str(self.test_data_dir / f"activity_{activity_id}.fit"), file_format="fit", status="success", size_bytes=100, checksum_sha256=f"checksum_{activity_id}" ) db.add(record) db.commit() # Test downloading missing activities in dry-run mode client = GarminClient() result = client.download_missing_activities(db, dry_run=True) # Should return the missing activities but not download them assert len(result["missing_activities"]) == 2 assert result["downloaded_count"] == 0 assert result["dry_run"] is True # Download should not be called in dry-run mode mock_download.assert_not_called() @patch('clients.garmin_client.GarminClient.download_activity_original') @patch('clients.garmin_client.GarminClient.get_all_activities') def test_download_missing_activities_real_run(self, mock_get_all_activities, mock_download): """Test downloading missing activities in real run mode.""" # Mock all activities from Garmin all_activities = [ {"activityId": 11111, "activityName": "Ride 1"}, {"activityId": 22222, "activityName": "Ride 2"}, {"activityId": 33333, "activityName": "Ride 3"} ] mock_get_all_activities.return_value = all_activities # Mock download to return a test file path def mock_download_func(activity_id, **kwargs): test_file = self.test_data_dir / f"activity_{activity_id}.fit" test_file.write_bytes(b"test content") return test_file mock_download.side_effect = mock_download_func # Create some downloaded records downloaded_ids = [11111] # Missing: 22222, 33333 with SessionLocal() as db: for activity_id in downloaded_ids: record = ActivityDownload( activity_id=activity_id, source="garmin-connect", file_path=str(self.test_data_dir / f"activity_{activity_id}.fit"), file_format="fit", status="success", size_bytes=100, checksum_sha256=f"checksum_{activity_id}" ) db.add(record) db.commit() # Test downloading missing activities in real run mode client = GarminClient() result = client.download_missing_activities(db, dry_run=False) # Should return the missing activities and download them assert len(result["missing_activities"]) == 2 assert result["downloaded_count"] == 2 assert result["dry_run"] is False # Download should be called for each missing activity assert mock_download.call_count == 2 mock_download.assert_has_calls([ call("22222", force_download=False, db_session=db), call("33333", force_download=False, db_session=db) ], any_order=True) # Verify database records were created for downloaded activities with SessionLocal() as db: records = db.query(ActivityDownload).all() assert len(records) == 3 # Original + 2 new downloads @patch('clients.garmin_client.GarminClient.download_activity_original') @patch('clients.garmin_client.GarminClient.get_all_activities') def test_download_missing_activities_with_limit(self, mock_get_all_activities, mock_download): """Test downloading missing activities with limit.""" # Mock many activities from Garmin all_activities = [ {"activityId": i, "activityName": f"Ride {i}"} for i in range(1, 11) ] mock_get_all_activities.return_value = all_activities # Mock download to return a test file path def mock_download_func(activity_id, **kwargs): test_file = self.test_data_dir / f"activity_{activity_id}.fit" test_file.write_bytes(b"test content") return test_file mock_download.side_effect = mock_download_func # Create some downloaded records downloaded_ids = [1, 3, 5, 7, 9] # Missing: 2, 4, 6, 8, 10 with SessionLocal() as db: for activity_id in downloaded_ids: record = ActivityDownload( activity_id=activity_id, source="garmin-connect", file_path=str(self.test_data_dir / f"activity_{activity_id}.fit"), file_format="fit", status="success", size_bytes=100, checksum_sha256=f"checksum_{activity_id}" ) db.add(record) db.commit() # Test downloading missing activities with limit client = GarminClient() result = client.download_missing_activities(db, dry_run=False, limit=2) # Should return the missing activities and download only up to limit assert len(result["missing_activities"]) == 5 # All missing are identified assert result["downloaded_count"] == 2 # But only 2 are downloaded due to limit assert result["dry_run"] is False # Download should be called only for the limit assert mock_download.call_count == 2 @patch('clients.garmin_client.GarminClient.download_activity_original') @patch('clients.garmin_client.GarminClient.get_all_activities') def test_download_missing_activities_with_force(self, mock_get_all_activities, mock_download): """Test downloading missing activities with force option.""" # Mock all activities from Garmin all_activities = [ {"activityId": 11111, "activityName": "Ride 1"}, {"activityId": 22222, "activityName": "Ride 2"} ] mock_get_all_activities.return_value = all_activities # Mock download to return a test file path def mock_download_func(activity_id, force_download=False, **kwargs): test_file = self.test_data_dir / f"activity_{activity_id}.fit" test_file.write_bytes(b"test content") return test_file mock_download.side_effect = mock_download_func # Create downloaded records for all activities (should still download with force) downloaded_ids = [11111, 22222] with SessionLocal() as db: for activity_id in downloaded_ids: record = ActivityDownload( activity_id=activity_id, source="garmin-connect", file_path=str(self.test_data_dir / f"activity_{activity_id}.fit"), file_format="fit", status="success", size_bytes=100, checksum_sha256=f"checksum_{activity_id}" ) db.add(record) db.commit() # Test downloading with force=True (should download all even if they exist) client = GarminClient() result = client.download_missing_activities(db, dry_run=False, force=True) # Should download all activities with force assert result["downloaded_count"] == 2 assert mock_download.call_count == 2 # Download should be called with force_download=True mock_download.assert_has_calls([ call("11111", force_download=True, db_session=db), call("22222", force_download=True, db_session=db) ], any_order=True) if __name__ == "__main__": pytest.main([__file__, "-v"])