From 93c52700ad22db90213ba44f33903581b6afbed4 Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Thu, 16 Jul 2026 14:56:01 +0000 Subject: [PATCH 1/5] feat(api-core): move mtls and client configuration logic to gapic_v1 public helpers Introduces client_cert and config_helpers modules containing use_client_cert_effective, get_client_cert_source, and read_environment_variables helpers. Exposes these modules as public in gapic_v1. --- .../google/api_core/gapic_v1/__init__.py | 6 +- .../google/api_core/gapic_v1/client_cert.py | 79 +++++++++++++++++++ .../api_core/gapic_v1/config_helpers.py | 50 ++++++++++++ packages/google-api-core/noxfile.py | 2 +- packages/google-api-core/tests/conftest.py | 31 ++++++++ .../tests/unit/gapic/test_client_cert.py | 79 +++++++++++++++++++ .../tests/unit/gapic/test_config_helpers.py | 47 +++++++++++ .../google-api-core/tests/unit/test_bidi.py | 5 +- 8 files changed, 294 insertions(+), 5 deletions(-) create mode 100644 packages/google-api-core/google/api_core/gapic_v1/client_cert.py create mode 100644 packages/google-api-core/google/api_core/gapic_v1/config_helpers.py create mode 100644 packages/google-api-core/tests/conftest.py create mode 100644 packages/google-api-core/tests/unit/gapic/test_client_cert.py create mode 100644 packages/google-api-core/tests/unit/gapic/test_config_helpers.py diff --git a/packages/google-api-core/google/api_core/gapic_v1/__init__.py b/packages/google-api-core/google/api_core/gapic_v1/__init__.py index 48a27ec21d24..0230f43ace0d 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/__init__.py +++ b/packages/google-api-core/google/api_core/gapic_v1/__init__.py @@ -25,9 +25,11 @@ # Older Python versions safely ignore this variable. __lazy_modules__: Set[str] = { "google.api_core.gapic_v1.client_info", + "google.api_core.gapic_v1.client_cert", + "google.api_core.gapic_v1.config_helpers", "google.api_core.gapic_v1.routing_header", } -__all__ = ["client_info", "routing_header"] +__all__ = ["client_info", "client_cert", "config_helpers", "routing_header"] if _has_grpc: __lazy_modules__.update( @@ -41,6 +43,8 @@ from google.api_core.gapic_v1 import ( # noqa: E402 client_info, + client_cert, + config_helpers, routing_header, ) diff --git a/packages/google-api-core/google/api_core/gapic_v1/client_cert.py b/packages/google-api-core/google/api_core/gapic_v1/client_cert.py new file mode 100644 index 000000000000..2f4a038778db --- /dev/null +++ b/packages/google-api-core/google/api_core/gapic_v1/client_cert.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +# 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. +# + +"""Helpers for client certificate handling and mTLS authentication.""" + +import os +from typing import Callable, Optional, Tuple + +from google.auth.transport import mtls # type: ignore + + +def use_client_cert_effective() -> bool: + """Returns whether client certificate should be used for mTLS if the + google-auth version supports should_use_client_cert automatic mTLS + enablement. + + Alternatively, read from the GOOGLE_API_USE_CLIENT_CERTIFICATE env var. + + Returns: + bool: whether client certificate should be used for mTLS + Raises: + ValueError: (If using a version of google-auth without + should_use_client_cert and GOOGLE_API_USE_CLIENT_CERTIFICATE is + set to an unexpected value.) + """ + # check if google-auth version supports should_use_client_cert for + # automatic mTLS enablement + if hasattr(mtls, "should_use_client_cert"): # pragma: NO COVER + return mtls.should_use_client_cert() + else: # pragma: NO COVER + # if unsupported, fallback to reading from env var + use_client_cert_str = os.getenv( + "GOOGLE_API_USE_CLIENT_CERTIFICATE", "false" + ).lower() + if use_client_cert_str not in ("true", "false"): + raise ValueError( + "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` " + "must be either `true` or `false`" + ) + return use_client_cert_str == "true" + + +def get_client_cert_source( + provided_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], + use_cert_flag: bool, +) -> Optional[Callable[[], Tuple[bytes, bytes]]]: + """Return the client cert source to be used by the client. + + Args: + provided_cert_source (Callable[[], Tuple[bytes, bytes]]): The client certificate source provided. + use_cert_flag (bool): A flag indicating whether to use the + client certificate. + + Returns: + Callable[[], Tuple[bytes, bytes]] or None: The client cert source to be used by the client. + """ + client_cert_source = None + if use_cert_flag: + if provided_cert_source: + client_cert_source = provided_cert_source + elif ( + hasattr(mtls, "has_default_client_cert_source") + and mtls.has_default_client_cert_source() + ): + client_cert_source = mtls.default_client_cert_source() + return client_cert_source diff --git a/packages/google-api-core/google/api_core/gapic_v1/config_helpers.py b/packages/google-api-core/google/api_core/gapic_v1/config_helpers.py new file mode 100644 index 000000000000..c5cc708e006e --- /dev/null +++ b/packages/google-api-core/google/api_core/gapic_v1/config_helpers.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# 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. +# + +"""Helpers for parsing environment variables.""" + +import os +from typing import Optional, Tuple + +from google.auth.exceptions import MutualTLSChannelError # type: ignore + +from google.api_core.gapic_v1.client_cert import use_client_cert_effective + + +def read_environment_variables() -> Tuple[bool, str, Optional[str]]: + """Returns the environment variables used by the client. + + Returns: + Tuple[bool, str, Optional[str]]: returns the + GOOGLE_API_USE_CLIENT_CERTIFICATE, GOOGLE_API_USE_MTLS_ENDPOINT, + and GOOGLE_CLOUD_UNIVERSE_DOMAIN environment variables. + + Raises: + ValueError: If GOOGLE_API_USE_CLIENT_CERTIFICATE is not + any of ["true", "false"]. + google.auth.exceptions.MutualTLSChannelError: If + GOOGLE_API_USE_MTLS_ENDPOINT is not any of + ["auto", "never", "always"]. + """ + use_client_cert = use_client_cert_effective() + use_mtls_endpoint = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto").lower() + universe_domain_env = os.getenv("GOOGLE_CLOUD_UNIVERSE_DOMAIN") + if use_mtls_endpoint not in ("auto", "never", "always"): + raise MutualTLSChannelError( + "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` " + "must be `never`, `auto` or `always`" + ) + return use_client_cert, use_mtls_endpoint, universe_domain_env diff --git a/packages/google-api-core/noxfile.py b/packages/google-api-core/noxfile.py index 0bad668a80dd..3cbebbaa84d2 100644 --- a/packages/google-api-core/noxfile.py +++ b/packages/google-api-core/noxfile.py @@ -350,7 +350,7 @@ def prerelease_deps(session): @nox.session(python=DEFAULT_PYTHON_VERSION) def core_deps_from_source(session): """Run the test suite installing dependencies from source.""" - default(session, prerelease=True) + default(session, install_deps_from_source=True) @nox.session(python=DEFAULT_PYTHON_VERSION) diff --git a/packages/google-api-core/tests/conftest.py b/packages/google-api-core/tests/conftest.py new file mode 100644 index 000000000000..1872207be107 --- /dev/null +++ b/packages/google-api-core/tests/conftest.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# 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. + +import os +from unittest import mock +import pytest + + +@pytest.fixture(scope="session", autouse=True) +def mock_mtls_env(): + """Autouse session-scoped fixture to isolate unit tests from workstation mTLS environments.""" + with mock.patch.dict( + os.environ, + { + "GOOGLE_API_USE_CLIENT_CERTIFICATE": "false", + "CLOUDSDK_CONTEXT_AWARE_USE_CLIENT_CERTIFICATE": "false", + }, + ): + yield diff --git a/packages/google-api-core/tests/unit/gapic/test_client_cert.py b/packages/google-api-core/tests/unit/gapic/test_client_cert.py new file mode 100644 index 000000000000..aa7ba9eb8f02 --- /dev/null +++ b/packages/google-api-core/tests/unit/gapic/test_client_cert.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +# 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. + +import os +from unittest import mock + +import pytest + + +from google.api_core.gapic_v1.client_cert import ( + get_client_cert_source, + use_client_cert_effective, +) + + +@mock.patch("google.auth.transport.mtls.should_use_client_cert", create=True) +def test_use_client_cert_effective_with_google_auth(mock_method): + # Test when google-auth supports the method + mock_method.return_value = True + assert use_client_cert_effective() is True + + mock_method.return_value = False + assert use_client_cert_effective() is False + + +@mock.patch.dict(os.environ, {}, clear=True) +def test_use_client_cert_effective_fallback(): + # We must patch hasattr to simulate google-auth lacking the method + with mock.patch("google.api_core.gapic_v1.client_cert.hasattr", return_value=False): + # Default is false + assert use_client_cert_effective() is False + + env_true = {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"} + with mock.patch.dict(os.environ, env_true): + assert use_client_cert_effective() is True + + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "false"} + ): + assert use_client_cert_effective() is False + + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "invalid"} + ): + match_str = "must be either `true` or `false`" + with pytest.raises(ValueError, match=match_str): + use_client_cert_effective() + + +@mock.patch( + "google.auth.transport.mtls.has_default_client_cert_source", create=True +) # noqa: E501 +@mock.patch( + "google.auth.transport.mtls.default_client_cert_source", create=True +) # noqa: E501 +def test_get_client_cert_source(mock_default, mock_has_default): + mock_default.return_value = b"default_cert" + mock_has_default.return_value = True + + # When use_cert_flag is False, return None + assert get_client_cert_source(b"provided", False) is None + + # When provided_cert_source is given, return provided + assert get_client_cert_source(b"provided", True) == b"provided" # noqa: E501 + + # When no provided cert but default is available + assert get_client_cert_source(None, True) == b"default_cert" diff --git a/packages/google-api-core/tests/unit/gapic/test_config_helpers.py b/packages/google-api-core/tests/unit/gapic/test_config_helpers.py new file mode 100644 index 000000000000..e0d634a3b119 --- /dev/null +++ b/packages/google-api-core/tests/unit/gapic/test_config_helpers.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# 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. + +import os +from unittest import mock + +import pytest + +from google.auth.exceptions import MutualTLSChannelError + +from google.api_core.gapic_v1.config_helpers import read_environment_variables + + +@mock.patch( + "google.api_core.gapic_v1.config_helpers.use_client_cert_effective" +) # noqa: E501 +@mock.patch.dict(os.environ, clear=True) +def test_read_environment_variables(mock_effective): + mock_effective.return_value = True + os.environ["GOOGLE_API_USE_MTLS_ENDPOINT"] = "always" + os.environ["GOOGLE_CLOUD_UNIVERSE_DOMAIN"] = "custom.com" + + cert, mtls, domain = read_environment_variables() + assert cert is True + assert mtls == "always" + assert domain == "custom.com" + + +@mock.patch.dict(os.environ, clear=True) +def test_read_environment_variables_invalid_mtls(): + os.environ["GOOGLE_API_USE_MTLS_ENDPOINT"] = "invalid" + with pytest.raises( + MutualTLSChannelError, match="must be `never`, `auto` or `always`" + ): + read_environment_variables() diff --git a/packages/google-api-core/tests/unit/test_bidi.py b/packages/google-api-core/tests/unit/test_bidi.py index 4a8eb74fac94..0f4810cb0a12 100644 --- a/packages/google-api-core/tests/unit/test_bidi.py +++ b/packages/google-api-core/tests/unit/test_bidi.py @@ -31,8 +31,7 @@ except ImportError: # pragma: NO COVER pytest.skip("No GRPC", allow_module_level=True) -from google.api_core import bidi -from google.api_core import exceptions +from google.api_core import bidi, exceptions class Test_RequestQueueGenerator(object): @@ -195,7 +194,7 @@ def test_delays_entry_attempts_above_threshold(self): # (NOTE: not using assert all(...), b/c the coverage check would complain) for i, entry in enumerate(entries): if i != 3: - assert entry["reported_wait"] == 0.0 + assert entry["reported_wait"] < 0.01 # The delayed entry is expected to have been delayed for a significant # chunk of the full second, and the actual and reported delay times From d90149ae8eed63493c085dfef1ba62708062bbf4 Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Thu, 16 Jul 2026 21:38:23 +0000 Subject: [PATCH 2/5] test(api-core): align mtls and config helper tests with generator templates --- .../tests/unit/gapic/test_client_cert.py | 134 +++++++++++------- .../tests/unit/gapic/test_config_helpers.py | 52 ++++--- 2 files changed, 110 insertions(+), 76 deletions(-) diff --git a/packages/google-api-core/tests/unit/gapic/test_client_cert.py b/packages/google-api-core/tests/unit/gapic/test_client_cert.py index aa7ba9eb8f02..1abc8eb22a14 100644 --- a/packages/google-api-core/tests/unit/gapic/test_client_cert.py +++ b/packages/google-api-core/tests/unit/gapic/test_client_cert.py @@ -18,62 +18,90 @@ import pytest - +from google.auth.transport import mtls from google.api_core.gapic_v1.client_cert import ( get_client_cert_source, use_client_cert_effective, ) -@mock.patch("google.auth.transport.mtls.should_use_client_cert", create=True) -def test_use_client_cert_effective_with_google_auth(mock_method): - # Test when google-auth supports the method - mock_method.return_value = True - assert use_client_cert_effective() is True - - mock_method.return_value = False - assert use_client_cert_effective() is False - - -@mock.patch.dict(os.environ, {}, clear=True) -def test_use_client_cert_effective_fallback(): - # We must patch hasattr to simulate google-auth lacking the method - with mock.patch("google.api_core.gapic_v1.client_cert.hasattr", return_value=False): - # Default is false - assert use_client_cert_effective() is False - - env_true = {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"} - with mock.patch.dict(os.environ, env_true): - assert use_client_cert_effective() is True - - with mock.patch.dict( - os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "false"} - ): - assert use_client_cert_effective() is False - - with mock.patch.dict( - os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "invalid"} - ): - match_str = "must be either `true` or `false`" - with pytest.raises(ValueError, match=match_str): - use_client_cert_effective() - - -@mock.patch( - "google.auth.transport.mtls.has_default_client_cert_source", create=True -) # noqa: E501 -@mock.patch( - "google.auth.transport.mtls.default_client_cert_source", create=True -) # noqa: E501 -def test_get_client_cert_source(mock_default, mock_has_default): - mock_default.return_value = b"default_cert" - mock_has_default.return_value = True - - # When use_cert_flag is False, return None - assert get_client_cert_source(b"provided", False) is None - - # When provided_cert_source is given, return provided - assert get_client_cert_source(b"provided", True) == b"provided" # noqa: E501 - - # When no provided cert but default is available - assert get_client_cert_source(None, True) == b"default_cert" +@pytest.mark.parametrize( + "has_method, method_val, env_val, expected", + [ + # should_use_client_cert is available cases + (True, True, None, "true"), + (True, False, None, "false"), + (True, False, "unsupported", "false"), + # should_use_client_cert is unavailable cases + (False, None, "true", "true"), + (False, None, "false", "false"), + (False, None, "True", "true"), + (False, None, "False", "false"), + (False, None, "TRUE", "true"), + (False, None, "FALSE", "false"), + (False, None, None, "false"), + (False, None, "unsupported", "value_error"), + ], + ids=[ + "google_auth_true", + "google_auth_false", + "google_auth_false_env_unsupported", + "fallback_env_true_lowercase", + "fallback_env_false_lowercase", + "fallback_env_true_titlecase", + "fallback_env_false_titlecase", + "fallback_env_true_uppercase", + "fallback_env_false_uppercase", + "fallback_env_unset", + "fallback_env_unsupported", + ] +) +def test_use_client_cert_effective(has_method, method_val, env_val, expected): + # Mock hasattr to control whether should_use_client_cert exists + original_hasattr = hasattr + def custom_hasattr(obj, name): + if obj is mtls and name == "should_use_client_cert": + return has_method + return original_hasattr(obj, name) + + with mock.patch("google.api_core.gapic_v1.client_cert.hasattr", custom_hasattr): + with mock.patch("google.auth.transport.mtls.should_use_client_cert", create=True, return_value=method_val): + env = {} + if env_val is not None: + env["GOOGLE_API_USE_CLIENT_CERTIFICATE"] = env_val + with mock.patch.dict(os.environ, env, clear=True): + if expected == "value_error": + with pytest.raises(ValueError, match="must be either `true` or `false`"): + use_client_cert_effective() + else: + assert use_client_cert_effective() is (expected == "true") + + +@pytest.mark.parametrize( + "provided, use_cert, has_default_avail, default_val, expected", + [ + (None, False, True, b"default", None), + (b"provided", False, True, b"default", None), + (b"provided", True, True, b"default", b"provided"), + (None, True, True, b"default", b"default"), + (None, True, False, b"default", None), + ], + ids=[ + "use_cert_false_no_provided", + "use_cert_false_with_provided", + "use_cert_true_with_provided", + "use_cert_true_no_provided_default_avail", + "use_cert_true_no_provided_default_unavail", + ] +) +def test_get_client_cert_source(provided, use_cert, has_default_avail, default_val, expected): + original_hasattr = hasattr + def custom_hasattr(obj, name): + if obj is mtls and name == "has_default_client_cert_source": + return has_default_avail + return original_hasattr(obj, name) + + with mock.patch("google.api_core.gapic_v1.client_cert.hasattr", custom_hasattr): + with mock.patch("google.auth.transport.mtls.has_default_client_cert_source", create=True, return_value=has_default_avail): + with mock.patch("google.auth.transport.mtls.default_client_cert_source", create=True, return_value=default_val): + assert get_client_cert_source(provided, use_cert) == expected diff --git a/packages/google-api-core/tests/unit/gapic/test_config_helpers.py b/packages/google-api-core/tests/unit/gapic/test_config_helpers.py index e0d634a3b119..dcc785ee6cbd 100644 --- a/packages/google-api-core/tests/unit/gapic/test_config_helpers.py +++ b/packages/google-api-core/tests/unit/gapic/test_config_helpers.py @@ -19,29 +19,35 @@ import pytest from google.auth.exceptions import MutualTLSChannelError - from google.api_core.gapic_v1.config_helpers import read_environment_variables -@mock.patch( - "google.api_core.gapic_v1.config_helpers.use_client_cert_effective" -) # noqa: E501 -@mock.patch.dict(os.environ, clear=True) -def test_read_environment_variables(mock_effective): - mock_effective.return_value = True - os.environ["GOOGLE_API_USE_MTLS_ENDPOINT"] = "always" - os.environ["GOOGLE_CLOUD_UNIVERSE_DOMAIN"] = "custom.com" - - cert, mtls, domain = read_environment_variables() - assert cert is True - assert mtls == "always" - assert domain == "custom.com" - - -@mock.patch.dict(os.environ, clear=True) -def test_read_environment_variables_invalid_mtls(): - os.environ["GOOGLE_API_USE_MTLS_ENDPOINT"] = "invalid" - with pytest.raises( - MutualTLSChannelError, match="must be `never`, `auto` or `always`" - ): - read_environment_variables() +@pytest.mark.parametrize( + "env, mock_cert_val, expected", + [ + ({}, False, (False, "auto", None)), + ({}, True, (True, "auto", None)), + ({"GOOGLE_API_USE_MTLS_ENDPOINT": "never"}, False, (False, "never", None)), + ({"GOOGLE_API_USE_MTLS_ENDPOINT": "always"}, False, (False, "always", None)), + ({"GOOGLE_API_USE_MTLS_ENDPOINT": "auto"}, False, (False, "auto", None)), + ({"GOOGLE_API_USE_MTLS_ENDPOINT": "invalid"}, False, "mutual_tls_error"), + ({"GOOGLE_CLOUD_UNIVERSE_DOMAIN": "foo.com"}, False, (False, "auto", "foo.com")), + ], + ids=[ + "default_env", + "client_cert_true", + "mtls_never", + "mtls_always", + "mtls_auto", + "mtls_invalid", + "universe_domain", + ] +) +def test_read_environment_variables(env, mock_cert_val, expected): + with mock.patch("google.api_core.gapic_v1.config_helpers.use_client_cert_effective", return_value=mock_cert_val): + with mock.patch.dict(os.environ, env, clear=True): + if expected == "mutual_tls_error": + with pytest.raises(MutualTLSChannelError, match="must be `never`, `auto` or `always`"): + read_environment_variables() + else: + assert read_environment_variables() == expected From daf631e0e3ea4a90f834a5dd25cf1e766067e75f Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Thu, 16 Jul 2026 21:43:47 +0000 Subject: [PATCH 3/5] style(api-core): format client cert and config helper tests with black --- .../tests/unit/gapic/test_client_cert.py | 32 +++++++++++++++---- .../tests/unit/gapic/test_config_helpers.py | 17 +++++++--- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/packages/google-api-core/tests/unit/gapic/test_client_cert.py b/packages/google-api-core/tests/unit/gapic/test_client_cert.py index 1abc8eb22a14..e3314bbb02f7 100644 --- a/packages/google-api-core/tests/unit/gapic/test_client_cert.py +++ b/packages/google-api-core/tests/unit/gapic/test_client_cert.py @@ -54,24 +54,31 @@ "fallback_env_false_uppercase", "fallback_env_unset", "fallback_env_unsupported", - ] + ], ) def test_use_client_cert_effective(has_method, method_val, env_val, expected): # Mock hasattr to control whether should_use_client_cert exists original_hasattr = hasattr + def custom_hasattr(obj, name): if obj is mtls and name == "should_use_client_cert": return has_method return original_hasattr(obj, name) with mock.patch("google.api_core.gapic_v1.client_cert.hasattr", custom_hasattr): - with mock.patch("google.auth.transport.mtls.should_use_client_cert", create=True, return_value=method_val): + with mock.patch( + "google.auth.transport.mtls.should_use_client_cert", + create=True, + return_value=method_val, + ): env = {} if env_val is not None: env["GOOGLE_API_USE_CLIENT_CERTIFICATE"] = env_val with mock.patch.dict(os.environ, env, clear=True): if expected == "value_error": - with pytest.raises(ValueError, match="must be either `true` or `false`"): + with pytest.raises( + ValueError, match="must be either `true` or `false`" + ): use_client_cert_effective() else: assert use_client_cert_effective() is (expected == "true") @@ -92,16 +99,27 @@ def custom_hasattr(obj, name): "use_cert_true_with_provided", "use_cert_true_no_provided_default_avail", "use_cert_true_no_provided_default_unavail", - ] + ], ) -def test_get_client_cert_source(provided, use_cert, has_default_avail, default_val, expected): +def test_get_client_cert_source( + provided, use_cert, has_default_avail, default_val, expected +): original_hasattr = hasattr + def custom_hasattr(obj, name): if obj is mtls and name == "has_default_client_cert_source": return has_default_avail return original_hasattr(obj, name) with mock.patch("google.api_core.gapic_v1.client_cert.hasattr", custom_hasattr): - with mock.patch("google.auth.transport.mtls.has_default_client_cert_source", create=True, return_value=has_default_avail): - with mock.patch("google.auth.transport.mtls.default_client_cert_source", create=True, return_value=default_val): + with mock.patch( + "google.auth.transport.mtls.has_default_client_cert_source", + create=True, + return_value=has_default_avail, + ): + with mock.patch( + "google.auth.transport.mtls.default_client_cert_source", + create=True, + return_value=default_val, + ): assert get_client_cert_source(provided, use_cert) == expected diff --git a/packages/google-api-core/tests/unit/gapic/test_config_helpers.py b/packages/google-api-core/tests/unit/gapic/test_config_helpers.py index dcc785ee6cbd..b9e27ed627a3 100644 --- a/packages/google-api-core/tests/unit/gapic/test_config_helpers.py +++ b/packages/google-api-core/tests/unit/gapic/test_config_helpers.py @@ -31,7 +31,11 @@ ({"GOOGLE_API_USE_MTLS_ENDPOINT": "always"}, False, (False, "always", None)), ({"GOOGLE_API_USE_MTLS_ENDPOINT": "auto"}, False, (False, "auto", None)), ({"GOOGLE_API_USE_MTLS_ENDPOINT": "invalid"}, False, "mutual_tls_error"), - ({"GOOGLE_CLOUD_UNIVERSE_DOMAIN": "foo.com"}, False, (False, "auto", "foo.com")), + ( + {"GOOGLE_CLOUD_UNIVERSE_DOMAIN": "foo.com"}, + False, + (False, "auto", "foo.com"), + ), ], ids=[ "default_env", @@ -41,13 +45,18 @@ "mtls_auto", "mtls_invalid", "universe_domain", - ] + ], ) def test_read_environment_variables(env, mock_cert_val, expected): - with mock.patch("google.api_core.gapic_v1.config_helpers.use_client_cert_effective", return_value=mock_cert_val): + with mock.patch( + "google.api_core.gapic_v1.config_helpers.use_client_cert_effective", + return_value=mock_cert_val, + ): with mock.patch.dict(os.environ, env, clear=True): if expected == "mutual_tls_error": - with pytest.raises(MutualTLSChannelError, match="must be `never`, `auto` or `always`"): + with pytest.raises( + MutualTLSChannelError, match="must be `never`, `auto` or `always`" + ): read_environment_variables() else: assert read_environment_variables() == expected From b5042220a98eebf57249c16db7d6dffca057a296 Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Thu, 16 Jul 2026 21:48:10 +0000 Subject: [PATCH 4/5] fix(api-core): raise ValueError on missing cert source in get_client_cert_source --- .../google/api_core/gapic_v1/client_cert.py | 11 +++++++---- .../tests/unit/gapic/test_client_cert.py | 10 ++++++++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/google-api-core/google/api_core/gapic_v1/client_cert.py b/packages/google-api-core/google/api_core/gapic_v1/client_cert.py index 2f4a038778db..c62d95f6a5ea 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/client_cert.py +++ b/packages/google-api-core/google/api_core/gapic_v1/client_cert.py @@ -67,13 +67,16 @@ def get_client_cert_source( Returns: Callable[[], Tuple[bytes, bytes]] or None: The client cert source to be used by the client. """ - client_cert_source = None if use_cert_flag: if provided_cert_source: - client_cert_source = provided_cert_source + return provided_cert_source elif ( hasattr(mtls, "has_default_client_cert_source") and mtls.has_default_client_cert_source() ): - client_cert_source = mtls.default_client_cert_source() - return client_cert_source + return mtls.default_client_cert_source() + else: + raise ValueError( + "Client certificate is required for mTLS, but no client certificate source was provided or found." + ) + return None diff --git a/packages/google-api-core/tests/unit/gapic/test_client_cert.py b/packages/google-api-core/tests/unit/gapic/test_client_cert.py index e3314bbb02f7..3c0878816478 100644 --- a/packages/google-api-core/tests/unit/gapic/test_client_cert.py +++ b/packages/google-api-core/tests/unit/gapic/test_client_cert.py @@ -91,7 +91,7 @@ def custom_hasattr(obj, name): (b"provided", False, True, b"default", None), (b"provided", True, True, b"default", b"provided"), (None, True, True, b"default", b"default"), - (None, True, False, b"default", None), + (None, True, False, b"default", "value_error"), ], ids=[ "use_cert_false_no_provided", @@ -122,4 +122,10 @@ def custom_hasattr(obj, name): create=True, return_value=default_val, ): - assert get_client_cert_source(provided, use_cert) == expected + if expected == "value_error": + with pytest.raises( + ValueError, match="Client certificate is required for mTLS" + ): + get_client_cert_source(provided, use_cert) + else: + assert get_client_cert_source(provided, use_cert) == expected From 2e0174c95485f5dfe3bd2073871bc225ae0667ed Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Thu, 16 Jul 2026 21:51:47 +0000 Subject: [PATCH 5/5] revert(api-core): revert unrelated changes to noxfile.py and test_bidi.py --- packages/google-api-core/noxfile.py | 2 +- packages/google-api-core/tests/unit/test_bidi.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/google-api-core/noxfile.py b/packages/google-api-core/noxfile.py index 3cbebbaa84d2..0bad668a80dd 100644 --- a/packages/google-api-core/noxfile.py +++ b/packages/google-api-core/noxfile.py @@ -350,7 +350,7 @@ def prerelease_deps(session): @nox.session(python=DEFAULT_PYTHON_VERSION) def core_deps_from_source(session): """Run the test suite installing dependencies from source.""" - default(session, install_deps_from_source=True) + default(session, prerelease=True) @nox.session(python=DEFAULT_PYTHON_VERSION) diff --git a/packages/google-api-core/tests/unit/test_bidi.py b/packages/google-api-core/tests/unit/test_bidi.py index 0f4810cb0a12..4a8eb74fac94 100644 --- a/packages/google-api-core/tests/unit/test_bidi.py +++ b/packages/google-api-core/tests/unit/test_bidi.py @@ -31,7 +31,8 @@ except ImportError: # pragma: NO COVER pytest.skip("No GRPC", allow_module_level=True) -from google.api_core import bidi, exceptions +from google.api_core import bidi +from google.api_core import exceptions class Test_RequestQueueGenerator(object): @@ -194,7 +195,7 @@ def test_delays_entry_attempts_above_threshold(self): # (NOTE: not using assert all(...), b/c the coverage check would complain) for i, entry in enumerate(entries): if i != 3: - assert entry["reported_wait"] < 0.01 + assert entry["reported_wait"] == 0.0 # The delayed entry is expected to have been delayed for a significant # chunk of the full second, and the actual and reported delay times