diff --git a/src/specify_cli/authentication/azure_devops.py b/src/specify_cli/authentication/azure_devops.py index 5d71a1957b..0ccdd5afb3 100644 --- a/src/specify_cli/authentication/azure_devops.py +++ b/src/specify_cli/authentication/azure_devops.py @@ -76,7 +76,17 @@ def _acquire_via_az_cli() -> str | None: payload = _json.loads(result.stdout) token = payload.get("accessToken", "").strip() return token or None - except (OSError, subprocess.TimeoutExpired, _json.JSONDecodeError, KeyError): + except ( + OSError, + subprocess.TimeoutExpired, + _json.JSONDecodeError, + UnicodeDecodeError, + KeyError, + ): + # UnicodeDecodeError: text=True decodes az stdout with the locale + # encoding, which raises (not a JSONDecodeError) if the output isn't + # decodable — this helper's contract is to return None on any + # failure, never to propagate. return None @staticmethod diff --git a/tests/test_authentication.py b/tests/test_authentication.py index e8bc2b6781..84f6d16fbd 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -502,6 +502,19 @@ def test_resolve_token_azure_cli_not_installed_returns_none(self): with patch("specify_cli.authentication.azure_devops.subprocess.run", side_effect=OSError("not found")): assert AzureDevOpsAuth().resolve_token(entry) is None + def test_resolve_token_azure_cli_undecodable_output_returns_none(self): + """Undecodable az output returns None, not a crash. With text=True, + subprocess.run decodes stdout with the locale encoding and raises + UnicodeDecodeError (not a JSONDecodeError) when it can't — the helper's + contract is to return None on any failure.""" + from unittest.mock import patch + entry = AuthConfigEntry( + hosts=("dev.azure.com",), provider="azure-devops", auth="azure-cli", + ) + boom = UnicodeDecodeError("utf-8", b"\xff\xfe", 0, 1, "invalid start byte") + with patch("specify_cli.authentication.azure_devops.subprocess.run", side_effect=boom): + assert AzureDevOpsAuth().resolve_token(entry) is None + def test_resolve_token_azure_ad_success(self, monkeypatch): """azure-ad acquires token via OAuth2 client credentials.""" from unittest.mock import patch, MagicMock