@@ -303,10 +303,12 @@ def test_register_duplicate_raises_key_error(self):
303303 class _UniqueStub (_StubProvider ):
304304 key = "__test_duplicate__"
305305
306- _register (_UniqueStub ())
307- with pytest .raises (KeyError , match = "already registered" ):
306+ try :
308307 _register (_UniqueStub ())
309- AUTH_REGISTRY .pop ("__test_duplicate__" , None )
308+ with pytest .raises (KeyError , match = "already registered" ):
309+ _register (_UniqueStub ())
310+ finally :
311+ AUTH_REGISTRY .pop ("__test_duplicate__" , None )
310312
311313 def test_register_empty_key_raises_value_error (self ):
312314 class _EmptyKey (_StubProvider ):
@@ -406,6 +408,76 @@ def test_supported_schemes(self):
406408 assert "azure-cli" in schemes
407409 assert "azure-ad" in schemes
408410
411+ def test_resolve_token_azure_cli_success (self ):
412+ """azure-cli acquires token via az CLI."""
413+ from unittest .mock import patch , MagicMock
414+ entry = AuthConfigEntry (
415+ hosts = ("dev.azure.com" ,), provider = "azure-devops" , auth = "azure-cli" ,
416+ )
417+ result = MagicMock ()
418+ result .returncode = 0
419+ result .stdout = '{"accessToken": "cli-acquired-token"}'
420+ with patch ("specify_cli.authentication.azure_devops.subprocess.run" , return_value = result ):
421+ assert AzureDevOpsAuth ().resolve_token (entry ) == "cli-acquired-token"
422+
423+ def test_resolve_token_azure_cli_failure_returns_none (self ):
424+ """azure-cli returns None when az CLI fails."""
425+ from unittest .mock import patch , MagicMock
426+ entry = AuthConfigEntry (
427+ hosts = ("dev.azure.com" ,), provider = "azure-devops" , auth = "azure-cli" ,
428+ )
429+ result = MagicMock ()
430+ result .returncode = 1
431+ result .stdout = ""
432+ with patch ("specify_cli.authentication.azure_devops.subprocess.run" , return_value = result ):
433+ assert AzureDevOpsAuth ().resolve_token (entry ) is None
434+
435+ def test_resolve_token_azure_cli_not_installed_returns_none (self ):
436+ """azure-cli returns None when az is not installed."""
437+ from unittest .mock import patch
438+ entry = AuthConfigEntry (
439+ hosts = ("dev.azure.com" ,), provider = "azure-devops" , auth = "azure-cli" ,
440+ )
441+ with patch ("specify_cli.authentication.azure_devops.subprocess.run" , side_effect = OSError ("not found" )):
442+ assert AzureDevOpsAuth ().resolve_token (entry ) is None
443+
444+ def test_resolve_token_azure_ad_success (self , monkeypatch ):
445+ """azure-ad acquires token via OAuth2 client credentials."""
446+ from unittest .mock import patch , MagicMock
447+ monkeypatch .setenv ("MY_SECRET" , "secret-value" )
448+ entry = AuthConfigEntry (
449+ hosts = ("dev.azure.com" ,), provider = "azure-devops" , auth = "azure-ad" ,
450+ tenant_id = "tid" , client_id = "cid" , client_secret_env = "MY_SECRET" ,
451+ )
452+ mock_resp = MagicMock ()
453+ mock_resp .read .return_value = b'{"access_token": "ad-acquired-token"}'
454+ mock_resp .__enter__ = lambda s : s
455+ mock_resp .__exit__ = MagicMock (return_value = False )
456+ with patch ("urllib.request.urlopen" , return_value = mock_resp ):
457+ assert AzureDevOpsAuth ().resolve_token (entry ) == "ad-acquired-token"
458+
459+ def test_resolve_token_azure_ad_missing_secret_returns_none (self , monkeypatch ):
460+ """azure-ad returns None when client secret env var is missing."""
461+ monkeypatch .delenv ("MY_SECRET" , raising = False )
462+ entry = AuthConfigEntry (
463+ hosts = ("dev.azure.com" ,), provider = "azure-devops" , auth = "azure-ad" ,
464+ tenant_id = "tid" , client_id = "cid" , client_secret_env = "MY_SECRET" ,
465+ )
466+ assert AzureDevOpsAuth ().resolve_token (entry ) is None
467+
468+ def test_resolve_token_azure_ad_network_error_returns_none (self , monkeypatch ):
469+ """azure-ad returns None on network errors."""
470+ import urllib .error
471+ from unittest .mock import patch
472+ monkeypatch .setenv ("MY_SECRET" , "secret-value" )
473+ entry = AuthConfigEntry (
474+ hosts = ("dev.azure.com" ,), provider = "azure-devops" , auth = "azure-ad" ,
475+ tenant_id = "tid" , client_id = "cid" , client_secret_env = "MY_SECRET" ,
476+ )
477+ with patch ("urllib.request.urlopen" ,
478+ side_effect = urllib .error .URLError ("connection refused" )):
479+ assert AzureDevOpsAuth ().resolve_token (entry ) is None
480+
409481
410482# ---------------------------------------------------------------------------
411483# open_url / build_request — positive tests
@@ -414,7 +486,7 @@ def test_supported_schemes(self):
414486
415487class TestAuthenticatedHttp :
416488 def _set_config (self , monkeypatch , entries ):
417- import specify_cli .authentication . http as _mod
489+ from specify_cli .authentication import http as _mod
418490 monkeypatch .setattr (_mod , "_config_override" , entries )
419491
420492 def test_build_request_attaches_auth_for_matching_host (self , monkeypatch ):
@@ -515,7 +587,7 @@ def fake_side_effect(req, timeout=None):
515587
516588class TestAuthenticatedHttpNegative :
517589 def _set_config (self , monkeypatch , entries ):
518- import specify_cli .authentication . http as _mod
590+ from specify_cli .authentication import http as _mod
519591 monkeypatch .setattr (_mod , "_config_override" , entries )
520592
521593 def test_500_raises_immediately (self , monkeypatch ):
@@ -601,7 +673,7 @@ def test_redirect_outside_hosts_strips_auth(self):
601673
602674class TestFetchLatestReleaseTagDelegation :
603675 def _set_config (self , monkeypatch , entries ):
604- import specify_cli .authentication . http as _mod
676+ from specify_cli .authentication import http as _mod
605677 monkeypatch .setattr (_mod , "_config_override" , entries )
606678
607679 def _capture_request (self ):
0 commit comments