feat: import FIT activities into Garmin
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>
This commit is contained in:
0
tests/garmin/__init__.py
Normal file
0
tests/garmin/__init__.py
Normal file
80
tests/garmin/test_uploader.py
Normal file
80
tests/garmin/test_uploader.py
Normal file
@@ -0,0 +1,80 @@
|
||||
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"]
|
||||
Reference in New Issue
Block a user