Files
hermes-agent/tests/hermes_cli/test_spotify_auth.py
Teknium ba030bc0db fix(test-seams): monkeypatch.setattr facade aliases — also patch the defining module (57 files)
Tests did monkeypatch.setattr(<facade module>, name) where name is now defined in a
sibling module and the production path reads the sibling's binding. Where production
reads through BOTH bindings the setattr is duplicated onto the defining module (import
added next to the existing alias import); where only the sibling reads it the target is
repointed. Seams whose production readers go through the facade are left alone.
2026-09-03 19:33:28 -07:00

180 lines
6.1 KiB
Python

from __future__ import annotations
from types import SimpleNamespace
import pytest
from hermes_cli import auth as auth_mod
import hermes_cli.auth_spotify as auth_spotify
from hermes_cli.auth import AuthError, resolve_spotify_runtime_credentials
def test_resolve_spotify_runtime_credentials_refreshes_without_changing_active_provider(
tmp_path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
with auth_mod._auth_store_lock():
store = auth_mod._load_auth_store()
store["active_provider"] = "nous"
auth_mod._store_provider_state(
store,
"spotify",
{
"client_id": "spotify-client",
"redirect_uri": "http://127.0.0.1:43827/spotify/callback",
"api_base_url": auth_mod.DEFAULT_SPOTIFY_API_BASE_URL,
"accounts_base_url": auth_mod.DEFAULT_SPOTIFY_ACCOUNTS_BASE_URL,
"scope": auth_mod.DEFAULT_SPOTIFY_SCOPE,
"access_token": "expired-token",
"refresh_token": "refresh-token",
"token_type": "Bearer",
"expires_at": "2000-01-01T00:00:00+00:00",
},
set_active=False,
)
auth_mod._save_auth_store(store)
monkeypatch.setattr(
auth_mod,
"_refresh_spotify_oauth_state",
lambda state, timeout_seconds=20.0: {
**state,
"access_token": "fresh-token",
"expires_at": "2099-01-01T00:00:00+00:00",
},
)
monkeypatch.setattr(
auth_spotify,
"_refresh_spotify_oauth_state",
lambda state, timeout_seconds=20.0: {
**state,
"access_token": "fresh-token",
"expires_at": "2099-01-01T00:00:00+00:00",
},
)
creds = auth_mod.resolve_spotify_runtime_credentials()
assert creds["access_token"] == "fresh-token"
persisted = auth_mod.get_provider_auth_state("spotify")
assert persisted is not None
assert persisted["access_token"] == "fresh-token"
assert auth_mod.get_active_provider() == "nous"
def test_auth_spotify_status_command_reports_logged_in(capsys, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
auth_mod,
"get_auth_status",
lambda provider=None: {
"logged_in": True,
"auth_type": "oauth_pkce",
"client_id": "spotify-client",
"redirect_uri": "http://127.0.0.1:43827/spotify/callback",
"scope": "user-library-read",
},
)
from hermes_cli.auth_commands import auth_status_command
auth_status_command(SimpleNamespace(provider="spotify"))
output = capsys.readouterr().out
assert "spotify: logged in" in output
assert "client_id: spotify-client" in output
# ---------------------------------------------------------------------------
# Quarantine: terminal refresh failure clears dead tokens (#28139)
# ---------------------------------------------------------------------------
_STALE_SPOTIFY_STATE = {
"client_id": "test-client",
"redirect_uri": "http://127.0.0.1:43827/spotify/callback",
"api_base_url": auth_mod.DEFAULT_SPOTIFY_API_BASE_URL,
"accounts_base_url": auth_mod.DEFAULT_SPOTIFY_ACCOUNTS_BASE_URL,
"scope": auth_mod.DEFAULT_SPOTIFY_SCOPE,
"granted_scope": auth_mod.DEFAULT_SPOTIFY_SCOPE,
"token_type": "Bearer",
"access_token": "dead-access-token",
"refresh_token": "dead-refresh-token",
"expires_at": "2000-01-01T00:00:00+00:00",
"expires_in": 3600,
"obtained_at": "2000-01-01T00:00:00+00:00",
"auth_type": "oauth_pkce",
}
def _seed_spotify_state(tmp_path, state: dict) -> None:
with auth_mod._auth_store_lock():
store = auth_mod._load_auth_store()
store["active_provider"] = "nous"
auth_mod._store_provider_state(store, "spotify", state, set_active=False)
auth_mod._save_auth_store(store)
def test_resolve_credentials_quarantines_dead_tokens_on_terminal_refresh_failure(
tmp_path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Terminal refresh failure (relogin_required=True + refresh_token present)
must clear access_token/refresh_token/expires_* from auth.json and write a
last_auth_error marker so subsequent calls fail fast without a network retry.
Mirrors Nous / xAI-OAuth / Codex-OAuth / MiniMax quarantine pattern.
"""
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
_seed_spotify_state(tmp_path, dict(_STALE_SPOTIFY_STATE))
def _terminal_refresh(_state, **_kw):
raise AuthError(
"Spotify token refresh failed. Run `hermes auth spotify` again.",
provider="spotify",
code="spotify_refresh_failed",
relogin_required=True,
)
monkeypatch.setattr(auth_mod, "_refresh_spotify_oauth_state", _terminal_refresh)
monkeypatch.setattr(auth_spotify, "_refresh_spotify_oauth_state", _terminal_refresh)
with pytest.raises(AuthError) as exc_info:
resolve_spotify_runtime_credentials(force_refresh=True)
assert exc_info.value.code == "spotify_refresh_failed"
assert exc_info.value.relogin_required is True
persisted = auth_mod.get_provider_auth_state("spotify")
assert persisted is not None
# Dead OAuth fields must be cleared.
assert "access_token" not in persisted
assert "refresh_token" not in persisted
assert "expires_at" not in persisted
assert "expires_in" not in persisted
assert "obtained_at" not in persisted
# Non-credential metadata must be preserved.
assert persisted["client_id"] == "test-client"
assert persisted["api_base_url"] == auth_mod.DEFAULT_SPOTIFY_API_BASE_URL
assert persisted["accounts_base_url"] == auth_mod.DEFAULT_SPOTIFY_ACCOUNTS_BASE_URL
# Structured diagnostic blob must be written.
err = persisted.get("last_auth_error")
assert isinstance(err, dict)
assert err["provider"] == "spotify"
assert err["code"] == "spotify_refresh_failed"
assert err["reason"] == "runtime_refresh_failure"
assert err["relogin_required"] is True
assert "at" in err
# Active provider must be unchanged.
assert auth_mod.get_active_provider() == "nous"