diff --git a/google/api_core/grpc_helpers.py b/google/api_core/grpc_helpers.py index 0ccbe126..5937f186 100644 --- a/google/api_core/grpc_helpers.py +++ b/google/api_core/grpc_helpers.py @@ -17,6 +17,8 @@ import collections import grpc +from packaging import version +import pkg_resources import six from google.api_core import exceptions @@ -33,6 +35,20 @@ except ImportError: HAS_GRPC_GCP = False +try: + # google.auth.__version__ was added in 1.26.0 + _GOOGLE_AUTH_VERSION = google.auth.__version__ +except AttributeError: + try: # try pkg_resources if it is available + _GOOGLE_AUTH_VERSION = pkg_resources.get_distribution("google-auth").version + except pkg_resources.DistributionNotFound: # pragma: NO COVER + _GOOGLE_AUTH_VERSION = None + +if _GOOGLE_AUTH_VERSION is not None and version.parse(_GOOGLE_AUTH_VERSION) >= version.parse("1.25.0"): + _GOOGLE_AUTH_HAS_DEFAULT_SCOPES_AND_DEFAULT_HOST = True +else: + _GOOGLE_AUTH_HAS_DEFAULT_SCOPES_AND_DEFAULT_HOST = False + # The list of gRPC Callable interfaces that return iterators. _STREAM_WRAP_CLASSES = (grpc.UnaryStreamMultiCallable, grpc.StreamStreamMultiCallable) @@ -179,9 +195,11 @@ def wrap_errors(callable_): def _create_composite_credentials( credentials=None, credentials_file=None, + default_scopes=None, scopes=None, ssl_credentials=None, - quota_project_id=None): + quota_project_id=None, + default_host=None): """Create the composite credentials for secure channels. Args: @@ -191,12 +209,16 @@ def _create_composite_credentials( credentials_file (str): A file with credentials that can be loaded with :func:`google.auth.load_credentials_from_file`. This argument is mutually exclusive with credentials. + default_scopes (Sequence[str]): A optional list of scopes needed for this + service. These are only used when credentials are not specified and + are passed to :func:`google.auth.default`. scopes (Sequence[str]): A optional list of scopes needed for this service. These are only used when credentials are not specified and are passed to :func:`google.auth.default`. ssl_credentials (grpc.ChannelCredentials): Optional SSL channel credentials. This can be used to specify different certificates. quota_project_id (str): An optional project to use for billing and quota. + default_host (str): The default endpoint. e.g., "pubsub.googleapis.com". Returns: grpc.ChannelCredentials: The composed channel credentials object. @@ -210,11 +232,38 @@ def _create_composite_credentials( ) if credentials_file: - credentials, _ = google.auth.load_credentials_from_file(credentials_file, scopes=scopes) + # TODO: remove this if/else once google-auth >= 1.25.0 is required + if _GOOGLE_AUTH_HAS_DEFAULT_SCOPES_AND_DEFAULT_HOST: + credentials, _ = google.auth.load_credentials_from_file( + credentials_file, + scopes=scopes, + default_scopes=default_scopes + ) + else: + credentials, _ = google.auth.load_credentials_from_file( + credentials_file, + scopes=scopes or default_scopes, + ) elif credentials: - credentials = google.auth.credentials.with_scopes_if_required(credentials, scopes) + # TODO: remove this if/else once google-auth >= 1.25.0 is required + if _GOOGLE_AUTH_HAS_DEFAULT_SCOPES_AND_DEFAULT_HOST: + credentials = google.auth.credentials.with_scopes_if_required( + credentials, + scopes=scopes, + default_scopes=default_scopes + ) + else: + credentials = google.auth.credentials.with_scopes_if_required( + credentials, + scopes=scopes or default_scopes, + ) + else: - credentials, _ = google.auth.default(scopes=scopes) + # TODO: remove this if/else once google-auth >= 1.25.0 is required + if _GOOGLE_AUTH_HAS_DEFAULT_SCOPES_AND_DEFAULT_HOST: + credentials, _ = google.auth.default(scopes=scopes, default_scopes=default_scopes) + else: + credentials, _ = google.auth.default(scopes=scopes or default_scopes) if quota_project_id and isinstance(credentials, google.auth.credentials.CredentialsWithQuotaProject): credentials = credentials.with_quota_project(quota_project_id) @@ -222,9 +271,16 @@ def _create_composite_credentials( request = google.auth.transport.requests.Request() # Create the metadata plugin for inserting the authorization header. - metadata_plugin = google.auth.transport.grpc.AuthMetadataPlugin( - credentials, request - ) + + # TODO: remove this if/else once google-auth >= 1.25.0 is required + if _GOOGLE_AUTH_HAS_DEFAULT_SCOPES_AND_DEFAULT_HOST: + metadata_plugin = google.auth.transport.grpc.AuthMetadataPlugin( + credentials, request, default_host=default_host, + ) + else: + metadata_plugin = google.auth.transport.grpc.AuthMetadataPlugin( + credentials, request + ) # Create a set of grpc.CallCredentials using the metadata plugin. google_auth_credentials = grpc.metadata_call_credentials(metadata_plugin) @@ -245,6 +301,8 @@ def create_channel( ssl_credentials=None, credentials_file=None, quota_project_id=None, + default_scopes=None, + default_host=None, **kwargs): """Create a secure channel with credentials. @@ -262,6 +320,9 @@ def create_channel( :func:`google.auth.load_credentials_from_file`. This argument is mutually exclusive with credentials. quota_project_id (str): An optional project to use for billing and quota. + default_scopes (Sequence[str]): Default scopes passed by a Google client + library. Use 'scopes' for user-defined scopes. + default_host (str): The default endpoint. e.g., "pubsub.googleapis.com". kwargs: Additional key-word args passed to :func:`grpc_gcp.secure_channel` or :func:`grpc.secure_channel`. @@ -275,9 +336,11 @@ def create_channel( composite_credentials = _create_composite_credentials( credentials=credentials, credentials_file=credentials_file, + default_scopes=default_scopes, scopes=scopes, ssl_credentials=ssl_credentials, quota_project_id=quota_project_id, + default_host=default_host, ) if HAS_GRPC_GCP: diff --git a/google/api_core/grpc_helpers_async.py b/google/api_core/grpc_helpers_async.py index 9a994e9f..14eb5a13 100644 --- a/google/api_core/grpc_helpers_async.py +++ b/google/api_core/grpc_helpers_async.py @@ -213,6 +213,8 @@ def create_channel( ssl_credentials=None, credentials_file=None, quota_project_id=None, + default_scopes=None, + default_host=None, **kwargs): """Create an AsyncIO secure channel with credentials. @@ -230,6 +232,9 @@ def create_channel( :func:`google.auth.load_credentials_from_file`. This argument is mutually exclusive with credentials. quota_project_id (str): An optional project to use for billing and quota. + default_scopes (Sequence[str]): Default scopes passed by a Google client + library. Use 'scopes' for user-defined scopes. + default_host (str): The default endpoint. e.g., "pubsub.googleapis.com". kwargs: Additional key-word args passed to :func:`aio.secure_channel`. Returns: @@ -243,8 +248,10 @@ def create_channel( credentials=credentials, credentials_file=credentials_file, scopes=scopes, + default_scopes=default_scopes, ssl_credentials=ssl_credentials, quota_project_id=quota_project_id, + default_host=default_host ) return aio.secure_channel(target, composite_credentials, **kwargs) diff --git a/setup.py b/setup.py index 30adb954..5de5aafb 100644 --- a/setup.py +++ b/setup.py @@ -34,6 +34,7 @@ "google-auth >= 1.21.1, < 2.0dev", "requests >= 2.18.0, < 3.0.0dev", "setuptools >= 40.3.0", + "packaging >= 14.3", "six >= 1.13.0", "pytz", 'futures >= 3.2.0; python_version < "3.2"', diff --git a/testing/constraints-3.6.txt b/testing/constraints-3.6.txt index 2d498173..1fcd1934 100644 --- a/testing/constraints-3.6.txt +++ b/testing/constraints-3.6.txt @@ -10,6 +10,7 @@ protobuf==3.12.0 google-auth==1.21.1 requests==2.18.0 setuptools==40.3.0 +packaging==14.3 six==1.13.0 grpcio==1.29.0 grpcio-gcp==0.2.2 diff --git a/tests/asyncio/test_grpc_helpers_async.py b/tests/asyncio/test_grpc_helpers_async.py index 766e11a9..3461cbe8 100644 --- a/tests/asyncio/test_grpc_helpers_async.py +++ b/tests/asyncio/test_grpc_helpers_async.py @@ -18,6 +18,7 @@ import pytest from google.api_core import exceptions +from google.api_core import grpc_helpers from google.api_core import grpc_helpers_async import google.auth.credentials @@ -263,6 +264,7 @@ def test_wrap_errors_streaming(wrap_stream_errors): @mock.patch("grpc.composite_channel_credentials") @mock.patch( "google.auth.default", + autospec=True, return_value=(mock.sentinel.credentials, mock.sentinel.projet), ) @mock.patch("grpc.experimental.aio.secure_channel") @@ -273,7 +275,45 @@ def test_create_channel_implicit(grpc_secure_channel, default, composite_creds_c channel = grpc_helpers_async.create_channel(target) assert channel is grpc_secure_channel.return_value - default.assert_called_once_with(scopes=None) + + # TODO: remove this if/else once google-auth >= 1.25.0 is required + if grpc_helpers._GOOGLE_AUTH_HAS_DEFAULT_SCOPES_AND_DEFAULT_HOST: + default.assert_called_once_with(scopes=None, default_scopes=None) + else: + default.assert_called_once_with(scopes=None) + grpc_secure_channel.assert_called_once_with(target, composite_creds) + + +@mock.patch("google.auth.transport.grpc.AuthMetadataPlugin", autospec=True) +@mock.patch( + "google.auth.transport.requests.Request", + autospec=True, + return_value=mock.sentinel.Request +) +@mock.patch("grpc.composite_channel_credentials") +@mock.patch( + "google.auth.default", + autospec=True, + return_value=(mock.sentinel.credentials, mock.sentinel.projet), +) +@mock.patch("grpc.experimental.aio.secure_channel") +def test_create_channel_implicit_with_default_host(grpc_secure_channel, default, composite_creds_call, request, auth_metadata_plugin): + target = "example.com:443" + default_host = "example.com" + composite_creds = composite_creds_call.return_value + + channel = grpc_helpers_async.create_channel(target, default_host=default_host) + + assert channel is grpc_secure_channel.return_value + + # TODO: remove this if/else once google-auth >= 1.25.0 is required + if grpc_helpers._GOOGLE_AUTH_HAS_DEFAULT_SCOPES_AND_DEFAULT_HOST: + default.assert_called_once_with(scopes=None, default_scopes=None) + auth_metadata_plugin.assert_called_once_with(mock.sentinel.credentials, mock.sentinel.Request, default_host=default_host) + else: + default.assert_called_once_with(scopes=None) + auth_metadata_plugin.assert_called_once_with(mock.sentinel.credentials, mock.sentinel.Request) + grpc_secure_channel.assert_called_once_with(target, composite_creds) @@ -292,7 +332,12 @@ def test_create_channel_implicit_with_ssl_creds( grpc_helpers_async.create_channel(target, ssl_credentials=ssl_creds) - default.assert_called_once_with(scopes=None) + # TODO: remove this if/else once google-auth >= 1.25.0 is required + if grpc_helpers._GOOGLE_AUTH_HAS_DEFAULT_SCOPES_AND_DEFAULT_HOST: + default.assert_called_once_with(scopes=None, default_scopes=None) + else: + default.assert_called_once_with(scopes=None) + composite_creds_call.assert_called_once_with(ssl_creds, mock.ANY) composite_creds = composite_creds_call.return_value grpc_secure_channel.assert_called_once_with(target, composite_creds) @@ -301,6 +346,7 @@ def test_create_channel_implicit_with_ssl_creds( @mock.patch("grpc.composite_channel_credentials") @mock.patch( "google.auth.default", + autospec=True, return_value=(mock.sentinel.credentials, mock.sentinel.projet), ) @mock.patch("grpc.experimental.aio.secure_channel") @@ -313,7 +359,39 @@ def test_create_channel_implicit_with_scopes( channel = grpc_helpers_async.create_channel(target, scopes=["one", "two"]) assert channel is grpc_secure_channel.return_value - default.assert_called_once_with(scopes=["one", "two"]) + + # TODO: remove this if/else once google-auth >= 1.25.0 is required + if grpc_helpers._GOOGLE_AUTH_HAS_DEFAULT_SCOPES_AND_DEFAULT_HOST: + default.assert_called_once_with(scopes=["one", "two"], default_scopes=None) + else: + default.assert_called_once_with(scopes=["one", "two"]) + + grpc_secure_channel.assert_called_once_with(target, composite_creds) + + +@mock.patch("grpc.composite_channel_credentials") +@mock.patch( + "google.auth.default", + autospec=True, + return_value=(mock.sentinel.credentials, mock.sentinel.projet), +) +@mock.patch("grpc.experimental.aio.secure_channel") +def test_create_channel_implicit_with_default_scopes( + grpc_secure_channel, default, composite_creds_call +): + target = "example.com:443" + composite_creds = composite_creds_call.return_value + + channel = grpc_helpers_async.create_channel(target, default_scopes=["three", "four"]) + + assert channel is grpc_secure_channel.return_value + + # TODO: remove this if/else once google-auth >= 1.25.0 is required + if grpc_helpers._GOOGLE_AUTH_HAS_DEFAULT_SCOPES_AND_DEFAULT_HOST: + default.assert_called_once_with(scopes=None, default_scopes=["three", "four"]) + else: + default.assert_called_once_with(scopes=["three", "four"]) + grpc_secure_channel.assert_called_once_with(target, composite_creds) @@ -331,7 +409,7 @@ def test_create_channel_explicit_with_duplicate_credentials(): @mock.patch("grpc.composite_channel_credentials") -@mock.patch("google.auth.credentials.with_scopes_if_required") +@mock.patch("google.auth.credentials.with_scopes_if_required", autospec=True) @mock.patch("grpc.experimental.aio.secure_channel") def test_create_channel_explicit(grpc_secure_channel, auth_creds, composite_creds_call): target = "example.com:443" @@ -339,7 +417,12 @@ def test_create_channel_explicit(grpc_secure_channel, auth_creds, composite_cred channel = grpc_helpers_async.create_channel(target, credentials=mock.sentinel.credentials) - auth_creds.assert_called_once_with(mock.sentinel.credentials, None) + # TODO: remove this if/else once google-auth >= 1.25.0 is required + if grpc_helpers._GOOGLE_AUTH_HAS_DEFAULT_SCOPES_AND_DEFAULT_HOST: + auth_creds.assert_called_once_with(mock.sentinel.credentials, scopes=None, default_scopes=None) + else: + auth_creds.assert_called_once_with(mock.sentinel.credentials, scopes=None) + assert channel is grpc_secure_channel.return_value grpc_secure_channel.assert_called_once_with(target, composite_creds) @@ -357,8 +440,36 @@ def test_create_channel_explicit_scoped(grpc_secure_channel, composite_creds_cal channel = grpc_helpers_async.create_channel( target, credentials=credentials, scopes=scopes ) + # TODO: remove this if/else once google-auth >= 1.25.0 is required + if grpc_helpers._GOOGLE_AUTH_HAS_DEFAULT_SCOPES_AND_DEFAULT_HOST: + credentials.with_scopes.assert_called_once_with(scopes, default_scopes=None) + else: + credentials.with_scopes.assert_called_once_with(scopes) + + assert channel is grpc_secure_channel.return_value + grpc_secure_channel.assert_called_once_with(target, composite_creds) + + +@mock.patch("grpc.composite_channel_credentials") +@mock.patch("grpc.experimental.aio.secure_channel") +def test_create_channel_explicit_default_scopes(grpc_secure_channel, composite_creds_call): + target = "example.com:443" + default_scopes = ["3", "4"] + composite_creds = composite_creds_call.return_value + + credentials = mock.create_autospec(google.auth.credentials.Scoped, instance=True) + credentials.requires_scopes = True + + channel = grpc_helpers_async.create_channel( + target, credentials=credentials, default_scopes=default_scopes + ) + + # TODO: remove this if/else once google-auth >= 1.25.0 is required + if grpc_helpers._GOOGLE_AUTH_HAS_DEFAULT_SCOPES_AND_DEFAULT_HOST: + credentials.with_scopes.assert_called_once_with(scopes=None, default_scopes=default_scopes) + else: + credentials.with_scopes.assert_called_once_with(scopes=default_scopes) - credentials.with_scopes.assert_called_once_with(scopes) assert channel is grpc_secure_channel.return_value grpc_secure_channel.assert_called_once_with(target, composite_creds) @@ -384,6 +495,7 @@ def test_create_channel_explicit_with_quota_project(grpc_secure_channel, composi @mock.patch("grpc.experimental.aio.secure_channel") @mock.patch( "google.auth.load_credentials_from_file", + autospec=True, return_value=(mock.sentinel.credentials, mock.sentinel.project) ) def test_create_channnel_with_credentials_file(load_credentials_from_file, grpc_secure_channel, composite_creds_call): @@ -396,7 +508,12 @@ def test_create_channnel_with_credentials_file(load_credentials_from_file, grpc_ target, credentials_file=credentials_file ) - google.auth.load_credentials_from_file.assert_called_once_with(credentials_file, scopes=None) + # TODO: remove this if/else once google-auth >= 1.25.0 is required + if grpc_helpers._GOOGLE_AUTH_HAS_DEFAULT_SCOPES_AND_DEFAULT_HOST: + google.auth.load_credentials_from_file.assert_called_once_with(credentials_file, scopes=None, default_scopes=None) + else: + google.auth.load_credentials_from_file.assert_called_once_with(credentials_file, scopes=None) + assert channel is grpc_secure_channel.return_value grpc_secure_channel.assert_called_once_with(target, composite_creds) @@ -405,6 +522,7 @@ def test_create_channnel_with_credentials_file(load_credentials_from_file, grpc_ @mock.patch("grpc.experimental.aio.secure_channel") @mock.patch( "google.auth.load_credentials_from_file", + autospec=True, return_value=(mock.sentinel.credentials, mock.sentinel.project) ) def test_create_channel_with_credentials_file_and_scopes(load_credentials_from_file, grpc_secure_channel, composite_creds_call): @@ -418,7 +536,40 @@ def test_create_channel_with_credentials_file_and_scopes(load_credentials_from_f target, credentials_file=credentials_file, scopes=scopes ) - google.auth.load_credentials_from_file.assert_called_once_with(credentials_file, scopes=scopes) + # TODO: remove this if/else once google-auth >= 1.25.0 is required + if grpc_helpers._GOOGLE_AUTH_HAS_DEFAULT_SCOPES_AND_DEFAULT_HOST: + google.auth.load_credentials_from_file.assert_called_once_with(credentials_file, scopes=scopes, default_scopes=None) + else: + google.auth.load_credentials_from_file.assert_called_once_with(credentials_file, scopes=scopes) + + assert channel is grpc_secure_channel.return_value + grpc_secure_channel.assert_called_once_with(target, composite_creds) + + +@mock.patch("grpc.composite_channel_credentials") +@mock.patch("grpc.experimental.aio.secure_channel") +@mock.patch( + "google.auth.load_credentials_from_file", + autospec=True, + return_value=(mock.sentinel.credentials, mock.sentinel.project) +) +def test_create_channel_with_credentials_file_and_default_scopes(load_credentials_from_file, grpc_secure_channel, composite_creds_call): + target = "example.com:443" + default_scopes = ["3", "4"] + + credentials_file = "/path/to/credentials/file.json" + composite_creds = composite_creds_call.return_value + + channel = grpc_helpers_async.create_channel( + target, credentials_file=credentials_file, default_scopes=default_scopes + ) + + # TODO: remove this if/else once google-auth >= 1.25.0 is required + if grpc_helpers._GOOGLE_AUTH_HAS_DEFAULT_SCOPES_AND_DEFAULT_HOST: + google.auth.load_credentials_from_file.assert_called_once_with(credentials_file, scopes=None, default_scopes=default_scopes) + else: + google.auth.load_credentials_from_file.assert_called_once_with(credentials_file, scopes=default_scopes) + assert channel is grpc_secure_channel.return_value grpc_secure_channel.assert_called_once_with(target, composite_creds) @@ -434,7 +585,12 @@ def test_create_channel_without_grpc_gcp(grpc_secure_channel): grpc_helpers_async.create_channel(target, credentials=credentials, scopes=scopes) grpc_secure_channel.assert_called() - credentials.with_scopes.assert_called_once_with(scopes) + + # TODO: remove this if/else once google-auth >= 1.25.0 is required + if grpc_helpers._GOOGLE_AUTH_HAS_DEFAULT_SCOPES_AND_DEFAULT_HOST: + credentials.with_scopes.assert_called_once_with(scopes, default_scopes=None) + else: + credentials.with_scopes.assert_called_once_with(scopes) @pytest.mark.asyncio diff --git a/tests/unit/test_grpc_helpers.py b/tests/unit/test_grpc_helpers.py index d6ec60a5..4e0ab806 100644 --- a/tests/unit/test_grpc_helpers.py +++ b/tests/unit/test_grpc_helpers.py @@ -222,6 +222,7 @@ def test_wrap_errors_streaming(wrap_stream_errors): @mock.patch("grpc.composite_channel_credentials") @mock.patch( "google.auth.default", + autospec=True, return_value=(mock.sentinel.credentials, mock.sentinel.projet), ) @mock.patch("grpc.secure_channel") @@ -232,7 +233,51 @@ def test_create_channel_implicit(grpc_secure_channel, default, composite_creds_c channel = grpc_helpers.create_channel(target) assert channel is grpc_secure_channel.return_value - default.assert_called_once_with(scopes=None) + + # TODO: remove this if/else once google-auth >= 1.25.0 is required + if grpc_helpers._GOOGLE_AUTH_HAS_DEFAULT_SCOPES_AND_DEFAULT_HOST: + default.assert_called_once_with(scopes=None, default_scopes=None) + else: + default.assert_called_once_with(scopes=None) + + if grpc_helpers.HAS_GRPC_GCP: + grpc_secure_channel.assert_called_once_with(target, composite_creds, None) + else: + grpc_secure_channel.assert_called_once_with(target, composite_creds) + + +@mock.patch("google.auth.transport.grpc.AuthMetadataPlugin", autospec=True) +@mock.patch( + "google.auth.transport.requests.Request", + autospec=True, + return_value=mock.sentinel.Request +) +@mock.patch("grpc.composite_channel_credentials") +@mock.patch( + "google.auth.default", + autospec=True, + return_value=(mock.sentinel.credentials, mock.sentinel.project), +) +@mock.patch("grpc.secure_channel") +def test_create_channel_implicit_with_default_host(grpc_secure_channel, default, composite_creds_call, request, auth_metadata_plugin): + target = "example.com:443" + default_host = "example.com" + composite_creds = composite_creds_call.return_value + + channel = grpc_helpers.create_channel(target, default_host=default_host) + + assert channel is grpc_secure_channel.return_value + + # TODO: remove this if/else once google-auth >= 1.25.0 is required + print(grpc_helpers._GOOGLE_AUTH_VERSION) + print(grpc_helpers._GOOGLE_AUTH_HAS_DEFAULT_SCOPES_AND_DEFAULT_HOST) + if grpc_helpers._GOOGLE_AUTH_HAS_DEFAULT_SCOPES_AND_DEFAULT_HOST: + default.assert_called_once_with(scopes=None, default_scopes=None) + auth_metadata_plugin.assert_called_once_with(mock.sentinel.credentials, mock.sentinel.Request, default_host=default_host) + else: + default.assert_called_once_with(scopes=None) + auth_metadata_plugin.assert_called_once_with(mock.sentinel.credentials, mock.sentinel.Request) + if grpc_helpers.HAS_GRPC_GCP: grpc_secure_channel.assert_called_once_with(target, composite_creds, None) else: @@ -242,6 +287,7 @@ def test_create_channel_implicit(grpc_secure_channel, default, composite_creds_c @mock.patch("grpc.composite_channel_credentials") @mock.patch( "google.auth.default", + autospec=True, return_value=(mock.sentinel.credentials, mock.sentinel.projet), ) @mock.patch("grpc.secure_channel") @@ -254,7 +300,12 @@ def test_create_channel_implicit_with_ssl_creds( grpc_helpers.create_channel(target, ssl_credentials=ssl_creds) - default.assert_called_once_with(scopes=None) + # TODO: remove this if/else once google-auth >= 1.25.0 is required + if grpc_helpers._GOOGLE_AUTH_HAS_DEFAULT_SCOPES_AND_DEFAULT_HOST: + default.assert_called_once_with(scopes=None, default_scopes=None) + else: + default.assert_called_once_with(scopes=None) + composite_creds_call.assert_called_once_with(ssl_creds, mock.ANY) composite_creds = composite_creds_call.return_value if grpc_helpers.HAS_GRPC_GCP: @@ -266,6 +317,7 @@ def test_create_channel_implicit_with_ssl_creds( @mock.patch("grpc.composite_channel_credentials") @mock.patch( "google.auth.default", + autospec=True, return_value=(mock.sentinel.credentials, mock.sentinel.projet), ) @mock.patch("grpc.secure_channel") @@ -278,7 +330,42 @@ def test_create_channel_implicit_with_scopes( channel = grpc_helpers.create_channel(target, scopes=["one", "two"]) assert channel is grpc_secure_channel.return_value - default.assert_called_once_with(scopes=["one", "two"]) + + # TODO: remove this if/else once google-auth >= 1.25.0 is required + if grpc_helpers._GOOGLE_AUTH_HAS_DEFAULT_SCOPES_AND_DEFAULT_HOST: + default.assert_called_once_with(scopes=["one", "two"], default_scopes=None) + else: + default.assert_called_once_with(scopes=["one", "two"]) + + if grpc_helpers.HAS_GRPC_GCP: + grpc_secure_channel.assert_called_once_with(target, composite_creds, None) + else: + grpc_secure_channel.assert_called_once_with(target, composite_creds) + + +@mock.patch("grpc.composite_channel_credentials") +@mock.patch( + "google.auth.default", + autospec=True, + return_value=(mock.sentinel.credentials, mock.sentinel.projet), +) +@mock.patch("grpc.secure_channel") +def test_create_channel_implicit_with_default_scopes( + grpc_secure_channel, default, composite_creds_call +): + target = "example.com:443" + composite_creds = composite_creds_call.return_value + + channel = grpc_helpers.create_channel(target, default_scopes=["three", "four"]) + + assert channel is grpc_secure_channel.return_value + + # TODO: remove this if/else once google-auth >= 1.25.0 is required + if grpc_helpers._GOOGLE_AUTH_HAS_DEFAULT_SCOPES_AND_DEFAULT_HOST: + default.assert_called_once_with(scopes=None, default_scopes=["three", "four"]) + else: + default.assert_called_once_with(scopes=["three", "four"]) + if grpc_helpers.HAS_GRPC_GCP: grpc_secure_channel.assert_called_once_with(target, composite_creds, None) else: @@ -297,7 +384,7 @@ def test_create_channel_explicit_with_duplicate_credentials(): @mock.patch("grpc.composite_channel_credentials") -@mock.patch("google.auth.credentials.with_scopes_if_required") +@mock.patch("google.auth.credentials.with_scopes_if_required", autospec=True) @mock.patch("grpc.secure_channel") def test_create_channel_explicit(grpc_secure_channel, auth_creds, composite_creds_call): target = "example.com:443" @@ -305,7 +392,11 @@ def test_create_channel_explicit(grpc_secure_channel, auth_creds, composite_cred channel = grpc_helpers.create_channel(target, credentials=mock.sentinel.credentials) - auth_creds.assert_called_once_with(mock.sentinel.credentials, None) + # TODO: remove this if/else once google-auth >= 1.25.0 is required + if grpc_helpers._GOOGLE_AUTH_HAS_DEFAULT_SCOPES_AND_DEFAULT_HOST: + auth_creds.assert_called_once_with(mock.sentinel.credentials, scopes=None, default_scopes=None) + else: + auth_creds.assert_called_once_with(mock.sentinel.credentials, scopes=None) assert channel is grpc_secure_channel.return_value if grpc_helpers.HAS_GRPC_GCP: grpc_secure_channel.assert_called_once_with(target, composite_creds, None) @@ -327,7 +418,39 @@ def test_create_channel_explicit_scoped(grpc_secure_channel, composite_creds_cal target, credentials=credentials, scopes=scopes ) - credentials.with_scopes.assert_called_once_with(scopes) + # TODO: remove this if/else once google-auth >= 1.25.0 is required + if grpc_helpers._GOOGLE_AUTH_HAS_DEFAULT_SCOPES_AND_DEFAULT_HOST: + credentials.with_scopes.assert_called_once_with(scopes, default_scopes=None) + else: + credentials.with_scopes.assert_called_once_with(scopes) + + assert channel is grpc_secure_channel.return_value + if grpc_helpers.HAS_GRPC_GCP: + grpc_secure_channel.assert_called_once_with(target, composite_creds, None) + else: + grpc_secure_channel.assert_called_once_with(target, composite_creds) + + +@mock.patch("grpc.composite_channel_credentials") +@mock.patch("grpc.secure_channel") +def test_create_channel_explicit_default_scopes(grpc_secure_channel, composite_creds_call): + target = "example.com:443" + default_scopes = ["3", "4"] + composite_creds = composite_creds_call.return_value + + credentials = mock.create_autospec(google.auth.credentials.Scoped, instance=True) + credentials.requires_scopes = True + + channel = grpc_helpers.create_channel( + target, credentials=credentials, default_scopes=default_scopes + ) + + # TODO: remove this if/else once google-auth >= 1.25.0 is required + if grpc_helpers._GOOGLE_AUTH_HAS_DEFAULT_SCOPES_AND_DEFAULT_HOST: + credentials.with_scopes.assert_called_once_with(scopes=None, default_scopes=default_scopes) + else: + credentials.with_scopes.assert_called_once_with(scopes=default_scopes) + assert channel is grpc_secure_channel.return_value if grpc_helpers.HAS_GRPC_GCP: grpc_secure_channel.assert_called_once_with(target, composite_creds, None) @@ -362,6 +485,7 @@ def test_create_channel_explicit_with_quota_project(grpc_secure_channel, composi @mock.patch("grpc.secure_channel") @mock.patch( "google.auth.load_credentials_from_file", + autospec=True, return_value=(mock.sentinel.credentials, mock.sentinel.project) ) def test_create_channel_with_credentials_file(load_credentials_from_file, grpc_secure_channel, composite_creds_call): @@ -374,7 +498,11 @@ def test_create_channel_with_credentials_file(load_credentials_from_file, grpc_s target, credentials_file=credentials_file ) - google.auth.load_credentials_from_file.assert_called_once_with(credentials_file, scopes=None) + # TODO: remove this if/else once google-auth >= 1.25.0 is required + if grpc_helpers._GOOGLE_AUTH_HAS_DEFAULT_SCOPES_AND_DEFAULT_HOST: + google.auth.load_credentials_from_file.assert_called_once_with(credentials_file, scopes=None, default_scopes=None) + else: + google.auth.load_credentials_from_file.assert_called_once_with(credentials_file, scopes=None) assert channel is grpc_secure_channel.return_value if grpc_helpers.HAS_GRPC_GCP: @@ -387,6 +515,7 @@ def test_create_channel_with_credentials_file(load_credentials_from_file, grpc_s @mock.patch("grpc.secure_channel") @mock.patch( "google.auth.load_credentials_from_file", + autospec=True, return_value=(mock.sentinel.credentials, mock.sentinel.project) ) def test_create_channel_with_credentials_file_and_scopes(load_credentials_from_file, grpc_secure_channel, composite_creds_call): @@ -400,7 +529,43 @@ def test_create_channel_with_credentials_file_and_scopes(load_credentials_from_f target, credentials_file=credentials_file, scopes=scopes ) - google.auth.load_credentials_from_file.assert_called_once_with(credentials_file, scopes=scopes) + # TODO: remove this if/else once google-auth >= 1.25.0 is required + if grpc_helpers._GOOGLE_AUTH_HAS_DEFAULT_SCOPES_AND_DEFAULT_HOST: + google.auth.load_credentials_from_file.assert_called_once_with(credentials_file, scopes=scopes, default_scopes=None) + else: + google.auth.load_credentials_from_file.assert_called_once_with(credentials_file, scopes=scopes) + + assert channel is grpc_secure_channel.return_value + if grpc_helpers.HAS_GRPC_GCP: + grpc_secure_channel.assert_called_once_with(target, composite_creds, None) + else: + grpc_secure_channel.assert_called_once_with(target, composite_creds) + + +@mock.patch("grpc.composite_channel_credentials") +@mock.patch("grpc.secure_channel") +@mock.patch( + "google.auth.load_credentials_from_file", + autospec=True, + return_value=(mock.sentinel.credentials, mock.sentinel.project) +) +def test_create_channel_with_credentials_file_and_default_scopes(load_credentials_from_file, grpc_secure_channel, composite_creds_call): + target = "example.com:443" + default_scopes = ["3", "4"] + + credentials_file = "/path/to/credentials/file.json" + composite_creds = composite_creds_call.return_value + + channel = grpc_helpers.create_channel( + target, credentials_file=credentials_file, default_scopes=default_scopes + ) + + # TODO: remove this if/else once google-auth >= 1.25.0 is required + if grpc_helpers._GOOGLE_AUTH_HAS_DEFAULT_SCOPES_AND_DEFAULT_HOST: + load_credentials_from_file.assert_called_once_with(credentials_file, scopes=None, default_scopes=default_scopes) + else: + load_credentials_from_file.assert_called_once_with(credentials_file, scopes=default_scopes) + assert channel is grpc_secure_channel.return_value if grpc_helpers.HAS_GRPC_GCP: grpc_secure_channel.assert_called_once_with(target, composite_creds, None) @@ -421,7 +586,12 @@ def test_create_channel_with_grpc_gcp(grpc_gcp_secure_channel): grpc_helpers.create_channel(target, credentials=credentials, scopes=scopes) grpc_gcp_secure_channel.assert_called() - credentials.with_scopes.assert_called_once_with(scopes) + + # TODO: remove this if/else once google-auth >= 1.25.0 is required + if grpc_helpers._GOOGLE_AUTH_HAS_DEFAULT_SCOPES_AND_DEFAULT_HOST: + credentials.with_scopes.assert_called_once_with(scopes, default_scopes=None) + else: + credentials.with_scopes.assert_called_once_with(scopes) @pytest.mark.skipif(grpc_helpers.HAS_GRPC_GCP, reason="grpc_gcp module not available") @@ -435,7 +605,12 @@ def test_create_channel_without_grpc_gcp(grpc_secure_channel): grpc_helpers.create_channel(target, credentials=credentials, scopes=scopes) grpc_secure_channel.assert_called() - credentials.with_scopes.assert_called_once_with(scopes) + + # TODO: remove this if/else once google-auth >= 1.25.0 is required + if grpc_helpers._GOOGLE_AUTH_HAS_DEFAULT_SCOPES_AND_DEFAULT_HOST: + credentials.with_scopes.assert_called_once_with(scopes, default_scopes=None) + else: + credentials.with_scopes.assert_called_once_with(scopes) class TestChannelStub(object):