fix(garmin): make wrapper.py's startup tokenstore login lazy
_startup_login() used to run unconditionally at process boot, before main()'s stdin dispatch loop started. Whenever the very first real command turned out to be an explicit authenticate, this was actively counterproductive: on success it duplicated a login authenticate was about to redo anyway, and on failure it was a wasted, unauthenticated hit against Garmin's servers moments before the real attempt -- exactly the kind of extra load that worsens rate-limiting risk. It also never handled MFA, so it couldn't stand in for authenticate regardless. Make it lazy instead: only _handle_call falls back to it, at most once per subprocess lifetime, and only if authenticate was never explicitly attempted first. This is what it was actually for -- silently resuming a cached tokenstore session for a data call that never goes through the explicit authenticate command (e.g. "Sync now" reaching an already-connected user's client right after a backend restart cleared the in-memory client cache).
This commit is contained in:
@@ -12,15 +12,18 @@ import wrapper
|
||||
def reset_state():
|
||||
original_state = wrapper._auth_state
|
||||
original_client = wrapper._client
|
||||
original_startup_attempted = wrapper._startup_login_attempted
|
||||
for q in (wrapper._mfa_input_queue, wrapper._login_result_queue):
|
||||
while not q.empty():
|
||||
try:
|
||||
q.get_nowait()
|
||||
except queue.Empty:
|
||||
break
|
||||
wrapper._startup_login_attempted = False
|
||||
yield
|
||||
wrapper._auth_state = original_state
|
||||
wrapper._client = original_client
|
||||
wrapper._startup_login_attempted = original_startup_attempted
|
||||
|
||||
|
||||
def test_authenticate_success():
|
||||
@@ -93,10 +96,18 @@ def test_call_dispatches_to_named_garminconnect_method():
|
||||
|
||||
|
||||
def test_call_unauthenticated_is_error():
|
||||
# Explicitly absent GARMIN_EMAIL/PASSWORD: this now indirectly triggers
|
||||
# the lazy _startup_login fallback (see _handle_call), which must
|
||||
# return immediately with nothing to resume, leaving this the same
|
||||
# "Not authenticated" error as before -- not dependent on whatever
|
||||
# happens to be in the ambient shell environment.
|
||||
wrapper._auth_state = "unauthenticated"
|
||||
resp = wrapper.dispatch({
|
||||
"id": 7, "cmd": "call", "params": {"method": "get_activities_by_date", "args": {}},
|
||||
})
|
||||
with patch.dict("os.environ", {}, clear=False):
|
||||
os.environ.pop("GARMIN_EMAIL", None)
|
||||
os.environ.pop("GARMIN_PASSWORD", None)
|
||||
resp = wrapper.dispatch({
|
||||
"id": 7, "cmd": "call", "params": {"method": "get_activities_by_date", "args": {}},
|
||||
})
|
||||
assert "error" in resp
|
||||
assert "Not authenticated" in resp["error"]
|
||||
|
||||
@@ -206,3 +217,55 @@ def test_startup_login_does_not_leak_into_shared_authenticate_queue():
|
||||
|
||||
assert resp == {"id": 20, "result": {"status": "success", "message": "Authenticated successfully."}}
|
||||
assert wrapper._auth_state == "authenticated"
|
||||
|
||||
|
||||
def test_call_triggers_startup_login_lazily_on_first_call():
|
||||
"""A data 'call' with no prior explicit authenticate on this subprocess
|
||||
-- e.g. "Sync now" reaching an already-connected user's client right
|
||||
after a backend restart -- gets exactly one lazy attempt to silently
|
||||
resume a cached tokenstore session before giving up."""
|
||||
wrapper._auth_state = "unauthenticated"
|
||||
wrapper._client = MagicMock()
|
||||
wrapper._client.get_activities_by_date.return_value = []
|
||||
|
||||
def fake_startup_login():
|
||||
wrapper._auth_state = "authenticated"
|
||||
|
||||
with patch("wrapper._startup_login", side_effect=fake_startup_login) as mock_startup:
|
||||
resp = wrapper.dispatch({
|
||||
"id": 30, "cmd": "call", "params": {"method": "get_activities_by_date", "args": {}},
|
||||
})
|
||||
|
||||
mock_startup.assert_called_once()
|
||||
assert resp == {"id": 30, "result": []}
|
||||
|
||||
|
||||
def test_call_does_not_retry_startup_login_after_first_attempt():
|
||||
"""If the lazy startup-login attempt doesn't actually authenticate, a
|
||||
second 'call' must not try it again -- one attempt per subprocess
|
||||
lifetime, not once per call."""
|
||||
wrapper._auth_state = "unauthenticated"
|
||||
|
||||
with patch("wrapper._startup_login") as mock_startup: # no-op: leaves state unauthenticated
|
||||
wrapper.dispatch({"id": 31, "cmd": "call", "params": {"method": "x", "args": {}}})
|
||||
wrapper.dispatch({"id": 32, "cmd": "call", "params": {"method": "x", "args": {}}})
|
||||
|
||||
mock_startup.assert_called_once()
|
||||
|
||||
|
||||
def test_authenticate_prevents_later_lazy_startup_login():
|
||||
"""Once authenticate has been explicitly called (regardless of
|
||||
outcome), a later 'call' must never fall back to _startup_login -- that
|
||||
would either duplicate a login authenticate already did, or waste an
|
||||
extra hit against Garmin after a failure."""
|
||||
env = {"GARMIN_EMAIL": "test@example.com", "GARMIN_PASSWORD": "secret"}
|
||||
with patch.dict("os.environ", env):
|
||||
with patch("wrapper.Garmin") as mock_garmin_cls:
|
||||
mock_garmin_cls.return_value = MagicMock()
|
||||
wrapper.dispatch({"id": 33, "cmd": "authenticate"})
|
||||
|
||||
wrapper._auth_state = "unauthenticated" # simulate a later, separate call finding no session
|
||||
with patch("wrapper._startup_login") as mock_startup:
|
||||
wrapper.dispatch({"id": 34, "cmd": "call", "params": {"method": "x", "args": {}}})
|
||||
|
||||
mock_startup.assert_not_called()
|
||||
|
||||
Reference in New Issue
Block a user