This commit is contained in:
Bastian Wagner
2026-08-15 20:31:57 +02:00
parent 85b0d861b4
commit 7c9e19ba0b
8 changed files with 146 additions and 5 deletions

View File

@@ -6,10 +6,11 @@ from sqlalchemy import select
from app.db.models import ActivityStatus, HealthState, SyncRun, SyncRunStatus, SyncUser
from app.db.repositories import UserRepository
from app.garmin.uploader import UploadResult
from app.mywhoosh.client import MyWhooshDeviceConflictError
from app.mywhoosh.models import MyWhooshActivity
from app.sync.manager import SyncManager
from tests.sync.conftest import FakeFitConverter, _create_user
from tests.sync.fakes import FakeMyWhooshClient
from tests.sync.fakes import FakeGarminUploader, FakeMyWhooshClient
@pytest.mark.asyncio
@@ -188,6 +189,46 @@ async def test_garmin_action_required_cleared_after_successful_import(
assert reloaded.health_state == HealthState.HEALTHY
class DeviceConflictMyWhooshClient:
"""Fake MyWhoosh client that always raises MyWhooshDeviceConflictError
from list_activities, simulating MyWhoosh's "already logged in from
another device" response."""
async def list_activities(self, email: str, password: str):
raise MyWhooshDeviceConflictError("You are already logged in from another device.")
async def download_fit(self, activity_file_id: str, email: str, password: str) -> bytes:
raise AssertionError("download_fit should not be reached in this test")
@pytest.mark.asyncio
async def test_device_conflict_sets_distinct_action_reason(seeded_user: SyncUser, session_factory, cipher, settings) -> None:
"""A MyWhoosh device-conflict response must be distinguishable in the UI
from a generic auth failure, so users get an actionable hint instead of
being told to re-check their password."""
mywhoosh = DeviceConflictMyWhooshClient()
converter = FakeFitConverter()
garmin = FakeGarminUploader()
manager = SyncManager(
session_factory=session_factory,
credential_cipher=cipher,
settings=settings,
mywhoosh_factory=lambda token_store: mywhoosh,
garmin_factory=lambda email, password, tokenstore: garmin,
fit_converter=converter,
)
outcome = await manager.sync_user(seeded_user.id)
assert outcome.status == "failed"
assert "another device" in outcome.message
with session_factory() as session:
reloaded = UserRepository(session).get(seeded_user.id)
assert reloaded.action_reason == "mywhoosh_device_conflict"
assert reloaded.mywhoosh_state == "device_conflict"
assert reloaded.health_state == HealthState.ACTION_REQUIRED
@pytest.mark.asyncio
async def test_mfa_code_reaches_real_garmin_uploader_via_sync_manager(
session_factory, cipher, settings, seeded_user: SyncUser