feat: Implement single sync job management and progress tracking

This commit is contained in:
2025-10-11 18:36:19 -07:00
parent 3819e4f5e2
commit 723ca04aa8
51 changed files with 1625 additions and 596 deletions

View File

@@ -1,80 +1,88 @@
from datetime import date
from unittest.mock import AsyncMock, patch
import pytest
from backend.src.main import app
from backend.src.schemas import User
from fastapi import HTTPException
from httpx import AsyncClient
from backend.src.services.sync_manager import current_sync_job_manager
from fastapi.testclient import TestClient
client = TestClient(app)
@pytest.fixture
def mock_garmin_activity_service():
with patch('backend.src.api.garmin_sync.GarminActivityService') as MockGarminActivityService:
service_instance = MockGarminActivityService.return_value
yield service_instance
def test_get_sync_status():
response = client.get("/api/sync/garmin/sync/status")
assert response.status_code == 200
@pytest.fixture
def mock_get_current_user():
with patch('backend.src.api.garmin_sync.get_current_user') as mock_current_user:
mock_current_user.return_value = User(id=1, name="Test User", email="test@example.com")
yield mock_current_user
@pytest.mark.asyncio
async def test_trigger_garmin_activity_sync_success(mock_garmin_activity_service, mock_get_current_user):
mock_garmin_activity_service.sync_activities_in_background = AsyncMock()
mock_garmin_activity_service.sync_activities_in_background.return_value = None
async with AsyncClient(app=app, base_url="http://test") as client:
response = await client.post(
"/api/sync/garmin/activities",
json={
"force_resync": False,
"start_date": "2023-01-01",
"end_date": "2023-01-31"
}
)
def test_trigger_activity_sync_success():
response = client.post("/api/sync/garmin/activities", json={})
assert response.status_code == 202
response_json = response.json()
assert "job_id" in response_json
assert "status" in response_json
assert response_json["status"] == "pending"
mock_garmin_activity_service.sync_activities_in_background.assert_called_once()
args, kwargs = mock_garmin_activity_service.sync_activities_in_background.call_args
assert not args[1] # force_resync
assert args[2] == date(2023, 1, 1) # start_date
assert args[3] == date(2023, 1, 31) # end_date
assert response.json() == {
"message": "Activity synchronization initiated successfully."
}
@pytest.mark.asyncio
async def test_trigger_garmin_activity_sync_no_dates(mock_garmin_activity_service, mock_get_current_user):
mock_garmin_activity_service.sync_activities_in_background = AsyncMock()
mock_garmin_activity_service.sync_activities_in_background.return_value = None
async with AsyncClient(app=app, base_url="http://test") as client:
response = await client.post(
"/api/sync/garmin/activities",
json={}
)
def test_trigger_activity_sync_conflict():
# Manually start a sync to simulate a conflict
current_sync_job_manager._current_job = current_sync_job_manager.start_sync(
"activities"
)
response = client.post("/api/sync/garmin/activities", json={})
assert response.status_code == 409
assert response.json() == {
"detail": "A synchronization is already in progress. Please wait or check status."
}
# Clean up
current_sync_job_manager._current_job = None
def test_trigger_workout_sync_success():
response = client.post(
"/api/sync/garmin/workouts",
json={"workout_id": "a1b2c3d4-e5f6-7890-1234-567890abcdef"},
)
assert response.status_code == 202
response_json = response.json()
assert "job_id" in response_json
assert "status" in response_json
assert response_json["status"] == "pending"
mock_garmin_activity_service.sync_activities_in_background.assert_called_once()
args, kwargs = mock_garmin_activity_service.sync_activities_in_background.call_args
assert not args[1] # force_resync
assert args[2] is None # start_date
assert args[3] is None # end_date
assert response.json() == {
"message": "Workout synchronization initiated successfully."
}
@pytest.mark.asyncio
async def test_trigger_garmin_activity_sync_unauthorized():
with patch('backend.src.api.garmin_sync.get_current_user', side_effect=HTTPException(status_code=401)):
async with AsyncClient(app=app, base_url="http://test") as client:
response = await client.post(
"/api/sync/garmin/activities",
json={}
)
assert response.status_code == 401
assert response.json() == {"detail": "Not Authenticated"} # Default FastAPI 401 detail
def test_trigger_workout_sync_conflict():
# Manually start a sync to simulate a conflict
current_sync_job_manager._current_job = current_sync_job_manager.start_sync(
"workouts"
)
response = client.post(
"/api/sync/garmin/workouts",
json={"workout_id": "a1b2c3d4-e5f6-7890-1234-567890abcdef"},
)
assert response.status_code == 409
assert response.json() == {
"detail": "A synchronization is already in progress. Please wait or check status."
}
# Clean up
current_sync_job_manager._current_job = None
def test_trigger_health_sync_success():
response = client.post("/api/sync/garmin/health", json={})
assert response.status_code == 202
assert response.json() == {
"message": "Health metrics synchronization initiated successfully."
}
def test_trigger_health_sync_conflict():
# Manually start a sync to simulate a conflict
current_sync_job_manager._current_job = current_sync_job_manager.start_sync(
"health"
)
response = client.post("/api/sync/garmin/health", json={})
assert response.status_code == 409
assert response.json() == {
"detail": "A synchronization is already in progress. Please wait or check status."
}
# Clean up
current_sync_job_manager._current_job = None