- Add Fitbit authentication flow (save credentials, OAuth callback handling) - Implement Garmin MFA support with successful session/cookie handling - Optimize segment discovery with new sampling and activity query services - Refactor database session management in discovery API for better testability - Enhance activity data parsing for charts and analysis - Update tests to use testcontainers and proper dependency injection - Clean up repository by ignoring and removing tracked transient files (.pyc, .db)
154 lines
4.8 KiB
Python
154 lines
4.8 KiB
Python
|
|
import pytest
|
|
from unittest.mock import MagicMock
|
|
from fastapi.testclient import TestClient
|
|
from datetime import datetime, timedelta
|
|
from src.api.activities import router
|
|
from src.models.activity import Activity
|
|
from src.models.bike_setup import BikeSetup
|
|
from src.models.activity_state import GarminActivityState
|
|
from src.api.activities import get_db
|
|
from main import app # Use main app instance
|
|
|
|
# Mock Objects matches Pydantic models structure where needed,
|
|
# but for SQLAlchemy response matching, standard objects work.
|
|
|
|
def mock_activity(garmin_id="12345", name="Test Ride"):
|
|
# Mocking a SQLAlchemy Activity object
|
|
act = MagicMock(spec=Activity)
|
|
act.id = int(garmin_id)
|
|
act.garmin_activity_id = garmin_id
|
|
act.activity_name = name
|
|
act.activity_type = "cycling"
|
|
act.start_time = datetime(2023, 1, 1, 10, 0, 0)
|
|
act.duration = 3600.0
|
|
act.file_type = "fit"
|
|
act.download_status = "downloaded"
|
|
act.downloaded_at = datetime(2023, 1, 1, 12, 0, 0)
|
|
act.avg_power = 200
|
|
act.avg_hr = 150
|
|
act.avg_cadence = 90
|
|
act.is_estimated_power = False
|
|
|
|
# Relationships
|
|
bike = MagicMock(spec=BikeSetup)
|
|
bike.id = 1
|
|
bike.name = "Road Bike"
|
|
bike.frame = "Carbon"
|
|
bike.chainring = 50
|
|
bike.rear_cog = 11
|
|
act.bike_setup = bike
|
|
act.bike_setup_id = 1
|
|
|
|
# File content for details overrides
|
|
act.file_content = b"mock_content"
|
|
|
|
# Extended stats for details
|
|
act.distance = 20000.0
|
|
act.calories = 800.0
|
|
act.max_hr = 180
|
|
act.avg_speed = 8.5
|
|
act.max_speed = 12.0
|
|
act.elevation_gain = 500.0
|
|
act.elevation_loss = 500.0
|
|
act.max_cadence = 100
|
|
act.steps = 0
|
|
act.vo2_max = 50.0
|
|
|
|
return act
|
|
|
|
def mock_activity_state(garmin_id="12345", name="Test Ride"):
|
|
state = MagicMock(spec=GarminActivityState)
|
|
state.garmin_activity_id = garmin_id
|
|
state.activity_name = name
|
|
state.activity_type = "cycling"
|
|
state.start_time = datetime(2023, 1, 1, 10, 0, 0)
|
|
state.sync_status = "synced"
|
|
return state
|
|
|
|
@pytest.fixture
|
|
def mock_db_session():
|
|
return MagicMock()
|
|
|
|
@pytest.fixture
|
|
def client(mock_db_session):
|
|
def override_get_db():
|
|
try:
|
|
yield mock_db_session
|
|
finally:
|
|
pass
|
|
|
|
app.dependency_overrides[get_db] = override_get_db
|
|
with TestClient(app) as c:
|
|
yield c
|
|
app.dependency_overrides.clear()
|
|
|
|
def test_list_activities(client, mock_db_session):
|
|
# Setup Mock Return
|
|
# list_activities queries (GarminActivityState, Activity)
|
|
|
|
state1 = mock_activity_state("1001", "Morning Ride")
|
|
act1 = mock_activity("1001", "Morning Ride")
|
|
|
|
state2 = mock_activity_state("1002", "Evening Ride")
|
|
act2 = mock_activity("1002", "Evening Ride")
|
|
|
|
# Mock query().outerjoin().order_by().offset().limit().all() chain
|
|
# It's a bit long chain to mock perfectly.
|
|
# db.query(...) returns Query object.
|
|
|
|
mock_query = mock_db_session.query.return_value
|
|
mock_query.outerjoin.return_value = mock_query
|
|
mock_query.order_by.return_value = mock_query
|
|
mock_query.offset.return_value = mock_query
|
|
mock_query.limit.return_value = mock_query
|
|
mock_query.all.return_value = [(state1, act1), (state2, act2)]
|
|
|
|
response = client.get("/api/activities/list?limit=10&offset=0")
|
|
|
|
# Check execution
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert len(data) == 2
|
|
assert data[0]["garmin_activity_id"] == "1001"
|
|
assert data[0]["activity_name"] == "Morning Ride"
|
|
assert data[0]["download_status"] == "downloaded"
|
|
assert data[0]["bike_setup"]["name"] == "Road Bike"
|
|
|
|
def test_get_activity_details(client, mock_db_session):
|
|
# Setup
|
|
act = mock_activity("2001", "Detail Test")
|
|
|
|
# db.query(Activity).filter(...).first()
|
|
mock_query = mock_db_session.query.return_value
|
|
mock_query.filter.return_value = mock_query
|
|
mock_query.first.return_value = act
|
|
|
|
response = client.get(f"/api/activities/2001/details")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
|
|
assert data["garmin_activity_id"] == "2001"
|
|
assert data["distance"] == 20000.0
|
|
assert data["bike_setup"]["name"] == "Road Bike"
|
|
|
|
def test_get_activity_streams_mock(client, mock_db_session):
|
|
act = mock_activity("3001", "Stream Test")
|
|
act.streams_json = None
|
|
act.file_content = None
|
|
|
|
# Mock query returning activity directly
|
|
mock_db_session.query.return_value.filter.return_value.first.return_value = act
|
|
|
|
response = client.get(f"/api/activities/3001/streams")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
|
|
# Expect empty streams structure
|
|
expected_keys = ["time", "heart_rate", "power", "altitude", "speed", "cadence", "respiration_rate"]
|
|
for k in expected_keys:
|
|
assert k in data
|
|
assert data[k] == []
|