"""Tests for `openrun.ingest.auth` — the web-friendly, MFA-resumable login flow. We stub garth at the `sso.login` / `sso.resume_login` / `save` boundary so the two-step (password → MFA code) state machine is testable without a real Garmin account or network. """ from __future__ import annotations from pathlib import Path import openrun.ingest.auth as auth class _FakeClient: username = "athlete" oauth1_token = None oauth2_token = None def _stub_garth(monkeypatch): client = _FakeClient() monkeypatch.setattr(auth.garth, "client", client) monkeypatch.setattr(auth.garth, "save", lambda td: None) return client # --- token store helpers --------------------------------------------------- def test_has_tokens(tmp_path: Path) -> None: assert auth.has_tokens(tmp_path) is False (tmp_path / "oauth1_token.json").write_text("{}") assert auth.has_tokens(tmp_path) is True def test_resume_no_tokens_is_false(tmp_path: Path, monkeypatch) -> None: called = {"n": 0} monkeypatch.setattr(auth.garth, "resume", lambda td: called.__setitem__("n", called["n"] + 1)) assert auth.resume(tmp_path) is False assert called["n"] == 0 # short-circuits before touching garth def test_resume_with_tokens(tmp_path: Path, monkeypatch) -> None: (tmp_path / "oauth1_token.json").write_text("{}") monkeypatch.setattr(auth.garth, "resume", lambda td: None) assert auth.resume(tmp_path) is True def test_resume_swallows_bad_tokens(tmp_path: Path, monkeypatch) -> None: (tmp_path / "oauth1_token.json").write_text("garbage") def _boom(td): raise ValueError("corrupt") monkeypatch.setattr(auth.garth, "resume", _boom) assert auth.resume(tmp_path) is False # --- login state machine --------------------------------------------------- def test_begin_login_no_mfa(tmp_path: Path, monkeypatch) -> None: client = _stub_garth(monkeypatch) monkeypatch.setattr(auth.garth.sso, "login", lambda *a, **k: ("oauth1", "oauth2")) kind, payload = auth.begin_login("e@x.com", "pw", tmp_path) assert kind == "ok" assert payload == "athlete" assert client.oauth1_token == "oauth1" and client.oauth2_token == "oauth2" def test_begin_login_needs_mfa(tmp_path: Path, monkeypatch) -> None: _stub_garth(monkeypatch) state = {"session": "opaque"} monkeypatch.setattr(auth.garth.sso, "login", lambda *a, **k: ("needs_mfa", state)) kind, payload = auth.begin_login("e@x.com", "pw", tmp_path) assert kind == "needs_mfa" assert payload is state def test_complete_mfa(tmp_path: Path, monkeypatch) -> None: client = _stub_garth(monkeypatch) seen = {} def _resume(state, code): seen["state"], seen["code"] = state, code return ("o1", "o2") monkeypatch.setattr(auth.garth.sso, "resume_login", _resume) user = auth.complete_mfa({"s": 1}, " 123456 ", tmp_path) assert user == "athlete" assert seen["code"] == "123456" # trimmed assert client.oauth1_token == "o1" and client.oauth2_token == "o2"