From 0d00ebc7220f4ee2dcc78a0367a1ad71bd8aedd7 Mon Sep 17 00:00:00 2001 From: ohmayr Date: Thu, 12 Sep 2024 06:40:14 +0000 Subject: [PATCH] feat: implement async rest interceptor class --- .../%sub/services/%service/_shared_macros.j2 | 6 +- .../%service/transports/__init__.py.j2 | 3 +- .../%service/transports/rest_asyncio.py.j2 | 19 +- .../gapic/%name_%version/%sub/test_macros.j2 | 11 +- .../cloud_redis/transports/__init__.py | 3 +- .../cloud_redis/transports/rest_asyncio.py | 174 ++++++++++++++++-- .../unit/gapic/redis_v1/test_cloud_redis.py | 96 ++++++++++ 7 files changed, 290 insertions(+), 22 deletions(-) diff --git a/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 b/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 index dc78d4b54..331fbca6c 100644 --- a/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 +++ b/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 @@ -193,11 +193,7 @@ def _get_http_options(): {% set async_class_prefix = "Async" if is_async else "" %} http_options = _Base{{ service_name }}RestTransport._Base{{method_name}}._get_http_options() - {% if not is_async %} - {# TODO (ohmayr): Make this unconditional once REST interceptors are supported for async. Googlers, - see internal tracking issue: b/362949568. #} - request, metadata = self._interceptor.pre_{{ method_name|snake_case }}(request, metadata) - {% endif %} + request, metadata = {{ await_prefix }}self._interceptor.pre_{{ method_name|snake_case }}(request, metadata) transcoded_request = _Base{{ service_name }}RestTransport._Base{{method_name}}._get_transcoded_request(http_options, request) {% if body_spec %} diff --git a/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/__init__.py.j2 b/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/__init__.py.j2 index 48232c710..9745b08d7 100644 --- a/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/__init__.py.j2 +++ b/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/__init__.py.j2 @@ -19,7 +19,8 @@ from .rest import {{ service.name }}RestInterceptor ASYNC_REST_CLASSES: Tuple[str, ...] try: from .rest_asyncio import Async{{ service.name }}RestTransport - ASYNC_REST_CLASSES = ('Async{{ service.name }}RestTransport',) + from .rest_asyncio import Async{{ service.name }}RestInterceptor + ASYNC_REST_CLASSES = ('Async{{ service.name }}RestTransport', 'Async{{ service.name }}RestInterceptor') HAS_REST_ASYNC = True except ImportError: # pragma: NO COVER ASYNC_REST_CLASSES = () diff --git a/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 b/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 index 0968e8800..5bda7e182 100644 --- a/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 +++ b/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 @@ -33,6 +33,16 @@ except ImportError as e: # pragma: NO COVER raise ImportError("async rest transport requires google-api-core >= 2.20.0. Install google-api-core using `pip install google-api-core==2.35.0`.") from e from google.protobuf import json_format +{% if service.has_lro %} +from google.api_core import operations_v1 +{% endif %} +{% if opts.add_iam_methods or api.has_iam_mixin %} +from google.iam.v1 import iam_policy_pb2 # type: ignore +from google.iam.v1 import policy_pb2 # type: ignore +{% endif %} +{% if api.has_location_mixin %} +from google.cloud.location import locations_pb2 # type: ignore +{% endif %} import json # type: ignore import dataclasses @@ -55,11 +65,13 @@ DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( rest_version=google.auth.__version__ ) -{# TODO: Add an `_interceptor` property once implemented #} +{{ shared_macros.create_interceptor_class(api, service, method, is_async=True) }} + @dataclasses.dataclass class Async{{service.name}}RestStub: _session: AsyncAuthorizedSession _host: str + _interceptor: Async{{service.name}}RestInterceptor class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): """Asynchronous REST backend transport for {{ service.name }}. @@ -78,6 +90,7 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): credentials: Optional[ga_credentials_async.Credentials] = None, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, url_scheme: str = 'https', + interceptor: Optional[Async{{ service.name }}RestInterceptor] = None, ) -> None: """Instantiate the transport. @@ -118,6 +131,7 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): # we update the type hints for credentials to include asynchronous credentials in the client layer. #} self._session = AsyncAuthorizedSession(self._credentials) # type: ignore + self._interceptor = interceptor or Async{{ service.name }}RestInterceptor() self._wrap_with_kind = True self._prep_wrapped_messages(client_info) @@ -190,6 +204,7 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): content = await response.read() json_format.Parse(content, pb_resp, ignore_unknown_fields=True) {% endif %}{# if method.server_streaming #} + resp = await self._interceptor.post_{{ method.name|snake_case }}(resp) return resp {% endif %}{# method.void #} @@ -207,7 +222,7 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): def {{method.transport_safe_name|snake_case}}(self) -> Callable[ [{{method.input.ident}}], {{method.output.ident}}]: - return self._{{method.name}}(self._session, self._host) # type: ignore + return self._{{method.name}}(self._session, self._host, self._interceptor) # type: ignore {% endfor %} diff --git a/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_macros.j2 b/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_macros.j2 index 5cbbb256d..34c9c4c7e 100644 --- a/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_macros.j2 +++ b/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_macros.j2 @@ -2264,7 +2264,6 @@ def test_initialize_client_w_{{transport_name}}(): {# inteceptor_class_test generates tests for rest interceptors. #} {% macro inteceptor_class_test(service, method, transport, is_async) %} -{% if not is_async %}{# TODO: Remove this guard once support for async rest interceptors is added. #} {% set await_prefix = get_await_prefix(is_async) %} {% set async_prefix = get_async_prefix(is_async) %} {% set async_decorator = get_async_decorator(is_async) %} @@ -2277,6 +2276,15 @@ def test_initialize_client_w_{{transport_name}}(): {% if 'grpc' in transport %} raise NotImplementedError("gRPC is currently not supported for this test case.") {% else %}{# 'rest' in transport #} + {% if transport_name == 'rest_asyncio' %} + if not HAS_GOOGLE_AUTH_AIO: + pytest.skip("google-auth >= 2.35.0 is required for async rest transport.") + elif not HAS_AIOHTTP_INSTALLED: + pytest.skip("aiohttp is required for async rest transport.") + elif not HAS_ASYNC_REST_SUPPORT_IN_CORE: + pytest.skip("google-api-core >= 2.20.0 is required for async rest transport.") + + {% endif %} transport = transports.{{async_method_prefix}}{{ service.name }}RestTransport( credentials={{get_credentials(is_async)}}, interceptor=None if null_interceptor else transports.{{async_method_prefix}}{{ service.name}}RestInterceptor(), @@ -2345,5 +2353,4 @@ def test_initialize_client_w_{{transport_name}}(): post.assert_called_once() {% endif %} {% endif %}{# end 'grpc' in transport #} -{% endif %}{# end not is_async #} {% endmacro%} diff --git a/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/__init__.py b/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/__init__.py index 7bd88e6c6..563cd5dd7 100755 --- a/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/__init__.py +++ b/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/__init__.py @@ -24,7 +24,8 @@ ASYNC_REST_CLASSES: Tuple[str, ...] try: from .rest_asyncio import AsyncCloudRedisRestTransport - ASYNC_REST_CLASSES = ('AsyncCloudRedisRestTransport',) + from .rest_asyncio import AsyncCloudRedisRestInterceptor + ASYNC_REST_CLASSES = ('AsyncCloudRedisRestTransport', 'AsyncCloudRedisRestInterceptor') HAS_REST_ASYNC = True except ImportError: # pragma: NO COVER ASYNC_REST_CLASSES = () diff --git a/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py b/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py index cdd7c50c8..3bdf6374a 100755 --- a/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py +++ b/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py @@ -36,6 +36,8 @@ raise ImportError("async rest transport requires google-api-core >= 2.20.0. Install google-api-core using `pip install google-api-core==2.35.0`.") from e from google.protobuf import json_format +from google.api_core import operations_v1 +from google.cloud.location import locations_pb2 # type: ignore import json # type: ignore import dataclasses @@ -61,10 +63,154 @@ rest_version=google.auth.__version__ ) + +class AsyncCloudRedisRestInterceptor: + """Asynchronous Interceptor for CloudRedis. + + Interceptors are used to manipulate requests, request metadata, and responses + in arbitrary ways. + Example use cases include: + * Logging + * Verifying requests according to service or custom semantics + * Stripping extraneous information from responses + + These use cases and more can be enabled by injecting an + instance of a custom subclass when constructing the AsyncCloudRedisRestTransport. + + .. code-block:: python + class MyCustomCloudRedisInterceptor(CloudRedisRestInterceptor): + async def pre_create_instance(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + async def post_create_instance(self, response): + logging.log(f"Received response: {response}") + return response + + async def pre_delete_instance(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + async def post_delete_instance(self, response): + logging.log(f"Received response: {response}") + return response + + async def pre_export_instance(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + async def post_export_instance(self, response): + logging.log(f"Received response: {response}") + return response + + async def pre_failover_instance(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + async def post_failover_instance(self, response): + logging.log(f"Received response: {response}") + return response + + async def pre_get_instance(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + async def post_get_instance(self, response): + logging.log(f"Received response: {response}") + return response + + async def pre_get_instance_auth_string(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + async def post_get_instance_auth_string(self, response): + logging.log(f"Received response: {response}") + return response + + async def pre_import_instance(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + async def post_import_instance(self, response): + logging.log(f"Received response: {response}") + return response + + async def pre_list_instances(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + async def post_list_instances(self, response): + logging.log(f"Received response: {response}") + return response + + async def pre_reschedule_maintenance(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + async def post_reschedule_maintenance(self, response): + logging.log(f"Received response: {response}") + return response + + async def pre_update_instance(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + async def post_update_instance(self, response): + logging.log(f"Received response: {response}") + return response + + async def pre_upgrade_instance(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + async def post_upgrade_instance(self, response): + logging.log(f"Received response: {response}") + return response + + transport = AsyncCloudRedisRestTransport(interceptor=MyCustomCloudRedisInterceptor()) + client = async CloudRedisClient(transport=transport) + + + """ + async def pre_get_instance(self, request: cloud_redis.GetInstanceRequest, metadata: Sequence[Tuple[str, str]]) -> Tuple[cloud_redis.GetInstanceRequest, Sequence[Tuple[str, str]]]: + """Pre-rpc interceptor for get_instance + + Override in a subclass to manipulate the request or metadata + before they are sent to the CloudRedis server. + """ + return request, metadata + + async def post_get_instance(self, response: cloud_redis.Instance) -> cloud_redis.Instance: + """Post-rpc interceptor for get_instance + + Override in a subclass to manipulate the response + after it is returned by the CloudRedis server but before + it is returned to user code. + """ + return response + async def pre_get_instance_auth_string(self, request: cloud_redis.GetInstanceAuthStringRequest, metadata: Sequence[Tuple[str, str]]) -> Tuple[cloud_redis.GetInstanceAuthStringRequest, Sequence[Tuple[str, str]]]: + """Pre-rpc interceptor for get_instance_auth_string + + Override in a subclass to manipulate the request or metadata + before they are sent to the CloudRedis server. + """ + return request, metadata + + async def post_get_instance_auth_string(self, response: cloud_redis.InstanceAuthString) -> cloud_redis.InstanceAuthString: + """Post-rpc interceptor for get_instance_auth_string + + Override in a subclass to manipulate the response + after it is returned by the CloudRedis server but before + it is returned to user code. + """ + return response + + @dataclasses.dataclass class AsyncCloudRedisRestStub: _session: AsyncAuthorizedSession _host: str + _interceptor: AsyncCloudRedisRestInterceptor class AsyncCloudRedisRestTransport(_BaseCloudRedisRestTransport): """Asynchronous REST backend transport for CloudRedis. @@ -103,6 +249,7 @@ def __init__(self, credentials: Optional[ga_credentials_async.Credentials] = None, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, url_scheme: str = 'https', + interceptor: Optional[AsyncCloudRedisRestInterceptor] = None, ) -> None: """Instantiate the transport. @@ -137,6 +284,7 @@ def __init__(self, api_audience=None ) self._session = AsyncAuthorizedSession(self._credentials) # type: ignore + self._interceptor = interceptor or AsyncCloudRedisRestInterceptor() self._wrap_with_kind = True self._prep_wrapped_messages(client_info) @@ -311,6 +459,7 @@ async def __call__(self, """ http_options = _BaseCloudRedisRestTransport._BaseGetInstance._get_http_options() + request, metadata = await self._interceptor.pre_get_instance(request, metadata) transcoded_request = _BaseCloudRedisRestTransport._BaseGetInstance._get_transcoded_request(http_options, request) # Jsonify the query params @@ -333,6 +482,7 @@ async def __call__(self, pb_resp = cloud_redis.Instance.pb(resp) content = await response.read() json_format.Parse(content, pb_resp, ignore_unknown_fields=True) + resp = await self._interceptor.post_get_instance(resp) return resp class _GetInstanceAuthString(_BaseCloudRedisRestTransport._BaseGetInstanceAuthString, AsyncCloudRedisRestStub): @@ -385,6 +535,7 @@ async def __call__(self, """ http_options = _BaseCloudRedisRestTransport._BaseGetInstanceAuthString._get_http_options() + request, metadata = await self._interceptor.pre_get_instance_auth_string(request, metadata) transcoded_request = _BaseCloudRedisRestTransport._BaseGetInstanceAuthString._get_transcoded_request(http_options, request) # Jsonify the query params @@ -407,6 +558,7 @@ async def __call__(self, pb_resp = cloud_redis.InstanceAuthString.pb(resp) content = await response.read() json_format.Parse(content, pb_resp, ignore_unknown_fields=True) + resp = await self._interceptor.post_get_instance_auth_string(resp) return resp class _ImportInstance(_BaseCloudRedisRestTransport._BaseImportInstance, AsyncCloudRedisRestStub): @@ -483,67 +635,67 @@ async def __call__(self, def create_instance(self) -> Callable[ [cloud_redis.CreateInstanceRequest], operations_pb2.Operation]: - return self._CreateInstance(self._session, self._host) # type: ignore + return self._CreateInstance(self._session, self._host, self._interceptor) # type: ignore @property def delete_instance(self) -> Callable[ [cloud_redis.DeleteInstanceRequest], operations_pb2.Operation]: - return self._DeleteInstance(self._session, self._host) # type: ignore + return self._DeleteInstance(self._session, self._host, self._interceptor) # type: ignore @property def export_instance(self) -> Callable[ [cloud_redis.ExportInstanceRequest], operations_pb2.Operation]: - return self._ExportInstance(self._session, self._host) # type: ignore + return self._ExportInstance(self._session, self._host, self._interceptor) # type: ignore @property def failover_instance(self) -> Callable[ [cloud_redis.FailoverInstanceRequest], operations_pb2.Operation]: - return self._FailoverInstance(self._session, self._host) # type: ignore + return self._FailoverInstance(self._session, self._host, self._interceptor) # type: ignore @property def get_instance(self) -> Callable[ [cloud_redis.GetInstanceRequest], cloud_redis.Instance]: - return self._GetInstance(self._session, self._host) # type: ignore + return self._GetInstance(self._session, self._host, self._interceptor) # type: ignore @property def get_instance_auth_string(self) -> Callable[ [cloud_redis.GetInstanceAuthStringRequest], cloud_redis.InstanceAuthString]: - return self._GetInstanceAuthString(self._session, self._host) # type: ignore + return self._GetInstanceAuthString(self._session, self._host, self._interceptor) # type: ignore @property def import_instance(self) -> Callable[ [cloud_redis.ImportInstanceRequest], operations_pb2.Operation]: - return self._ImportInstance(self._session, self._host) # type: ignore + return self._ImportInstance(self._session, self._host, self._interceptor) # type: ignore @property def list_instances(self) -> Callable[ [cloud_redis.ListInstancesRequest], cloud_redis.ListInstancesResponse]: - return self._ListInstances(self._session, self._host) # type: ignore + return self._ListInstances(self._session, self._host, self._interceptor) # type: ignore @property def reschedule_maintenance(self) -> Callable[ [cloud_redis.RescheduleMaintenanceRequest], operations_pb2.Operation]: - return self._RescheduleMaintenance(self._session, self._host) # type: ignore + return self._RescheduleMaintenance(self._session, self._host, self._interceptor) # type: ignore @property def update_instance(self) -> Callable[ [cloud_redis.UpdateInstanceRequest], operations_pb2.Operation]: - return self._UpdateInstance(self._session, self._host) # type: ignore + return self._UpdateInstance(self._session, self._host, self._interceptor) # type: ignore @property def upgrade_instance(self) -> Callable[ [cloud_redis.UpgradeInstanceRequest], operations_pb2.Operation]: - return self._UpgradeInstance(self._session, self._host) # type: ignore + return self._UpgradeInstance(self._session, self._host, self._interceptor) # type: ignore @property def kind(self) -> str: diff --git a/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py b/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py index 1abc37816..fe3468f57 100755 --- a/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py +++ b/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py @@ -8633,6 +8633,54 @@ async def test_get_instance_rest_asyncio_call_success(request_type): assert response.available_maintenance_versions == ['available_maintenance_versions_value'] +@pytest.mark.asyncio +@pytest.mark.parametrize("null_interceptor", [True, False]) +async def test_get_instance_rest_asyncio_interceptors(null_interceptor): + if not HAS_GOOGLE_AUTH_AIO: + pytest.skip("google-auth >= 2.35.0 is required for async rest transport.") + elif not HAS_AIOHTTP_INSTALLED: + pytest.skip("aiohttp is required for async rest transport.") + elif not HAS_ASYNC_REST_SUPPORT_IN_CORE: + pytest.skip("google-api-core >= 2.20.0 is required for async rest transport.") + + transport = transports.AsyncCloudRedisRestTransport( + credentials=async_anonymous_credentials(), + interceptor=None if null_interceptor else transports.AsyncCloudRedisRestInterceptor(), + ) + client = CloudRedisAsyncClient(transport=transport) + + with mock.patch.object(type(client.transport._session), "request") as req, \ + mock.patch.object(path_template, "transcode") as transcode, \ + mock.patch.object(transports.AsyncCloudRedisRestInterceptor, "post_get_instance") as post, \ + mock.patch.object(transports.AsyncCloudRedisRestInterceptor, "pre_get_instance") as pre: + pre.assert_not_called() + post.assert_not_called() + pb_message = cloud_redis.GetInstanceRequest.pb(cloud_redis.GetInstanceRequest()) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = mock.Mock() + req.return_value.status_code = 200 + return_value = cloud_redis.Instance.to_json(cloud_redis.Instance()) + req.return_value.read = mock.AsyncMock(return_value=return_value) + + request = cloud_redis.GetInstanceRequest() + metadata =[ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + post.return_value = cloud_redis.Instance() + + await client.get_instance(request, metadata=[("key", "val"), ("cephalopod", "squid"),]) + + pre.assert_called_once() + post.assert_called_once() + @pytest.mark.asyncio async def test_get_instance_auth_string_rest_asyncio_bad_request(request_type=cloud_redis.GetInstanceAuthStringRequest): if not HAS_GOOGLE_AUTH_AIO: @@ -8705,6 +8753,54 @@ async def test_get_instance_auth_string_rest_asyncio_call_success(request_type): assert response.auth_string == 'auth_string_value' +@pytest.mark.asyncio +@pytest.mark.parametrize("null_interceptor", [True, False]) +async def test_get_instance_auth_string_rest_asyncio_interceptors(null_interceptor): + if not HAS_GOOGLE_AUTH_AIO: + pytest.skip("google-auth >= 2.35.0 is required for async rest transport.") + elif not HAS_AIOHTTP_INSTALLED: + pytest.skip("aiohttp is required for async rest transport.") + elif not HAS_ASYNC_REST_SUPPORT_IN_CORE: + pytest.skip("google-api-core >= 2.20.0 is required for async rest transport.") + + transport = transports.AsyncCloudRedisRestTransport( + credentials=async_anonymous_credentials(), + interceptor=None if null_interceptor else transports.AsyncCloudRedisRestInterceptor(), + ) + client = CloudRedisAsyncClient(transport=transport) + + with mock.patch.object(type(client.transport._session), "request") as req, \ + mock.patch.object(path_template, "transcode") as transcode, \ + mock.patch.object(transports.AsyncCloudRedisRestInterceptor, "post_get_instance_auth_string") as post, \ + mock.patch.object(transports.AsyncCloudRedisRestInterceptor, "pre_get_instance_auth_string") as pre: + pre.assert_not_called() + post.assert_not_called() + pb_message = cloud_redis.GetInstanceAuthStringRequest.pb(cloud_redis.GetInstanceAuthStringRequest()) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = mock.Mock() + req.return_value.status_code = 200 + return_value = cloud_redis.InstanceAuthString.to_json(cloud_redis.InstanceAuthString()) + req.return_value.read = mock.AsyncMock(return_value=return_value) + + request = cloud_redis.GetInstanceAuthStringRequest() + metadata =[ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + post.return_value = cloud_redis.InstanceAuthString() + + await client.get_instance_auth_string(request, metadata=[("key", "val"), ("cephalopod", "squid"),]) + + pre.assert_called_once() + post.assert_called_once() + @pytest.mark.asyncio async def test_create_instance_rest_asyncio_error(): if not HAS_GOOGLE_AUTH_AIO: