feat: Implement Garmin sync, login improvements, and utility scripts

This commit is contained in:
2025-10-11 11:56:25 -07:00
parent 56a93cd8df
commit 3819e4f5e2
921 changed files with 2058 additions and 371 deletions

View File

@@ -0,0 +1,169 @@
from datetime import datetime, timedelta
from unittest.mock import AsyncMock, patch
import pytest
from backend.src.main import app
from backend.src.schemas import GarminCredentials
from httpx import AsyncClient
@pytest.fixture
def mock_garmin_auth_service():
with patch('backend.src.api.garmin_auth.GarminAuthService') as MockGarminAuthService:
service_instance = MockGarminAuthService.return_value
yield service_instance
@pytest.fixture
def mock_central_db_service():
with patch('backend.src.api.garmin_auth.CentralDBService') as MockCentralDBService:
service_instance = MockCentralDBService.return_value
yield service_instance
@pytest.mark.asyncio
async def test_garmin_login_success_new_credentials(mock_garmin_auth_service, mock_central_db_service):
username = "test@example.com"
password = "password123"
mock_garmin_auth_service.initial_login.return_value = GarminCredentials(
garmin_username=username,
garmin_password_plaintext=password,
access_token="mock_access_token",
access_token_secret="mock_access_token_secret",
token_expiration_date=datetime.utcnow() + timedelta(hours=1)
)
mock_central_db_service.get_garmin_credentials.return_value = None # No existing credentials
mock_central_db_service.create_garmin_credentials.return_value = AsyncMock() # Simulate successful creation
async with AsyncClient(app=app, base_url="http://test") as client:
response = await client.post(
"/api/garmin/login",
json={
"username": username,
"password": password
}
)
assert response.status_code == 200
assert response.json() == {"message": "Garmin account linked successfully."}
mock_garmin_auth_service.initial_login.assert_called_once_with(username, password)
mock_central_db_service.get_garmin_credentials.assert_called_once_with(1) # Assuming user_id 1
mock_central_db_service.create_garmin_credentials.assert_called_once()
@pytest.mark.asyncio
async def test_garmin_login_success_update_credentials(mock_garmin_auth_service, mock_central_db_service):
username = "test@example.com"
password = "password123"
mock_garmin_auth_service.initial_login.return_value = GarminCredentials(
garmin_username=username,
garmin_password_plaintext=password,
access_token="mock_access_token_new",
access_token_secret="mock_access_token_secret_new",
token_expiration_date=datetime.utcnow() + timedelta(hours=1)
)
mock_central_db_service.get_garmin_credentials.return_value = GarminCredentials(
garmin_username=username,
garmin_password_plaintext="old_password",
access_token="old_access_token",
access_token_secret="old_access_token_secret",
token_expiration_date=datetime.utcnow() - timedelta(hours=1)
) # Existing credentials
mock_central_db_service.update_garmin_credentials.return_value = AsyncMock() # Simulate successful update
async with AsyncClient(app=app, base_url="http://test") as client:
response = await client.post(
"/api/garmin/login",
json={
"username": username,
"password": password
}
)
assert response.status_code == 200
assert response.json() == {"message": "Garmin account linked successfully."}
mock_garmin_auth_service.initial_login.assert_called_once_with(username, password)
mock_central_db_service.get_garmin_credentials.assert_called_once_with(1)
mock_central_db_service.update_garmin_credentials.assert_called_once()
@pytest.mark.asyncio
async def test_garmin_login_failure_invalid_credentials(mock_garmin_auth_service, mock_central_db_service):
username = "invalid@example.com"
password = "wrongpassword"
mock_garmin_auth_service.initial_login.return_value = None # Simulate failed Garmin login
async with AsyncClient(app=app, base_url="http://test") as client:
response = await client.post(
"/api/garmin/login",
json={
"username": username,
"password": password
}
)
assert response.status_code == 401
assert response.json() == {"detail": "Invalid Garmin credentials provided."}
mock_garmin_auth_service.initial_login.assert_called_once_with(username, password)
mock_central_db_service.get_garmin_credentials.assert_not_called()
mock_central_db_service.create_garmin_credentials.assert_not_called()
mock_central_db_service.update_garmin_credentials.assert_not_called()
@pytest.mark.asyncio
async def test_garmin_login_failure_central_db_create_error(mock_garmin_auth_service, mock_central_db_service):
username = "test@example.com"
password = "password123"
mock_garmin_auth_service.initial_login.return_value = GarminCredentials(
garmin_username=username,
garmin_password_plaintext=password,
access_token="mock_access_token",
access_token_secret="mock_access_token_secret",
token_expiration_date=datetime.utcnow() + timedelta(hours=1)
)
mock_central_db_service.get_garmin_credentials.return_value = None
mock_central_db_service.create_garmin_credentials.return_value = None # Simulate CentralDB create failure
async with AsyncClient(app=app, base_url="http://test") as client:
response = await client.post(
"/api/garmin/login",
json={
"username": username,
"password": password
}
)
assert response.status_code == 500
assert response.json() == {"detail": "Failed to store Garmin credentials in CentralDB."}
@pytest.mark.asyncio
async def test_garmin_login_failure_central_db_update_error(mock_garmin_auth_service, mock_central_db_service):
username = "test@example.com"
password = "password123"
mock_garmin_auth_service.initial_login.return_value = GarminCredentials(
garmin_username=username,
garmin_password_plaintext=password,
access_token="mock_access_token_new",
access_token_secret="mock_access_token_secret_new",
token_expiration_date=datetime.utcnow() + timedelta(hours=1)
)
mock_central_db_service.get_garmin_credentials.return_value = GarminCredentials(
garmin_username=username,
garmin_password_plaintext="old_password",
access_token="old_access_token",
access_token_secret="old_access_token_secret",
token_expiration_date=datetime.utcnow() - timedelta(hours=1)
)
mock_central_db_service.update_garmin_credentials.return_value = None # Simulate CentralDB update failure
async with AsyncClient(app=app, base_url="http://test") as client:
response = await client.post(
"/api/garmin/login",
json={
"username": username,
"password": password
}
)
assert response.status_code == 500
assert response.json() == {"detail": "Failed to update Garmin credentials in CentralDB."}

View File

@@ -0,0 +1,80 @@
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
@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
@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"
}
)
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
@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={}
)
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
@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