@@ -503,3 +503,74 @@ def test_non_json_success_body_raises_token_exchange_error() -> None:
503503
504504 with pytest .raises (TokenExchangeError ):
505505 mgr .bearer_value ()
506+
507+
508+ def test_missing_access_token_raises_token_exchange_error () -> None :
509+ """A 200 with valid JSON but no ``access_token`` (e.g. a misrouted endpoint
510+ returning some other JSON document) must surface as a TokenExchangeError,
511+ not a bare KeyError."""
512+ pool = _FakePool ([_FakeResponse (200 , {"token_type" : "Bearer" })])
513+ mgr = _TokenManager ("hd_secret_token" , _config (), pool = pool )
514+
515+ with pytest .raises (TokenExchangeError ):
516+ mgr .bearer_value ()
517+
518+
519+ # --------------------------------------------------------------------------
520+ # Refresh that fails by *raising* (not just a non-200) still re-mints
521+ # --------------------------------------------------------------------------
522+
523+
524+ def test_refresh_raising_falls_back_to_api_token_mint () -> None :
525+ """The refresh step is best-effort: if it fails in *any* way -- not just a
526+ non-200, but a malformed/non-JSON body or a transport error -- the manager
527+ must drop the refresh token and re-mint from the held API token rather than
528+ letting the exception escape ``bearer_value()``."""
529+ short_lived = _mint_response (
530+ access_token = "eyJ.short.jwt" ,
531+ refresh_token = "rt_doomed" ,
532+ expires_in = _LEEWAY - 5 ,
533+ )
534+ # Refresh returns 200 but a non-JSON body -> would raise inside _mint.
535+ refresh_garbage = _FakeResponse (200 , b"<html>oops</html>" )
536+ remint = _mint_response (access_token = "eyJ.reminted.jwt" , expires_in = 300 )
537+ pool = _FakePool ([short_lived , refresh_garbage , remint ])
538+ mgr = _TokenManager ("hd_secret_token" , _config (), pool = pool )
539+
540+ assert mgr .bearer_value () == "eyJ.short.jwt"
541+ # Second call: refresh raises internally -> fall back to api_token mint.
542+ assert mgr .bearer_value () == "eyJ.reminted.jwt"
543+
544+ assert len (pool .calls ) == 3
545+ assert _form (pool .calls [1 ]["body" ])["grant_type" ] == ["refresh_token" ]
546+ assert _form (pool .calls [2 ]["body" ])["grant_type" ] == ["api_token" ]
547+
548+
549+ # --------------------------------------------------------------------------
550+ # auth_settings() reads the token exactly once (no double bearer_value())
551+ # --------------------------------------------------------------------------
552+
553+
554+ def test_auth_settings_reads_token_once (monkeypatch : pytest .MonkeyPatch ) -> None :
555+ """``auth_settings()`` must resolve the bearer token a single time, not
556+ once for the null-check and again for the value -- otherwise it acquires the
557+ manager lock twice per request and a concurrent ``api_key`` reset between the
558+ two reads could yield ``'Bearer ' + None``."""
559+ pool = _FakePool ([_mint_response (access_token = "eyJ.once.jwt" )])
560+ cfg = _config ()
561+ mgr = _TokenManager ("hd_secret_token" , cfg , pool = pool )
562+ cfg ._token_manager = mgr
563+
564+ count = {"n" : 0 }
565+ real = mgr .bearer_value
566+
567+ def counting () -> str :
568+ count ["n" ] += 1
569+ return real ()
570+
571+ monkeypatch .setattr (mgr , "bearer_value" , counting )
572+
573+ auth = cfg .auth_settings ()
574+
575+ assert _bearer_from (auth ) == "Bearer eyJ.once.jwt"
576+ assert count ["n" ] == 1
0 commit comments