diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 index 41ee1a4d0c02..247e74130d0c 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 @@ -12,13 +12,157 @@ Add conditional logic to check if static code exists in google-api-core and use falling back to the local implementation if not present. #} {# TODO(https://github.com/googleapis/google-cloud-python/issues/17883): Backfill compatibility functions being removed from the client layer. #} +"""A compatibility module for older versions of google-api-core.""" -{% if has_auto_populated_fields %} -from typing import Union import uuid +from typing import Optional, Union +from urllib.parse import urlparse, urlunparse + +from google.auth.exceptions import MutualTLSChannelError +from google.api_core.universe import EmptyUniverseError import google.protobuf.message +DEFAULT_UNIVERSE = "googleapis.com" + + +def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: + """Converts api endpoint to mTLS endpoint. + + Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to + "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. + Other URLs (including those that do not match these domain suffixes or + already contain '.mtls.') are passed through as-is. + + Args: + api_endpoint (Optional[str]): the api endpoint to convert. + + Returns: + Optional[str]: converted mTLS api endpoint. + """ + if not api_endpoint or ".mtls." in api_endpoint.lower(): + return api_endpoint + + has_scheme = "://" in api_endpoint + if not has_scheme: + parsed = urlparse("//" + api_endpoint) + else: + parsed = urlparse(api_endpoint) + + host = parsed.hostname + if not host: + return api_endpoint + + port = f":{parsed.port}" if parsed.port else "" + + lowered_host = host.lower() + suffix_sandbox = ".sandbox.googleapis.com" + suffix_google = ".googleapis.com" + if lowered_host.endswith(suffix_sandbox): + new_host = host[: -len(suffix_sandbox)] + ".mtls.sandbox.googleapis.com" + elif lowered_host.endswith(suffix_google): + new_host = host[: -len(suffix_google)] + ".mtls.googleapis.com" + else: + return api_endpoint + + netloc = new_host + port + new_parsed = parsed._replace(netloc=netloc) + + if not has_scheme: + return urlunparse(new_parsed)[2:] + else: + return urlunparse(new_parsed) + +def get_api_endpoint( + api_override: Optional[str], + universe_domain: str, + default_universe: str, + default_mtls_endpoint: Optional[str], + default_endpoint_template: str, + use_mtls: bool, +) -> str: + """Return the API endpoint used by the client. + + Args: + api_override (Optional[str]): The API endpoint override. If specified, + this is always returned. + universe_domain (str): The universe domain used by the client. + default_universe (str): The default universe domain. + default_mtls_endpoint (Optional[str]): The default mTLS endpoint. + default_endpoint_template (str): The default endpoint template containing + a placeholder `{UNIVERSE_DOMAIN}`. + use_mtls (bool): Whether to use the mTLS endpoint. + + Returns: + str: The API endpoint to be used by the client. + + Raises: + google.auth.exceptions.MutualTLSChannelError: If mTLS is requested but + not supported in the configured universe domain. + ValueError: If mTLS is requested but no mTLS endpoint is available. + """ + if api_override is not None: + return api_override + + if use_mtls: + if universe_domain.lower() != default_universe.lower(): + raise MutualTLSChannelError( + f"mTLS is not supported in any universe other than {default_universe}." + ) + if not default_mtls_endpoint: + raise ValueError("mTLS endpoint is not available.") + return default_mtls_endpoint + else: + return default_endpoint_template.format(UNIVERSE_DOMAIN=universe_domain) + +def get_universe_domain( + *potential_universes: Optional[str], + default_universe: str, +) -> str: + """Return the universe domain used by the client. + + Args: + *potential_universes (Optional[str]): Potential universe domains in order of preference. + default_universe (str): The default universe domain. + + Returns: + str: The universe domain to be used by the client. + + Raises: + EmptyUniverseError: If the resolved universe domain is an empty string. + """ + resolved = next( + (x.strip() for x in potential_universes if x is not None), + default_universe, + ) + + if not resolved: + raise EmptyUniverseError() + return resolved + +def determine_domain( + client_universe_domain: Optional[str], universe_domain_env: Optional[str] +) -> str: + """Return the universe domain used by the client. + + Args: + client_universe_domain (Optional[str]): The universe domain configured via the client options. + universe_domain_env (Optional[str]): The universe domain configured via the + "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. + + Returns: + str: The universe domain to be used by the client. + + Raises: + ValueError: If the universe domain is an empty string. + """ + return get_universe_domain( + client_universe_domain, + universe_domain_env, + default_universe=DEFAULT_UNIVERSE, + ) + +{% if has_auto_populated_fields %} def setup_request_id( request: Union[google.protobuf.message.Message, dict, None], @@ -63,5 +207,6 @@ def setup_request_id( else: if not getattr(request, field_name, None): setattr(request, field_name, str(uuid.uuid4())) + {% endif %} {% endblock %} diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 index 33ecf1fe31d9..42878666bfff 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 @@ -30,6 +30,7 @@ from google.api_core import exceptions as core_exceptions from google.api_core import extended_operation {% endif %} from google.api_core import gapic_v1 +from {{package_path}}._compat import determine_domain, get_api_endpoint, get_default_mtls_endpoint {% if has_auto_populated_fields %} from {{package_path}}._compat import setup_request_id {% endif %} @@ -146,44 +147,10 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta): """{{ service.meta.doc|rst(width=72, indent=4) }}{% if service.version|length %} This class implements API version {{ service.version }}.{% endif %}""" - @staticmethod - def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: - """Converts api endpoint to mTLS endpoint. - - Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to - "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. - Args: - api_endpoint (Optional[str]): the api endpoint to convert. - Returns: - Optional[str]: converted mTLS api endpoint. - """ - if not api_endpoint: - return api_endpoint - - mtls_endpoint_re = re.compile( - r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" - ) - - m = mtls_endpoint_re.match(api_endpoint) - if m is None: - # Could not parse api_endpoint; return as-is. - return api_endpoint - - name, mtls, sandbox, googledomain = m.groups() - if mtls or not googledomain: - return api_endpoint - - if sandbox: - return api_endpoint.replace( - "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" - ) - - return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") - # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = {% if service.host %}"{{ service.host }}"{% else %}None{% endif %} - DEFAULT_MTLS_ENDPOINT = _get_default_mtls_endpoint.__func__( # type: ignore + DEFAULT_MTLS_ENDPOINT = get_default_mtls_endpoint( DEFAULT_ENDPOINT ) @@ -394,54 +361,6 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta): client_cert_source = mtls.default_client_cert_source() return client_cert_source - @staticmethod - def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: - """Return the API endpoint used by the client. - - Args: - api_override (str): The API endpoint override. If specified, this is always - the return value of this function and the other arguments are not used. - client_cert_source (bytes): The client certificate source used by the client. - universe_domain (str): The universe domain used by the client. - use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. - Possible values are "always", "auto", or "never". - - Returns: - str: The API endpoint to be used by the client. - """ - if api_override is not None: - api_endpoint = api_override - elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): - _default_universe = {{ service.client_name }}._DEFAULT_UNIVERSE - if universe_domain != _default_universe: - raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") - api_endpoint = {{ service.client_name }}.DEFAULT_MTLS_ENDPOINT - else: - api_endpoint = {{ service.client_name }}._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) - return api_endpoint - - @staticmethod - def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client. - - Args: - client_universe_domain (Optional[str]): The universe domain configured via the client options. - universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. - - Returns: - str: The universe domain to be used by the client. - - Raises: - ValueError: If the universe domain is an empty string. - """ - universe_domain = {{ service.client_name }}._DEFAULT_UNIVERSE - if client_universe_domain is not None: - universe_domain = client_universe_domain - elif universe_domain_env is not None: - universe_domain = universe_domain_env - if len(universe_domain.strip()) == 0: - raise ValueError("Universe Domain cannot be an empty string.") - return universe_domain def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. @@ -570,7 +489,7 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta): self._use_client_cert, self._use_mtls_endpoint, self._universe_domain_env = {{ service.client_name }}._read_environment_variables() self._client_cert_source = {{ service.client_name }}._get_client_cert_source(self._client_options.client_cert_source, self._use_client_cert) - self._universe_domain = {{ service.client_name }}._get_universe_domain(universe_domain_opt, self._universe_domain_env) + self._universe_domain = determine_domain(universe_domain_opt, self._universe_domain_env) self._api_endpoint: str = "" # updated below, depending on `transport` # Initialize the universe domain validation. @@ -601,8 +520,8 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta): self._transport = cast({{ service.name }}Transport, transport) self._api_endpoint = self._transport.host - self._api_endpoint = (self._api_endpoint or - {{ service.client_name }}._get_api_endpoint( + self._api_endpoint = (self._api_endpoint or + get_api_endpoint( self._client_options.api_endpoint, self._client_cert_source, self._universe_domain, diff --git a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 index df4b71348c0b..1e7ba49b5537 100644 --- a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 +++ b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 @@ -156,31 +156,15 @@ def set_event_loop(): asyncio.set_event_loop(None) -def test__get_default_mtls_endpoint(): - api_endpoint = "example.googleapis.com" - api_mtls_endpoint = "example.mtls.googleapis.com" - sandbox_endpoint = "example.sandbox.googleapis.com" - sandbox_mtls_endpoint = "example.mtls.sandbox.googleapis.com" - non_googleapi = "api.example.com" - custom_endpoint = ".custom" - - assert {{ service.client_name }}._get_default_mtls_endpoint(None) is None - assert {{ service.client_name }}._get_default_mtls_endpoint(api_endpoint) == api_mtls_endpoint - assert {{ service.client_name }}._get_default_mtls_endpoint(api_mtls_endpoint) == api_mtls_endpoint - assert {{ service.client_name }}._get_default_mtls_endpoint(sandbox_endpoint) == sandbox_mtls_endpoint - assert {{ service.client_name }}._get_default_mtls_endpoint(sandbox_mtls_endpoint) == sandbox_mtls_endpoint - assert {{ service.client_name }}._get_default_mtls_endpoint(non_googleapi) == non_googleapi - assert {{ service.client_name }}._get_default_mtls_endpoint(custom_endpoint) == custom_endpoint - def test__read_environment_variables(): assert {{ service.client_name }}._read_environment_variables() == (False, "auto", None) - + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"}): assert {{ service.client_name }}._read_environment_variables() == (True, "auto", None) - + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "false"}): assert {{ service.client_name }}._read_environment_variables() == (False, "auto", None) - + with mock.patch.dict( os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "Unsupported"} ): @@ -200,10 +184,10 @@ def test__read_environment_variables(): with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "never"}): assert {{ service.client_name }}._read_environment_variables() == (False, "never", None) - + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "always"}): assert {{ service.client_name }}._read_environment_variables() == (False, "always", None) - + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "auto"}): assert {{ service.client_name }}._read_environment_variables() == (False, "auto", None) @@ -293,7 +277,7 @@ def test_use_client_cert_effective(): assert {{ service.client_name }}._use_client_cert_effective() is False # Test case 12: Test when `should_use_client_cert` is available and the - # `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is unset. Also, + # `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is unset. Also, # the GOOGLE_API_CONFIG environment variable is unset. if hasattr(google.auth.transport.mtls, "should_use_client_cert"): with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": ""}): @@ -303,7 +287,7 @@ def test_use_client_cert_effective(): def test__get_client_cert_source(): mock_provided_cert_source = mock.Mock() mock_default_cert_source = mock.Mock() - + assert {{ service.client_name }}._get_client_cert_source(None, False) is None assert {{ service.client_name }}._get_client_cert_source(mock_provided_cert_source, False) is None assert {{ service.client_name }}._get_client_cert_source(mock_provided_cert_source, True) == mock_provided_cert_source @@ -313,29 +297,7 @@ def test__get_client_cert_source(): assert {{ service.client_name }}._get_client_cert_source(None, True) is mock_default_cert_source assert {{ service.client_name }}._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source -@mock.patch.object({{ service.client_name }}, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template({{ service.client_name }})) -{% if 'grpc' in opts.transport %} -@mock.patch.object({{ service.async_client_name }}, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template({{ service.async_client_name }})) -{% endif %} -def test__get_api_endpoint(): - api_override = "foo.com" - mock_client_cert_source = mock.Mock() - default_universe = {{ service.client_name }}._DEFAULT_UNIVERSE - default_endpoint = {{ service.client_name }}._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) - mock_universe = "bar.com" - mock_endpoint = {{ service.client_name }}._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) - - assert {{ service.client_name }}._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override - assert {{ service.client_name }}._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == {{ service.client_name }}.DEFAULT_MTLS_ENDPOINT - assert {{ service.client_name }}._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint - assert {{ service.client_name }}._get_api_endpoint(None, None, default_universe, "always") == {{ service.client_name }}.DEFAULT_MTLS_ENDPOINT - assert {{ service.client_name }}._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == {{ service.client_name }}.DEFAULT_MTLS_ENDPOINT - assert {{ service.client_name }}._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint - assert {{ service.client_name }}._get_api_endpoint(None, None, default_universe, "never") == default_endpoint - with pytest.raises(MutualTLSChannelError) as excinfo: - {{ service.client_name }}._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") - assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." {% if service.version %} {% for method in service.methods.values() %}{% with method_name = method.name|snake_case %} @@ -384,17 +346,7 @@ def test_{{ method_name }}_api_version_header(transport_name): {% endfor %} {% endif %}{# service.version #} -def test__get_universe_domain(): - client_universe_domain = "foo.com" - universe_domain_env = "bar.com" - assert {{ service.client_name }}._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain - assert {{ service.client_name }}._get_universe_domain(None, universe_domain_env) == universe_domain_env - assert {{ service.client_name }}._get_universe_domain(None, None) == {{ service.client_name }}._DEFAULT_UNIVERSE - - with pytest.raises(ValueError) as excinfo: - {{ service.client_name }}._get_universe_domain("", None) - assert str(excinfo.value) == "Universe Domain cannot be an empty string." @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), @@ -789,7 +741,7 @@ def test_{{ service.client_name|snake_case }}_get_mtls_endpoint_and_cert_source( ) assert api_endpoint == mock_api_endpoint assert cert_source is None - + # Test cases for mTLS enablement when GOOGLE_API_USE_CLIENT_CERTIFICATE is unset. test_cases = [ ( @@ -823,7 +775,7 @@ def test_{{ service.client_name|snake_case }}_get_mtls_endpoint_and_cert_source( config_filename = "mock_certificate_config.json" config_file_content = json.dumps(config_data) m = mock.mock_open(read_data=config_file_content) - with mock.patch("builtins.open", m), mock.patch("os.path.exists", side_effect=lambda path: os.path.basename(path) == config_filename): + with mock.patch("builtins.open", m): with mock.patch.dict( os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": config_filename} ): @@ -852,10 +804,10 @@ def test_{{ service.client_name|snake_case }}_get_mtls_endpoint_and_cert_source( }, }, mock_client_cert_source, - ), + ), ( # With workloads not present in config, mTLS is disabled. - { + { "version": 1, "cert_configs": {}, }, @@ -871,7 +823,7 @@ def test_{{ service.client_name|snake_case }}_get_mtls_endpoint_and_cert_source( config_filename = "mock_certificate_config.json" config_file_content = json.dumps(config_data) m = mock.mock_open(read_data=config_file_content) - with mock.patch("builtins.open", m), mock.patch("os.path.exists", side_effect=lambda path: os.path.basename(path) == config_filename): + with mock.patch("builtins.open", m): with mock.patch.dict( os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": config_filename} ): @@ -918,7 +870,7 @@ def test_{{ service.client_name|snake_case }}_get_mtls_endpoint_and_cert_source( with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "Unsupported"}): with pytest.raises(MutualTLSChannelError) as excinfo: client_class.get_mtls_endpoint_and_cert_source() - + assert str(excinfo.value) == "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` must be `never`, `auto` or `always`" @pytest.mark.parametrize("client_class", [ @@ -983,7 +935,7 @@ def test_{{ service.client_name|snake_case }}_client_api_endpoint(client_class): with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "never"}): client = client_class(client_options=options, credentials=ga_credentials.AnonymousCredentials()) assert client.api_endpoint == default_endpoint - + @pytest.mark.parametrize("client_class,transport_class,transport_name", [ {% if 'grpc' in opts.transport %} diff --git a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_compat.py.j2 b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_compat.py.j2 index db8515efa7a2..47217df675c6 100644 --- a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_compat.py.j2 +++ b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_compat.py.j2 @@ -10,13 +10,215 @@ google-api-core has the functions in `_compat.py.j2`. #} {# TODO(https://github.com/googleapis/google-cloud-python/issues/17883): Backfill compatibility functions tests being removed from the client layer. #} - -{% if has_auto_populated_fields %} import re import pytest {% set package_path = api.naming.module_namespace|join('.') + "." + api.naming.versioned_module_name %} +from {{package_path}}._compat import determine_domain, get_api_endpoint, get_default_mtls_endpoint +{% if has_auto_populated_fields %} from {{package_path}}._compat import setup_request_id +{% endif %} + +from google.auth.exceptions import MutualTLSChannelError + + +def test_get_universe_domain(): + # When universe_domain is provided + assert ( + universe.get_universe_domain("foo.com", default_universe="default.com") + == "foo.com" + ) + assert ( + universe.get_universe_domain(" foo.com ", default_universe="default.com") + == "foo.com" + ) + + # When universe_domain is None, falls back to default_universe + assert ( + universe.get_universe_domain(None, default_universe="default.com") + == "default.com" + ) + + # When multiple potential universes are provided, resolves in order of preference + assert ( + universe.get_universe_domain( + "foo.com", "bar.com", default_universe="default.com" + ) + == "foo.com" + ) + assert ( + universe.get_universe_domain(None, "bar.com", default_universe="default.com") + == "bar.com" + ) + assert ( + universe.get_universe_domain(None, None, default_universe="default.com") + == "default.com" + ) + + # EmptyUniverseError raised when resolved value is empty string + with pytest.raises(universe.EmptyUniverseError) as excinfo: + universe.get_universe_domain("", default_universe="default.com") + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + + with pytest.raises(universe.EmptyUniverseError) as excinfo: + universe.get_universe_domain(" ", default_universe="default.com") + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + + with pytest.raises(universe.EmptyUniverseError) as excinfo: + universe.get_universe_domain(None, "", default_universe="default.com") + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + + +def test_get_default_mtls_endpoint(): + # Test valid API endpoints + assert ( + universe.get_default_mtls_endpoint("foo.googleapis.com") + == "foo.mtls.googleapis.com" + ) + assert ( + universe.get_default_mtls_endpoint("foo.sandbox.googleapis.com") + == "foo.mtls.sandbox.googleapis.com" + ) + # Test case-insensitivity + assert ( + universe.get_default_mtls_endpoint("foo.GoogleAPIs.com") + == "foo.mtls.googleapis.com" + ) + assert ( + universe.get_default_mtls_endpoint("foo.Sandbox.GoogleAPIs.com") + == "foo.mtls.sandbox.googleapis.com" + ) + + # Test valid API endpoints with schemes + assert ( + universe.get_default_mtls_endpoint("https://foo.googleapis.com") + == "https://foo.mtls.googleapis.com" + ) + assert ( + universe.get_default_mtls_endpoint("http://foo.googleapis.com:8080/v1") + == "http://foo.mtls.googleapis.com:8080/v1" + ) + + # Test valid API endpoints with ports + assert ( + universe.get_default_mtls_endpoint("foo.googleapis.com:443") + == "foo.mtls.googleapis.com:443" + ) + assert ( + universe.get_default_mtls_endpoint("foo.sandbox.googleapis.com:443") + == "foo.mtls.sandbox.googleapis.com:443" + ) + # Test case-insensitivity with ports + assert ( + universe.get_default_mtls_endpoint("foo.GoogleAPIs.com:443") + == "foo.mtls.googleapis.com:443" + ) + assert ( + universe.get_default_mtls_endpoint("foo.Sandbox.GoogleAPIs.com:443") + == "foo.mtls.sandbox.googleapis.com:443" + ) + + # Test endpoints that shouldn't be converted + assert ( + universe.get_default_mtls_endpoint("foo.mtls.googleapis.com") + == "foo.mtls.googleapis.com" + ) + assert universe.get_default_mtls_endpoint("foo.com") == "foo.com" + assert universe.get_default_mtls_endpoint("foo.com:8080") == "foo.com:8080" + + # Test empty/None endpoints + assert universe.get_default_mtls_endpoint("") == "" + assert universe.get_default_mtls_endpoint(None) is None + + # Test endpoints without host + assert universe.get_default_mtls_endpoint("http://") == "http://" + assert universe.get_default_mtls_endpoint("https://") == "https://" + + +@pytest.mark.parametrize( + "api_override,universe_domain,default_universe,default_mtls_endpoint,default_endpoint_template,use_mtls,expected", + [ + ( + "foo.com", + "googleapis.com", + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + True, + "foo.com", + ), + ( + None, + "googleapis.com", + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + True, + "foo.mtls.googleapis.com", + ), + ( + None, + "googleapis.com", + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + False, + "foo.googleapis.com", + ), + ( + None, + "bar.com", + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + True, + MutualTLSChannelError, + ), + ( + None, + "googleapis.com", + "googleapis.com", + None, + "foo.{UNIVERSE_DOMAIN}", + True, + ValueError, + ), + ], +) +def test_get_api_endpoint( + api_override, + universe_domain, + default_universe, + default_mtls_endpoint, + default_endpoint_template, + use_mtls, + expected, +): + if isinstance(expected, type) and issubclass(expected, Exception): + with pytest.raises(expected): + universe.get_api_endpoint( + api_override, + universe_domain, + default_universe, + default_mtls_endpoint, + default_endpoint_template, + use_mtls, + ) + else: + assert ( + universe.get_api_endpoint( + api_override, + universe_domain, + default_universe, + default_mtls_endpoint, + default_endpoint_template, + use_mtls, + ) + == expected + ) + + +{% if has_auto_populated_fields %} class MockRequest: def __init__(self, **kwargs): diff --git a/packages/gapic-generator/noxfile.py b/packages/gapic-generator/noxfile.py index 621a37b9278c..368a42b3bf88 100644 --- a/packages/gapic-generator/noxfile.py +++ b/packages/gapic-generator/noxfile.py @@ -392,6 +392,8 @@ def showcase_library( # Install the library without a constraints file. session.install("-e", tmp_dir) + session.install("-e", "../google-api-core", "--no-deps") + yield tmp_dir @@ -614,7 +616,13 @@ def showcase_mypy( session.chdir(lib) # Run the tests. - session.run("mypy", "-p", "google", "--check-untyped-defs") + session.run( + "mypy", + f"--config-file={MYPY_CONFIG_FILE}", + "-p", + "google", + "--check-untyped-defs", + ) @nox.session(python=NEWEST_PYTHON) diff --git a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/_compat.py b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/_compat.py index 7c684e94b03d..a84aecd6ca3d 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/_compat.py @@ -14,3 +14,152 @@ # limitations under the License. # """A compatibility module for older versions of google-api-core.""" +"""A compatibility module for older versions of google-api-core.""" + +import uuid + +from typing import Optional, Union +from urllib.parse import urlparse, urlunparse + +from google.auth.exceptions import MutualTLSChannelError +from google.api_core.universe import EmptyUniverseError +import google.protobuf.message + +DEFAULT_UNIVERSE = "googleapis.com" + + +def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: + """Converts api endpoint to mTLS endpoint. + + Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to + "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. + Other URLs (including those that do not match these domain suffixes or + already contain '.mtls.') are passed through as-is. + + Args: + api_endpoint (Optional[str]): the api endpoint to convert. + + Returns: + Optional[str]: converted mTLS api endpoint. + """ + if not api_endpoint or ".mtls." in api_endpoint.lower(): + return api_endpoint + + has_scheme = "://" in api_endpoint + if not has_scheme: + parsed = urlparse("//" + api_endpoint) + else: + parsed = urlparse(api_endpoint) + + host = parsed.hostname + if not host: + return api_endpoint + + port = f":{parsed.port}" if parsed.port else "" + + lowered_host = host.lower() + suffix_sandbox = ".sandbox.googleapis.com" + suffix_google = ".googleapis.com" + if lowered_host.endswith(suffix_sandbox): + new_host = host[: -len(suffix_sandbox)] + ".mtls.sandbox.googleapis.com" + elif lowered_host.endswith(suffix_google): + new_host = host[: -len(suffix_google)] + ".mtls.googleapis.com" + else: + return api_endpoint + + netloc = new_host + port + new_parsed = parsed._replace(netloc=netloc) + + if not has_scheme: + return urlunparse(new_parsed)[2:] + else: + return urlunparse(new_parsed) + +def get_api_endpoint( + api_override: Optional[str], + universe_domain: str, + default_universe: str, + default_mtls_endpoint: Optional[str], + default_endpoint_template: str, + use_mtls: bool, +) -> str: + """Return the API endpoint used by the client. + + Args: + api_override (Optional[str]): The API endpoint override. If specified, + this is always returned. + universe_domain (str): The universe domain used by the client. + default_universe (str): The default universe domain. + default_mtls_endpoint (Optional[str]): The default mTLS endpoint. + default_endpoint_template (str): The default endpoint template containing + a placeholder `{UNIVERSE_DOMAIN}`. + use_mtls (bool): Whether to use the mTLS endpoint. + + Returns: + str: The API endpoint to be used by the client. + + Raises: + google.auth.exceptions.MutualTLSChannelError: If mTLS is requested but + not supported in the configured universe domain. + ValueError: If mTLS is requested but no mTLS endpoint is available. + """ + if api_override is not None: + return api_override + + if use_mtls: + if universe_domain.lower() != default_universe.lower(): + raise MutualTLSChannelError( + f"mTLS is not supported in any universe other than {default_universe}." + ) + if not default_mtls_endpoint: + raise ValueError("mTLS endpoint is not available.") + return default_mtls_endpoint + else: + return default_endpoint_template.format(UNIVERSE_DOMAIN=universe_domain) + +def get_universe_domain( + *potential_universes: Optional[str], + default_universe: str, +) -> str: + """Return the universe domain used by the client. + + Args: + *potential_universes (Optional[str]): Potential universe domains in order of preference. + default_universe (str): The default universe domain. + + Returns: + str: The universe domain to be used by the client. + + Raises: + EmptyUniverseError: If the resolved universe domain is an empty string. + """ + resolved = next( + (x.strip() for x in potential_universes if x is not None), + default_universe, + ) + + if not resolved: + raise EmptyUniverseError() + return resolved + +def determine_domain( + client_universe_domain: Optional[str], universe_domain_env: Optional[str] +) -> str: + """Return the universe domain used by the client. + + Args: + client_universe_domain (Optional[str]): The universe domain configured via the client options. + universe_domain_env (Optional[str]): The universe domain configured via the + "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. + + Returns: + str: The universe domain to be used by the client. + + Raises: + ValueError: If the universe domain is an empty string. + """ + return get_universe_domain( + client_universe_domain, + universe_domain_env, + default_universe=DEFAULT_UNIVERSE, + ) diff --git a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/client.py b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/client.py index 590b6fa1c615..85dd0c48ac9e 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/client.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/client.py @@ -27,6 +27,7 @@ from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.cloud.asset_v1._compat import determine_domain, get_api_endpoint, get_default_mtls_endpoint from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -100,43 +101,9 @@ def get_transport_class(cls, class AssetServiceClient(metaclass=AssetServiceClientMeta): """Asset service definition.""" - @staticmethod - def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: - """Converts api endpoint to mTLS endpoint. - - Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to - "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. - Args: - api_endpoint (Optional[str]): the api endpoint to convert. - Returns: - Optional[str]: converted mTLS api endpoint. - """ - if not api_endpoint: - return api_endpoint - - mtls_endpoint_re = re.compile( - r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" - ) - - m = mtls_endpoint_re.match(api_endpoint) - if m is None: - # Could not parse api_endpoint; return as-is. - return api_endpoint - - name, mtls, sandbox, googledomain = m.groups() - if mtls or not googledomain: - return api_endpoint - - if sandbox: - return api_endpoint.replace( - "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" - ) - - return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") - # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "cloudasset.googleapis.com" - DEFAULT_MTLS_ENDPOINT = _get_default_mtls_endpoint.__func__( # type: ignore + DEFAULT_MTLS_ENDPOINT = get_default_mtls_endpoint( DEFAULT_ENDPOINT ) @@ -449,55 +416,6 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): client_cert_source = mtls.default_client_cert_source() return client_cert_source - @staticmethod - def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: - """Return the API endpoint used by the client. - - Args: - api_override (str): The API endpoint override. If specified, this is always - the return value of this function and the other arguments are not used. - client_cert_source (bytes): The client certificate source used by the client. - universe_domain (str): The universe domain used by the client. - use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. - Possible values are "always", "auto", or "never". - - Returns: - str: The API endpoint to be used by the client. - """ - if api_override is not None: - api_endpoint = api_override - elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): - _default_universe = AssetServiceClient._DEFAULT_UNIVERSE - if universe_domain != _default_universe: - raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") - api_endpoint = AssetServiceClient.DEFAULT_MTLS_ENDPOINT - else: - api_endpoint = AssetServiceClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) - return api_endpoint - - @staticmethod - def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client. - - Args: - client_universe_domain (Optional[str]): The universe domain configured via the client options. - universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. - - Returns: - str: The universe domain to be used by the client. - - Raises: - ValueError: If the universe domain is an empty string. - """ - universe_domain = AssetServiceClient._DEFAULT_UNIVERSE - if client_universe_domain is not None: - universe_domain = client_universe_domain - elif universe_domain_env is not None: - universe_domain = universe_domain_env - if len(universe_domain.strip()) == 0: - raise ValueError("Universe Domain cannot be an empty string.") - return universe_domain - def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. @@ -622,7 +540,7 @@ def __init__(self, *, self._use_client_cert, self._use_mtls_endpoint, self._universe_domain_env = AssetServiceClient._read_environment_variables() self._client_cert_source = AssetServiceClient._get_client_cert_source(self._client_options.client_cert_source, self._use_client_cert) - self._universe_domain = AssetServiceClient._get_universe_domain(universe_domain_opt, self._universe_domain_env) + self._universe_domain = determine_domain(universe_domain_opt, self._universe_domain_env) self._api_endpoint: str = "" # updated below, depending on `transport` # Initialize the universe domain validation. @@ -654,7 +572,7 @@ def __init__(self, *, self._api_endpoint = self._transport.host self._api_endpoint = (self._api_endpoint or - AssetServiceClient._get_api_endpoint( + get_api_endpoint( self._client_options.api_endpoint, self._client_cert_source, self._universe_domain, diff --git a/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py b/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py index 2df35ac7af42..b3172586a6ef 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py @@ -120,22 +120,6 @@ def set_event_loop(): asyncio.set_event_loop(None) -def test__get_default_mtls_endpoint(): - api_endpoint = "example.googleapis.com" - api_mtls_endpoint = "example.mtls.googleapis.com" - sandbox_endpoint = "example.sandbox.googleapis.com" - sandbox_mtls_endpoint = "example.mtls.sandbox.googleapis.com" - non_googleapi = "api.example.com" - custom_endpoint = ".custom" - - assert AssetServiceClient._get_default_mtls_endpoint(None) is None - assert AssetServiceClient._get_default_mtls_endpoint(api_endpoint) == api_mtls_endpoint - assert AssetServiceClient._get_default_mtls_endpoint(api_mtls_endpoint) == api_mtls_endpoint - assert AssetServiceClient._get_default_mtls_endpoint(sandbox_endpoint) == sandbox_mtls_endpoint - assert AssetServiceClient._get_default_mtls_endpoint(sandbox_mtls_endpoint) == sandbox_mtls_endpoint - assert AssetServiceClient._get_default_mtls_endpoint(non_googleapi) == non_googleapi - assert AssetServiceClient._get_default_mtls_endpoint(custom_endpoint) == custom_endpoint - def test__read_environment_variables(): assert AssetServiceClient._read_environment_variables() == (False, "auto", None) @@ -277,40 +261,6 @@ def test__get_client_cert_source(): assert AssetServiceClient._get_client_cert_source(None, True) is mock_default_cert_source assert AssetServiceClient._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source -@mock.patch.object(AssetServiceClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(AssetServiceClient)) -@mock.patch.object(AssetServiceAsyncClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(AssetServiceAsyncClient)) -def test__get_api_endpoint(): - api_override = "foo.com" - mock_client_cert_source = mock.Mock() - default_universe = AssetServiceClient._DEFAULT_UNIVERSE - default_endpoint = AssetServiceClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) - mock_universe = "bar.com" - mock_endpoint = AssetServiceClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) - - assert AssetServiceClient._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override - assert AssetServiceClient._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == AssetServiceClient.DEFAULT_MTLS_ENDPOINT - assert AssetServiceClient._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint - assert AssetServiceClient._get_api_endpoint(None, None, default_universe, "always") == AssetServiceClient.DEFAULT_MTLS_ENDPOINT - assert AssetServiceClient._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == AssetServiceClient.DEFAULT_MTLS_ENDPOINT - assert AssetServiceClient._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint - assert AssetServiceClient._get_api_endpoint(None, None, default_universe, "never") == default_endpoint - - with pytest.raises(MutualTLSChannelError) as excinfo: - AssetServiceClient._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") - assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." - - -def test__get_universe_domain(): - client_universe_domain = "foo.com" - universe_domain_env = "bar.com" - - assert AssetServiceClient._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain - assert AssetServiceClient._get_universe_domain(None, universe_domain_env) == universe_domain_env - assert AssetServiceClient._get_universe_domain(None, None) == AssetServiceClient._DEFAULT_UNIVERSE - - with pytest.raises(ValueError) as excinfo: - AssetServiceClient._get_universe_domain("", None) - assert str(excinfo.value) == "Universe Domain cannot be an empty string." @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), @@ -700,7 +650,7 @@ def test_asset_service_client_get_mtls_endpoint_and_cert_source(client_class): config_filename = "mock_certificate_config.json" config_file_content = json.dumps(config_data) m = mock.mock_open(read_data=config_file_content) - with mock.patch("builtins.open", m), mock.patch("os.path.exists", side_effect=lambda path: os.path.basename(path) == config_filename): + with mock.patch("builtins.open", m): with mock.patch.dict( os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": config_filename} ): @@ -748,7 +698,7 @@ def test_asset_service_client_get_mtls_endpoint_and_cert_source(client_class): config_filename = "mock_certificate_config.json" config_file_content = json.dumps(config_data) m = mock.mock_open(read_data=config_file_content) - with mock.patch("builtins.open", m), mock.patch("os.path.exists", side_effect=lambda path: os.path.basename(path) == config_filename): + with mock.patch("builtins.open", m): with mock.patch.dict( os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": config_filename} ): diff --git a/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_compat.py b/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_compat.py index 8491204d1041..0bfffaf2f065 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_compat.py @@ -14,3 +14,206 @@ # limitations under the License. # """Tests for the compatibility module for older versions of google-api-core.""" + +import re +import pytest + +from google.cloud.asset_v1._compat import determine_domain, get_api_endpoint, get_default_mtls_endpoint + +from google.auth.exceptions import MutualTLSChannelError + + +def test_get_universe_domain(): + # When universe_domain is provided + assert ( + universe.get_universe_domain("foo.com", default_universe="default.com") + == "foo.com" + ) + assert ( + universe.get_universe_domain(" foo.com ", default_universe="default.com") + == "foo.com" + ) + + # When universe_domain is None, falls back to default_universe + assert ( + universe.get_universe_domain(None, default_universe="default.com") + == "default.com" + ) + + # When multiple potential universes are provided, resolves in order of preference + assert ( + universe.get_universe_domain( + "foo.com", "bar.com", default_universe="default.com" + ) + == "foo.com" + ) + assert ( + universe.get_universe_domain(None, "bar.com", default_universe="default.com") + == "bar.com" + ) + assert ( + universe.get_universe_domain(None, None, default_universe="default.com") + == "default.com" + ) + + # EmptyUniverseError raised when resolved value is empty string + with pytest.raises(universe.EmptyUniverseError) as excinfo: + universe.get_universe_domain("", default_universe="default.com") + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + + with pytest.raises(universe.EmptyUniverseError) as excinfo: + universe.get_universe_domain(" ", default_universe="default.com") + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + + with pytest.raises(universe.EmptyUniverseError) as excinfo: + universe.get_universe_domain(None, "", default_universe="default.com") + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + + +def test_get_default_mtls_endpoint(): + # Test valid API endpoints + assert ( + universe.get_default_mtls_endpoint("foo.googleapis.com") + == "foo.mtls.googleapis.com" + ) + assert ( + universe.get_default_mtls_endpoint("foo.sandbox.googleapis.com") + == "foo.mtls.sandbox.googleapis.com" + ) + # Test case-insensitivity + assert ( + universe.get_default_mtls_endpoint("foo.GoogleAPIs.com") + == "foo.mtls.googleapis.com" + ) + assert ( + universe.get_default_mtls_endpoint("foo.Sandbox.GoogleAPIs.com") + == "foo.mtls.sandbox.googleapis.com" + ) + + # Test valid API endpoints with schemes + assert ( + universe.get_default_mtls_endpoint("https://foo.googleapis.com") + == "https://foo.mtls.googleapis.com" + ) + assert ( + universe.get_default_mtls_endpoint("http://foo.googleapis.com:8080/v1") + == "http://foo.mtls.googleapis.com:8080/v1" + ) + + # Test valid API endpoints with ports + assert ( + universe.get_default_mtls_endpoint("foo.googleapis.com:443") + == "foo.mtls.googleapis.com:443" + ) + assert ( + universe.get_default_mtls_endpoint("foo.sandbox.googleapis.com:443") + == "foo.mtls.sandbox.googleapis.com:443" + ) + # Test case-insensitivity with ports + assert ( + universe.get_default_mtls_endpoint("foo.GoogleAPIs.com:443") + == "foo.mtls.googleapis.com:443" + ) + assert ( + universe.get_default_mtls_endpoint("foo.Sandbox.GoogleAPIs.com:443") + == "foo.mtls.sandbox.googleapis.com:443" + ) + + # Test endpoints that shouldn't be converted + assert ( + universe.get_default_mtls_endpoint("foo.mtls.googleapis.com") + == "foo.mtls.googleapis.com" + ) + assert universe.get_default_mtls_endpoint("foo.com") == "foo.com" + assert universe.get_default_mtls_endpoint("foo.com:8080") == "foo.com:8080" + + # Test empty/None endpoints + assert universe.get_default_mtls_endpoint("") == "" + assert universe.get_default_mtls_endpoint(None) is None + + # Test endpoints without host + assert universe.get_default_mtls_endpoint("http://") == "http://" + assert universe.get_default_mtls_endpoint("https://") == "https://" + + +@pytest.mark.parametrize( + "api_override,universe_domain,default_universe,default_mtls_endpoint,default_endpoint_template,use_mtls,expected", + [ + ( + "foo.com", + "googleapis.com", + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + True, + "foo.com", + ), + ( + None, + "googleapis.com", + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + True, + "foo.mtls.googleapis.com", + ), + ( + None, + "googleapis.com", + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + False, + "foo.googleapis.com", + ), + ( + None, + "bar.com", + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + True, + MutualTLSChannelError, + ), + ( + None, + "googleapis.com", + "googleapis.com", + None, + "foo.{UNIVERSE_DOMAIN}", + True, + ValueError, + ), + ], +) +def test_get_api_endpoint( + api_override, + universe_domain, + default_universe, + default_mtls_endpoint, + default_endpoint_template, + use_mtls, + expected, +): + if isinstance(expected, type) and issubclass(expected, Exception): + with pytest.raises(expected): + universe.get_api_endpoint( + api_override, + universe_domain, + default_universe, + default_mtls_endpoint, + default_endpoint_template, + use_mtls, + ) + else: + assert ( + universe.get_api_endpoint( + api_override, + universe_domain, + default_universe, + default_mtls_endpoint, + default_endpoint_template, + use_mtls, + ) + == expected + ) diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/_compat.py b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/_compat.py index 7c684e94b03d..a84aecd6ca3d 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/_compat.py @@ -14,3 +14,152 @@ # limitations under the License. # """A compatibility module for older versions of google-api-core.""" +"""A compatibility module for older versions of google-api-core.""" + +import uuid + +from typing import Optional, Union +from urllib.parse import urlparse, urlunparse + +from google.auth.exceptions import MutualTLSChannelError +from google.api_core.universe import EmptyUniverseError +import google.protobuf.message + +DEFAULT_UNIVERSE = "googleapis.com" + + +def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: + """Converts api endpoint to mTLS endpoint. + + Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to + "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. + Other URLs (including those that do not match these domain suffixes or + already contain '.mtls.') are passed through as-is. + + Args: + api_endpoint (Optional[str]): the api endpoint to convert. + + Returns: + Optional[str]: converted mTLS api endpoint. + """ + if not api_endpoint or ".mtls." in api_endpoint.lower(): + return api_endpoint + + has_scheme = "://" in api_endpoint + if not has_scheme: + parsed = urlparse("//" + api_endpoint) + else: + parsed = urlparse(api_endpoint) + + host = parsed.hostname + if not host: + return api_endpoint + + port = f":{parsed.port}" if parsed.port else "" + + lowered_host = host.lower() + suffix_sandbox = ".sandbox.googleapis.com" + suffix_google = ".googleapis.com" + if lowered_host.endswith(suffix_sandbox): + new_host = host[: -len(suffix_sandbox)] + ".mtls.sandbox.googleapis.com" + elif lowered_host.endswith(suffix_google): + new_host = host[: -len(suffix_google)] + ".mtls.googleapis.com" + else: + return api_endpoint + + netloc = new_host + port + new_parsed = parsed._replace(netloc=netloc) + + if not has_scheme: + return urlunparse(new_parsed)[2:] + else: + return urlunparse(new_parsed) + +def get_api_endpoint( + api_override: Optional[str], + universe_domain: str, + default_universe: str, + default_mtls_endpoint: Optional[str], + default_endpoint_template: str, + use_mtls: bool, +) -> str: + """Return the API endpoint used by the client. + + Args: + api_override (Optional[str]): The API endpoint override. If specified, + this is always returned. + universe_domain (str): The universe domain used by the client. + default_universe (str): The default universe domain. + default_mtls_endpoint (Optional[str]): The default mTLS endpoint. + default_endpoint_template (str): The default endpoint template containing + a placeholder `{UNIVERSE_DOMAIN}`. + use_mtls (bool): Whether to use the mTLS endpoint. + + Returns: + str: The API endpoint to be used by the client. + + Raises: + google.auth.exceptions.MutualTLSChannelError: If mTLS is requested but + not supported in the configured universe domain. + ValueError: If mTLS is requested but no mTLS endpoint is available. + """ + if api_override is not None: + return api_override + + if use_mtls: + if universe_domain.lower() != default_universe.lower(): + raise MutualTLSChannelError( + f"mTLS is not supported in any universe other than {default_universe}." + ) + if not default_mtls_endpoint: + raise ValueError("mTLS endpoint is not available.") + return default_mtls_endpoint + else: + return default_endpoint_template.format(UNIVERSE_DOMAIN=universe_domain) + +def get_universe_domain( + *potential_universes: Optional[str], + default_universe: str, +) -> str: + """Return the universe domain used by the client. + + Args: + *potential_universes (Optional[str]): Potential universe domains in order of preference. + default_universe (str): The default universe domain. + + Returns: + str: The universe domain to be used by the client. + + Raises: + EmptyUniverseError: If the resolved universe domain is an empty string. + """ + resolved = next( + (x.strip() for x in potential_universes if x is not None), + default_universe, + ) + + if not resolved: + raise EmptyUniverseError() + return resolved + +def determine_domain( + client_universe_domain: Optional[str], universe_domain_env: Optional[str] +) -> str: + """Return the universe domain used by the client. + + Args: + client_universe_domain (Optional[str]): The universe domain configured via the client options. + universe_domain_env (Optional[str]): The universe domain configured via the + "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. + + Returns: + str: The universe domain to be used by the client. + + Raises: + ValueError: If the universe domain is an empty string. + """ + return get_universe_domain( + client_universe_domain, + universe_domain_env, + default_universe=DEFAULT_UNIVERSE, + ) diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/client.py b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/client.py index 4ad970da32e9..59bed32f7797 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/client.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/client.py @@ -27,6 +27,7 @@ from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.iam.credentials_v1._compat import determine_domain, get_api_endpoint, get_default_mtls_endpoint from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -103,43 +104,9 @@ class IAMCredentialsClient(metaclass=IAMCredentialsClientMeta): self-signed JSON Web Tokens (JWTs), and more. """ - @staticmethod - def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: - """Converts api endpoint to mTLS endpoint. - - Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to - "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. - Args: - api_endpoint (Optional[str]): the api endpoint to convert. - Returns: - Optional[str]: converted mTLS api endpoint. - """ - if not api_endpoint: - return api_endpoint - - mtls_endpoint_re = re.compile( - r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" - ) - - m = mtls_endpoint_re.match(api_endpoint) - if m is None: - # Could not parse api_endpoint; return as-is. - return api_endpoint - - name, mtls, sandbox, googledomain = m.groups() - if mtls or not googledomain: - return api_endpoint - - if sandbox: - return api_endpoint.replace( - "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" - ) - - return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") - # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "iamcredentials.googleapis.com" - DEFAULT_MTLS_ENDPOINT = _get_default_mtls_endpoint.__func__( # type: ignore + DEFAULT_MTLS_ENDPOINT = get_default_mtls_endpoint( DEFAULT_ENDPOINT ) @@ -386,55 +353,6 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): client_cert_source = mtls.default_client_cert_source() return client_cert_source - @staticmethod - def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: - """Return the API endpoint used by the client. - - Args: - api_override (str): The API endpoint override. If specified, this is always - the return value of this function and the other arguments are not used. - client_cert_source (bytes): The client certificate source used by the client. - universe_domain (str): The universe domain used by the client. - use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. - Possible values are "always", "auto", or "never". - - Returns: - str: The API endpoint to be used by the client. - """ - if api_override is not None: - api_endpoint = api_override - elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): - _default_universe = IAMCredentialsClient._DEFAULT_UNIVERSE - if universe_domain != _default_universe: - raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") - api_endpoint = IAMCredentialsClient.DEFAULT_MTLS_ENDPOINT - else: - api_endpoint = IAMCredentialsClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) - return api_endpoint - - @staticmethod - def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client. - - Args: - client_universe_domain (Optional[str]): The universe domain configured via the client options. - universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. - - Returns: - str: The universe domain to be used by the client. - - Raises: - ValueError: If the universe domain is an empty string. - """ - universe_domain = IAMCredentialsClient._DEFAULT_UNIVERSE - if client_universe_domain is not None: - universe_domain = client_universe_domain - elif universe_domain_env is not None: - universe_domain = universe_domain_env - if len(universe_domain.strip()) == 0: - raise ValueError("Universe Domain cannot be an empty string.") - return universe_domain - def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. @@ -559,7 +477,7 @@ def __init__(self, *, self._use_client_cert, self._use_mtls_endpoint, self._universe_domain_env = IAMCredentialsClient._read_environment_variables() self._client_cert_source = IAMCredentialsClient._get_client_cert_source(self._client_options.client_cert_source, self._use_client_cert) - self._universe_domain = IAMCredentialsClient._get_universe_domain(universe_domain_opt, self._universe_domain_env) + self._universe_domain = determine_domain(universe_domain_opt, self._universe_domain_env) self._api_endpoint: str = "" # updated below, depending on `transport` # Initialize the universe domain validation. @@ -591,7 +509,7 @@ def __init__(self, *, self._api_endpoint = self._transport.host self._api_endpoint = (self._api_endpoint or - IAMCredentialsClient._get_api_endpoint( + get_api_endpoint( self._client_options.api_endpoint, self._client_cert_source, self._universe_domain, diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_compat.py b/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_compat.py index 8491204d1041..4dccd22a58ea 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_compat.py @@ -14,3 +14,206 @@ # limitations under the License. # """Tests for the compatibility module for older versions of google-api-core.""" + +import re +import pytest + +from google.iam.credentials_v1._compat import determine_domain, get_api_endpoint, get_default_mtls_endpoint + +from google.auth.exceptions import MutualTLSChannelError + + +def test_get_universe_domain(): + # When universe_domain is provided + assert ( + universe.get_universe_domain("foo.com", default_universe="default.com") + == "foo.com" + ) + assert ( + universe.get_universe_domain(" foo.com ", default_universe="default.com") + == "foo.com" + ) + + # When universe_domain is None, falls back to default_universe + assert ( + universe.get_universe_domain(None, default_universe="default.com") + == "default.com" + ) + + # When multiple potential universes are provided, resolves in order of preference + assert ( + universe.get_universe_domain( + "foo.com", "bar.com", default_universe="default.com" + ) + == "foo.com" + ) + assert ( + universe.get_universe_domain(None, "bar.com", default_universe="default.com") + == "bar.com" + ) + assert ( + universe.get_universe_domain(None, None, default_universe="default.com") + == "default.com" + ) + + # EmptyUniverseError raised when resolved value is empty string + with pytest.raises(universe.EmptyUniverseError) as excinfo: + universe.get_universe_domain("", default_universe="default.com") + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + + with pytest.raises(universe.EmptyUniverseError) as excinfo: + universe.get_universe_domain(" ", default_universe="default.com") + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + + with pytest.raises(universe.EmptyUniverseError) as excinfo: + universe.get_universe_domain(None, "", default_universe="default.com") + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + + +def test_get_default_mtls_endpoint(): + # Test valid API endpoints + assert ( + universe.get_default_mtls_endpoint("foo.googleapis.com") + == "foo.mtls.googleapis.com" + ) + assert ( + universe.get_default_mtls_endpoint("foo.sandbox.googleapis.com") + == "foo.mtls.sandbox.googleapis.com" + ) + # Test case-insensitivity + assert ( + universe.get_default_mtls_endpoint("foo.GoogleAPIs.com") + == "foo.mtls.googleapis.com" + ) + assert ( + universe.get_default_mtls_endpoint("foo.Sandbox.GoogleAPIs.com") + == "foo.mtls.sandbox.googleapis.com" + ) + + # Test valid API endpoints with schemes + assert ( + universe.get_default_mtls_endpoint("https://foo.googleapis.com") + == "https://foo.mtls.googleapis.com" + ) + assert ( + universe.get_default_mtls_endpoint("http://foo.googleapis.com:8080/v1") + == "http://foo.mtls.googleapis.com:8080/v1" + ) + + # Test valid API endpoints with ports + assert ( + universe.get_default_mtls_endpoint("foo.googleapis.com:443") + == "foo.mtls.googleapis.com:443" + ) + assert ( + universe.get_default_mtls_endpoint("foo.sandbox.googleapis.com:443") + == "foo.mtls.sandbox.googleapis.com:443" + ) + # Test case-insensitivity with ports + assert ( + universe.get_default_mtls_endpoint("foo.GoogleAPIs.com:443") + == "foo.mtls.googleapis.com:443" + ) + assert ( + universe.get_default_mtls_endpoint("foo.Sandbox.GoogleAPIs.com:443") + == "foo.mtls.sandbox.googleapis.com:443" + ) + + # Test endpoints that shouldn't be converted + assert ( + universe.get_default_mtls_endpoint("foo.mtls.googleapis.com") + == "foo.mtls.googleapis.com" + ) + assert universe.get_default_mtls_endpoint("foo.com") == "foo.com" + assert universe.get_default_mtls_endpoint("foo.com:8080") == "foo.com:8080" + + # Test empty/None endpoints + assert universe.get_default_mtls_endpoint("") == "" + assert universe.get_default_mtls_endpoint(None) is None + + # Test endpoints without host + assert universe.get_default_mtls_endpoint("http://") == "http://" + assert universe.get_default_mtls_endpoint("https://") == "https://" + + +@pytest.mark.parametrize( + "api_override,universe_domain,default_universe,default_mtls_endpoint,default_endpoint_template,use_mtls,expected", + [ + ( + "foo.com", + "googleapis.com", + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + True, + "foo.com", + ), + ( + None, + "googleapis.com", + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + True, + "foo.mtls.googleapis.com", + ), + ( + None, + "googleapis.com", + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + False, + "foo.googleapis.com", + ), + ( + None, + "bar.com", + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + True, + MutualTLSChannelError, + ), + ( + None, + "googleapis.com", + "googleapis.com", + None, + "foo.{UNIVERSE_DOMAIN}", + True, + ValueError, + ), + ], +) +def test_get_api_endpoint( + api_override, + universe_domain, + default_universe, + default_mtls_endpoint, + default_endpoint_template, + use_mtls, + expected, +): + if isinstance(expected, type) and issubclass(expected, Exception): + with pytest.raises(expected): + universe.get_api_endpoint( + api_override, + universe_domain, + default_universe, + default_mtls_endpoint, + default_endpoint_template, + use_mtls, + ) + else: + assert ( + universe.get_api_endpoint( + api_override, + universe_domain, + default_universe, + default_mtls_endpoint, + default_endpoint_template, + use_mtls, + ) + == expected + ) diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py b/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py index fb1efaef9fb7..013bc48eb07b 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py @@ -111,22 +111,6 @@ def set_event_loop(): asyncio.set_event_loop(None) -def test__get_default_mtls_endpoint(): - api_endpoint = "example.googleapis.com" - api_mtls_endpoint = "example.mtls.googleapis.com" - sandbox_endpoint = "example.sandbox.googleapis.com" - sandbox_mtls_endpoint = "example.mtls.sandbox.googleapis.com" - non_googleapi = "api.example.com" - custom_endpoint = ".custom" - - assert IAMCredentialsClient._get_default_mtls_endpoint(None) is None - assert IAMCredentialsClient._get_default_mtls_endpoint(api_endpoint) == api_mtls_endpoint - assert IAMCredentialsClient._get_default_mtls_endpoint(api_mtls_endpoint) == api_mtls_endpoint - assert IAMCredentialsClient._get_default_mtls_endpoint(sandbox_endpoint) == sandbox_mtls_endpoint - assert IAMCredentialsClient._get_default_mtls_endpoint(sandbox_mtls_endpoint) == sandbox_mtls_endpoint - assert IAMCredentialsClient._get_default_mtls_endpoint(non_googleapi) == non_googleapi - assert IAMCredentialsClient._get_default_mtls_endpoint(custom_endpoint) == custom_endpoint - def test__read_environment_variables(): assert IAMCredentialsClient._read_environment_variables() == (False, "auto", None) @@ -268,40 +252,6 @@ def test__get_client_cert_source(): assert IAMCredentialsClient._get_client_cert_source(None, True) is mock_default_cert_source assert IAMCredentialsClient._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source -@mock.patch.object(IAMCredentialsClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(IAMCredentialsClient)) -@mock.patch.object(IAMCredentialsAsyncClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(IAMCredentialsAsyncClient)) -def test__get_api_endpoint(): - api_override = "foo.com" - mock_client_cert_source = mock.Mock() - default_universe = IAMCredentialsClient._DEFAULT_UNIVERSE - default_endpoint = IAMCredentialsClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) - mock_universe = "bar.com" - mock_endpoint = IAMCredentialsClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) - - assert IAMCredentialsClient._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override - assert IAMCredentialsClient._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == IAMCredentialsClient.DEFAULT_MTLS_ENDPOINT - assert IAMCredentialsClient._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint - assert IAMCredentialsClient._get_api_endpoint(None, None, default_universe, "always") == IAMCredentialsClient.DEFAULT_MTLS_ENDPOINT - assert IAMCredentialsClient._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == IAMCredentialsClient.DEFAULT_MTLS_ENDPOINT - assert IAMCredentialsClient._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint - assert IAMCredentialsClient._get_api_endpoint(None, None, default_universe, "never") == default_endpoint - - with pytest.raises(MutualTLSChannelError) as excinfo: - IAMCredentialsClient._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") - assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." - - -def test__get_universe_domain(): - client_universe_domain = "foo.com" - universe_domain_env = "bar.com" - - assert IAMCredentialsClient._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain - assert IAMCredentialsClient._get_universe_domain(None, universe_domain_env) == universe_domain_env - assert IAMCredentialsClient._get_universe_domain(None, None) == IAMCredentialsClient._DEFAULT_UNIVERSE - - with pytest.raises(ValueError) as excinfo: - IAMCredentialsClient._get_universe_domain("", None) - assert str(excinfo.value) == "Universe Domain cannot be an empty string." @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), @@ -691,7 +641,7 @@ def test_iam_credentials_client_get_mtls_endpoint_and_cert_source(client_class): config_filename = "mock_certificate_config.json" config_file_content = json.dumps(config_data) m = mock.mock_open(read_data=config_file_content) - with mock.patch("builtins.open", m), mock.patch("os.path.exists", side_effect=lambda path: os.path.basename(path) == config_filename): + with mock.patch("builtins.open", m): with mock.patch.dict( os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": config_filename} ): @@ -739,7 +689,7 @@ def test_iam_credentials_client_get_mtls_endpoint_and_cert_source(client_class): config_filename = "mock_certificate_config.json" config_file_content = json.dumps(config_data) m = mock.mock_open(read_data=config_file_content) - with mock.patch("builtins.open", m), mock.patch("os.path.exists", side_effect=lambda path: os.path.basename(path) == config_filename): + with mock.patch("builtins.open", m): with mock.patch.dict( os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": config_filename} ): diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/_compat.py b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/_compat.py index 7c684e94b03d..a84aecd6ca3d 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/_compat.py @@ -14,3 +14,152 @@ # limitations under the License. # """A compatibility module for older versions of google-api-core.""" +"""A compatibility module for older versions of google-api-core.""" + +import uuid + +from typing import Optional, Union +from urllib.parse import urlparse, urlunparse + +from google.auth.exceptions import MutualTLSChannelError +from google.api_core.universe import EmptyUniverseError +import google.protobuf.message + +DEFAULT_UNIVERSE = "googleapis.com" + + +def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: + """Converts api endpoint to mTLS endpoint. + + Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to + "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. + Other URLs (including those that do not match these domain suffixes or + already contain '.mtls.') are passed through as-is. + + Args: + api_endpoint (Optional[str]): the api endpoint to convert. + + Returns: + Optional[str]: converted mTLS api endpoint. + """ + if not api_endpoint or ".mtls." in api_endpoint.lower(): + return api_endpoint + + has_scheme = "://" in api_endpoint + if not has_scheme: + parsed = urlparse("//" + api_endpoint) + else: + parsed = urlparse(api_endpoint) + + host = parsed.hostname + if not host: + return api_endpoint + + port = f":{parsed.port}" if parsed.port else "" + + lowered_host = host.lower() + suffix_sandbox = ".sandbox.googleapis.com" + suffix_google = ".googleapis.com" + if lowered_host.endswith(suffix_sandbox): + new_host = host[: -len(suffix_sandbox)] + ".mtls.sandbox.googleapis.com" + elif lowered_host.endswith(suffix_google): + new_host = host[: -len(suffix_google)] + ".mtls.googleapis.com" + else: + return api_endpoint + + netloc = new_host + port + new_parsed = parsed._replace(netloc=netloc) + + if not has_scheme: + return urlunparse(new_parsed)[2:] + else: + return urlunparse(new_parsed) + +def get_api_endpoint( + api_override: Optional[str], + universe_domain: str, + default_universe: str, + default_mtls_endpoint: Optional[str], + default_endpoint_template: str, + use_mtls: bool, +) -> str: + """Return the API endpoint used by the client. + + Args: + api_override (Optional[str]): The API endpoint override. If specified, + this is always returned. + universe_domain (str): The universe domain used by the client. + default_universe (str): The default universe domain. + default_mtls_endpoint (Optional[str]): The default mTLS endpoint. + default_endpoint_template (str): The default endpoint template containing + a placeholder `{UNIVERSE_DOMAIN}`. + use_mtls (bool): Whether to use the mTLS endpoint. + + Returns: + str: The API endpoint to be used by the client. + + Raises: + google.auth.exceptions.MutualTLSChannelError: If mTLS is requested but + not supported in the configured universe domain. + ValueError: If mTLS is requested but no mTLS endpoint is available. + """ + if api_override is not None: + return api_override + + if use_mtls: + if universe_domain.lower() != default_universe.lower(): + raise MutualTLSChannelError( + f"mTLS is not supported in any universe other than {default_universe}." + ) + if not default_mtls_endpoint: + raise ValueError("mTLS endpoint is not available.") + return default_mtls_endpoint + else: + return default_endpoint_template.format(UNIVERSE_DOMAIN=universe_domain) + +def get_universe_domain( + *potential_universes: Optional[str], + default_universe: str, +) -> str: + """Return the universe domain used by the client. + + Args: + *potential_universes (Optional[str]): Potential universe domains in order of preference. + default_universe (str): The default universe domain. + + Returns: + str: The universe domain to be used by the client. + + Raises: + EmptyUniverseError: If the resolved universe domain is an empty string. + """ + resolved = next( + (x.strip() for x in potential_universes if x is not None), + default_universe, + ) + + if not resolved: + raise EmptyUniverseError() + return resolved + +def determine_domain( + client_universe_domain: Optional[str], universe_domain_env: Optional[str] +) -> str: + """Return the universe domain used by the client. + + Args: + client_universe_domain (Optional[str]): The universe domain configured via the client options. + universe_domain_env (Optional[str]): The universe domain configured via the + "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. + + Returns: + str: The universe domain to be used by the client. + + Raises: + ValueError: If the universe domain is an empty string. + """ + return get_universe_domain( + client_universe_domain, + universe_domain_env, + default_universe=DEFAULT_UNIVERSE, + ) diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/client.py b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/client.py index 7255d3c91709..d84eca145f6a 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/client.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/client.py @@ -27,6 +27,7 @@ from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.cloud.eventarc_v1._compat import determine_domain, get_api_endpoint, get_default_mtls_endpoint from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -121,43 +122,9 @@ class EventarcClient(metaclass=EventarcClientMeta): destinations. """ - @staticmethod - def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: - """Converts api endpoint to mTLS endpoint. - - Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to - "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. - Args: - api_endpoint (Optional[str]): the api endpoint to convert. - Returns: - Optional[str]: converted mTLS api endpoint. - """ - if not api_endpoint: - return api_endpoint - - mtls_endpoint_re = re.compile( - r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" - ) - - m = mtls_endpoint_re.match(api_endpoint) - if m is None: - # Could not parse api_endpoint; return as-is. - return api_endpoint - - name, mtls, sandbox, googledomain = m.groups() - if mtls or not googledomain: - return api_endpoint - - if sandbox: - return api_endpoint.replace( - "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" - ) - - return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") - # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "eventarc.googleapis.com" - DEFAULT_MTLS_ENDPOINT = _get_default_mtls_endpoint.__func__( # type: ignore + DEFAULT_MTLS_ENDPOINT = get_default_mtls_endpoint( DEFAULT_ENDPOINT ) @@ -569,55 +536,6 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): client_cert_source = mtls.default_client_cert_source() return client_cert_source - @staticmethod - def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: - """Return the API endpoint used by the client. - - Args: - api_override (str): The API endpoint override. If specified, this is always - the return value of this function and the other arguments are not used. - client_cert_source (bytes): The client certificate source used by the client. - universe_domain (str): The universe domain used by the client. - use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. - Possible values are "always", "auto", or "never". - - Returns: - str: The API endpoint to be used by the client. - """ - if api_override is not None: - api_endpoint = api_override - elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): - _default_universe = EventarcClient._DEFAULT_UNIVERSE - if universe_domain != _default_universe: - raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") - api_endpoint = EventarcClient.DEFAULT_MTLS_ENDPOINT - else: - api_endpoint = EventarcClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) - return api_endpoint - - @staticmethod - def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client. - - Args: - client_universe_domain (Optional[str]): The universe domain configured via the client options. - universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. - - Returns: - str: The universe domain to be used by the client. - - Raises: - ValueError: If the universe domain is an empty string. - """ - universe_domain = EventarcClient._DEFAULT_UNIVERSE - if client_universe_domain is not None: - universe_domain = client_universe_domain - elif universe_domain_env is not None: - universe_domain = universe_domain_env - if len(universe_domain.strip()) == 0: - raise ValueError("Universe Domain cannot be an empty string.") - return universe_domain - def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. @@ -742,7 +660,7 @@ def __init__(self, *, self._use_client_cert, self._use_mtls_endpoint, self._universe_domain_env = EventarcClient._read_environment_variables() self._client_cert_source = EventarcClient._get_client_cert_source(self._client_options.client_cert_source, self._use_client_cert) - self._universe_domain = EventarcClient._get_universe_domain(universe_domain_opt, self._universe_domain_env) + self._universe_domain = determine_domain(universe_domain_opt, self._universe_domain_env) self._api_endpoint: str = "" # updated below, depending on `transport` # Initialize the universe domain validation. @@ -774,7 +692,7 @@ def __init__(self, *, self._api_endpoint = self._transport.host self._api_endpoint = (self._api_endpoint or - EventarcClient._get_api_endpoint( + get_api_endpoint( self._client_options.api_endpoint, self._client_cert_source, self._universe_domain, diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_compat.py b/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_compat.py index 8491204d1041..3ac12e2e8db4 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_compat.py @@ -14,3 +14,206 @@ # limitations under the License. # """Tests for the compatibility module for older versions of google-api-core.""" + +import re +import pytest + +from google.cloud.eventarc_v1._compat import determine_domain, get_api_endpoint, get_default_mtls_endpoint + +from google.auth.exceptions import MutualTLSChannelError + + +def test_get_universe_domain(): + # When universe_domain is provided + assert ( + universe.get_universe_domain("foo.com", default_universe="default.com") + == "foo.com" + ) + assert ( + universe.get_universe_domain(" foo.com ", default_universe="default.com") + == "foo.com" + ) + + # When universe_domain is None, falls back to default_universe + assert ( + universe.get_universe_domain(None, default_universe="default.com") + == "default.com" + ) + + # When multiple potential universes are provided, resolves in order of preference + assert ( + universe.get_universe_domain( + "foo.com", "bar.com", default_universe="default.com" + ) + == "foo.com" + ) + assert ( + universe.get_universe_domain(None, "bar.com", default_universe="default.com") + == "bar.com" + ) + assert ( + universe.get_universe_domain(None, None, default_universe="default.com") + == "default.com" + ) + + # EmptyUniverseError raised when resolved value is empty string + with pytest.raises(universe.EmptyUniverseError) as excinfo: + universe.get_universe_domain("", default_universe="default.com") + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + + with pytest.raises(universe.EmptyUniverseError) as excinfo: + universe.get_universe_domain(" ", default_universe="default.com") + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + + with pytest.raises(universe.EmptyUniverseError) as excinfo: + universe.get_universe_domain(None, "", default_universe="default.com") + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + + +def test_get_default_mtls_endpoint(): + # Test valid API endpoints + assert ( + universe.get_default_mtls_endpoint("foo.googleapis.com") + == "foo.mtls.googleapis.com" + ) + assert ( + universe.get_default_mtls_endpoint("foo.sandbox.googleapis.com") + == "foo.mtls.sandbox.googleapis.com" + ) + # Test case-insensitivity + assert ( + universe.get_default_mtls_endpoint("foo.GoogleAPIs.com") + == "foo.mtls.googleapis.com" + ) + assert ( + universe.get_default_mtls_endpoint("foo.Sandbox.GoogleAPIs.com") + == "foo.mtls.sandbox.googleapis.com" + ) + + # Test valid API endpoints with schemes + assert ( + universe.get_default_mtls_endpoint("https://foo.googleapis.com") + == "https://foo.mtls.googleapis.com" + ) + assert ( + universe.get_default_mtls_endpoint("http://foo.googleapis.com:8080/v1") + == "http://foo.mtls.googleapis.com:8080/v1" + ) + + # Test valid API endpoints with ports + assert ( + universe.get_default_mtls_endpoint("foo.googleapis.com:443") + == "foo.mtls.googleapis.com:443" + ) + assert ( + universe.get_default_mtls_endpoint("foo.sandbox.googleapis.com:443") + == "foo.mtls.sandbox.googleapis.com:443" + ) + # Test case-insensitivity with ports + assert ( + universe.get_default_mtls_endpoint("foo.GoogleAPIs.com:443") + == "foo.mtls.googleapis.com:443" + ) + assert ( + universe.get_default_mtls_endpoint("foo.Sandbox.GoogleAPIs.com:443") + == "foo.mtls.sandbox.googleapis.com:443" + ) + + # Test endpoints that shouldn't be converted + assert ( + universe.get_default_mtls_endpoint("foo.mtls.googleapis.com") + == "foo.mtls.googleapis.com" + ) + assert universe.get_default_mtls_endpoint("foo.com") == "foo.com" + assert universe.get_default_mtls_endpoint("foo.com:8080") == "foo.com:8080" + + # Test empty/None endpoints + assert universe.get_default_mtls_endpoint("") == "" + assert universe.get_default_mtls_endpoint(None) is None + + # Test endpoints without host + assert universe.get_default_mtls_endpoint("http://") == "http://" + assert universe.get_default_mtls_endpoint("https://") == "https://" + + +@pytest.mark.parametrize( + "api_override,universe_domain,default_universe,default_mtls_endpoint,default_endpoint_template,use_mtls,expected", + [ + ( + "foo.com", + "googleapis.com", + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + True, + "foo.com", + ), + ( + None, + "googleapis.com", + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + True, + "foo.mtls.googleapis.com", + ), + ( + None, + "googleapis.com", + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + False, + "foo.googleapis.com", + ), + ( + None, + "bar.com", + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + True, + MutualTLSChannelError, + ), + ( + None, + "googleapis.com", + "googleapis.com", + None, + "foo.{UNIVERSE_DOMAIN}", + True, + ValueError, + ), + ], +) +def test_get_api_endpoint( + api_override, + universe_domain, + default_universe, + default_mtls_endpoint, + default_endpoint_template, + use_mtls, + expected, +): + if isinstance(expected, type) and issubclass(expected, Exception): + with pytest.raises(expected): + universe.get_api_endpoint( + api_override, + universe_domain, + default_universe, + default_mtls_endpoint, + default_endpoint_template, + use_mtls, + ) + else: + assert ( + universe.get_api_endpoint( + api_override, + universe_domain, + default_universe, + default_mtls_endpoint, + default_endpoint_template, + use_mtls, + ) + == expected + ) diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py b/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py index a1c06b19b6f7..426994e546a2 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py @@ -139,22 +139,6 @@ def set_event_loop(): asyncio.set_event_loop(None) -def test__get_default_mtls_endpoint(): - api_endpoint = "example.googleapis.com" - api_mtls_endpoint = "example.mtls.googleapis.com" - sandbox_endpoint = "example.sandbox.googleapis.com" - sandbox_mtls_endpoint = "example.mtls.sandbox.googleapis.com" - non_googleapi = "api.example.com" - custom_endpoint = ".custom" - - assert EventarcClient._get_default_mtls_endpoint(None) is None - assert EventarcClient._get_default_mtls_endpoint(api_endpoint) == api_mtls_endpoint - assert EventarcClient._get_default_mtls_endpoint(api_mtls_endpoint) == api_mtls_endpoint - assert EventarcClient._get_default_mtls_endpoint(sandbox_endpoint) == sandbox_mtls_endpoint - assert EventarcClient._get_default_mtls_endpoint(sandbox_mtls_endpoint) == sandbox_mtls_endpoint - assert EventarcClient._get_default_mtls_endpoint(non_googleapi) == non_googleapi - assert EventarcClient._get_default_mtls_endpoint(custom_endpoint) == custom_endpoint - def test__read_environment_variables(): assert EventarcClient._read_environment_variables() == (False, "auto", None) @@ -296,40 +280,6 @@ def test__get_client_cert_source(): assert EventarcClient._get_client_cert_source(None, True) is mock_default_cert_source assert EventarcClient._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source -@mock.patch.object(EventarcClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(EventarcClient)) -@mock.patch.object(EventarcAsyncClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(EventarcAsyncClient)) -def test__get_api_endpoint(): - api_override = "foo.com" - mock_client_cert_source = mock.Mock() - default_universe = EventarcClient._DEFAULT_UNIVERSE - default_endpoint = EventarcClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) - mock_universe = "bar.com" - mock_endpoint = EventarcClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) - - assert EventarcClient._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override - assert EventarcClient._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == EventarcClient.DEFAULT_MTLS_ENDPOINT - assert EventarcClient._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint - assert EventarcClient._get_api_endpoint(None, None, default_universe, "always") == EventarcClient.DEFAULT_MTLS_ENDPOINT - assert EventarcClient._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == EventarcClient.DEFAULT_MTLS_ENDPOINT - assert EventarcClient._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint - assert EventarcClient._get_api_endpoint(None, None, default_universe, "never") == default_endpoint - - with pytest.raises(MutualTLSChannelError) as excinfo: - EventarcClient._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") - assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." - - -def test__get_universe_domain(): - client_universe_domain = "foo.com" - universe_domain_env = "bar.com" - - assert EventarcClient._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain - assert EventarcClient._get_universe_domain(None, universe_domain_env) == universe_domain_env - assert EventarcClient._get_universe_domain(None, None) == EventarcClient._DEFAULT_UNIVERSE - - with pytest.raises(ValueError) as excinfo: - EventarcClient._get_universe_domain("", None) - assert str(excinfo.value) == "Universe Domain cannot be an empty string." @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), @@ -719,7 +669,7 @@ def test_eventarc_client_get_mtls_endpoint_and_cert_source(client_class): config_filename = "mock_certificate_config.json" config_file_content = json.dumps(config_data) m = mock.mock_open(read_data=config_file_content) - with mock.patch("builtins.open", m), mock.patch("os.path.exists", side_effect=lambda path: os.path.basename(path) == config_filename): + with mock.patch("builtins.open", m): with mock.patch.dict( os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": config_filename} ): @@ -767,7 +717,7 @@ def test_eventarc_client_get_mtls_endpoint_and_cert_source(client_class): config_filename = "mock_certificate_config.json" config_file_content = json.dumps(config_data) m = mock.mock_open(read_data=config_file_content) - with mock.patch("builtins.open", m), mock.patch("os.path.exists", side_effect=lambda path: os.path.basename(path) == config_filename): + with mock.patch("builtins.open", m): with mock.patch.dict( os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": config_filename} ): diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/_compat.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/_compat.py index 7c684e94b03d..a84aecd6ca3d 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/_compat.py @@ -14,3 +14,152 @@ # limitations under the License. # """A compatibility module for older versions of google-api-core.""" +"""A compatibility module for older versions of google-api-core.""" + +import uuid + +from typing import Optional, Union +from urllib.parse import urlparse, urlunparse + +from google.auth.exceptions import MutualTLSChannelError +from google.api_core.universe import EmptyUniverseError +import google.protobuf.message + +DEFAULT_UNIVERSE = "googleapis.com" + + +def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: + """Converts api endpoint to mTLS endpoint. + + Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to + "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. + Other URLs (including those that do not match these domain suffixes or + already contain '.mtls.') are passed through as-is. + + Args: + api_endpoint (Optional[str]): the api endpoint to convert. + + Returns: + Optional[str]: converted mTLS api endpoint. + """ + if not api_endpoint or ".mtls." in api_endpoint.lower(): + return api_endpoint + + has_scheme = "://" in api_endpoint + if not has_scheme: + parsed = urlparse("//" + api_endpoint) + else: + parsed = urlparse(api_endpoint) + + host = parsed.hostname + if not host: + return api_endpoint + + port = f":{parsed.port}" if parsed.port else "" + + lowered_host = host.lower() + suffix_sandbox = ".sandbox.googleapis.com" + suffix_google = ".googleapis.com" + if lowered_host.endswith(suffix_sandbox): + new_host = host[: -len(suffix_sandbox)] + ".mtls.sandbox.googleapis.com" + elif lowered_host.endswith(suffix_google): + new_host = host[: -len(suffix_google)] + ".mtls.googleapis.com" + else: + return api_endpoint + + netloc = new_host + port + new_parsed = parsed._replace(netloc=netloc) + + if not has_scheme: + return urlunparse(new_parsed)[2:] + else: + return urlunparse(new_parsed) + +def get_api_endpoint( + api_override: Optional[str], + universe_domain: str, + default_universe: str, + default_mtls_endpoint: Optional[str], + default_endpoint_template: str, + use_mtls: bool, +) -> str: + """Return the API endpoint used by the client. + + Args: + api_override (Optional[str]): The API endpoint override. If specified, + this is always returned. + universe_domain (str): The universe domain used by the client. + default_universe (str): The default universe domain. + default_mtls_endpoint (Optional[str]): The default mTLS endpoint. + default_endpoint_template (str): The default endpoint template containing + a placeholder `{UNIVERSE_DOMAIN}`. + use_mtls (bool): Whether to use the mTLS endpoint. + + Returns: + str: The API endpoint to be used by the client. + + Raises: + google.auth.exceptions.MutualTLSChannelError: If mTLS is requested but + not supported in the configured universe domain. + ValueError: If mTLS is requested but no mTLS endpoint is available. + """ + if api_override is not None: + return api_override + + if use_mtls: + if universe_domain.lower() != default_universe.lower(): + raise MutualTLSChannelError( + f"mTLS is not supported in any universe other than {default_universe}." + ) + if not default_mtls_endpoint: + raise ValueError("mTLS endpoint is not available.") + return default_mtls_endpoint + else: + return default_endpoint_template.format(UNIVERSE_DOMAIN=universe_domain) + +def get_universe_domain( + *potential_universes: Optional[str], + default_universe: str, +) -> str: + """Return the universe domain used by the client. + + Args: + *potential_universes (Optional[str]): Potential universe domains in order of preference. + default_universe (str): The default universe domain. + + Returns: + str: The universe domain to be used by the client. + + Raises: + EmptyUniverseError: If the resolved universe domain is an empty string. + """ + resolved = next( + (x.strip() for x in potential_universes if x is not None), + default_universe, + ) + + if not resolved: + raise EmptyUniverseError() + return resolved + +def determine_domain( + client_universe_domain: Optional[str], universe_domain_env: Optional[str] +) -> str: + """Return the universe domain used by the client. + + Args: + client_universe_domain (Optional[str]): The universe domain configured via the client options. + universe_domain_env (Optional[str]): The universe domain configured via the + "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. + + Returns: + str: The universe domain to be used by the client. + + Raises: + ValueError: If the universe domain is an empty string. + """ + return get_universe_domain( + client_universe_domain, + universe_domain_env, + default_universe=DEFAULT_UNIVERSE, + ) diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/client.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/client.py index 15922e6d865a..07bf993c0c81 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/client.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/client.py @@ -27,6 +27,7 @@ from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.cloud.logging_v2._compat import determine_domain, get_api_endpoint, get_default_mtls_endpoint from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -96,43 +97,9 @@ def get_transport_class(cls, class ConfigServiceV2Client(metaclass=ConfigServiceV2ClientMeta): """Service for configuring sinks used to route log entries.""" - @staticmethod - def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: - """Converts api endpoint to mTLS endpoint. - - Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to - "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. - Args: - api_endpoint (Optional[str]): the api endpoint to convert. - Returns: - Optional[str]: converted mTLS api endpoint. - """ - if not api_endpoint: - return api_endpoint - - mtls_endpoint_re = re.compile( - r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" - ) - - m = mtls_endpoint_re.match(api_endpoint) - if m is None: - # Could not parse api_endpoint; return as-is. - return api_endpoint - - name, mtls, sandbox, googledomain = m.groups() - if mtls or not googledomain: - return api_endpoint - - if sandbox: - return api_endpoint.replace( - "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" - ) - - return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") - # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "logging.googleapis.com" - DEFAULT_MTLS_ENDPOINT = _get_default_mtls_endpoint.__func__( # type: ignore + DEFAULT_MTLS_ENDPOINT = get_default_mtls_endpoint( DEFAULT_ENDPOINT ) @@ -445,55 +412,6 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): client_cert_source = mtls.default_client_cert_source() return client_cert_source - @staticmethod - def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: - """Return the API endpoint used by the client. - - Args: - api_override (str): The API endpoint override. If specified, this is always - the return value of this function and the other arguments are not used. - client_cert_source (bytes): The client certificate source used by the client. - universe_domain (str): The universe domain used by the client. - use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. - Possible values are "always", "auto", or "never". - - Returns: - str: The API endpoint to be used by the client. - """ - if api_override is not None: - api_endpoint = api_override - elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): - _default_universe = ConfigServiceV2Client._DEFAULT_UNIVERSE - if universe_domain != _default_universe: - raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") - api_endpoint = ConfigServiceV2Client.DEFAULT_MTLS_ENDPOINT - else: - api_endpoint = ConfigServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) - return api_endpoint - - @staticmethod - def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client. - - Args: - client_universe_domain (Optional[str]): The universe domain configured via the client options. - universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. - - Returns: - str: The universe domain to be used by the client. - - Raises: - ValueError: If the universe domain is an empty string. - """ - universe_domain = ConfigServiceV2Client._DEFAULT_UNIVERSE - if client_universe_domain is not None: - universe_domain = client_universe_domain - elif universe_domain_env is not None: - universe_domain = universe_domain_env - if len(universe_domain.strip()) == 0: - raise ValueError("Universe Domain cannot be an empty string.") - return universe_domain - def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. @@ -615,7 +533,7 @@ def __init__(self, *, self._use_client_cert, self._use_mtls_endpoint, self._universe_domain_env = ConfigServiceV2Client._read_environment_variables() self._client_cert_source = ConfigServiceV2Client._get_client_cert_source(self._client_options.client_cert_source, self._use_client_cert) - self._universe_domain = ConfigServiceV2Client._get_universe_domain(universe_domain_opt, self._universe_domain_env) + self._universe_domain = determine_domain(universe_domain_opt, self._universe_domain_env) self._api_endpoint: str = "" # updated below, depending on `transport` # Initialize the universe domain validation. @@ -647,7 +565,7 @@ def __init__(self, *, self._api_endpoint = self._transport.host self._api_endpoint = (self._api_endpoint or - ConfigServiceV2Client._get_api_endpoint( + get_api_endpoint( self._client_options.api_endpoint, self._client_cert_source, self._universe_domain, diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/client.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/client.py index e89762755eda..adc1866df4b2 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/client.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/client.py @@ -27,6 +27,7 @@ from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.cloud.logging_v2._compat import determine_domain, get_api_endpoint, get_default_mtls_endpoint from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -93,43 +94,9 @@ def get_transport_class(cls, class LoggingServiceV2Client(metaclass=LoggingServiceV2ClientMeta): """Service for ingesting and querying logs.""" - @staticmethod - def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: - """Converts api endpoint to mTLS endpoint. - - Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to - "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. - Args: - api_endpoint (Optional[str]): the api endpoint to convert. - Returns: - Optional[str]: converted mTLS api endpoint. - """ - if not api_endpoint: - return api_endpoint - - mtls_endpoint_re = re.compile( - r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" - ) - - m = mtls_endpoint_re.match(api_endpoint) - if m is None: - # Could not parse api_endpoint; return as-is. - return api_endpoint - - name, mtls, sandbox, googledomain = m.groups() - if mtls or not googledomain: - return api_endpoint - - if sandbox: - return api_endpoint.replace( - "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" - ) - - return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") - # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "logging.googleapis.com" - DEFAULT_MTLS_ENDPOINT = _get_default_mtls_endpoint.__func__( # type: ignore + DEFAULT_MTLS_ENDPOINT = get_default_mtls_endpoint( DEFAULT_ENDPOINT ) @@ -376,55 +343,6 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): client_cert_source = mtls.default_client_cert_source() return client_cert_source - @staticmethod - def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: - """Return the API endpoint used by the client. - - Args: - api_override (str): The API endpoint override. If specified, this is always - the return value of this function and the other arguments are not used. - client_cert_source (bytes): The client certificate source used by the client. - universe_domain (str): The universe domain used by the client. - use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. - Possible values are "always", "auto", or "never". - - Returns: - str: The API endpoint to be used by the client. - """ - if api_override is not None: - api_endpoint = api_override - elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): - _default_universe = LoggingServiceV2Client._DEFAULT_UNIVERSE - if universe_domain != _default_universe: - raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") - api_endpoint = LoggingServiceV2Client.DEFAULT_MTLS_ENDPOINT - else: - api_endpoint = LoggingServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) - return api_endpoint - - @staticmethod - def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client. - - Args: - client_universe_domain (Optional[str]): The universe domain configured via the client options. - universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. - - Returns: - str: The universe domain to be used by the client. - - Raises: - ValueError: If the universe domain is an empty string. - """ - universe_domain = LoggingServiceV2Client._DEFAULT_UNIVERSE - if client_universe_domain is not None: - universe_domain = client_universe_domain - elif universe_domain_env is not None: - universe_domain = universe_domain_env - if len(universe_domain.strip()) == 0: - raise ValueError("Universe Domain cannot be an empty string.") - return universe_domain - def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. @@ -546,7 +464,7 @@ def __init__(self, *, self._use_client_cert, self._use_mtls_endpoint, self._universe_domain_env = LoggingServiceV2Client._read_environment_variables() self._client_cert_source = LoggingServiceV2Client._get_client_cert_source(self._client_options.client_cert_source, self._use_client_cert) - self._universe_domain = LoggingServiceV2Client._get_universe_domain(universe_domain_opt, self._universe_domain_env) + self._universe_domain = determine_domain(universe_domain_opt, self._universe_domain_env) self._api_endpoint: str = "" # updated below, depending on `transport` # Initialize the universe domain validation. @@ -578,7 +496,7 @@ def __init__(self, *, self._api_endpoint = self._transport.host self._api_endpoint = (self._api_endpoint or - LoggingServiceV2Client._get_api_endpoint( + get_api_endpoint( self._client_options.api_endpoint, self._client_cert_source, self._universe_domain, diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/client.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/client.py index 90e9355f8c26..4060176f9907 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/client.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/client.py @@ -27,6 +27,7 @@ from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.cloud.logging_v2._compat import determine_domain, get_api_endpoint, get_default_mtls_endpoint from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -94,43 +95,9 @@ def get_transport_class(cls, class MetricsServiceV2Client(metaclass=MetricsServiceV2ClientMeta): """Service for configuring logs-based metrics.""" - @staticmethod - def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: - """Converts api endpoint to mTLS endpoint. - - Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to - "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. - Args: - api_endpoint (Optional[str]): the api endpoint to convert. - Returns: - Optional[str]: converted mTLS api endpoint. - """ - if not api_endpoint: - return api_endpoint - - mtls_endpoint_re = re.compile( - r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" - ) - - m = mtls_endpoint_re.match(api_endpoint) - if m is None: - # Could not parse api_endpoint; return as-is. - return api_endpoint - - name, mtls, sandbox, googledomain = m.groups() - if mtls or not googledomain: - return api_endpoint - - if sandbox: - return api_endpoint.replace( - "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" - ) - - return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") - # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "logging.googleapis.com" - DEFAULT_MTLS_ENDPOINT = _get_default_mtls_endpoint.__func__( # type: ignore + DEFAULT_MTLS_ENDPOINT = get_default_mtls_endpoint( DEFAULT_ENDPOINT ) @@ -377,55 +344,6 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): client_cert_source = mtls.default_client_cert_source() return client_cert_source - @staticmethod - def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: - """Return the API endpoint used by the client. - - Args: - api_override (str): The API endpoint override. If specified, this is always - the return value of this function and the other arguments are not used. - client_cert_source (bytes): The client certificate source used by the client. - universe_domain (str): The universe domain used by the client. - use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. - Possible values are "always", "auto", or "never". - - Returns: - str: The API endpoint to be used by the client. - """ - if api_override is not None: - api_endpoint = api_override - elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): - _default_universe = MetricsServiceV2Client._DEFAULT_UNIVERSE - if universe_domain != _default_universe: - raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") - api_endpoint = MetricsServiceV2Client.DEFAULT_MTLS_ENDPOINT - else: - api_endpoint = MetricsServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) - return api_endpoint - - @staticmethod - def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client. - - Args: - client_universe_domain (Optional[str]): The universe domain configured via the client options. - universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. - - Returns: - str: The universe domain to be used by the client. - - Raises: - ValueError: If the universe domain is an empty string. - """ - universe_domain = MetricsServiceV2Client._DEFAULT_UNIVERSE - if client_universe_domain is not None: - universe_domain = client_universe_domain - elif universe_domain_env is not None: - universe_domain = universe_domain_env - if len(universe_domain.strip()) == 0: - raise ValueError("Universe Domain cannot be an empty string.") - return universe_domain - def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. @@ -547,7 +465,7 @@ def __init__(self, *, self._use_client_cert, self._use_mtls_endpoint, self._universe_domain_env = MetricsServiceV2Client._read_environment_variables() self._client_cert_source = MetricsServiceV2Client._get_client_cert_source(self._client_options.client_cert_source, self._use_client_cert) - self._universe_domain = MetricsServiceV2Client._get_universe_domain(universe_domain_opt, self._universe_domain_env) + self._universe_domain = determine_domain(universe_domain_opt, self._universe_domain_env) self._api_endpoint: str = "" # updated below, depending on `transport` # Initialize the universe domain validation. @@ -579,7 +497,7 @@ def __init__(self, *, self._api_endpoint = self._transport.host self._api_endpoint = (self._api_endpoint or - MetricsServiceV2Client._get_api_endpoint( + get_api_endpoint( self._client_options.api_endpoint, self._client_cert_source, self._universe_domain, diff --git a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_compat.py b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_compat.py index 8491204d1041..1a22e6c701d0 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_compat.py @@ -14,3 +14,206 @@ # limitations under the License. # """Tests for the compatibility module for older versions of google-api-core.""" + +import re +import pytest + +from google.cloud.logging_v2._compat import determine_domain, get_api_endpoint, get_default_mtls_endpoint + +from google.auth.exceptions import MutualTLSChannelError + + +def test_get_universe_domain(): + # When universe_domain is provided + assert ( + universe.get_universe_domain("foo.com", default_universe="default.com") + == "foo.com" + ) + assert ( + universe.get_universe_domain(" foo.com ", default_universe="default.com") + == "foo.com" + ) + + # When universe_domain is None, falls back to default_universe + assert ( + universe.get_universe_domain(None, default_universe="default.com") + == "default.com" + ) + + # When multiple potential universes are provided, resolves in order of preference + assert ( + universe.get_universe_domain( + "foo.com", "bar.com", default_universe="default.com" + ) + == "foo.com" + ) + assert ( + universe.get_universe_domain(None, "bar.com", default_universe="default.com") + == "bar.com" + ) + assert ( + universe.get_universe_domain(None, None, default_universe="default.com") + == "default.com" + ) + + # EmptyUniverseError raised when resolved value is empty string + with pytest.raises(universe.EmptyUniverseError) as excinfo: + universe.get_universe_domain("", default_universe="default.com") + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + + with pytest.raises(universe.EmptyUniverseError) as excinfo: + universe.get_universe_domain(" ", default_universe="default.com") + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + + with pytest.raises(universe.EmptyUniverseError) as excinfo: + universe.get_universe_domain(None, "", default_universe="default.com") + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + + +def test_get_default_mtls_endpoint(): + # Test valid API endpoints + assert ( + universe.get_default_mtls_endpoint("foo.googleapis.com") + == "foo.mtls.googleapis.com" + ) + assert ( + universe.get_default_mtls_endpoint("foo.sandbox.googleapis.com") + == "foo.mtls.sandbox.googleapis.com" + ) + # Test case-insensitivity + assert ( + universe.get_default_mtls_endpoint("foo.GoogleAPIs.com") + == "foo.mtls.googleapis.com" + ) + assert ( + universe.get_default_mtls_endpoint("foo.Sandbox.GoogleAPIs.com") + == "foo.mtls.sandbox.googleapis.com" + ) + + # Test valid API endpoints with schemes + assert ( + universe.get_default_mtls_endpoint("https://foo.googleapis.com") + == "https://foo.mtls.googleapis.com" + ) + assert ( + universe.get_default_mtls_endpoint("http://foo.googleapis.com:8080/v1") + == "http://foo.mtls.googleapis.com:8080/v1" + ) + + # Test valid API endpoints with ports + assert ( + universe.get_default_mtls_endpoint("foo.googleapis.com:443") + == "foo.mtls.googleapis.com:443" + ) + assert ( + universe.get_default_mtls_endpoint("foo.sandbox.googleapis.com:443") + == "foo.mtls.sandbox.googleapis.com:443" + ) + # Test case-insensitivity with ports + assert ( + universe.get_default_mtls_endpoint("foo.GoogleAPIs.com:443") + == "foo.mtls.googleapis.com:443" + ) + assert ( + universe.get_default_mtls_endpoint("foo.Sandbox.GoogleAPIs.com:443") + == "foo.mtls.sandbox.googleapis.com:443" + ) + + # Test endpoints that shouldn't be converted + assert ( + universe.get_default_mtls_endpoint("foo.mtls.googleapis.com") + == "foo.mtls.googleapis.com" + ) + assert universe.get_default_mtls_endpoint("foo.com") == "foo.com" + assert universe.get_default_mtls_endpoint("foo.com:8080") == "foo.com:8080" + + # Test empty/None endpoints + assert universe.get_default_mtls_endpoint("") == "" + assert universe.get_default_mtls_endpoint(None) is None + + # Test endpoints without host + assert universe.get_default_mtls_endpoint("http://") == "http://" + assert universe.get_default_mtls_endpoint("https://") == "https://" + + +@pytest.mark.parametrize( + "api_override,universe_domain,default_universe,default_mtls_endpoint,default_endpoint_template,use_mtls,expected", + [ + ( + "foo.com", + "googleapis.com", + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + True, + "foo.com", + ), + ( + None, + "googleapis.com", + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + True, + "foo.mtls.googleapis.com", + ), + ( + None, + "googleapis.com", + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + False, + "foo.googleapis.com", + ), + ( + None, + "bar.com", + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + True, + MutualTLSChannelError, + ), + ( + None, + "googleapis.com", + "googleapis.com", + None, + "foo.{UNIVERSE_DOMAIN}", + True, + ValueError, + ), + ], +) +def test_get_api_endpoint( + api_override, + universe_domain, + default_universe, + default_mtls_endpoint, + default_endpoint_template, + use_mtls, + expected, +): + if isinstance(expected, type) and issubclass(expected, Exception): + with pytest.raises(expected): + universe.get_api_endpoint( + api_override, + universe_domain, + default_universe, + default_mtls_endpoint, + default_endpoint_template, + use_mtls, + ) + else: + assert ( + universe.get_api_endpoint( + api_override, + universe_domain, + default_universe, + default_mtls_endpoint, + default_endpoint_template, + use_mtls, + ) + == expected + ) diff --git a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py index 4e6d6a028dcc..5a225db2646e 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py @@ -112,22 +112,6 @@ def set_event_loop(): asyncio.set_event_loop(None) -def test__get_default_mtls_endpoint(): - api_endpoint = "example.googleapis.com" - api_mtls_endpoint = "example.mtls.googleapis.com" - sandbox_endpoint = "example.sandbox.googleapis.com" - sandbox_mtls_endpoint = "example.mtls.sandbox.googleapis.com" - non_googleapi = "api.example.com" - custom_endpoint = ".custom" - - assert ConfigServiceV2Client._get_default_mtls_endpoint(None) is None - assert ConfigServiceV2Client._get_default_mtls_endpoint(api_endpoint) == api_mtls_endpoint - assert ConfigServiceV2Client._get_default_mtls_endpoint(api_mtls_endpoint) == api_mtls_endpoint - assert ConfigServiceV2Client._get_default_mtls_endpoint(sandbox_endpoint) == sandbox_mtls_endpoint - assert ConfigServiceV2Client._get_default_mtls_endpoint(sandbox_mtls_endpoint) == sandbox_mtls_endpoint - assert ConfigServiceV2Client._get_default_mtls_endpoint(non_googleapi) == non_googleapi - assert ConfigServiceV2Client._get_default_mtls_endpoint(custom_endpoint) == custom_endpoint - def test__read_environment_variables(): assert ConfigServiceV2Client._read_environment_variables() == (False, "auto", None) @@ -269,40 +253,6 @@ def test__get_client_cert_source(): assert ConfigServiceV2Client._get_client_cert_source(None, True) is mock_default_cert_source assert ConfigServiceV2Client._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source -@mock.patch.object(ConfigServiceV2Client, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(ConfigServiceV2Client)) -@mock.patch.object(ConfigServiceV2AsyncClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(ConfigServiceV2AsyncClient)) -def test__get_api_endpoint(): - api_override = "foo.com" - mock_client_cert_source = mock.Mock() - default_universe = ConfigServiceV2Client._DEFAULT_UNIVERSE - default_endpoint = ConfigServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) - mock_universe = "bar.com" - mock_endpoint = ConfigServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) - - assert ConfigServiceV2Client._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override - assert ConfigServiceV2Client._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == ConfigServiceV2Client.DEFAULT_MTLS_ENDPOINT - assert ConfigServiceV2Client._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint - assert ConfigServiceV2Client._get_api_endpoint(None, None, default_universe, "always") == ConfigServiceV2Client.DEFAULT_MTLS_ENDPOINT - assert ConfigServiceV2Client._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == ConfigServiceV2Client.DEFAULT_MTLS_ENDPOINT - assert ConfigServiceV2Client._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint - assert ConfigServiceV2Client._get_api_endpoint(None, None, default_universe, "never") == default_endpoint - - with pytest.raises(MutualTLSChannelError) as excinfo: - ConfigServiceV2Client._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") - assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." - - -def test__get_universe_domain(): - client_universe_domain = "foo.com" - universe_domain_env = "bar.com" - - assert ConfigServiceV2Client._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain - assert ConfigServiceV2Client._get_universe_domain(None, universe_domain_env) == universe_domain_env - assert ConfigServiceV2Client._get_universe_domain(None, None) == ConfigServiceV2Client._DEFAULT_UNIVERSE - - with pytest.raises(ValueError) as excinfo: - ConfigServiceV2Client._get_universe_domain("", None) - assert str(excinfo.value) == "Universe Domain cannot be an empty string." @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), @@ -679,7 +629,7 @@ def test_config_service_v2_client_get_mtls_endpoint_and_cert_source(client_class config_filename = "mock_certificate_config.json" config_file_content = json.dumps(config_data) m = mock.mock_open(read_data=config_file_content) - with mock.patch("builtins.open", m), mock.patch("os.path.exists", side_effect=lambda path: os.path.basename(path) == config_filename): + with mock.patch("builtins.open", m): with mock.patch.dict( os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": config_filename} ): @@ -727,7 +677,7 @@ def test_config_service_v2_client_get_mtls_endpoint_and_cert_source(client_class config_filename = "mock_certificate_config.json" config_file_content = json.dumps(config_data) m = mock.mock_open(read_data=config_file_content) - with mock.patch("builtins.open", m), mock.patch("os.path.exists", side_effect=lambda path: os.path.basename(path) == config_filename): + with mock.patch("builtins.open", m): with mock.patch.dict( os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": config_filename} ): diff --git a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py index b961e45ea907..9c6761bac811 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py @@ -107,22 +107,6 @@ def set_event_loop(): asyncio.set_event_loop(None) -def test__get_default_mtls_endpoint(): - api_endpoint = "example.googleapis.com" - api_mtls_endpoint = "example.mtls.googleapis.com" - sandbox_endpoint = "example.sandbox.googleapis.com" - sandbox_mtls_endpoint = "example.mtls.sandbox.googleapis.com" - non_googleapi = "api.example.com" - custom_endpoint = ".custom" - - assert LoggingServiceV2Client._get_default_mtls_endpoint(None) is None - assert LoggingServiceV2Client._get_default_mtls_endpoint(api_endpoint) == api_mtls_endpoint - assert LoggingServiceV2Client._get_default_mtls_endpoint(api_mtls_endpoint) == api_mtls_endpoint - assert LoggingServiceV2Client._get_default_mtls_endpoint(sandbox_endpoint) == sandbox_mtls_endpoint - assert LoggingServiceV2Client._get_default_mtls_endpoint(sandbox_mtls_endpoint) == sandbox_mtls_endpoint - assert LoggingServiceV2Client._get_default_mtls_endpoint(non_googleapi) == non_googleapi - assert LoggingServiceV2Client._get_default_mtls_endpoint(custom_endpoint) == custom_endpoint - def test__read_environment_variables(): assert LoggingServiceV2Client._read_environment_variables() == (False, "auto", None) @@ -264,40 +248,6 @@ def test__get_client_cert_source(): assert LoggingServiceV2Client._get_client_cert_source(None, True) is mock_default_cert_source assert LoggingServiceV2Client._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source -@mock.patch.object(LoggingServiceV2Client, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(LoggingServiceV2Client)) -@mock.patch.object(LoggingServiceV2AsyncClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(LoggingServiceV2AsyncClient)) -def test__get_api_endpoint(): - api_override = "foo.com" - mock_client_cert_source = mock.Mock() - default_universe = LoggingServiceV2Client._DEFAULT_UNIVERSE - default_endpoint = LoggingServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) - mock_universe = "bar.com" - mock_endpoint = LoggingServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) - - assert LoggingServiceV2Client._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override - assert LoggingServiceV2Client._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == LoggingServiceV2Client.DEFAULT_MTLS_ENDPOINT - assert LoggingServiceV2Client._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint - assert LoggingServiceV2Client._get_api_endpoint(None, None, default_universe, "always") == LoggingServiceV2Client.DEFAULT_MTLS_ENDPOINT - assert LoggingServiceV2Client._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == LoggingServiceV2Client.DEFAULT_MTLS_ENDPOINT - assert LoggingServiceV2Client._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint - assert LoggingServiceV2Client._get_api_endpoint(None, None, default_universe, "never") == default_endpoint - - with pytest.raises(MutualTLSChannelError) as excinfo: - LoggingServiceV2Client._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") - assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." - - -def test__get_universe_domain(): - client_universe_domain = "foo.com" - universe_domain_env = "bar.com" - - assert LoggingServiceV2Client._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain - assert LoggingServiceV2Client._get_universe_domain(None, universe_domain_env) == universe_domain_env - assert LoggingServiceV2Client._get_universe_domain(None, None) == LoggingServiceV2Client._DEFAULT_UNIVERSE - - with pytest.raises(ValueError) as excinfo: - LoggingServiceV2Client._get_universe_domain("", None) - assert str(excinfo.value) == "Universe Domain cannot be an empty string." @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), @@ -674,7 +624,7 @@ def test_logging_service_v2_client_get_mtls_endpoint_and_cert_source(client_clas config_filename = "mock_certificate_config.json" config_file_content = json.dumps(config_data) m = mock.mock_open(read_data=config_file_content) - with mock.patch("builtins.open", m), mock.patch("os.path.exists", side_effect=lambda path: os.path.basename(path) == config_filename): + with mock.patch("builtins.open", m): with mock.patch.dict( os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": config_filename} ): @@ -722,7 +672,7 @@ def test_logging_service_v2_client_get_mtls_endpoint_and_cert_source(client_clas config_filename = "mock_certificate_config.json" config_file_content = json.dumps(config_data) m = mock.mock_open(read_data=config_file_content) - with mock.patch("builtins.open", m), mock.patch("os.path.exists", side_effect=lambda path: os.path.basename(path) == config_filename): + with mock.patch("builtins.open", m): with mock.patch.dict( os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": config_filename} ): diff --git a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py index ef11eed3796f..1cf37e07e01d 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py @@ -108,22 +108,6 @@ def set_event_loop(): asyncio.set_event_loop(None) -def test__get_default_mtls_endpoint(): - api_endpoint = "example.googleapis.com" - api_mtls_endpoint = "example.mtls.googleapis.com" - sandbox_endpoint = "example.sandbox.googleapis.com" - sandbox_mtls_endpoint = "example.mtls.sandbox.googleapis.com" - non_googleapi = "api.example.com" - custom_endpoint = ".custom" - - assert MetricsServiceV2Client._get_default_mtls_endpoint(None) is None - assert MetricsServiceV2Client._get_default_mtls_endpoint(api_endpoint) == api_mtls_endpoint - assert MetricsServiceV2Client._get_default_mtls_endpoint(api_mtls_endpoint) == api_mtls_endpoint - assert MetricsServiceV2Client._get_default_mtls_endpoint(sandbox_endpoint) == sandbox_mtls_endpoint - assert MetricsServiceV2Client._get_default_mtls_endpoint(sandbox_mtls_endpoint) == sandbox_mtls_endpoint - assert MetricsServiceV2Client._get_default_mtls_endpoint(non_googleapi) == non_googleapi - assert MetricsServiceV2Client._get_default_mtls_endpoint(custom_endpoint) == custom_endpoint - def test__read_environment_variables(): assert MetricsServiceV2Client._read_environment_variables() == (False, "auto", None) @@ -265,40 +249,6 @@ def test__get_client_cert_source(): assert MetricsServiceV2Client._get_client_cert_source(None, True) is mock_default_cert_source assert MetricsServiceV2Client._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source -@mock.patch.object(MetricsServiceV2Client, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(MetricsServiceV2Client)) -@mock.patch.object(MetricsServiceV2AsyncClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(MetricsServiceV2AsyncClient)) -def test__get_api_endpoint(): - api_override = "foo.com" - mock_client_cert_source = mock.Mock() - default_universe = MetricsServiceV2Client._DEFAULT_UNIVERSE - default_endpoint = MetricsServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) - mock_universe = "bar.com" - mock_endpoint = MetricsServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) - - assert MetricsServiceV2Client._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override - assert MetricsServiceV2Client._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == MetricsServiceV2Client.DEFAULT_MTLS_ENDPOINT - assert MetricsServiceV2Client._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint - assert MetricsServiceV2Client._get_api_endpoint(None, None, default_universe, "always") == MetricsServiceV2Client.DEFAULT_MTLS_ENDPOINT - assert MetricsServiceV2Client._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == MetricsServiceV2Client.DEFAULT_MTLS_ENDPOINT - assert MetricsServiceV2Client._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint - assert MetricsServiceV2Client._get_api_endpoint(None, None, default_universe, "never") == default_endpoint - - with pytest.raises(MutualTLSChannelError) as excinfo: - MetricsServiceV2Client._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") - assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." - - -def test__get_universe_domain(): - client_universe_domain = "foo.com" - universe_domain_env = "bar.com" - - assert MetricsServiceV2Client._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain - assert MetricsServiceV2Client._get_universe_domain(None, universe_domain_env) == universe_domain_env - assert MetricsServiceV2Client._get_universe_domain(None, None) == MetricsServiceV2Client._DEFAULT_UNIVERSE - - with pytest.raises(ValueError) as excinfo: - MetricsServiceV2Client._get_universe_domain("", None) - assert str(excinfo.value) == "Universe Domain cannot be an empty string." @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), @@ -675,7 +625,7 @@ def test_metrics_service_v2_client_get_mtls_endpoint_and_cert_source(client_clas config_filename = "mock_certificate_config.json" config_file_content = json.dumps(config_data) m = mock.mock_open(read_data=config_file_content) - with mock.patch("builtins.open", m), mock.patch("os.path.exists", side_effect=lambda path: os.path.basename(path) == config_filename): + with mock.patch("builtins.open", m): with mock.patch.dict( os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": config_filename} ): @@ -723,7 +673,7 @@ def test_metrics_service_v2_client_get_mtls_endpoint_and_cert_source(client_clas config_filename = "mock_certificate_config.json" config_file_content = json.dumps(config_data) m = mock.mock_open(read_data=config_file_content) - with mock.patch("builtins.open", m), mock.patch("os.path.exists", side_effect=lambda path: os.path.basename(path) == config_filename): + with mock.patch("builtins.open", m): with mock.patch.dict( os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": config_filename} ): diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/_compat.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/_compat.py index 7c684e94b03d..a84aecd6ca3d 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/_compat.py @@ -14,3 +14,152 @@ # limitations under the License. # """A compatibility module for older versions of google-api-core.""" +"""A compatibility module for older versions of google-api-core.""" + +import uuid + +from typing import Optional, Union +from urllib.parse import urlparse, urlunparse + +from google.auth.exceptions import MutualTLSChannelError +from google.api_core.universe import EmptyUniverseError +import google.protobuf.message + +DEFAULT_UNIVERSE = "googleapis.com" + + +def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: + """Converts api endpoint to mTLS endpoint. + + Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to + "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. + Other URLs (including those that do not match these domain suffixes or + already contain '.mtls.') are passed through as-is. + + Args: + api_endpoint (Optional[str]): the api endpoint to convert. + + Returns: + Optional[str]: converted mTLS api endpoint. + """ + if not api_endpoint or ".mtls." in api_endpoint.lower(): + return api_endpoint + + has_scheme = "://" in api_endpoint + if not has_scheme: + parsed = urlparse("//" + api_endpoint) + else: + parsed = urlparse(api_endpoint) + + host = parsed.hostname + if not host: + return api_endpoint + + port = f":{parsed.port}" if parsed.port else "" + + lowered_host = host.lower() + suffix_sandbox = ".sandbox.googleapis.com" + suffix_google = ".googleapis.com" + if lowered_host.endswith(suffix_sandbox): + new_host = host[: -len(suffix_sandbox)] + ".mtls.sandbox.googleapis.com" + elif lowered_host.endswith(suffix_google): + new_host = host[: -len(suffix_google)] + ".mtls.googleapis.com" + else: + return api_endpoint + + netloc = new_host + port + new_parsed = parsed._replace(netloc=netloc) + + if not has_scheme: + return urlunparse(new_parsed)[2:] + else: + return urlunparse(new_parsed) + +def get_api_endpoint( + api_override: Optional[str], + universe_domain: str, + default_universe: str, + default_mtls_endpoint: Optional[str], + default_endpoint_template: str, + use_mtls: bool, +) -> str: + """Return the API endpoint used by the client. + + Args: + api_override (Optional[str]): The API endpoint override. If specified, + this is always returned. + universe_domain (str): The universe domain used by the client. + default_universe (str): The default universe domain. + default_mtls_endpoint (Optional[str]): The default mTLS endpoint. + default_endpoint_template (str): The default endpoint template containing + a placeholder `{UNIVERSE_DOMAIN}`. + use_mtls (bool): Whether to use the mTLS endpoint. + + Returns: + str: The API endpoint to be used by the client. + + Raises: + google.auth.exceptions.MutualTLSChannelError: If mTLS is requested but + not supported in the configured universe domain. + ValueError: If mTLS is requested but no mTLS endpoint is available. + """ + if api_override is not None: + return api_override + + if use_mtls: + if universe_domain.lower() != default_universe.lower(): + raise MutualTLSChannelError( + f"mTLS is not supported in any universe other than {default_universe}." + ) + if not default_mtls_endpoint: + raise ValueError("mTLS endpoint is not available.") + return default_mtls_endpoint + else: + return default_endpoint_template.format(UNIVERSE_DOMAIN=universe_domain) + +def get_universe_domain( + *potential_universes: Optional[str], + default_universe: str, +) -> str: + """Return the universe domain used by the client. + + Args: + *potential_universes (Optional[str]): Potential universe domains in order of preference. + default_universe (str): The default universe domain. + + Returns: + str: The universe domain to be used by the client. + + Raises: + EmptyUniverseError: If the resolved universe domain is an empty string. + """ + resolved = next( + (x.strip() for x in potential_universes if x is not None), + default_universe, + ) + + if not resolved: + raise EmptyUniverseError() + return resolved + +def determine_domain( + client_universe_domain: Optional[str], universe_domain_env: Optional[str] +) -> str: + """Return the universe domain used by the client. + + Args: + client_universe_domain (Optional[str]): The universe domain configured via the client options. + universe_domain_env (Optional[str]): The universe domain configured via the + "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. + + Returns: + str: The universe domain to be used by the client. + + Raises: + ValueError: If the universe domain is an empty string. + """ + return get_universe_domain( + client_universe_domain, + universe_domain_env, + default_universe=DEFAULT_UNIVERSE, + ) diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/client.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/client.py index 61204cb87a52..f9ad551a6169 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/client.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/client.py @@ -27,6 +27,7 @@ from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.cloud.logging_v2._compat import determine_domain, get_api_endpoint, get_default_mtls_endpoint from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -96,43 +97,9 @@ def get_transport_class(cls, class BaseConfigServiceV2Client(metaclass=BaseConfigServiceV2ClientMeta): """Service for configuring sinks used to route log entries.""" - @staticmethod - def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: - """Converts api endpoint to mTLS endpoint. - - Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to - "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. - Args: - api_endpoint (Optional[str]): the api endpoint to convert. - Returns: - Optional[str]: converted mTLS api endpoint. - """ - if not api_endpoint: - return api_endpoint - - mtls_endpoint_re = re.compile( - r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" - ) - - m = mtls_endpoint_re.match(api_endpoint) - if m is None: - # Could not parse api_endpoint; return as-is. - return api_endpoint - - name, mtls, sandbox, googledomain = m.groups() - if mtls or not googledomain: - return api_endpoint - - if sandbox: - return api_endpoint.replace( - "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" - ) - - return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") - # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "logging.googleapis.com" - DEFAULT_MTLS_ENDPOINT = _get_default_mtls_endpoint.__func__( # type: ignore + DEFAULT_MTLS_ENDPOINT = get_default_mtls_endpoint( DEFAULT_ENDPOINT ) @@ -445,55 +412,6 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): client_cert_source = mtls.default_client_cert_source() return client_cert_source - @staticmethod - def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: - """Return the API endpoint used by the client. - - Args: - api_override (str): The API endpoint override. If specified, this is always - the return value of this function and the other arguments are not used. - client_cert_source (bytes): The client certificate source used by the client. - universe_domain (str): The universe domain used by the client. - use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. - Possible values are "always", "auto", or "never". - - Returns: - str: The API endpoint to be used by the client. - """ - if api_override is not None: - api_endpoint = api_override - elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): - _default_universe = BaseConfigServiceV2Client._DEFAULT_UNIVERSE - if universe_domain != _default_universe: - raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") - api_endpoint = BaseConfigServiceV2Client.DEFAULT_MTLS_ENDPOINT - else: - api_endpoint = BaseConfigServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) - return api_endpoint - - @staticmethod - def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client. - - Args: - client_universe_domain (Optional[str]): The universe domain configured via the client options. - universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. - - Returns: - str: The universe domain to be used by the client. - - Raises: - ValueError: If the universe domain is an empty string. - """ - universe_domain = BaseConfigServiceV2Client._DEFAULT_UNIVERSE - if client_universe_domain is not None: - universe_domain = client_universe_domain - elif universe_domain_env is not None: - universe_domain = universe_domain_env - if len(universe_domain.strip()) == 0: - raise ValueError("Universe Domain cannot be an empty string.") - return universe_domain - def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. @@ -615,7 +533,7 @@ def __init__(self, *, self._use_client_cert, self._use_mtls_endpoint, self._universe_domain_env = BaseConfigServiceV2Client._read_environment_variables() self._client_cert_source = BaseConfigServiceV2Client._get_client_cert_source(self._client_options.client_cert_source, self._use_client_cert) - self._universe_domain = BaseConfigServiceV2Client._get_universe_domain(universe_domain_opt, self._universe_domain_env) + self._universe_domain = determine_domain(universe_domain_opt, self._universe_domain_env) self._api_endpoint: str = "" # updated below, depending on `transport` # Initialize the universe domain validation. @@ -647,7 +565,7 @@ def __init__(self, *, self._api_endpoint = self._transport.host self._api_endpoint = (self._api_endpoint or - BaseConfigServiceV2Client._get_api_endpoint( + get_api_endpoint( self._client_options.api_endpoint, self._client_cert_source, self._universe_domain, diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/client.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/client.py index e89762755eda..adc1866df4b2 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/client.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/client.py @@ -27,6 +27,7 @@ from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.cloud.logging_v2._compat import determine_domain, get_api_endpoint, get_default_mtls_endpoint from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -93,43 +94,9 @@ def get_transport_class(cls, class LoggingServiceV2Client(metaclass=LoggingServiceV2ClientMeta): """Service for ingesting and querying logs.""" - @staticmethod - def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: - """Converts api endpoint to mTLS endpoint. - - Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to - "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. - Args: - api_endpoint (Optional[str]): the api endpoint to convert. - Returns: - Optional[str]: converted mTLS api endpoint. - """ - if not api_endpoint: - return api_endpoint - - mtls_endpoint_re = re.compile( - r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" - ) - - m = mtls_endpoint_re.match(api_endpoint) - if m is None: - # Could not parse api_endpoint; return as-is. - return api_endpoint - - name, mtls, sandbox, googledomain = m.groups() - if mtls or not googledomain: - return api_endpoint - - if sandbox: - return api_endpoint.replace( - "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" - ) - - return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") - # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "logging.googleapis.com" - DEFAULT_MTLS_ENDPOINT = _get_default_mtls_endpoint.__func__( # type: ignore + DEFAULT_MTLS_ENDPOINT = get_default_mtls_endpoint( DEFAULT_ENDPOINT ) @@ -376,55 +343,6 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): client_cert_source = mtls.default_client_cert_source() return client_cert_source - @staticmethod - def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: - """Return the API endpoint used by the client. - - Args: - api_override (str): The API endpoint override. If specified, this is always - the return value of this function and the other arguments are not used. - client_cert_source (bytes): The client certificate source used by the client. - universe_domain (str): The universe domain used by the client. - use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. - Possible values are "always", "auto", or "never". - - Returns: - str: The API endpoint to be used by the client. - """ - if api_override is not None: - api_endpoint = api_override - elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): - _default_universe = LoggingServiceV2Client._DEFAULT_UNIVERSE - if universe_domain != _default_universe: - raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") - api_endpoint = LoggingServiceV2Client.DEFAULT_MTLS_ENDPOINT - else: - api_endpoint = LoggingServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) - return api_endpoint - - @staticmethod - def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client. - - Args: - client_universe_domain (Optional[str]): The universe domain configured via the client options. - universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. - - Returns: - str: The universe domain to be used by the client. - - Raises: - ValueError: If the universe domain is an empty string. - """ - universe_domain = LoggingServiceV2Client._DEFAULT_UNIVERSE - if client_universe_domain is not None: - universe_domain = client_universe_domain - elif universe_domain_env is not None: - universe_domain = universe_domain_env - if len(universe_domain.strip()) == 0: - raise ValueError("Universe Domain cannot be an empty string.") - return universe_domain - def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. @@ -546,7 +464,7 @@ def __init__(self, *, self._use_client_cert, self._use_mtls_endpoint, self._universe_domain_env = LoggingServiceV2Client._read_environment_variables() self._client_cert_source = LoggingServiceV2Client._get_client_cert_source(self._client_options.client_cert_source, self._use_client_cert) - self._universe_domain = LoggingServiceV2Client._get_universe_domain(universe_domain_opt, self._universe_domain_env) + self._universe_domain = determine_domain(universe_domain_opt, self._universe_domain_env) self._api_endpoint: str = "" # updated below, depending on `transport` # Initialize the universe domain validation. @@ -578,7 +496,7 @@ def __init__(self, *, self._api_endpoint = self._transport.host self._api_endpoint = (self._api_endpoint or - LoggingServiceV2Client._get_api_endpoint( + get_api_endpoint( self._client_options.api_endpoint, self._client_cert_source, self._universe_domain, diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/client.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/client.py index fa55137223d1..92e4a0a2611f 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/client.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/client.py @@ -27,6 +27,7 @@ from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.cloud.logging_v2._compat import determine_domain, get_api_endpoint, get_default_mtls_endpoint from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -94,43 +95,9 @@ def get_transport_class(cls, class BaseMetricsServiceV2Client(metaclass=BaseMetricsServiceV2ClientMeta): """Service for configuring logs-based metrics.""" - @staticmethod - def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: - """Converts api endpoint to mTLS endpoint. - - Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to - "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. - Args: - api_endpoint (Optional[str]): the api endpoint to convert. - Returns: - Optional[str]: converted mTLS api endpoint. - """ - if not api_endpoint: - return api_endpoint - - mtls_endpoint_re = re.compile( - r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" - ) - - m = mtls_endpoint_re.match(api_endpoint) - if m is None: - # Could not parse api_endpoint; return as-is. - return api_endpoint - - name, mtls, sandbox, googledomain = m.groups() - if mtls or not googledomain: - return api_endpoint - - if sandbox: - return api_endpoint.replace( - "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" - ) - - return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") - # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "logging.googleapis.com" - DEFAULT_MTLS_ENDPOINT = _get_default_mtls_endpoint.__func__( # type: ignore + DEFAULT_MTLS_ENDPOINT = get_default_mtls_endpoint( DEFAULT_ENDPOINT ) @@ -377,55 +344,6 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): client_cert_source = mtls.default_client_cert_source() return client_cert_source - @staticmethod - def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: - """Return the API endpoint used by the client. - - Args: - api_override (str): The API endpoint override. If specified, this is always - the return value of this function and the other arguments are not used. - client_cert_source (bytes): The client certificate source used by the client. - universe_domain (str): The universe domain used by the client. - use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. - Possible values are "always", "auto", or "never". - - Returns: - str: The API endpoint to be used by the client. - """ - if api_override is not None: - api_endpoint = api_override - elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): - _default_universe = BaseMetricsServiceV2Client._DEFAULT_UNIVERSE - if universe_domain != _default_universe: - raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") - api_endpoint = BaseMetricsServiceV2Client.DEFAULT_MTLS_ENDPOINT - else: - api_endpoint = BaseMetricsServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) - return api_endpoint - - @staticmethod - def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client. - - Args: - client_universe_domain (Optional[str]): The universe domain configured via the client options. - universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. - - Returns: - str: The universe domain to be used by the client. - - Raises: - ValueError: If the universe domain is an empty string. - """ - universe_domain = BaseMetricsServiceV2Client._DEFAULT_UNIVERSE - if client_universe_domain is not None: - universe_domain = client_universe_domain - elif universe_domain_env is not None: - universe_domain = universe_domain_env - if len(universe_domain.strip()) == 0: - raise ValueError("Universe Domain cannot be an empty string.") - return universe_domain - def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. @@ -547,7 +465,7 @@ def __init__(self, *, self._use_client_cert, self._use_mtls_endpoint, self._universe_domain_env = BaseMetricsServiceV2Client._read_environment_variables() self._client_cert_source = BaseMetricsServiceV2Client._get_client_cert_source(self._client_options.client_cert_source, self._use_client_cert) - self._universe_domain = BaseMetricsServiceV2Client._get_universe_domain(universe_domain_opt, self._universe_domain_env) + self._universe_domain = determine_domain(universe_domain_opt, self._universe_domain_env) self._api_endpoint: str = "" # updated below, depending on `transport` # Initialize the universe domain validation. @@ -579,7 +497,7 @@ def __init__(self, *, self._api_endpoint = self._transport.host self._api_endpoint = (self._api_endpoint or - BaseMetricsServiceV2Client._get_api_endpoint( + get_api_endpoint( self._client_options.api_endpoint, self._client_cert_source, self._universe_domain, diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_compat.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_compat.py index 8491204d1041..1a22e6c701d0 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_compat.py @@ -14,3 +14,206 @@ # limitations under the License. # """Tests for the compatibility module for older versions of google-api-core.""" + +import re +import pytest + +from google.cloud.logging_v2._compat import determine_domain, get_api_endpoint, get_default_mtls_endpoint + +from google.auth.exceptions import MutualTLSChannelError + + +def test_get_universe_domain(): + # When universe_domain is provided + assert ( + universe.get_universe_domain("foo.com", default_universe="default.com") + == "foo.com" + ) + assert ( + universe.get_universe_domain(" foo.com ", default_universe="default.com") + == "foo.com" + ) + + # When universe_domain is None, falls back to default_universe + assert ( + universe.get_universe_domain(None, default_universe="default.com") + == "default.com" + ) + + # When multiple potential universes are provided, resolves in order of preference + assert ( + universe.get_universe_domain( + "foo.com", "bar.com", default_universe="default.com" + ) + == "foo.com" + ) + assert ( + universe.get_universe_domain(None, "bar.com", default_universe="default.com") + == "bar.com" + ) + assert ( + universe.get_universe_domain(None, None, default_universe="default.com") + == "default.com" + ) + + # EmptyUniverseError raised when resolved value is empty string + with pytest.raises(universe.EmptyUniverseError) as excinfo: + universe.get_universe_domain("", default_universe="default.com") + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + + with pytest.raises(universe.EmptyUniverseError) as excinfo: + universe.get_universe_domain(" ", default_universe="default.com") + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + + with pytest.raises(universe.EmptyUniverseError) as excinfo: + universe.get_universe_domain(None, "", default_universe="default.com") + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + + +def test_get_default_mtls_endpoint(): + # Test valid API endpoints + assert ( + universe.get_default_mtls_endpoint("foo.googleapis.com") + == "foo.mtls.googleapis.com" + ) + assert ( + universe.get_default_mtls_endpoint("foo.sandbox.googleapis.com") + == "foo.mtls.sandbox.googleapis.com" + ) + # Test case-insensitivity + assert ( + universe.get_default_mtls_endpoint("foo.GoogleAPIs.com") + == "foo.mtls.googleapis.com" + ) + assert ( + universe.get_default_mtls_endpoint("foo.Sandbox.GoogleAPIs.com") + == "foo.mtls.sandbox.googleapis.com" + ) + + # Test valid API endpoints with schemes + assert ( + universe.get_default_mtls_endpoint("https://foo.googleapis.com") + == "https://foo.mtls.googleapis.com" + ) + assert ( + universe.get_default_mtls_endpoint("http://foo.googleapis.com:8080/v1") + == "http://foo.mtls.googleapis.com:8080/v1" + ) + + # Test valid API endpoints with ports + assert ( + universe.get_default_mtls_endpoint("foo.googleapis.com:443") + == "foo.mtls.googleapis.com:443" + ) + assert ( + universe.get_default_mtls_endpoint("foo.sandbox.googleapis.com:443") + == "foo.mtls.sandbox.googleapis.com:443" + ) + # Test case-insensitivity with ports + assert ( + universe.get_default_mtls_endpoint("foo.GoogleAPIs.com:443") + == "foo.mtls.googleapis.com:443" + ) + assert ( + universe.get_default_mtls_endpoint("foo.Sandbox.GoogleAPIs.com:443") + == "foo.mtls.sandbox.googleapis.com:443" + ) + + # Test endpoints that shouldn't be converted + assert ( + universe.get_default_mtls_endpoint("foo.mtls.googleapis.com") + == "foo.mtls.googleapis.com" + ) + assert universe.get_default_mtls_endpoint("foo.com") == "foo.com" + assert universe.get_default_mtls_endpoint("foo.com:8080") == "foo.com:8080" + + # Test empty/None endpoints + assert universe.get_default_mtls_endpoint("") == "" + assert universe.get_default_mtls_endpoint(None) is None + + # Test endpoints without host + assert universe.get_default_mtls_endpoint("http://") == "http://" + assert universe.get_default_mtls_endpoint("https://") == "https://" + + +@pytest.mark.parametrize( + "api_override,universe_domain,default_universe,default_mtls_endpoint,default_endpoint_template,use_mtls,expected", + [ + ( + "foo.com", + "googleapis.com", + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + True, + "foo.com", + ), + ( + None, + "googleapis.com", + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + True, + "foo.mtls.googleapis.com", + ), + ( + None, + "googleapis.com", + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + False, + "foo.googleapis.com", + ), + ( + None, + "bar.com", + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + True, + MutualTLSChannelError, + ), + ( + None, + "googleapis.com", + "googleapis.com", + None, + "foo.{UNIVERSE_DOMAIN}", + True, + ValueError, + ), + ], +) +def test_get_api_endpoint( + api_override, + universe_domain, + default_universe, + default_mtls_endpoint, + default_endpoint_template, + use_mtls, + expected, +): + if isinstance(expected, type) and issubclass(expected, Exception): + with pytest.raises(expected): + universe.get_api_endpoint( + api_override, + universe_domain, + default_universe, + default_mtls_endpoint, + default_endpoint_template, + use_mtls, + ) + else: + assert ( + universe.get_api_endpoint( + api_override, + universe_domain, + default_universe, + default_mtls_endpoint, + default_endpoint_template, + use_mtls, + ) + == expected + ) diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py index 056a60ca77f0..2e54a6f15d2a 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py @@ -112,22 +112,6 @@ def set_event_loop(): asyncio.set_event_loop(None) -def test__get_default_mtls_endpoint(): - api_endpoint = "example.googleapis.com" - api_mtls_endpoint = "example.mtls.googleapis.com" - sandbox_endpoint = "example.sandbox.googleapis.com" - sandbox_mtls_endpoint = "example.mtls.sandbox.googleapis.com" - non_googleapi = "api.example.com" - custom_endpoint = ".custom" - - assert BaseConfigServiceV2Client._get_default_mtls_endpoint(None) is None - assert BaseConfigServiceV2Client._get_default_mtls_endpoint(api_endpoint) == api_mtls_endpoint - assert BaseConfigServiceV2Client._get_default_mtls_endpoint(api_mtls_endpoint) == api_mtls_endpoint - assert BaseConfigServiceV2Client._get_default_mtls_endpoint(sandbox_endpoint) == sandbox_mtls_endpoint - assert BaseConfigServiceV2Client._get_default_mtls_endpoint(sandbox_mtls_endpoint) == sandbox_mtls_endpoint - assert BaseConfigServiceV2Client._get_default_mtls_endpoint(non_googleapi) == non_googleapi - assert BaseConfigServiceV2Client._get_default_mtls_endpoint(custom_endpoint) == custom_endpoint - def test__read_environment_variables(): assert BaseConfigServiceV2Client._read_environment_variables() == (False, "auto", None) @@ -269,40 +253,6 @@ def test__get_client_cert_source(): assert BaseConfigServiceV2Client._get_client_cert_source(None, True) is mock_default_cert_source assert BaseConfigServiceV2Client._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source -@mock.patch.object(BaseConfigServiceV2Client, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(BaseConfigServiceV2Client)) -@mock.patch.object(BaseConfigServiceV2AsyncClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(BaseConfigServiceV2AsyncClient)) -def test__get_api_endpoint(): - api_override = "foo.com" - mock_client_cert_source = mock.Mock() - default_universe = BaseConfigServiceV2Client._DEFAULT_UNIVERSE - default_endpoint = BaseConfigServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) - mock_universe = "bar.com" - mock_endpoint = BaseConfigServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) - - assert BaseConfigServiceV2Client._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override - assert BaseConfigServiceV2Client._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == BaseConfigServiceV2Client.DEFAULT_MTLS_ENDPOINT - assert BaseConfigServiceV2Client._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint - assert BaseConfigServiceV2Client._get_api_endpoint(None, None, default_universe, "always") == BaseConfigServiceV2Client.DEFAULT_MTLS_ENDPOINT - assert BaseConfigServiceV2Client._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == BaseConfigServiceV2Client.DEFAULT_MTLS_ENDPOINT - assert BaseConfigServiceV2Client._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint - assert BaseConfigServiceV2Client._get_api_endpoint(None, None, default_universe, "never") == default_endpoint - - with pytest.raises(MutualTLSChannelError) as excinfo: - BaseConfigServiceV2Client._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") - assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." - - -def test__get_universe_domain(): - client_universe_domain = "foo.com" - universe_domain_env = "bar.com" - - assert BaseConfigServiceV2Client._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain - assert BaseConfigServiceV2Client._get_universe_domain(None, universe_domain_env) == universe_domain_env - assert BaseConfigServiceV2Client._get_universe_domain(None, None) == BaseConfigServiceV2Client._DEFAULT_UNIVERSE - - with pytest.raises(ValueError) as excinfo: - BaseConfigServiceV2Client._get_universe_domain("", None) - assert str(excinfo.value) == "Universe Domain cannot be an empty string." @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), @@ -679,7 +629,7 @@ def test_base_config_service_v2_client_get_mtls_endpoint_and_cert_source(client_ config_filename = "mock_certificate_config.json" config_file_content = json.dumps(config_data) m = mock.mock_open(read_data=config_file_content) - with mock.patch("builtins.open", m), mock.patch("os.path.exists", side_effect=lambda path: os.path.basename(path) == config_filename): + with mock.patch("builtins.open", m): with mock.patch.dict( os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": config_filename} ): @@ -727,7 +677,7 @@ def test_base_config_service_v2_client_get_mtls_endpoint_and_cert_source(client_ config_filename = "mock_certificate_config.json" config_file_content = json.dumps(config_data) m = mock.mock_open(read_data=config_file_content) - with mock.patch("builtins.open", m), mock.patch("os.path.exists", side_effect=lambda path: os.path.basename(path) == config_filename): + with mock.patch("builtins.open", m): with mock.patch.dict( os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": config_filename} ): diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py index b961e45ea907..9c6761bac811 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py @@ -107,22 +107,6 @@ def set_event_loop(): asyncio.set_event_loop(None) -def test__get_default_mtls_endpoint(): - api_endpoint = "example.googleapis.com" - api_mtls_endpoint = "example.mtls.googleapis.com" - sandbox_endpoint = "example.sandbox.googleapis.com" - sandbox_mtls_endpoint = "example.mtls.sandbox.googleapis.com" - non_googleapi = "api.example.com" - custom_endpoint = ".custom" - - assert LoggingServiceV2Client._get_default_mtls_endpoint(None) is None - assert LoggingServiceV2Client._get_default_mtls_endpoint(api_endpoint) == api_mtls_endpoint - assert LoggingServiceV2Client._get_default_mtls_endpoint(api_mtls_endpoint) == api_mtls_endpoint - assert LoggingServiceV2Client._get_default_mtls_endpoint(sandbox_endpoint) == sandbox_mtls_endpoint - assert LoggingServiceV2Client._get_default_mtls_endpoint(sandbox_mtls_endpoint) == sandbox_mtls_endpoint - assert LoggingServiceV2Client._get_default_mtls_endpoint(non_googleapi) == non_googleapi - assert LoggingServiceV2Client._get_default_mtls_endpoint(custom_endpoint) == custom_endpoint - def test__read_environment_variables(): assert LoggingServiceV2Client._read_environment_variables() == (False, "auto", None) @@ -264,40 +248,6 @@ def test__get_client_cert_source(): assert LoggingServiceV2Client._get_client_cert_source(None, True) is mock_default_cert_source assert LoggingServiceV2Client._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source -@mock.patch.object(LoggingServiceV2Client, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(LoggingServiceV2Client)) -@mock.patch.object(LoggingServiceV2AsyncClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(LoggingServiceV2AsyncClient)) -def test__get_api_endpoint(): - api_override = "foo.com" - mock_client_cert_source = mock.Mock() - default_universe = LoggingServiceV2Client._DEFAULT_UNIVERSE - default_endpoint = LoggingServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) - mock_universe = "bar.com" - mock_endpoint = LoggingServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) - - assert LoggingServiceV2Client._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override - assert LoggingServiceV2Client._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == LoggingServiceV2Client.DEFAULT_MTLS_ENDPOINT - assert LoggingServiceV2Client._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint - assert LoggingServiceV2Client._get_api_endpoint(None, None, default_universe, "always") == LoggingServiceV2Client.DEFAULT_MTLS_ENDPOINT - assert LoggingServiceV2Client._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == LoggingServiceV2Client.DEFAULT_MTLS_ENDPOINT - assert LoggingServiceV2Client._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint - assert LoggingServiceV2Client._get_api_endpoint(None, None, default_universe, "never") == default_endpoint - - with pytest.raises(MutualTLSChannelError) as excinfo: - LoggingServiceV2Client._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") - assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." - - -def test__get_universe_domain(): - client_universe_domain = "foo.com" - universe_domain_env = "bar.com" - - assert LoggingServiceV2Client._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain - assert LoggingServiceV2Client._get_universe_domain(None, universe_domain_env) == universe_domain_env - assert LoggingServiceV2Client._get_universe_domain(None, None) == LoggingServiceV2Client._DEFAULT_UNIVERSE - - with pytest.raises(ValueError) as excinfo: - LoggingServiceV2Client._get_universe_domain("", None) - assert str(excinfo.value) == "Universe Domain cannot be an empty string." @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), @@ -674,7 +624,7 @@ def test_logging_service_v2_client_get_mtls_endpoint_and_cert_source(client_clas config_filename = "mock_certificate_config.json" config_file_content = json.dumps(config_data) m = mock.mock_open(read_data=config_file_content) - with mock.patch("builtins.open", m), mock.patch("os.path.exists", side_effect=lambda path: os.path.basename(path) == config_filename): + with mock.patch("builtins.open", m): with mock.patch.dict( os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": config_filename} ): @@ -722,7 +672,7 @@ def test_logging_service_v2_client_get_mtls_endpoint_and_cert_source(client_clas config_filename = "mock_certificate_config.json" config_file_content = json.dumps(config_data) m = mock.mock_open(read_data=config_file_content) - with mock.patch("builtins.open", m), mock.patch("os.path.exists", side_effect=lambda path: os.path.basename(path) == config_filename): + with mock.patch("builtins.open", m): with mock.patch.dict( os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": config_filename} ): diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py index df7160640471..4e8f233ce49e 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py @@ -108,22 +108,6 @@ def set_event_loop(): asyncio.set_event_loop(None) -def test__get_default_mtls_endpoint(): - api_endpoint = "example.googleapis.com" - api_mtls_endpoint = "example.mtls.googleapis.com" - sandbox_endpoint = "example.sandbox.googleapis.com" - sandbox_mtls_endpoint = "example.mtls.sandbox.googleapis.com" - non_googleapi = "api.example.com" - custom_endpoint = ".custom" - - assert BaseMetricsServiceV2Client._get_default_mtls_endpoint(None) is None - assert BaseMetricsServiceV2Client._get_default_mtls_endpoint(api_endpoint) == api_mtls_endpoint - assert BaseMetricsServiceV2Client._get_default_mtls_endpoint(api_mtls_endpoint) == api_mtls_endpoint - assert BaseMetricsServiceV2Client._get_default_mtls_endpoint(sandbox_endpoint) == sandbox_mtls_endpoint - assert BaseMetricsServiceV2Client._get_default_mtls_endpoint(sandbox_mtls_endpoint) == sandbox_mtls_endpoint - assert BaseMetricsServiceV2Client._get_default_mtls_endpoint(non_googleapi) == non_googleapi - assert BaseMetricsServiceV2Client._get_default_mtls_endpoint(custom_endpoint) == custom_endpoint - def test__read_environment_variables(): assert BaseMetricsServiceV2Client._read_environment_variables() == (False, "auto", None) @@ -265,40 +249,6 @@ def test__get_client_cert_source(): assert BaseMetricsServiceV2Client._get_client_cert_source(None, True) is mock_default_cert_source assert BaseMetricsServiceV2Client._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source -@mock.patch.object(BaseMetricsServiceV2Client, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(BaseMetricsServiceV2Client)) -@mock.patch.object(BaseMetricsServiceV2AsyncClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(BaseMetricsServiceV2AsyncClient)) -def test__get_api_endpoint(): - api_override = "foo.com" - mock_client_cert_source = mock.Mock() - default_universe = BaseMetricsServiceV2Client._DEFAULT_UNIVERSE - default_endpoint = BaseMetricsServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) - mock_universe = "bar.com" - mock_endpoint = BaseMetricsServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) - - assert BaseMetricsServiceV2Client._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override - assert BaseMetricsServiceV2Client._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == BaseMetricsServiceV2Client.DEFAULT_MTLS_ENDPOINT - assert BaseMetricsServiceV2Client._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint - assert BaseMetricsServiceV2Client._get_api_endpoint(None, None, default_universe, "always") == BaseMetricsServiceV2Client.DEFAULT_MTLS_ENDPOINT - assert BaseMetricsServiceV2Client._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == BaseMetricsServiceV2Client.DEFAULT_MTLS_ENDPOINT - assert BaseMetricsServiceV2Client._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint - assert BaseMetricsServiceV2Client._get_api_endpoint(None, None, default_universe, "never") == default_endpoint - - with pytest.raises(MutualTLSChannelError) as excinfo: - BaseMetricsServiceV2Client._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") - assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." - - -def test__get_universe_domain(): - client_universe_domain = "foo.com" - universe_domain_env = "bar.com" - - assert BaseMetricsServiceV2Client._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain - assert BaseMetricsServiceV2Client._get_universe_domain(None, universe_domain_env) == universe_domain_env - assert BaseMetricsServiceV2Client._get_universe_domain(None, None) == BaseMetricsServiceV2Client._DEFAULT_UNIVERSE - - with pytest.raises(ValueError) as excinfo: - BaseMetricsServiceV2Client._get_universe_domain("", None) - assert str(excinfo.value) == "Universe Domain cannot be an empty string." @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), @@ -675,7 +625,7 @@ def test_base_metrics_service_v2_client_get_mtls_endpoint_and_cert_source(client config_filename = "mock_certificate_config.json" config_file_content = json.dumps(config_data) m = mock.mock_open(read_data=config_file_content) - with mock.patch("builtins.open", m), mock.patch("os.path.exists", side_effect=lambda path: os.path.basename(path) == config_filename): + with mock.patch("builtins.open", m): with mock.patch.dict( os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": config_filename} ): @@ -723,7 +673,7 @@ def test_base_metrics_service_v2_client_get_mtls_endpoint_and_cert_source(client config_filename = "mock_certificate_config.json" config_file_content = json.dumps(config_data) m = mock.mock_open(read_data=config_file_content) - with mock.patch("builtins.open", m), mock.patch("os.path.exists", side_effect=lambda path: os.path.basename(path) == config_filename): + with mock.patch("builtins.open", m): with mock.patch.dict( os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": config_filename} ): diff --git a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/_compat.py b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/_compat.py index 7c684e94b03d..a84aecd6ca3d 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/_compat.py @@ -14,3 +14,152 @@ # limitations under the License. # """A compatibility module for older versions of google-api-core.""" +"""A compatibility module for older versions of google-api-core.""" + +import uuid + +from typing import Optional, Union +from urllib.parse import urlparse, urlunparse + +from google.auth.exceptions import MutualTLSChannelError +from google.api_core.universe import EmptyUniverseError +import google.protobuf.message + +DEFAULT_UNIVERSE = "googleapis.com" + + +def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: + """Converts api endpoint to mTLS endpoint. + + Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to + "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. + Other URLs (including those that do not match these domain suffixes or + already contain '.mtls.') are passed through as-is. + + Args: + api_endpoint (Optional[str]): the api endpoint to convert. + + Returns: + Optional[str]: converted mTLS api endpoint. + """ + if not api_endpoint or ".mtls." in api_endpoint.lower(): + return api_endpoint + + has_scheme = "://" in api_endpoint + if not has_scheme: + parsed = urlparse("//" + api_endpoint) + else: + parsed = urlparse(api_endpoint) + + host = parsed.hostname + if not host: + return api_endpoint + + port = f":{parsed.port}" if parsed.port else "" + + lowered_host = host.lower() + suffix_sandbox = ".sandbox.googleapis.com" + suffix_google = ".googleapis.com" + if lowered_host.endswith(suffix_sandbox): + new_host = host[: -len(suffix_sandbox)] + ".mtls.sandbox.googleapis.com" + elif lowered_host.endswith(suffix_google): + new_host = host[: -len(suffix_google)] + ".mtls.googleapis.com" + else: + return api_endpoint + + netloc = new_host + port + new_parsed = parsed._replace(netloc=netloc) + + if not has_scheme: + return urlunparse(new_parsed)[2:] + else: + return urlunparse(new_parsed) + +def get_api_endpoint( + api_override: Optional[str], + universe_domain: str, + default_universe: str, + default_mtls_endpoint: Optional[str], + default_endpoint_template: str, + use_mtls: bool, +) -> str: + """Return the API endpoint used by the client. + + Args: + api_override (Optional[str]): The API endpoint override. If specified, + this is always returned. + universe_domain (str): The universe domain used by the client. + default_universe (str): The default universe domain. + default_mtls_endpoint (Optional[str]): The default mTLS endpoint. + default_endpoint_template (str): The default endpoint template containing + a placeholder `{UNIVERSE_DOMAIN}`. + use_mtls (bool): Whether to use the mTLS endpoint. + + Returns: + str: The API endpoint to be used by the client. + + Raises: + google.auth.exceptions.MutualTLSChannelError: If mTLS is requested but + not supported in the configured universe domain. + ValueError: If mTLS is requested but no mTLS endpoint is available. + """ + if api_override is not None: + return api_override + + if use_mtls: + if universe_domain.lower() != default_universe.lower(): + raise MutualTLSChannelError( + f"mTLS is not supported in any universe other than {default_universe}." + ) + if not default_mtls_endpoint: + raise ValueError("mTLS endpoint is not available.") + return default_mtls_endpoint + else: + return default_endpoint_template.format(UNIVERSE_DOMAIN=universe_domain) + +def get_universe_domain( + *potential_universes: Optional[str], + default_universe: str, +) -> str: + """Return the universe domain used by the client. + + Args: + *potential_universes (Optional[str]): Potential universe domains in order of preference. + default_universe (str): The default universe domain. + + Returns: + str: The universe domain to be used by the client. + + Raises: + EmptyUniverseError: If the resolved universe domain is an empty string. + """ + resolved = next( + (x.strip() for x in potential_universes if x is not None), + default_universe, + ) + + if not resolved: + raise EmptyUniverseError() + return resolved + +def determine_domain( + client_universe_domain: Optional[str], universe_domain_env: Optional[str] +) -> str: + """Return the universe domain used by the client. + + Args: + client_universe_domain (Optional[str]): The universe domain configured via the client options. + universe_domain_env (Optional[str]): The universe domain configured via the + "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. + + Returns: + str: The universe domain to be used by the client. + + Raises: + ValueError: If the universe domain is an empty string. + """ + return get_universe_domain( + client_universe_domain, + universe_domain_env, + default_universe=DEFAULT_UNIVERSE, + ) diff --git a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/client.py b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/client.py index 33ccce478c8e..c3da2c4adecb 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/client.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/client.py @@ -27,6 +27,7 @@ from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.cloud.redis_v1._compat import determine_domain, get_api_endpoint, get_default_mtls_endpoint from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -131,43 +132,9 @@ class CloudRedisClient(metaclass=CloudRedisClientMeta): - ``projects/redpepper-1290/locations/us-central1/instances/my-redis`` """ - @staticmethod - def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: - """Converts api endpoint to mTLS endpoint. - - Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to - "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. - Args: - api_endpoint (Optional[str]): the api endpoint to convert. - Returns: - Optional[str]: converted mTLS api endpoint. - """ - if not api_endpoint: - return api_endpoint - - mtls_endpoint_re = re.compile( - r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" - ) - - m = mtls_endpoint_re.match(api_endpoint) - if m is None: - # Could not parse api_endpoint; return as-is. - return api_endpoint - - name, mtls, sandbox, googledomain = m.groups() - if mtls or not googledomain: - return api_endpoint - - if sandbox: - return api_endpoint.replace( - "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" - ) - - return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") - # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "redis.googleapis.com" - DEFAULT_MTLS_ENDPOINT = _get_default_mtls_endpoint.__func__( # type: ignore + DEFAULT_MTLS_ENDPOINT = get_default_mtls_endpoint( DEFAULT_ENDPOINT ) @@ -414,55 +381,6 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): client_cert_source = mtls.default_client_cert_source() return client_cert_source - @staticmethod - def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: - """Return the API endpoint used by the client. - - Args: - api_override (str): The API endpoint override. If specified, this is always - the return value of this function and the other arguments are not used. - client_cert_source (bytes): The client certificate source used by the client. - universe_domain (str): The universe domain used by the client. - use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. - Possible values are "always", "auto", or "never". - - Returns: - str: The API endpoint to be used by the client. - """ - if api_override is not None: - api_endpoint = api_override - elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): - _default_universe = CloudRedisClient._DEFAULT_UNIVERSE - if universe_domain != _default_universe: - raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") - api_endpoint = CloudRedisClient.DEFAULT_MTLS_ENDPOINT - else: - api_endpoint = CloudRedisClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) - return api_endpoint - - @staticmethod - def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client. - - Args: - client_universe_domain (Optional[str]): The universe domain configured via the client options. - universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. - - Returns: - str: The universe domain to be used by the client. - - Raises: - ValueError: If the universe domain is an empty string. - """ - universe_domain = CloudRedisClient._DEFAULT_UNIVERSE - if client_universe_domain is not None: - universe_domain = client_universe_domain - elif universe_domain_env is not None: - universe_domain = universe_domain_env - if len(universe_domain.strip()) == 0: - raise ValueError("Universe Domain cannot be an empty string.") - return universe_domain - def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. @@ -587,7 +505,7 @@ def __init__(self, *, self._use_client_cert, self._use_mtls_endpoint, self._universe_domain_env = CloudRedisClient._read_environment_variables() self._client_cert_source = CloudRedisClient._get_client_cert_source(self._client_options.client_cert_source, self._use_client_cert) - self._universe_domain = CloudRedisClient._get_universe_domain(universe_domain_opt, self._universe_domain_env) + self._universe_domain = determine_domain(universe_domain_opt, self._universe_domain_env) self._api_endpoint: str = "" # updated below, depending on `transport` # Initialize the universe domain validation. @@ -619,7 +537,7 @@ def __init__(self, *, self._api_endpoint = self._transport.host self._api_endpoint = (self._api_endpoint or - CloudRedisClient._get_api_endpoint( + get_api_endpoint( self._client_options.api_endpoint, self._client_cert_source, self._universe_domain, diff --git a/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py b/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py index 0de09163e097..60289630a697 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py @@ -126,22 +126,6 @@ def set_event_loop(): asyncio.set_event_loop(None) -def test__get_default_mtls_endpoint(): - api_endpoint = "example.googleapis.com" - api_mtls_endpoint = "example.mtls.googleapis.com" - sandbox_endpoint = "example.sandbox.googleapis.com" - sandbox_mtls_endpoint = "example.mtls.sandbox.googleapis.com" - non_googleapi = "api.example.com" - custom_endpoint = ".custom" - - assert CloudRedisClient._get_default_mtls_endpoint(None) is None - assert CloudRedisClient._get_default_mtls_endpoint(api_endpoint) == api_mtls_endpoint - assert CloudRedisClient._get_default_mtls_endpoint(api_mtls_endpoint) == api_mtls_endpoint - assert CloudRedisClient._get_default_mtls_endpoint(sandbox_endpoint) == sandbox_mtls_endpoint - assert CloudRedisClient._get_default_mtls_endpoint(sandbox_mtls_endpoint) == sandbox_mtls_endpoint - assert CloudRedisClient._get_default_mtls_endpoint(non_googleapi) == non_googleapi - assert CloudRedisClient._get_default_mtls_endpoint(custom_endpoint) == custom_endpoint - def test__read_environment_variables(): assert CloudRedisClient._read_environment_variables() == (False, "auto", None) @@ -283,40 +267,6 @@ def test__get_client_cert_source(): assert CloudRedisClient._get_client_cert_source(None, True) is mock_default_cert_source assert CloudRedisClient._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source -@mock.patch.object(CloudRedisClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(CloudRedisClient)) -@mock.patch.object(CloudRedisAsyncClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(CloudRedisAsyncClient)) -def test__get_api_endpoint(): - api_override = "foo.com" - mock_client_cert_source = mock.Mock() - default_universe = CloudRedisClient._DEFAULT_UNIVERSE - default_endpoint = CloudRedisClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) - mock_universe = "bar.com" - mock_endpoint = CloudRedisClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) - - assert CloudRedisClient._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override - assert CloudRedisClient._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == CloudRedisClient.DEFAULT_MTLS_ENDPOINT - assert CloudRedisClient._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint - assert CloudRedisClient._get_api_endpoint(None, None, default_universe, "always") == CloudRedisClient.DEFAULT_MTLS_ENDPOINT - assert CloudRedisClient._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == CloudRedisClient.DEFAULT_MTLS_ENDPOINT - assert CloudRedisClient._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint - assert CloudRedisClient._get_api_endpoint(None, None, default_universe, "never") == default_endpoint - - with pytest.raises(MutualTLSChannelError) as excinfo: - CloudRedisClient._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") - assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." - - -def test__get_universe_domain(): - client_universe_domain = "foo.com" - universe_domain_env = "bar.com" - - assert CloudRedisClient._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain - assert CloudRedisClient._get_universe_domain(None, universe_domain_env) == universe_domain_env - assert CloudRedisClient._get_universe_domain(None, None) == CloudRedisClient._DEFAULT_UNIVERSE - - with pytest.raises(ValueError) as excinfo: - CloudRedisClient._get_universe_domain("", None) - assert str(excinfo.value) == "Universe Domain cannot be an empty string." @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), @@ -706,7 +656,7 @@ def test_cloud_redis_client_get_mtls_endpoint_and_cert_source(client_class): config_filename = "mock_certificate_config.json" config_file_content = json.dumps(config_data) m = mock.mock_open(read_data=config_file_content) - with mock.patch("builtins.open", m), mock.patch("os.path.exists", side_effect=lambda path: os.path.basename(path) == config_filename): + with mock.patch("builtins.open", m): with mock.patch.dict( os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": config_filename} ): @@ -754,7 +704,7 @@ def test_cloud_redis_client_get_mtls_endpoint_and_cert_source(client_class): config_filename = "mock_certificate_config.json" config_file_content = json.dumps(config_data) m = mock.mock_open(read_data=config_file_content) - with mock.patch("builtins.open", m), mock.patch("os.path.exists", side_effect=lambda path: os.path.basename(path) == config_filename): + with mock.patch("builtins.open", m): with mock.patch.dict( os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": config_filename} ): diff --git a/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_compat.py b/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_compat.py index 8491204d1041..f6d387ac1868 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_compat.py @@ -14,3 +14,206 @@ # limitations under the License. # """Tests for the compatibility module for older versions of google-api-core.""" + +import re +import pytest + +from google.cloud.redis_v1._compat import determine_domain, get_api_endpoint, get_default_mtls_endpoint + +from google.auth.exceptions import MutualTLSChannelError + + +def test_get_universe_domain(): + # When universe_domain is provided + assert ( + universe.get_universe_domain("foo.com", default_universe="default.com") + == "foo.com" + ) + assert ( + universe.get_universe_domain(" foo.com ", default_universe="default.com") + == "foo.com" + ) + + # When universe_domain is None, falls back to default_universe + assert ( + universe.get_universe_domain(None, default_universe="default.com") + == "default.com" + ) + + # When multiple potential universes are provided, resolves in order of preference + assert ( + universe.get_universe_domain( + "foo.com", "bar.com", default_universe="default.com" + ) + == "foo.com" + ) + assert ( + universe.get_universe_domain(None, "bar.com", default_universe="default.com") + == "bar.com" + ) + assert ( + universe.get_universe_domain(None, None, default_universe="default.com") + == "default.com" + ) + + # EmptyUniverseError raised when resolved value is empty string + with pytest.raises(universe.EmptyUniverseError) as excinfo: + universe.get_universe_domain("", default_universe="default.com") + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + + with pytest.raises(universe.EmptyUniverseError) as excinfo: + universe.get_universe_domain(" ", default_universe="default.com") + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + + with pytest.raises(universe.EmptyUniverseError) as excinfo: + universe.get_universe_domain(None, "", default_universe="default.com") + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + + +def test_get_default_mtls_endpoint(): + # Test valid API endpoints + assert ( + universe.get_default_mtls_endpoint("foo.googleapis.com") + == "foo.mtls.googleapis.com" + ) + assert ( + universe.get_default_mtls_endpoint("foo.sandbox.googleapis.com") + == "foo.mtls.sandbox.googleapis.com" + ) + # Test case-insensitivity + assert ( + universe.get_default_mtls_endpoint("foo.GoogleAPIs.com") + == "foo.mtls.googleapis.com" + ) + assert ( + universe.get_default_mtls_endpoint("foo.Sandbox.GoogleAPIs.com") + == "foo.mtls.sandbox.googleapis.com" + ) + + # Test valid API endpoints with schemes + assert ( + universe.get_default_mtls_endpoint("https://foo.googleapis.com") + == "https://foo.mtls.googleapis.com" + ) + assert ( + universe.get_default_mtls_endpoint("http://foo.googleapis.com:8080/v1") + == "http://foo.mtls.googleapis.com:8080/v1" + ) + + # Test valid API endpoints with ports + assert ( + universe.get_default_mtls_endpoint("foo.googleapis.com:443") + == "foo.mtls.googleapis.com:443" + ) + assert ( + universe.get_default_mtls_endpoint("foo.sandbox.googleapis.com:443") + == "foo.mtls.sandbox.googleapis.com:443" + ) + # Test case-insensitivity with ports + assert ( + universe.get_default_mtls_endpoint("foo.GoogleAPIs.com:443") + == "foo.mtls.googleapis.com:443" + ) + assert ( + universe.get_default_mtls_endpoint("foo.Sandbox.GoogleAPIs.com:443") + == "foo.mtls.sandbox.googleapis.com:443" + ) + + # Test endpoints that shouldn't be converted + assert ( + universe.get_default_mtls_endpoint("foo.mtls.googleapis.com") + == "foo.mtls.googleapis.com" + ) + assert universe.get_default_mtls_endpoint("foo.com") == "foo.com" + assert universe.get_default_mtls_endpoint("foo.com:8080") == "foo.com:8080" + + # Test empty/None endpoints + assert universe.get_default_mtls_endpoint("") == "" + assert universe.get_default_mtls_endpoint(None) is None + + # Test endpoints without host + assert universe.get_default_mtls_endpoint("http://") == "http://" + assert universe.get_default_mtls_endpoint("https://") == "https://" + + +@pytest.mark.parametrize( + "api_override,universe_domain,default_universe,default_mtls_endpoint,default_endpoint_template,use_mtls,expected", + [ + ( + "foo.com", + "googleapis.com", + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + True, + "foo.com", + ), + ( + None, + "googleapis.com", + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + True, + "foo.mtls.googleapis.com", + ), + ( + None, + "googleapis.com", + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + False, + "foo.googleapis.com", + ), + ( + None, + "bar.com", + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + True, + MutualTLSChannelError, + ), + ( + None, + "googleapis.com", + "googleapis.com", + None, + "foo.{UNIVERSE_DOMAIN}", + True, + ValueError, + ), + ], +) +def test_get_api_endpoint( + api_override, + universe_domain, + default_universe, + default_mtls_endpoint, + default_endpoint_template, + use_mtls, + expected, +): + if isinstance(expected, type) and issubclass(expected, Exception): + with pytest.raises(expected): + universe.get_api_endpoint( + api_override, + universe_domain, + default_universe, + default_mtls_endpoint, + default_endpoint_template, + use_mtls, + ) + else: + assert ( + universe.get_api_endpoint( + api_override, + universe_domain, + default_universe, + default_mtls_endpoint, + default_endpoint_template, + use_mtls, + ) + == expected + ) diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/_compat.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/_compat.py index 7c684e94b03d..a84aecd6ca3d 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/_compat.py @@ -14,3 +14,152 @@ # limitations under the License. # """A compatibility module for older versions of google-api-core.""" +"""A compatibility module for older versions of google-api-core.""" + +import uuid + +from typing import Optional, Union +from urllib.parse import urlparse, urlunparse + +from google.auth.exceptions import MutualTLSChannelError +from google.api_core.universe import EmptyUniverseError +import google.protobuf.message + +DEFAULT_UNIVERSE = "googleapis.com" + + +def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: + """Converts api endpoint to mTLS endpoint. + + Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to + "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. + Other URLs (including those that do not match these domain suffixes or + already contain '.mtls.') are passed through as-is. + + Args: + api_endpoint (Optional[str]): the api endpoint to convert. + + Returns: + Optional[str]: converted mTLS api endpoint. + """ + if not api_endpoint or ".mtls." in api_endpoint.lower(): + return api_endpoint + + has_scheme = "://" in api_endpoint + if not has_scheme: + parsed = urlparse("//" + api_endpoint) + else: + parsed = urlparse(api_endpoint) + + host = parsed.hostname + if not host: + return api_endpoint + + port = f":{parsed.port}" if parsed.port else "" + + lowered_host = host.lower() + suffix_sandbox = ".sandbox.googleapis.com" + suffix_google = ".googleapis.com" + if lowered_host.endswith(suffix_sandbox): + new_host = host[: -len(suffix_sandbox)] + ".mtls.sandbox.googleapis.com" + elif lowered_host.endswith(suffix_google): + new_host = host[: -len(suffix_google)] + ".mtls.googleapis.com" + else: + return api_endpoint + + netloc = new_host + port + new_parsed = parsed._replace(netloc=netloc) + + if not has_scheme: + return urlunparse(new_parsed)[2:] + else: + return urlunparse(new_parsed) + +def get_api_endpoint( + api_override: Optional[str], + universe_domain: str, + default_universe: str, + default_mtls_endpoint: Optional[str], + default_endpoint_template: str, + use_mtls: bool, +) -> str: + """Return the API endpoint used by the client. + + Args: + api_override (Optional[str]): The API endpoint override. If specified, + this is always returned. + universe_domain (str): The universe domain used by the client. + default_universe (str): The default universe domain. + default_mtls_endpoint (Optional[str]): The default mTLS endpoint. + default_endpoint_template (str): The default endpoint template containing + a placeholder `{UNIVERSE_DOMAIN}`. + use_mtls (bool): Whether to use the mTLS endpoint. + + Returns: + str: The API endpoint to be used by the client. + + Raises: + google.auth.exceptions.MutualTLSChannelError: If mTLS is requested but + not supported in the configured universe domain. + ValueError: If mTLS is requested but no mTLS endpoint is available. + """ + if api_override is not None: + return api_override + + if use_mtls: + if universe_domain.lower() != default_universe.lower(): + raise MutualTLSChannelError( + f"mTLS is not supported in any universe other than {default_universe}." + ) + if not default_mtls_endpoint: + raise ValueError("mTLS endpoint is not available.") + return default_mtls_endpoint + else: + return default_endpoint_template.format(UNIVERSE_DOMAIN=universe_domain) + +def get_universe_domain( + *potential_universes: Optional[str], + default_universe: str, +) -> str: + """Return the universe domain used by the client. + + Args: + *potential_universes (Optional[str]): Potential universe domains in order of preference. + default_universe (str): The default universe domain. + + Returns: + str: The universe domain to be used by the client. + + Raises: + EmptyUniverseError: If the resolved universe domain is an empty string. + """ + resolved = next( + (x.strip() for x in potential_universes if x is not None), + default_universe, + ) + + if not resolved: + raise EmptyUniverseError() + return resolved + +def determine_domain( + client_universe_domain: Optional[str], universe_domain_env: Optional[str] +) -> str: + """Return the universe domain used by the client. + + Args: + client_universe_domain (Optional[str]): The universe domain configured via the client options. + universe_domain_env (Optional[str]): The universe domain configured via the + "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. + + Returns: + str: The universe domain to be used by the client. + + Raises: + ValueError: If the universe domain is an empty string. + """ + return get_universe_domain( + client_universe_domain, + universe_domain_env, + default_universe=DEFAULT_UNIVERSE, + ) diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/client.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/client.py index 031573ef83d7..f1059a858ceb 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/client.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/client.py @@ -27,6 +27,7 @@ from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.cloud.redis_v1._compat import determine_domain, get_api_endpoint, get_default_mtls_endpoint from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -131,43 +132,9 @@ class CloudRedisClient(metaclass=CloudRedisClientMeta): - ``projects/redpepper-1290/locations/us-central1/instances/my-redis`` """ - @staticmethod - def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: - """Converts api endpoint to mTLS endpoint. - - Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to - "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. - Args: - api_endpoint (Optional[str]): the api endpoint to convert. - Returns: - Optional[str]: converted mTLS api endpoint. - """ - if not api_endpoint: - return api_endpoint - - mtls_endpoint_re = re.compile( - r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" - ) - - m = mtls_endpoint_re.match(api_endpoint) - if m is None: - # Could not parse api_endpoint; return as-is. - return api_endpoint - - name, mtls, sandbox, googledomain = m.groups() - if mtls or not googledomain: - return api_endpoint - - if sandbox: - return api_endpoint.replace( - "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" - ) - - return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") - # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "redis.googleapis.com" - DEFAULT_MTLS_ENDPOINT = _get_default_mtls_endpoint.__func__( # type: ignore + DEFAULT_MTLS_ENDPOINT = get_default_mtls_endpoint( DEFAULT_ENDPOINT ) @@ -414,55 +381,6 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): client_cert_source = mtls.default_client_cert_source() return client_cert_source - @staticmethod - def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: - """Return the API endpoint used by the client. - - Args: - api_override (str): The API endpoint override. If specified, this is always - the return value of this function and the other arguments are not used. - client_cert_source (bytes): The client certificate source used by the client. - universe_domain (str): The universe domain used by the client. - use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. - Possible values are "always", "auto", or "never". - - Returns: - str: The API endpoint to be used by the client. - """ - if api_override is not None: - api_endpoint = api_override - elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): - _default_universe = CloudRedisClient._DEFAULT_UNIVERSE - if universe_domain != _default_universe: - raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") - api_endpoint = CloudRedisClient.DEFAULT_MTLS_ENDPOINT - else: - api_endpoint = CloudRedisClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) - return api_endpoint - - @staticmethod - def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client. - - Args: - client_universe_domain (Optional[str]): The universe domain configured via the client options. - universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. - - Returns: - str: The universe domain to be used by the client. - - Raises: - ValueError: If the universe domain is an empty string. - """ - universe_domain = CloudRedisClient._DEFAULT_UNIVERSE - if client_universe_domain is not None: - universe_domain = client_universe_domain - elif universe_domain_env is not None: - universe_domain = universe_domain_env - if len(universe_domain.strip()) == 0: - raise ValueError("Universe Domain cannot be an empty string.") - return universe_domain - def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. @@ -587,7 +505,7 @@ def __init__(self, *, self._use_client_cert, self._use_mtls_endpoint, self._universe_domain_env = CloudRedisClient._read_environment_variables() self._client_cert_source = CloudRedisClient._get_client_cert_source(self._client_options.client_cert_source, self._use_client_cert) - self._universe_domain = CloudRedisClient._get_universe_domain(universe_domain_opt, self._universe_domain_env) + self._universe_domain = determine_domain(universe_domain_opt, self._universe_domain_env) self._api_endpoint: str = "" # updated below, depending on `transport` # Initialize the universe domain validation. @@ -619,7 +537,7 @@ def __init__(self, *, self._api_endpoint = self._transport.host self._api_endpoint = (self._api_endpoint or - CloudRedisClient._get_api_endpoint( + get_api_endpoint( self._client_options.api_endpoint, self._client_cert_source, self._universe_domain, diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py index a7f6c15a439c..57c82e06731b 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py @@ -126,22 +126,6 @@ def set_event_loop(): asyncio.set_event_loop(None) -def test__get_default_mtls_endpoint(): - api_endpoint = "example.googleapis.com" - api_mtls_endpoint = "example.mtls.googleapis.com" - sandbox_endpoint = "example.sandbox.googleapis.com" - sandbox_mtls_endpoint = "example.mtls.sandbox.googleapis.com" - non_googleapi = "api.example.com" - custom_endpoint = ".custom" - - assert CloudRedisClient._get_default_mtls_endpoint(None) is None - assert CloudRedisClient._get_default_mtls_endpoint(api_endpoint) == api_mtls_endpoint - assert CloudRedisClient._get_default_mtls_endpoint(api_mtls_endpoint) == api_mtls_endpoint - assert CloudRedisClient._get_default_mtls_endpoint(sandbox_endpoint) == sandbox_mtls_endpoint - assert CloudRedisClient._get_default_mtls_endpoint(sandbox_mtls_endpoint) == sandbox_mtls_endpoint - assert CloudRedisClient._get_default_mtls_endpoint(non_googleapi) == non_googleapi - assert CloudRedisClient._get_default_mtls_endpoint(custom_endpoint) == custom_endpoint - def test__read_environment_variables(): assert CloudRedisClient._read_environment_variables() == (False, "auto", None) @@ -283,40 +267,6 @@ def test__get_client_cert_source(): assert CloudRedisClient._get_client_cert_source(None, True) is mock_default_cert_source assert CloudRedisClient._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source -@mock.patch.object(CloudRedisClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(CloudRedisClient)) -@mock.patch.object(CloudRedisAsyncClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(CloudRedisAsyncClient)) -def test__get_api_endpoint(): - api_override = "foo.com" - mock_client_cert_source = mock.Mock() - default_universe = CloudRedisClient._DEFAULT_UNIVERSE - default_endpoint = CloudRedisClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) - mock_universe = "bar.com" - mock_endpoint = CloudRedisClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) - - assert CloudRedisClient._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override - assert CloudRedisClient._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == CloudRedisClient.DEFAULT_MTLS_ENDPOINT - assert CloudRedisClient._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint - assert CloudRedisClient._get_api_endpoint(None, None, default_universe, "always") == CloudRedisClient.DEFAULT_MTLS_ENDPOINT - assert CloudRedisClient._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == CloudRedisClient.DEFAULT_MTLS_ENDPOINT - assert CloudRedisClient._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint - assert CloudRedisClient._get_api_endpoint(None, None, default_universe, "never") == default_endpoint - - with pytest.raises(MutualTLSChannelError) as excinfo: - CloudRedisClient._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") - assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." - - -def test__get_universe_domain(): - client_universe_domain = "foo.com" - universe_domain_env = "bar.com" - - assert CloudRedisClient._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain - assert CloudRedisClient._get_universe_domain(None, universe_domain_env) == universe_domain_env - assert CloudRedisClient._get_universe_domain(None, None) == CloudRedisClient._DEFAULT_UNIVERSE - - with pytest.raises(ValueError) as excinfo: - CloudRedisClient._get_universe_domain("", None) - assert str(excinfo.value) == "Universe Domain cannot be an empty string." @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), @@ -706,7 +656,7 @@ def test_cloud_redis_client_get_mtls_endpoint_and_cert_source(client_class): config_filename = "mock_certificate_config.json" config_file_content = json.dumps(config_data) m = mock.mock_open(read_data=config_file_content) - with mock.patch("builtins.open", m), mock.patch("os.path.exists", side_effect=lambda path: os.path.basename(path) == config_filename): + with mock.patch("builtins.open", m): with mock.patch.dict( os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": config_filename} ): @@ -754,7 +704,7 @@ def test_cloud_redis_client_get_mtls_endpoint_and_cert_source(client_class): config_filename = "mock_certificate_config.json" config_file_content = json.dumps(config_data) m = mock.mock_open(read_data=config_file_content) - with mock.patch("builtins.open", m), mock.patch("os.path.exists", side_effect=lambda path: os.path.basename(path) == config_filename): + with mock.patch("builtins.open", m): with mock.patch.dict( os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": config_filename} ): diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_compat.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_compat.py index 8491204d1041..f6d387ac1868 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_compat.py @@ -14,3 +14,206 @@ # limitations under the License. # """Tests for the compatibility module for older versions of google-api-core.""" + +import re +import pytest + +from google.cloud.redis_v1._compat import determine_domain, get_api_endpoint, get_default_mtls_endpoint + +from google.auth.exceptions import MutualTLSChannelError + + +def test_get_universe_domain(): + # When universe_domain is provided + assert ( + universe.get_universe_domain("foo.com", default_universe="default.com") + == "foo.com" + ) + assert ( + universe.get_universe_domain(" foo.com ", default_universe="default.com") + == "foo.com" + ) + + # When universe_domain is None, falls back to default_universe + assert ( + universe.get_universe_domain(None, default_universe="default.com") + == "default.com" + ) + + # When multiple potential universes are provided, resolves in order of preference + assert ( + universe.get_universe_domain( + "foo.com", "bar.com", default_universe="default.com" + ) + == "foo.com" + ) + assert ( + universe.get_universe_domain(None, "bar.com", default_universe="default.com") + == "bar.com" + ) + assert ( + universe.get_universe_domain(None, None, default_universe="default.com") + == "default.com" + ) + + # EmptyUniverseError raised when resolved value is empty string + with pytest.raises(universe.EmptyUniverseError) as excinfo: + universe.get_universe_domain("", default_universe="default.com") + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + + with pytest.raises(universe.EmptyUniverseError) as excinfo: + universe.get_universe_domain(" ", default_universe="default.com") + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + + with pytest.raises(universe.EmptyUniverseError) as excinfo: + universe.get_universe_domain(None, "", default_universe="default.com") + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + + +def test_get_default_mtls_endpoint(): + # Test valid API endpoints + assert ( + universe.get_default_mtls_endpoint("foo.googleapis.com") + == "foo.mtls.googleapis.com" + ) + assert ( + universe.get_default_mtls_endpoint("foo.sandbox.googleapis.com") + == "foo.mtls.sandbox.googleapis.com" + ) + # Test case-insensitivity + assert ( + universe.get_default_mtls_endpoint("foo.GoogleAPIs.com") + == "foo.mtls.googleapis.com" + ) + assert ( + universe.get_default_mtls_endpoint("foo.Sandbox.GoogleAPIs.com") + == "foo.mtls.sandbox.googleapis.com" + ) + + # Test valid API endpoints with schemes + assert ( + universe.get_default_mtls_endpoint("https://foo.googleapis.com") + == "https://foo.mtls.googleapis.com" + ) + assert ( + universe.get_default_mtls_endpoint("http://foo.googleapis.com:8080/v1") + == "http://foo.mtls.googleapis.com:8080/v1" + ) + + # Test valid API endpoints with ports + assert ( + universe.get_default_mtls_endpoint("foo.googleapis.com:443") + == "foo.mtls.googleapis.com:443" + ) + assert ( + universe.get_default_mtls_endpoint("foo.sandbox.googleapis.com:443") + == "foo.mtls.sandbox.googleapis.com:443" + ) + # Test case-insensitivity with ports + assert ( + universe.get_default_mtls_endpoint("foo.GoogleAPIs.com:443") + == "foo.mtls.googleapis.com:443" + ) + assert ( + universe.get_default_mtls_endpoint("foo.Sandbox.GoogleAPIs.com:443") + == "foo.mtls.sandbox.googleapis.com:443" + ) + + # Test endpoints that shouldn't be converted + assert ( + universe.get_default_mtls_endpoint("foo.mtls.googleapis.com") + == "foo.mtls.googleapis.com" + ) + assert universe.get_default_mtls_endpoint("foo.com") == "foo.com" + assert universe.get_default_mtls_endpoint("foo.com:8080") == "foo.com:8080" + + # Test empty/None endpoints + assert universe.get_default_mtls_endpoint("") == "" + assert universe.get_default_mtls_endpoint(None) is None + + # Test endpoints without host + assert universe.get_default_mtls_endpoint("http://") == "http://" + assert universe.get_default_mtls_endpoint("https://") == "https://" + + +@pytest.mark.parametrize( + "api_override,universe_domain,default_universe,default_mtls_endpoint,default_endpoint_template,use_mtls,expected", + [ + ( + "foo.com", + "googleapis.com", + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + True, + "foo.com", + ), + ( + None, + "googleapis.com", + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + True, + "foo.mtls.googleapis.com", + ), + ( + None, + "googleapis.com", + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + False, + "foo.googleapis.com", + ), + ( + None, + "bar.com", + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + True, + MutualTLSChannelError, + ), + ( + None, + "googleapis.com", + "googleapis.com", + None, + "foo.{UNIVERSE_DOMAIN}", + True, + ValueError, + ), + ], +) +def test_get_api_endpoint( + api_override, + universe_domain, + default_universe, + default_mtls_endpoint, + default_endpoint_template, + use_mtls, + expected, +): + if isinstance(expected, type) and issubclass(expected, Exception): + with pytest.raises(expected): + universe.get_api_endpoint( + api_override, + universe_domain, + default_universe, + default_mtls_endpoint, + default_endpoint_template, + use_mtls, + ) + else: + assert ( + universe.get_api_endpoint( + api_override, + universe_domain, + default_universe, + default_mtls_endpoint, + default_endpoint_template, + use_mtls, + ) + == expected + ) diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/_compat.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/_compat.py index 1b76bd6ca40b..a21cae8ea589 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/_compat.py @@ -14,12 +14,156 @@ # limitations under the License. # """A compatibility module for older versions of google-api-core.""" +"""A compatibility module for older versions of google-api-core.""" -from typing import Union import uuid +from typing import Optional, Union +from urllib.parse import urlparse, urlunparse + +from google.auth.exceptions import MutualTLSChannelError +from google.api_core.universe import EmptyUniverseError import google.protobuf.message +DEFAULT_UNIVERSE = "googleapis.com" + + +def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: + """Converts api endpoint to mTLS endpoint. + + Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to + "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. + Other URLs (including those that do not match these domain suffixes or + already contain '.mtls.') are passed through as-is. + + Args: + api_endpoint (Optional[str]): the api endpoint to convert. + + Returns: + Optional[str]: converted mTLS api endpoint. + """ + if not api_endpoint or ".mtls." in api_endpoint.lower(): + return api_endpoint + + has_scheme = "://" in api_endpoint + if not has_scheme: + parsed = urlparse("//" + api_endpoint) + else: + parsed = urlparse(api_endpoint) + + host = parsed.hostname + if not host: + return api_endpoint + + port = f":{parsed.port}" if parsed.port else "" + + lowered_host = host.lower() + suffix_sandbox = ".sandbox.googleapis.com" + suffix_google = ".googleapis.com" + if lowered_host.endswith(suffix_sandbox): + new_host = host[: -len(suffix_sandbox)] + ".mtls.sandbox.googleapis.com" + elif lowered_host.endswith(suffix_google): + new_host = host[: -len(suffix_google)] + ".mtls.googleapis.com" + else: + return api_endpoint + + netloc = new_host + port + new_parsed = parsed._replace(netloc=netloc) + + if not has_scheme: + return urlunparse(new_parsed)[2:] + else: + return urlunparse(new_parsed) + +def get_api_endpoint( + api_override: Optional[str], + universe_domain: str, + default_universe: str, + default_mtls_endpoint: Optional[str], + default_endpoint_template: str, + use_mtls: bool, +) -> str: + """Return the API endpoint used by the client. + + Args: + api_override (Optional[str]): The API endpoint override. If specified, + this is always returned. + universe_domain (str): The universe domain used by the client. + default_universe (str): The default universe domain. + default_mtls_endpoint (Optional[str]): The default mTLS endpoint. + default_endpoint_template (str): The default endpoint template containing + a placeholder `{UNIVERSE_DOMAIN}`. + use_mtls (bool): Whether to use the mTLS endpoint. + + Returns: + str: The API endpoint to be used by the client. + + Raises: + google.auth.exceptions.MutualTLSChannelError: If mTLS is requested but + not supported in the configured universe domain. + ValueError: If mTLS is requested but no mTLS endpoint is available. + """ + if api_override is not None: + return api_override + + if use_mtls: + if universe_domain.lower() != default_universe.lower(): + raise MutualTLSChannelError( + f"mTLS is not supported in any universe other than {default_universe}." + ) + if not default_mtls_endpoint: + raise ValueError("mTLS endpoint is not available.") + return default_mtls_endpoint + else: + return default_endpoint_template.format(UNIVERSE_DOMAIN=universe_domain) + +def get_universe_domain( + *potential_universes: Optional[str], + default_universe: str, +) -> str: + """Return the universe domain used by the client. + + Args: + *potential_universes (Optional[str]): Potential universe domains in order of preference. + default_universe (str): The default universe domain. + + Returns: + str: The universe domain to be used by the client. + + Raises: + EmptyUniverseError: If the resolved universe domain is an empty string. + """ + resolved = next( + (x.strip() for x in potential_universes if x is not None), + default_universe, + ) + + if not resolved: + raise EmptyUniverseError() + return resolved + +def determine_domain( + client_universe_domain: Optional[str], universe_domain_env: Optional[str] +) -> str: + """Return the universe domain used by the client. + + Args: + client_universe_domain (Optional[str]): The universe domain configured via the client options. + universe_domain_env (Optional[str]): The universe domain configured via the + "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. + + Returns: + str: The universe domain to be used by the client. + + Raises: + ValueError: If the universe domain is an empty string. + """ + return get_universe_domain( + client_universe_domain, + universe_domain_env, + default_universe=DEFAULT_UNIVERSE, + ) + def setup_request_id( request: Union[google.protobuf.message.Message, dict, None], diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/client.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/client.py index 9ae9009a3c7c..9d4bf56bb3e7 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/client.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/client.py @@ -28,6 +28,7 @@ from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.cloud.storagebatchoperations_v1._compat import determine_domain, get_api_endpoint, get_default_mtls_endpoint from google.cloud.storagebatchoperations_v1._compat import setup_request_id from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore @@ -105,43 +106,9 @@ class StorageBatchOperationsClient(metaclass=StorageBatchOperationsClientMeta): objects. """ - @staticmethod - def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: - """Converts api endpoint to mTLS endpoint. - - Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to - "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. - Args: - api_endpoint (Optional[str]): the api endpoint to convert. - Returns: - Optional[str]: converted mTLS api endpoint. - """ - if not api_endpoint: - return api_endpoint - - mtls_endpoint_re = re.compile( - r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" - ) - - m = mtls_endpoint_re.match(api_endpoint) - if m is None: - # Could not parse api_endpoint; return as-is. - return api_endpoint - - name, mtls, sandbox, googledomain = m.groups() - if mtls or not googledomain: - return api_endpoint - - if sandbox: - return api_endpoint.replace( - "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" - ) - - return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") - # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "storagebatchoperations.googleapis.com" - DEFAULT_MTLS_ENDPOINT = _get_default_mtls_endpoint.__func__( # type: ignore + DEFAULT_MTLS_ENDPOINT = get_default_mtls_endpoint( DEFAULT_ENDPOINT ) @@ -410,55 +377,6 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): client_cert_source = mtls.default_client_cert_source() return client_cert_source - @staticmethod - def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: - """Return the API endpoint used by the client. - - Args: - api_override (str): The API endpoint override. If specified, this is always - the return value of this function and the other arguments are not used. - client_cert_source (bytes): The client certificate source used by the client. - universe_domain (str): The universe domain used by the client. - use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. - Possible values are "always", "auto", or "never". - - Returns: - str: The API endpoint to be used by the client. - """ - if api_override is not None: - api_endpoint = api_override - elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): - _default_universe = StorageBatchOperationsClient._DEFAULT_UNIVERSE - if universe_domain != _default_universe: - raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") - api_endpoint = StorageBatchOperationsClient.DEFAULT_MTLS_ENDPOINT - else: - api_endpoint = StorageBatchOperationsClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) - return api_endpoint - - @staticmethod - def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client. - - Args: - client_universe_domain (Optional[str]): The universe domain configured via the client options. - universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. - - Returns: - str: The universe domain to be used by the client. - - Raises: - ValueError: If the universe domain is an empty string. - """ - universe_domain = StorageBatchOperationsClient._DEFAULT_UNIVERSE - if client_universe_domain is not None: - universe_domain = client_universe_domain - elif universe_domain_env is not None: - universe_domain = universe_domain_env - if len(universe_domain.strip()) == 0: - raise ValueError("Universe Domain cannot be an empty string.") - return universe_domain - def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. @@ -583,7 +501,7 @@ def __init__(self, *, self._use_client_cert, self._use_mtls_endpoint, self._universe_domain_env = StorageBatchOperationsClient._read_environment_variables() self._client_cert_source = StorageBatchOperationsClient._get_client_cert_source(self._client_options.client_cert_source, self._use_client_cert) - self._universe_domain = StorageBatchOperationsClient._get_universe_domain(universe_domain_opt, self._universe_domain_env) + self._universe_domain = determine_domain(universe_domain_opt, self._universe_domain_env) self._api_endpoint: str = "" # updated below, depending on `transport` # Initialize the universe domain validation. @@ -615,7 +533,7 @@ def __init__(self, *, self._api_endpoint = self._transport.host self._api_endpoint = (self._api_endpoint or - StorageBatchOperationsClient._get_api_endpoint( + get_api_endpoint( self._client_options.api_endpoint, self._client_cert_source, self._universe_domain, diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_compat.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_compat.py index d84f0d95b840..0e543596024a 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_compat.py @@ -15,12 +15,211 @@ # """Tests for the compatibility module for older versions of google-api-core.""" - import re import pytest +from google.cloud.storagebatchoperations_v1._compat import determine_domain, get_api_endpoint, get_default_mtls_endpoint from google.cloud.storagebatchoperations_v1._compat import setup_request_id +from google.auth.exceptions import MutualTLSChannelError + + +def test_get_universe_domain(): + # When universe_domain is provided + assert ( + universe.get_universe_domain("foo.com", default_universe="default.com") + == "foo.com" + ) + assert ( + universe.get_universe_domain(" foo.com ", default_universe="default.com") + == "foo.com" + ) + + # When universe_domain is None, falls back to default_universe + assert ( + universe.get_universe_domain(None, default_universe="default.com") + == "default.com" + ) + + # When multiple potential universes are provided, resolves in order of preference + assert ( + universe.get_universe_domain( + "foo.com", "bar.com", default_universe="default.com" + ) + == "foo.com" + ) + assert ( + universe.get_universe_domain(None, "bar.com", default_universe="default.com") + == "bar.com" + ) + assert ( + universe.get_universe_domain(None, None, default_universe="default.com") + == "default.com" + ) + + # EmptyUniverseError raised when resolved value is empty string + with pytest.raises(universe.EmptyUniverseError) as excinfo: + universe.get_universe_domain("", default_universe="default.com") + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + + with pytest.raises(universe.EmptyUniverseError) as excinfo: + universe.get_universe_domain(" ", default_universe="default.com") + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + + with pytest.raises(universe.EmptyUniverseError) as excinfo: + universe.get_universe_domain(None, "", default_universe="default.com") + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + + +def test_get_default_mtls_endpoint(): + # Test valid API endpoints + assert ( + universe.get_default_mtls_endpoint("foo.googleapis.com") + == "foo.mtls.googleapis.com" + ) + assert ( + universe.get_default_mtls_endpoint("foo.sandbox.googleapis.com") + == "foo.mtls.sandbox.googleapis.com" + ) + # Test case-insensitivity + assert ( + universe.get_default_mtls_endpoint("foo.GoogleAPIs.com") + == "foo.mtls.googleapis.com" + ) + assert ( + universe.get_default_mtls_endpoint("foo.Sandbox.GoogleAPIs.com") + == "foo.mtls.sandbox.googleapis.com" + ) + + # Test valid API endpoints with schemes + assert ( + universe.get_default_mtls_endpoint("https://foo.googleapis.com") + == "https://foo.mtls.googleapis.com" + ) + assert ( + universe.get_default_mtls_endpoint("http://foo.googleapis.com:8080/v1") + == "http://foo.mtls.googleapis.com:8080/v1" + ) + + # Test valid API endpoints with ports + assert ( + universe.get_default_mtls_endpoint("foo.googleapis.com:443") + == "foo.mtls.googleapis.com:443" + ) + assert ( + universe.get_default_mtls_endpoint("foo.sandbox.googleapis.com:443") + == "foo.mtls.sandbox.googleapis.com:443" + ) + # Test case-insensitivity with ports + assert ( + universe.get_default_mtls_endpoint("foo.GoogleAPIs.com:443") + == "foo.mtls.googleapis.com:443" + ) + assert ( + universe.get_default_mtls_endpoint("foo.Sandbox.GoogleAPIs.com:443") + == "foo.mtls.sandbox.googleapis.com:443" + ) + + # Test endpoints that shouldn't be converted + assert ( + universe.get_default_mtls_endpoint("foo.mtls.googleapis.com") + == "foo.mtls.googleapis.com" + ) + assert universe.get_default_mtls_endpoint("foo.com") == "foo.com" + assert universe.get_default_mtls_endpoint("foo.com:8080") == "foo.com:8080" + + # Test empty/None endpoints + assert universe.get_default_mtls_endpoint("") == "" + assert universe.get_default_mtls_endpoint(None) is None + + # Test endpoints without host + assert universe.get_default_mtls_endpoint("http://") == "http://" + assert universe.get_default_mtls_endpoint("https://") == "https://" + + +@pytest.mark.parametrize( + "api_override,universe_domain,default_universe,default_mtls_endpoint,default_endpoint_template,use_mtls,expected", + [ + ( + "foo.com", + "googleapis.com", + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + True, + "foo.com", + ), + ( + None, + "googleapis.com", + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + True, + "foo.mtls.googleapis.com", + ), + ( + None, + "googleapis.com", + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + False, + "foo.googleapis.com", + ), + ( + None, + "bar.com", + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + True, + MutualTLSChannelError, + ), + ( + None, + "googleapis.com", + "googleapis.com", + None, + "foo.{UNIVERSE_DOMAIN}", + True, + ValueError, + ), + ], +) +def test_get_api_endpoint( + api_override, + universe_domain, + default_universe, + default_mtls_endpoint, + default_endpoint_template, + use_mtls, + expected, +): + if isinstance(expected, type) and issubclass(expected, Exception): + with pytest.raises(expected): + universe.get_api_endpoint( + api_override, + universe_domain, + default_universe, + default_mtls_endpoint, + default_endpoint_template, + use_mtls, + ) + else: + assert ( + universe.get_api_endpoint( + api_override, + universe_domain, + default_universe, + default_mtls_endpoint, + default_endpoint_template, + use_mtls, + ) + == expected + ) + + class MockRequest: def __init__(self, **kwargs): for k, v in kwargs.items(): diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py index a986fff2abec..ba81133c7260 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py @@ -120,22 +120,6 @@ def set_event_loop(): asyncio.set_event_loop(None) -def test__get_default_mtls_endpoint(): - api_endpoint = "example.googleapis.com" - api_mtls_endpoint = "example.mtls.googleapis.com" - sandbox_endpoint = "example.sandbox.googleapis.com" - sandbox_mtls_endpoint = "example.mtls.sandbox.googleapis.com" - non_googleapi = "api.example.com" - custom_endpoint = ".custom" - - assert StorageBatchOperationsClient._get_default_mtls_endpoint(None) is None - assert StorageBatchOperationsClient._get_default_mtls_endpoint(api_endpoint) == api_mtls_endpoint - assert StorageBatchOperationsClient._get_default_mtls_endpoint(api_mtls_endpoint) == api_mtls_endpoint - assert StorageBatchOperationsClient._get_default_mtls_endpoint(sandbox_endpoint) == sandbox_mtls_endpoint - assert StorageBatchOperationsClient._get_default_mtls_endpoint(sandbox_mtls_endpoint) == sandbox_mtls_endpoint - assert StorageBatchOperationsClient._get_default_mtls_endpoint(non_googleapi) == non_googleapi - assert StorageBatchOperationsClient._get_default_mtls_endpoint(custom_endpoint) == custom_endpoint - def test__read_environment_variables(): assert StorageBatchOperationsClient._read_environment_variables() == (False, "auto", None) @@ -277,40 +261,6 @@ def test__get_client_cert_source(): assert StorageBatchOperationsClient._get_client_cert_source(None, True) is mock_default_cert_source assert StorageBatchOperationsClient._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source -@mock.patch.object(StorageBatchOperationsClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(StorageBatchOperationsClient)) -@mock.patch.object(StorageBatchOperationsAsyncClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(StorageBatchOperationsAsyncClient)) -def test__get_api_endpoint(): - api_override = "foo.com" - mock_client_cert_source = mock.Mock() - default_universe = StorageBatchOperationsClient._DEFAULT_UNIVERSE - default_endpoint = StorageBatchOperationsClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) - mock_universe = "bar.com" - mock_endpoint = StorageBatchOperationsClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) - - assert StorageBatchOperationsClient._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override - assert StorageBatchOperationsClient._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == StorageBatchOperationsClient.DEFAULT_MTLS_ENDPOINT - assert StorageBatchOperationsClient._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint - assert StorageBatchOperationsClient._get_api_endpoint(None, None, default_universe, "always") == StorageBatchOperationsClient.DEFAULT_MTLS_ENDPOINT - assert StorageBatchOperationsClient._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == StorageBatchOperationsClient.DEFAULT_MTLS_ENDPOINT - assert StorageBatchOperationsClient._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint - assert StorageBatchOperationsClient._get_api_endpoint(None, None, default_universe, "never") == default_endpoint - - with pytest.raises(MutualTLSChannelError) as excinfo: - StorageBatchOperationsClient._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") - assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." - - -def test__get_universe_domain(): - client_universe_domain = "foo.com" - universe_domain_env = "bar.com" - - assert StorageBatchOperationsClient._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain - assert StorageBatchOperationsClient._get_universe_domain(None, universe_domain_env) == universe_domain_env - assert StorageBatchOperationsClient._get_universe_domain(None, None) == StorageBatchOperationsClient._DEFAULT_UNIVERSE - - with pytest.raises(ValueError) as excinfo: - StorageBatchOperationsClient._get_universe_domain("", None) - assert str(excinfo.value) == "Universe Domain cannot be an empty string." @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), @@ -700,7 +650,7 @@ def test_storage_batch_operations_client_get_mtls_endpoint_and_cert_source(clien config_filename = "mock_certificate_config.json" config_file_content = json.dumps(config_data) m = mock.mock_open(read_data=config_file_content) - with mock.patch("builtins.open", m), mock.patch("os.path.exists", side_effect=lambda path: os.path.basename(path) == config_filename): + with mock.patch("builtins.open", m): with mock.patch.dict( os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": config_filename} ): @@ -748,7 +698,7 @@ def test_storage_batch_operations_client_get_mtls_endpoint_and_cert_source(clien config_filename = "mock_certificate_config.json" config_file_content = json.dumps(config_data) m = mock.mock_open(read_data=config_file_content) - with mock.patch("builtins.open", m), mock.patch("os.path.exists", side_effect=lambda path: os.path.basename(path) == config_filename): + with mock.patch("builtins.open", m): with mock.patch.dict( os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": config_filename} ): diff --git a/packages/gapic-generator/tests/unit/generator/test_generator.py b/packages/gapic-generator/tests/unit/generator/test_generator.py index 9d8545c4192f..f1566ee6e5a7 100644 --- a/packages/gapic-generator/tests/unit/generator/test_generator.py +++ b/packages/gapic-generator/tests/unit/generator/test_generator.py @@ -117,6 +117,8 @@ def test_get_response_ignores_private_files(): list_templates.return_value = [ "foo/bar/baz.py.j2", "foo/bar/_base.py.j2", + "foo/bar/__init__.py.j2", + "foo/bar/_compat.py.j2", "molluscs/squid/sample.py.j2", ] with mock.patch.object(jinja2.Environment, "get_template") as get_template: @@ -128,12 +130,13 @@ def test_get_response_ignores_private_files(): get_template.assert_has_calls( [ mock.call("molluscs/squid/sample.py.j2"), + mock.call("foo/bar/__init__.py.j2"), + mock.call("foo/bar/_compat.py.j2"), mock.call("foo/bar/baz.py.j2"), - ] + ], + any_order=True, ) - assert len(cgr.file) == 1 - assert cgr.file[0].name == "foo/bar/baz.py" - assert cgr.file[0].content == "I am a template result.\n" + assert len(cgr.file) == 3 def test_get_response_fails_invalid_file_paths():