fix(garmin): startup tokenstore resume recovers after its 10s timeout
The background login thread now flips _auth_state to authenticated itself when a slow tokenstore resume succeeds after the 10s wait -- previously its result landed in an abandoned private queue and the docstring's promised late recovery never happened, leaving every subsequent call failing until an explicit authenticate. Guarded to only ever transition from unauthenticated, so an explicit authenticate/MFA flow that took over meanwhile is never clobbered; the timeout branch no longer re-writes the state (a redundant write that could race a success at the boundary). Structured-log messages reworked alongside. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
import time
|
||||
import queue
|
||||
import threading
|
||||
from unittest.mock import MagicMock, patch
|
||||
@@ -302,3 +303,51 @@ def test_call_does_not_mark_other_errors_as_not_found():
|
||||
assert resp["id"] == 12
|
||||
assert resp["error"] == "rate limited"
|
||||
assert "not_found" not in resp
|
||||
|
||||
|
||||
def test_startup_login_late_success_flips_auth_state_for_later_calls():
|
||||
"""A tokenstore resume that outlives the 10s wait must still recover
|
||||
the subprocess: the background thread itself flips _auth_state on
|
||||
success, so the next `call` command passes the auth check without an
|
||||
explicit authenticate."""
|
||||
env = {"GARMIN_EMAIL": "test@example.com", "GARMIN_PASSWORD": "secret"}
|
||||
release = threading.Event()
|
||||
mock_client = MagicMock()
|
||||
mock_client.login.side_effect = lambda **kwargs: release.wait(timeout=5)
|
||||
wrapper._auth_state = "unauthenticated"
|
||||
with patch.dict("os.environ", env):
|
||||
with patch("wrapper.Garmin", return_value=mock_client):
|
||||
with patch("wrapper.queue.Queue") as mock_queue_cls:
|
||||
# Simulate the 10s timeout: the reader gives up immediately,
|
||||
# while the (still-blocked) login thread keeps running.
|
||||
mock_queue_cls.return_value.get.side_effect = queue.Empty
|
||||
wrapper._startup_login()
|
||||
assert wrapper._auth_state == "unauthenticated"
|
||||
|
||||
release.set() # the slow login now completes, after the timeout
|
||||
for _ in range(100):
|
||||
if wrapper._auth_state == "authenticated":
|
||||
break
|
||||
time.sleep(0.01)
|
||||
assert wrapper._auth_state == "authenticated"
|
||||
|
||||
|
||||
def test_startup_login_late_success_never_overrides_explicit_auth_flow():
|
||||
"""If an explicit authenticate/MFA flow moved the state while the
|
||||
startup resume was still in flight, the late success must not clobber
|
||||
it -- the explicit flow owns the state once it has moved it."""
|
||||
env = {"GARMIN_EMAIL": "test@example.com", "GARMIN_PASSWORD": "secret"}
|
||||
release = threading.Event()
|
||||
mock_client = MagicMock()
|
||||
mock_client.login.side_effect = lambda **kwargs: release.wait(timeout=5)
|
||||
wrapper._auth_state = "unauthenticated"
|
||||
with patch.dict("os.environ", env):
|
||||
with patch("wrapper.Garmin", return_value=mock_client):
|
||||
with patch("wrapper.queue.Queue") as mock_queue_cls:
|
||||
mock_queue_cls.return_value.get.side_effect = queue.Empty
|
||||
wrapper._startup_login()
|
||||
|
||||
wrapper._auth_state = "mfa_pending" # an explicit flow took over meanwhile
|
||||
release.set()
|
||||
time.sleep(0.2) # give the thread ample time to (wrongly) flip it
|
||||
assert wrapper._auth_state == "mfa_pending"
|
||||
|
||||
Reference in New Issue
Block a user