Files
FitTrack_GarminSync/examples/Garmin_Analyser/tests/test_download_missing.py

430 lines
18 KiB
Python

"""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"])