Implement GarminUploader adapter around garminconnect library: - Import activities using import_activity() not upload_activity() - Treat duplicate activity responses as terminal success - Raise GarminUploadBlocked for MFA to allow UI code collection - Distinguish auth, transient, and other errors appropriately - Helper functions for duplicate detection and activity ID extraction Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
81 lines
2.5 KiB
Python
81 lines
2.5 KiB
Python
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from app.garmin.uploader import GarminUploadBlocked, GarminUploader
|
|
|
|
|
|
class FakeGarmin:
|
|
def __init__(self, *args, prompt_mfa=None, import_result=None, login_error=None, import_error=None, **kwargs):
|
|
self.prompt_mfa = prompt_mfa
|
|
self.import_result = import_result or {"activityId": 42}
|
|
self.login_error = login_error
|
|
self.import_error = import_error
|
|
self.login_path = None
|
|
|
|
def login(self, tokenstore=None):
|
|
self.login_path = tokenstore
|
|
if self.login_error:
|
|
raise self.login_error
|
|
|
|
def import_activity(self, activity_path: str):
|
|
if self.import_error:
|
|
raise self.import_error
|
|
return self.import_result
|
|
|
|
|
|
def test_successful_import_returns_activity_id(tmp_path: Path) -> None:
|
|
uploader = GarminUploader(
|
|
email="g@example.com",
|
|
password="pw",
|
|
tokenstore=tmp_path / "garmin",
|
|
client_factory=lambda *a, **kw: FakeGarmin(*a, **kw, import_result={"activityId": 42}),
|
|
)
|
|
result = uploader.import_fit(tmp_path / "ride.fit")
|
|
assert result.status == "imported"
|
|
assert result.garmin_activity_id == "42"
|
|
|
|
|
|
def test_duplicate_is_terminal_success(tmp_path: Path) -> None:
|
|
uploader = GarminUploader(
|
|
email="g@example.com",
|
|
password="pw",
|
|
tokenstore=tmp_path / "garmin",
|
|
client_factory=lambda *a, **kw: FakeGarmin(*a, **kw, import_error=RuntimeError("409 duplicate")),
|
|
)
|
|
result = uploader.import_fit(tmp_path / "ride.fit")
|
|
assert result.duplicate is True
|
|
assert result.status == "duplicate"
|
|
|
|
|
|
def test_mfa_without_code_is_blocked(tmp_path: Path) -> None:
|
|
class MfaGarmin(FakeGarmin):
|
|
def login(self, tokenstore=None):
|
|
self.prompt_mfa()
|
|
|
|
uploader = GarminUploader(
|
|
email="g@example.com",
|
|
password="pw",
|
|
tokenstore=tmp_path / "garmin",
|
|
client_factory=MfaGarmin,
|
|
)
|
|
with pytest.raises(GarminUploadBlocked):
|
|
uploader.import_fit(tmp_path / "ride.fit")
|
|
|
|
|
|
def test_mfa_code_is_returned_only_to_prompt(tmp_path: Path) -> None:
|
|
seen = []
|
|
|
|
class MfaGarmin(FakeGarmin):
|
|
def login(self, tokenstore=None):
|
|
seen.append(self.prompt_mfa())
|
|
|
|
uploader = GarminUploader(
|
|
email="g@example.com",
|
|
password="pw",
|
|
tokenstore=tmp_path / "garmin",
|
|
client_factory=MfaGarmin,
|
|
)
|
|
uploader.import_fit(tmp_path / "ride.fit", mfa_code="123456")
|
|
assert seen == ["123456"]
|