ec2b666284
Continuous Integration / Pre-commit Linter (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.10) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.11) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.12) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.13) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.10) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.11) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.12) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.13) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.14) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Waiting to run
Copybara PR Handler / close-imported-pr (push) Waiting to run
304 lines
12 KiB
Python
304 lines
12 KiB
Python
# Copyright 2026 Google LLC
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""Unit tests for _mtls_utils."""
|
|
|
|
import os
|
|
from unittest.mock import MagicMock
|
|
from unittest.mock import patch
|
|
|
|
from google.adk.utils import _mtls_utils
|
|
from google.auth import exceptions as ga_exceptions
|
|
from google.auth.transport import mtls
|
|
import pytest
|
|
|
|
_DEFAULT_TEMPLATE = "service.{location}.rep.googleapis.com"
|
|
_MTLS_TEMPLATE = "service.{location}.rep.mtls.googleapis.com"
|
|
_LOCATION = "us-central1"
|
|
|
|
|
|
class TestMtlsUtils:
|
|
"""Tests for _mtls_utils functions."""
|
|
|
|
@patch.object(mtls, "should_use_client_cert", autospec=True)
|
|
def test_use_client_cert_effective_with_mtls_cert_true(
|
|
self, mock_should_use_client_cert
|
|
):
|
|
mock_should_use_client_cert.return_value = True
|
|
assert _mtls_utils.use_client_cert_effective() is True
|
|
mock_should_use_client_cert.assert_called_once()
|
|
|
|
@patch.object(mtls, "should_use_client_cert", autospec=True)
|
|
def test_use_client_cert_effective_with_mtls_cert_false(
|
|
self, mock_should_use_client_cert
|
|
):
|
|
mock_should_use_client_cert.return_value = False
|
|
assert _mtls_utils.use_client_cert_effective() is False
|
|
mock_should_use_client_cert.assert_called_once()
|
|
|
|
@patch.object(mtls, "should_use_client_cert", autospec=True)
|
|
@patch.dict("os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"})
|
|
def test_use_client_cert_effective_fallback_true(
|
|
self, mock_should_use_client_cert
|
|
):
|
|
mock_should_use_client_cert.side_effect = AttributeError
|
|
assert _mtls_utils.use_client_cert_effective() is True
|
|
|
|
@patch.object(mtls, "should_use_client_cert", autospec=True)
|
|
@patch.dict("os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "false"})
|
|
def test_use_client_cert_effective_fallback_false(
|
|
self, mock_should_use_client_cert
|
|
):
|
|
mock_should_use_client_cert.side_effect = AttributeError
|
|
assert _mtls_utils.use_client_cert_effective() is False
|
|
|
|
@patch.object(mtls, "should_use_client_cert", autospec=True)
|
|
@patch.dict("os.environ", {}, clear=True)
|
|
def test_use_client_cert_effective_fallback_default_false(
|
|
self, mock_should_use_client_cert
|
|
):
|
|
mock_should_use_client_cert.side_effect = AttributeError
|
|
assert _mtls_utils.use_client_cert_effective() is False
|
|
|
|
@patch.object(_mtls_utils, "use_client_cert_effective", autospec=True)
|
|
@patch.dict("os.environ", {"GOOGLE_API_USE_MTLS_ENDPOINT": "always"})
|
|
def test_get_api_endpoint_always(self, mock_use_client_cert):
|
|
endpoint = _mtls_utils.get_api_endpoint(
|
|
_LOCATION, _DEFAULT_TEMPLATE, _MTLS_TEMPLATE
|
|
)
|
|
assert endpoint == _MTLS_TEMPLATE.format(location=_LOCATION)
|
|
mock_use_client_cert.assert_not_called()
|
|
|
|
@patch.object(_mtls_utils, "use_client_cert_effective", autospec=True)
|
|
@patch.dict("os.environ", {"GOOGLE_API_USE_MTLS_ENDPOINT": "never"})
|
|
def test_get_api_endpoint_never(self, mock_use_client_cert):
|
|
endpoint = _mtls_utils.get_api_endpoint(
|
|
_LOCATION, _DEFAULT_TEMPLATE, _MTLS_TEMPLATE
|
|
)
|
|
assert endpoint == _DEFAULT_TEMPLATE.format(location=_LOCATION)
|
|
mock_use_client_cert.assert_not_called()
|
|
|
|
@patch.object(_mtls_utils, "use_client_cert_effective", autospec=True)
|
|
@patch.dict("os.environ", {"GOOGLE_API_USE_MTLS_ENDPOINT": "auto"})
|
|
def test_get_api_endpoint_auto_with_cert(self, mock_use_client_cert):
|
|
mock_use_client_cert.return_value = True
|
|
endpoint = _mtls_utils.get_api_endpoint(
|
|
_LOCATION, _DEFAULT_TEMPLATE, _MTLS_TEMPLATE
|
|
)
|
|
assert endpoint == _MTLS_TEMPLATE.format(location=_LOCATION)
|
|
mock_use_client_cert.assert_called_once()
|
|
|
|
@patch.object(_mtls_utils, "use_client_cert_effective", autospec=True)
|
|
@patch.dict("os.environ", {"GOOGLE_API_USE_MTLS_ENDPOINT": "auto"})
|
|
def test_get_api_endpoint_auto_without_cert(self, mock_use_client_cert):
|
|
mock_use_client_cert.return_value = False
|
|
endpoint = _mtls_utils.get_api_endpoint(
|
|
_LOCATION, _DEFAULT_TEMPLATE, _MTLS_TEMPLATE
|
|
)
|
|
assert endpoint == _DEFAULT_TEMPLATE.format(location=_LOCATION)
|
|
mock_use_client_cert.assert_called_once()
|
|
|
|
@patch.object(_mtls_utils, "use_client_cert_effective", autospec=True)
|
|
@patch.dict("os.environ", {"GOOGLE_API_USE_MTLS_ENDPOINT": "invalid_value"})
|
|
def test_get_api_endpoint_invalid_fallback_to_auto(
|
|
self, mock_use_client_cert
|
|
):
|
|
mock_use_client_cert.return_value = True
|
|
endpoint = _mtls_utils.get_api_endpoint(
|
|
_LOCATION, _DEFAULT_TEMPLATE, _MTLS_TEMPLATE
|
|
)
|
|
assert endpoint == _MTLS_TEMPLATE.format(location=_LOCATION)
|
|
mock_use_client_cert.assert_called_once()
|
|
|
|
@pytest.mark.parametrize(
|
|
"url, expected",
|
|
[
|
|
("https://oauth2.googleapis.com/token", True),
|
|
("https://openidconnect.googleapis.com/v1/userinfo", True),
|
|
("https://oauth2.mtls.googleapis.com/token", False),
|
|
("https://example.com/token", False),
|
|
("https://accounts.google.com/o/oauth2/v2/auth", False),
|
|
(None, False),
|
|
("", False),
|
|
],
|
|
)
|
|
def test_is_non_mtls_googleapis_endpoint(self, url, expected):
|
|
assert _mtls_utils.is_non_mtls_googleapis_endpoint(url) is expected
|
|
|
|
@patch.dict("os.environ", {}, clear=True)
|
|
@pytest.mark.parametrize(
|
|
"url, expected",
|
|
[
|
|
(
|
|
"https://oauth2.googleapis.com/token",
|
|
"https://oauth2.mtls.googleapis.com/token",
|
|
),
|
|
(
|
|
"https://openidconnect.googleapis.com/v1/userinfo",
|
|
"https://openidconnect.mtls.googleapis.com/v1/userinfo",
|
|
),
|
|
# Non-Google providers are never rewritten.
|
|
("https://example.com/token", "https://example.com/token"),
|
|
# Already-mTLS hosts are left alone.
|
|
(
|
|
"https://oauth2.mtls.googleapis.com/token",
|
|
"https://oauth2.mtls.googleapis.com/token",
|
|
),
|
|
],
|
|
)
|
|
def test_effective_googleapis_endpoint_rewrites(self, url, expected):
|
|
assert _mtls_utils.effective_googleapis_endpoint(url) == expected
|
|
|
|
@patch.dict(
|
|
"os.environ", {"GOOGLE_API_USE_MTLS_ENDPOINT": "never"}, clear=True
|
|
)
|
|
def test_effective_googleapis_endpoint_never_opts_out(self):
|
|
url = "https://oauth2.googleapis.com/token"
|
|
assert _mtls_utils.effective_googleapis_endpoint(url) == url
|
|
|
|
@patch.dict("os.environ", {}, clear=True)
|
|
def test_effective_googleapis_endpoint_preserves_query(self):
|
|
url = "https://iam.googleapis.com/v1/token?foo=bar"
|
|
assert (
|
|
_mtls_utils.effective_googleapis_endpoint(url)
|
|
== "https://iam.mtls.googleapis.com/v1/token?foo=bar"
|
|
)
|
|
|
|
@patch("google.auth.transport.mtls.has_default_client_cert_source")
|
|
@patch("google.auth.transport._mtls_helper.get_client_cert_and_key")
|
|
@patch("google.auth.transport.requests._MutualTlsAdapter")
|
|
def test_configure_session_for_mtls_mounts_adapter(
|
|
self, mock_adapter, mock_get_cert, mock_has_cert_source
|
|
):
|
|
mock_has_cert_source.return_value = False
|
|
mock_get_cert.return_value = (True, b"cert", b"key")
|
|
session = MagicMock()
|
|
|
|
result = _mtls_utils.configure_session_for_mtls(session)
|
|
|
|
assert result is True
|
|
mock_adapter.assert_called_once_with(b"cert", b"key")
|
|
session.mount.assert_called_once_with("https://", mock_adapter.return_value)
|
|
|
|
@patch("google.auth.transport.mtls.has_default_client_cert_source")
|
|
@patch("google.auth.transport._mtls_helper.get_client_cert_and_key")
|
|
def test_configure_session_for_mtls_no_cert(
|
|
self, mock_get_cert, mock_has_cert_source
|
|
):
|
|
mock_has_cert_source.return_value = False
|
|
mock_get_cert.return_value = (False, None, None)
|
|
session = MagicMock()
|
|
|
|
result = _mtls_utils.configure_session_for_mtls(session)
|
|
|
|
assert result is False
|
|
session.mount.assert_not_called()
|
|
|
|
@patch("google.auth.transport.mtls.has_default_client_cert_source")
|
|
@patch("google.auth.transport._mtls_helper.get_client_cert_and_key")
|
|
def test_configure_session_for_mtls_cert_error_falls_back(
|
|
self, mock_get_cert, mock_has_cert_source
|
|
):
|
|
mock_has_cert_source.return_value = False
|
|
mock_get_cert.side_effect = ga_exceptions.ClientCertError("boom")
|
|
session = MagicMock()
|
|
|
|
result = _mtls_utils.configure_session_for_mtls(session)
|
|
|
|
assert result is False
|
|
session.mount.assert_not_called()
|
|
|
|
|
|
class TestMtlsClientCerts:
|
|
"""Tests for MtlsClientCerts."""
|
|
|
|
@patch.object(mtls, "has_default_client_cert_source", autospec=True)
|
|
def test_get_certs_no_default_source(self, mock_has_cert):
|
|
mock_has_cert.return_value = False
|
|
certs = _mtls_utils.MtlsClientCerts()
|
|
cert_path, key_path, passphrase = certs.get_certs()
|
|
assert cert_path is None
|
|
assert key_path is None
|
|
assert passphrase is None
|
|
mock_has_cert.assert_called_once()
|
|
|
|
@patch.object(mtls, "has_default_client_cert_source", autospec=True)
|
|
@patch.object(mtls, "default_client_encrypted_cert_source", autospec=True)
|
|
def test_get_certs_with_default_source(
|
|
self, mock_encrypted_source, mock_has_cert
|
|
):
|
|
mock_has_cert.return_value = True
|
|
|
|
mock_cert_source = MagicMock()
|
|
mock_cert_source.return_value = (None, None, b"test_passphrase")
|
|
mock_encrypted_source.return_value = mock_cert_source
|
|
|
|
certs = _mtls_utils.MtlsClientCerts()
|
|
cert_path, key_path, passphrase = certs.get_certs()
|
|
|
|
assert cert_path is not None
|
|
assert key_path is not None
|
|
assert passphrase == b"test_passphrase"
|
|
|
|
assert os.path.exists(certs._tempdir.name)
|
|
assert cert_path.startswith(certs._tempdir.name)
|
|
assert key_path.startswith(certs._tempdir.name)
|
|
|
|
# Getting certs again should return cached values without calling mtls again
|
|
cert_path2, key_path2, passphrase2 = certs.get_certs()
|
|
assert cert_path2 == cert_path
|
|
assert key_path2 == key_path
|
|
assert passphrase2 == passphrase
|
|
mock_has_cert.assert_called_once()
|
|
mock_encrypted_source.assert_called_once()
|
|
|
|
@patch.object(mtls, "has_default_client_cert_source", autospec=True)
|
|
@patch.object(mtls, "default_client_encrypted_cert_source", autospec=True)
|
|
def test_get_certs_extraction_failure(
|
|
self, mock_encrypted_source, mock_has_cert
|
|
):
|
|
mock_has_cert.return_value = True
|
|
mock_encrypted_source.side_effect = Exception("extraction failed")
|
|
|
|
certs = _mtls_utils.MtlsClientCerts()
|
|
with pytest.raises(
|
|
RuntimeError, match="Failed to extract default client certificates"
|
|
):
|
|
certs.get_certs()
|
|
|
|
assert certs._tempdir is None
|
|
|
|
@patch.object(mtls, "has_default_client_cert_source", autospec=True)
|
|
@patch.object(mtls, "default_client_encrypted_cert_source", autospec=True)
|
|
def test_close_cleans_up_tempdir(self, mock_encrypted_source, mock_has_cert):
|
|
mock_has_cert.return_value = True
|
|
mock_cert_source = MagicMock()
|
|
mock_cert_source.return_value = (None, None, b"test_passphrase")
|
|
mock_encrypted_source.return_value = mock_cert_source
|
|
|
|
certs = _mtls_utils.MtlsClientCerts()
|
|
certs.get_certs()
|
|
|
|
tempdir_name = certs._tempdir.name
|
|
assert os.path.exists(tempdir_name)
|
|
|
|
certs.close()
|
|
|
|
assert not os.path.exists(tempdir_name)
|
|
assert certs._tempdir is None
|
|
assert certs.cert_path is None
|
|
assert certs.key_path is None
|
|
assert certs.passphrase is None
|
|
assert not certs._initialized
|