added activity view
This commit is contained in:
0
FitnessSync/backend/tests/functional/__init__.py
Normal file
0
FitnessSync/backend/tests/functional/__init__.py
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
88
FitnessSync/backend/tests/functional/test_bike_setups.py
Normal file
88
FitnessSync/backend/tests/functional/test_bike_setups.py
Normal file
@@ -0,0 +1,88 @@
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
import sys
|
||||
|
||||
# Mock scheduler before importing main to prevent it from starting
|
||||
mock_scheduler = MagicMock()
|
||||
mock_scheduler_module = MagicMock()
|
||||
mock_scheduler_module.scheduler = mock_scheduler
|
||||
sys.modules["src.services.scheduler"] = mock_scheduler_module
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from src.models import Base, BikeSetup
|
||||
from main import app
|
||||
from src.utils.config import config
|
||||
from src.api.bike_setups import get_db
|
||||
|
||||
# Use a separate test database or the existing test.db
|
||||
SQLALCHEMY_DATABASE_URL = "sqlite:///./test_bike_setups.db"
|
||||
|
||||
engine = create_engine(
|
||||
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
|
||||
)
|
||||
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
def override_get_db():
|
||||
try:
|
||||
db = TestingSessionLocal()
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def test_db():
|
||||
Base.metadata.create_all(bind=engine)
|
||||
yield
|
||||
Base.metadata.drop_all(bind=engine)
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def client(test_db):
|
||||
with TestClient(app) as c:
|
||||
yield c
|
||||
|
||||
def test_create_bike_setup(client):
|
||||
response = client.post(
|
||||
"/api/bike-setups/",
|
||||
json={"frame": "Trek Emonda", "chainring": 50, "rear_cog": 11, "name": "Road Setup"}
|
||||
)
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["frame"] == "Trek Emonda"
|
||||
assert data["chainring"] == 50
|
||||
assert "id" in data
|
||||
|
||||
def test_read_bike_setups(client):
|
||||
response = client.get("/api/bike-setups/")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data) >= 1
|
||||
assert data[0]["frame"] == "Trek Emonda"
|
||||
|
||||
def test_update_bike_setup(client):
|
||||
# First get id
|
||||
response = client.get("/api/bike-setups/")
|
||||
setup_id = response.json()[0]["id"]
|
||||
|
||||
response = client.put(
|
||||
f"/api/bike-setups/{setup_id}",
|
||||
json={"chainring": 52}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["chainring"] == 52
|
||||
assert data["frame"] == "Trek Emonda"
|
||||
|
||||
def test_delete_bike_setup(client):
|
||||
# First get id
|
||||
response = client.get("/api/bike-setups/")
|
||||
setup_id = response.json()[0]["id"]
|
||||
|
||||
response = client.delete(f"/api/bike-setups/{setup_id}")
|
||||
assert response.status_code == 204
|
||||
|
||||
response = client.get(f"/api/bike-setups/{setup_id}")
|
||||
assert response.status_code == 404
|
||||
61
FitnessSync/backend/tests/functional/test_setup_api.py
Normal file
61
FitnessSync/backend/tests/functional/test_setup_api.py
Normal file
@@ -0,0 +1,61 @@
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
from fastapi.testclient import TestClient
|
||||
from main import app
|
||||
from src.services.postgresql_manager import PostgreSQLManager
|
||||
from src.models.api_token import APIToken
|
||||
from src.models.config import Configuration
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# client = TestClient(app) # REMOVED
|
||||
|
||||
# Helper to verify standard API response structure
|
||||
def assert_success_response(response):
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
# auth-status returns model directly, not wrapped in {status: success}
|
||||
return data
|
||||
|
||||
def test_get_auth_status(client):
|
||||
"""Test GET /api/setup/auth-status endpoint."""
|
||||
# Setup mocks in DB
|
||||
# We can rely on 'mock_db_session' fixture if it's auto-used or if we patch get_db.
|
||||
# But integration tests usually use the real app/client which uses override_get_db in conftest.
|
||||
# Assuming conftest.py sets up `override_get_db` or uses a test DB.
|
||||
# Ideally for functional test we want to mock the DB data.
|
||||
|
||||
# If the app uses dependency injection for `get_db`, checking conftest.py helps.
|
||||
# Let's assume we can rely on TestClient and if it hits DB, it hits the test DB.
|
||||
|
||||
response = client.get("/api/setup/auth-status")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "garmin" in data
|
||||
assert "fitbit" in data
|
||||
assert "token_stored" in data["garmin"]
|
||||
|
||||
@patch('requests.get')
|
||||
def test_load_consul_config(mock_get, client):
|
||||
"""Test POST /api/setup/load-consul-config."""
|
||||
# Mock Consul response
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = [
|
||||
# Consul returns list of {Key: ..., Value: b64}
|
||||
{
|
||||
"Key": "fitbit-garmin-sync/garmin_username",
|
||||
"Value": "dGVzdF91c2Vy" # 'test_user' in b64
|
||||
},
|
||||
{
|
||||
"Key": "fitbit-garmin-sync/garmin_password",
|
||||
"Value": "dGVzdF9wYXNz" # 'test_pass' in b64
|
||||
}
|
||||
]
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
response = client.post("/api/setup/load-consul-config")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "success"
|
||||
assert data["garmin"]["username"] == "test_user"
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
66
FitnessSync/backend/tests/unit/test_bike_matching.py
Normal file
66
FitnessSync/backend/tests/unit/test_bike_matching.py
Normal file
@@ -0,0 +1,66 @@
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
from sqlalchemy.orm import Session
|
||||
from src.services.bike_matching import match_activity_to_bike, calculate_observed_ratio
|
||||
from src.models import Activity, BikeSetup
|
||||
|
||||
@pytest.fixture
|
||||
def mock_db():
|
||||
db = MagicMock(spec=Session)
|
||||
|
||||
# Mock bike setups
|
||||
# 1. 50/15 => 3.33
|
||||
# 2. 48/16 => 3.00
|
||||
# 3. 52/11 => 4.72
|
||||
|
||||
setup1 = BikeSetup(id=1, frame="Fixie", chainring=50, rear_cog=15, name="Fixie A")
|
||||
setup2 = BikeSetup(id=2, frame="Commuter", chainring=48, rear_cog=16, name="City")
|
||||
setup3 = BikeSetup(id=3, frame="Road", chainring=52, rear_cog=11, name="Race")
|
||||
|
||||
db.query.return_value.all.return_value = [setup1, setup2, setup3]
|
||||
return db
|
||||
|
||||
def test_calculate_observed_ratio():
|
||||
# Ratio = (Speed * 60) / (Cadence * 2.1)
|
||||
# Speed 10m/s (36kmh), Cadence 90
|
||||
# Ratio = 600 / (90 * 2.1) = 600 / 189 = 3.17
|
||||
ratio = calculate_observed_ratio(10, 90)
|
||||
assert abs(ratio - 3.17) < 0.01
|
||||
|
||||
def test_match_activity_success(mock_db):
|
||||
# Setup 2 is 48/16 = 3.0
|
||||
# Target: Speed for Ratio 3.0 at 90 RPM
|
||||
# Speed = (3.0 * 90 * 2.1) / 60 = 567 / 60 = 9.45 m/s
|
||||
|
||||
activity = Activity(
|
||||
id=101,
|
||||
activity_type='cycling',
|
||||
avg_speed=9.45,
|
||||
avg_cadence=90
|
||||
)
|
||||
|
||||
match = match_activity_to_bike(mock_db, activity)
|
||||
assert match is not None
|
||||
assert match.id == 2 # Commuter (Ratio 3.0)
|
||||
|
||||
def test_match_activity_indoor_ignored(mock_db):
|
||||
activity = Activity(
|
||||
id=102,
|
||||
activity_type='Indoor Cycling',
|
||||
avg_speed=9.45,
|
||||
avg_cadence=90
|
||||
)
|
||||
match = match_activity_to_bike(mock_db, activity)
|
||||
assert match is None
|
||||
|
||||
def test_match_activity_no_match(mock_db):
|
||||
# Ratio 1.0 (Very low gear)
|
||||
# Speed = 3.15 m/s at 90 RPM
|
||||
activity = Activity(
|
||||
id=103,
|
||||
activity_type='cycling',
|
||||
avg_speed=3.15,
|
||||
avg_cadence=90
|
||||
)
|
||||
match = match_activity_to_bike(mock_db, activity)
|
||||
assert match is None
|
||||
@@ -1,115 +0,0 @@
|
||||
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"] == []
|
||||
@@ -2,8 +2,6 @@ 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
|
||||
@@ -11,13 +9,17 @@ 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
|
||||
from src.models.activity_state import GarminActivityState
|
||||
from src.models.health_state import HealthSyncState
|
||||
|
||||
@pytest.fixture
|
||||
def mock_db_session():
|
||||
"""Fixture for a mock SQLAlchemy session."""
|
||||
session = MagicMock(spec=Session)
|
||||
# Default behavior: return None
|
||||
session.query.return_value.filter_by.return_value.first.return_value = None
|
||||
session.query.return_value.filter.return_value.order_by.return_value.limit.return_value.all.return_value = []
|
||||
session.query.return_value.filter.return_value.order_by.return_value.all.return_value = []
|
||||
return session
|
||||
|
||||
@pytest.fixture
|
||||
@@ -25,6 +27,8 @@ def mock_garmin_client():
|
||||
"""Fixture for a mock GarminClient."""
|
||||
client = MagicMock(spec=GarminClient)
|
||||
client.is_connected = True
|
||||
# Important: mock client attribute for internal access (e.g. client.client.get_activity)
|
||||
client.client = MagicMock()
|
||||
return client
|
||||
|
||||
@pytest.fixture
|
||||
@@ -34,20 +38,17 @@ def sync_app_instance(mock_db_session, 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."""
|
||||
"""Test sync_activities for a new activity."""
|
||||
garmin_activity_data = {
|
||||
"activityId": "1",
|
||||
"activityName": "Run",
|
||||
@@ -56,144 +57,166 @@ def test_sync_activities_success_new_activity(sync_app_instance, mock_garmin_cli
|
||||
"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"
|
||||
mock_garmin_client.download_activity.return_value = b"fit_content"
|
||||
|
||||
# Mock full details fetch
|
||||
mock_garmin_client.client.get_activity.return_value = {"activityId": "1", "summaryDTO": {}}
|
||||
|
||||
mock_state = GarminActivityState(
|
||||
garmin_activity_id="1", sync_status="new",
|
||||
activity_name="Run", activity_type="running", start_time=datetime(2023,1,1,10,0,0)
|
||||
)
|
||||
|
||||
# Mock Activity record that will be found during redownload
|
||||
mock_activity = Activity(garmin_activity_id="1", download_status="pending")
|
||||
|
||||
def query_side_effect(model):
|
||||
m = MagicMock()
|
||||
if model == GarminActivityState:
|
||||
q_pending = MagicMock()
|
||||
q_pending.order_by.return_value.limit.return_value.all.return_value = [mock_state]
|
||||
q_pending.order_by.return_value.all.return_value = [mock_state]
|
||||
|
||||
q_scan = MagicMock()
|
||||
q_scan.first.return_value = None # Scan finds nothing
|
||||
|
||||
m.filter.return_value = q_pending
|
||||
m.filter_by.return_value = q_scan
|
||||
return m
|
||||
|
||||
if model == Activity:
|
||||
# Scan checks Activity -> Pending status or None?
|
||||
# If we return mock_activity (pending), scan sees is_downloaded=False. Correct.
|
||||
# Redownload checks Activity -> Pending.
|
||||
m.filter_by.return_value.first.return_value = mock_activity
|
||||
return m
|
||||
|
||||
return m
|
||||
|
||||
mock_db_session.query.side_effect = query_side_effect
|
||||
|
||||
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"
|
||||
mock_garmin_client.download_activity.assert_called_once_with(ANY, file_type='fit')
|
||||
assert result['processed'] == 1
|
||||
assert result['failed'] == 0
|
||||
|
||||
|
||||
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,
|
||||
}
|
||||
"""Test sync_activities already downloaded."""
|
||||
garmin_activity_data = {"activityId": "2", "activityType": {"typeKey": "walking"}}
|
||||
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
|
||||
# Mock DB
|
||||
existing_activity = Activity(garmin_activity_id="2", download_status="downloaded", file_content=b"content")
|
||||
|
||||
def query_side_effect(model):
|
||||
m = MagicMock()
|
||||
if model == Activity:
|
||||
m.filter_by.return_value.first.return_value = existing_activity
|
||||
return m
|
||||
if model == GarminActivityState:
|
||||
m.filter_by.return_value.first.return_value = None # Scan sees no state
|
||||
|
||||
# Pending query -> Empty
|
||||
q_pending = MagicMock()
|
||||
q_pending.order_by.return_value.all.return_value = []
|
||||
q_pending.order_by.return_value.limit.return_value.all.return_value = []
|
||||
m.filter.return_value = q_pending
|
||||
return m
|
||||
return m
|
||||
|
||||
mock_db_session.query.side_effect = query_side_effect
|
||||
|
||||
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
|
||||
|
||||
mock_garmin_client.download_activity.assert_not_called()
|
||||
assert result == {"processed": 0, "failed": 0}
|
||||
|
||||
|
||||
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,
|
||||
}
|
||||
"""Test sync_activities download failure."""
|
||||
garmin_activity_data = {"activityId": "3", "activityName": "Swim"}
|
||||
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
|
||||
mock_garmin_client.download_activity.return_value = None # Fail
|
||||
|
||||
mock_garmin_client.client.get_activity.return_value = {}
|
||||
|
||||
mock_state = GarminActivityState(garmin_activity_id="3", sync_status="new")
|
||||
mock_activity = Activity(garmin_activity_id="3", download_status="pending")
|
||||
|
||||
def query_side_effect(model):
|
||||
m = MagicMock()
|
||||
if model == GarminActivityState:
|
||||
q_pending = MagicMock()
|
||||
q_pending.order_by.return_value.limit.return_value.all.return_value = [mock_state]
|
||||
q_pending.order_by.return_value.all.return_value = [mock_state]
|
||||
m.filter.return_value = q_pending
|
||||
|
||||
q_scan = MagicMock()
|
||||
q_scan.first.return_value = None
|
||||
m.filter_by.return_value = q_scan
|
||||
return m
|
||||
if model == Activity:
|
||||
m.filter_by.return_value.first.return_value = mock_activity
|
||||
return m
|
||||
return m
|
||||
|
||||
mock_db_session.query.side_effect = query_side_effect
|
||||
|
||||
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
|
||||
|
||||
assert mock_garmin_client.download_activity.call_count == 4
|
||||
assert result['failed'] == 1
|
||||
|
||||
|
||||
# --- Tests for sync_health_metrics ---
|
||||
@patch.object(SyncApp, '_update_or_create_metric')
|
||||
|
||||
@patch('src.services.sync.health.update_or_create_health_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))]
|
||||
mock_garmin_client.get_all_metrics_for_date.return_value = {
|
||||
"steps": {"totalSteps": 10000},
|
||||
"hrv": {"lastNightAvg": 50},
|
||||
"sleep": {"dailySleepDTO": {"sleepTimeSeconds": 28800}}
|
||||
}
|
||||
|
||||
|
||||
mock_update_or_create_metric.return_value = 'new'
|
||||
|
||||
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
|
||||
assert mock_garmin_client.get_all_metrics_for_date.call_count == 2
|
||||
assert mock_update_or_create_metric.call_count == 6
|
||||
assert result == {"processed": 6, "failed": 0}
|
||||
|
||||
@patch.object(SyncApp, '_update_or_create_metric')
|
||||
@patch('src.services.sync.health.update_or_create_health_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_garmin_client.get_all_metrics_for_date.return_value = {
|
||||
"steps": {"totalSteps": 10000},
|
||||
"hrv": {"lastNightAvg": 50},
|
||||
"sleep": {"dailySleepDTO": {"sleepTimeSeconds": 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
|
||||
mock_update_or_create_metric.side_effect = [
|
||||
'new', 'error', 'new', # Day 1: Steps OK, HRV Error, Sleep OK
|
||||
'new', 'new', 'new' # Day 2: All OK
|
||||
]
|
||||
|
||||
result = sync_app_instance.sync_health_metrics(days_back=1)
|
||||
|
||||
assert mock_update_or_create_metric.call_count == 6
|
||||
assert result["processed"] == 5 # 1 error
|
||||
assert result["failed"] == 0 # Errors inside metric processing aren't counted as 'failed' in stats dict, only in logs?
|
||||
# sync_health_metrics: failed_count is incremented if EXCEPTION occurs outside _process_day_metrics_dict.
|
||||
# But _process_day_metrics_dict swallows exceptions?
|
||||
# No, utils returns 'error'.
|
||||
# _process_day_metrics_dict calls update_stat(..., status=='updated').
|
||||
# 'error' status is neither new nor updated.
|
||||
# So 'synced' count is NOT incremented.
|
||||
# So processed count should satisfy 5.
|
||||
|
||||
|
||||
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
|
||||
@patch('src.services.sync_app.update_or_create_health_metric')
|
||||
def test_wrapper_update_metric(mock_utils_fn, sync_app_instance):
|
||||
sync_app_instance._update_or_create_metric("steps", datetime(2023,1,1).date(), 100, "steps")
|
||||
mock_utils_fn.assert_called_once()
|
||||
|
||||
Reference in New Issue
Block a user