Files
mywhoosh2garmin/tests/db/test_sync_state.py
Bastian Wagner 1d5bbdb2a2 feat: add durable sync state transitions
Add state-transition methods to ActivityRepository for advancing activity
stages (mark_downloaded, mark_converted, mark_imported, mark_duplicate,
mark_failed) with proper retention of last_completed_stage on failure.
Add list_pending_for_user to filter activities for processing. Implement
SyncRunRepository for creating and finalizing sync runs with counts and
summary errors.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 15:33:46 +02:00

133 lines
5.0 KiB
Python

from app.db.models import ActivityStatus, SyncRunStatus
def test_failure_retains_last_completed_stage(activity_repository, seeded_activity) -> None:
activity_repository.mark_downloaded(seeded_activity.id, "/data/activities/1/a/source.fit")
activity_repository.mark_failed(seeded_activity.id, "Garmin timeout", retryable=True)
activity = activity_repository.get(seeded_activity.id)
assert activity.status == ActivityStatus.FAILED
assert activity.last_completed_stage == ActivityStatus.DOWNLOADED
assert activity.retryable is True
def test_converted_activity_is_pending_until_terminal(activity_repository, seeded_activity) -> None:
activity_repository.mark_converted(seeded_activity.id, "/data/activities/1/a/converted.fit")
ids = [item.id for item in activity_repository.list_pending_for_user(seeded_activity.user_id)]
assert seeded_activity.id in ids
def test_list_pending_excludes_imported(activity_repository, seeded_activity) -> None:
activity_repository.mark_downloaded(seeded_activity.id, "/data/activities/1/a/source.fit")
activity_repository.mark_converted(seeded_activity.id, "/data/activities/1/a/converted.fit")
activity_repository.mark_imported(seeded_activity.id, "garmin-123")
ids = [item.id for item in activity_repository.list_pending_for_user(seeded_activity.user_id)]
assert seeded_activity.id not in ids
def test_list_pending_excludes_duplicate(activity_repository, seeded_activity) -> None:
activity_repository.mark_downloaded(seeded_activity.id, "/data/activities/1/a/source.fit")
activity_repository.mark_duplicate(seeded_activity.id)
ids = [item.id for item in activity_repository.list_pending_for_user(seeded_activity.user_id)]
assert seeded_activity.id not in ids
def test_list_pending_excludes_non_retryable_failed(activity_repository, seeded_activity) -> None:
activity_repository.mark_downloaded(seeded_activity.id, "/data/activities/1/a/source.fit")
activity_repository.mark_failed(seeded_activity.id, "Cannot retry", retryable=False)
ids = [item.id for item in activity_repository.list_pending_for_user(seeded_activity.user_id)]
assert seeded_activity.id not in ids
def test_list_pending_includes_retryable_failed(activity_repository, seeded_activity) -> None:
activity_repository.mark_downloaded(seeded_activity.id, "/data/activities/1/a/source.fit")
activity_repository.mark_failed(seeded_activity.id, "Temporary error", retryable=True)
ids = [item.id for item in activity_repository.list_pending_for_user(seeded_activity.user_id)]
assert seeded_activity.id in ids
def test_sync_run_start_and_finish(sync_run_repository, user_repository) -> None:
user = user_repository.create(
name="Test User",
enabled=True,
health_state="healthy",
mywhoosh_email_enc="test@example.com",
mywhoosh_password_enc="password",
garmin_email_enc="test@garmin.com",
garmin_password_enc="garmin_password",
)
sync_run = sync_run_repository.start(user.id)
assert sync_run.status == SyncRunStatus.RUNNING
assert sync_run.discovered_count == 0
assert sync_run.imported_count == 0
assert sync_run.skipped_count == 0
assert sync_run.failed_count == 0
finished = sync_run_repository.finish(
sync_run.id,
status=SyncRunStatus.SUCCESS,
discovered=5,
imported=3,
skipped=1,
failed=1,
)
assert finished.status == SyncRunStatus.SUCCESS
assert finished.discovered_count == 5
assert finished.imported_count == 3
assert finished.skipped_count == 1
assert finished.failed_count == 1
assert finished.finished_at is not None
# Reload from DB to verify persisted
reloaded = sync_run_repository.get(sync_run.id)
assert reloaded.status == SyncRunStatus.SUCCESS
assert reloaded.discovered_count == 5
assert reloaded.imported_count == 3
def test_sync_run_finish_with_error(sync_run_repository, user_repository) -> None:
user = user_repository.create(
name="Test User",
enabled=True,
health_state="healthy",
mywhoosh_email_enc="test@example.com",
mywhoosh_password_enc="password",
garmin_email_enc="test@garmin.com",
garmin_password_enc="garmin_password",
)
sync_run = sync_run_repository.start(user.id)
error_msg = "Connection timeout"
finished = sync_run_repository.finish(
sync_run.id,
status=SyncRunStatus.FAILED,
discovered=0,
imported=0,
skipped=0,
failed=0,
summary_error=error_msg,
)
assert finished.summary_error == error_msg
# Verify truncation works
long_error = "x" * 5000
finished_long = sync_run_repository.finish(
sync_run.id,
status=SyncRunStatus.FAILED,
discovered=0,
imported=0,
skipped=0,
failed=0,
summary_error=long_error,
)
assert len(finished_long.summary_error) == 2000
assert finished_long.summary_error == long_error[:2000]