From 5b4bc833275807799dae197bb7192dbd94c5e1ec Mon Sep 17 00:00:00 2001 From: taimo3810 Date: Tue, 13 Jan 2026 21:14:46 +0900 Subject: [PATCH 1/3] fix: prevent URL-encoding of $alt query parameter in REST transport The `requests` library encodes '$' as '%24' when using the `params` argument, which causes API errors for parameters like '$alt'. This fix manually builds the URL query string using `urlencode` with `safe="$"` to preserve the '$' character. Changes: - Build query string manually in _get_response method - Add urlencode import to REST transport templates - Update tests to verify query params in URL - Add unit tests for URL query params encoding Fixes: https://github.com/googleapis/gapic-generator-python/issues/2514 --- .../%sub/services/%service/_shared_macros.j2 | 13 +++- .../services/%service/transports/rest.py.j2 | 4 +- .../%name_%version/%sub/test_%service.py.j2 | 59 ++++++++++++++++++- .../%sub/services/%service/_shared_macros.j2 | 13 +++- .../services/%service/transports/rest.py.j2 | 4 +- .../%service/transports/rest_asyncio.py.j2 | 3 +- .../%name_%version/%sub/test_%service.py.j2 | 3 + .../gapic/%name_%version/%sub/test_macros.j2 | 54 ++++++++++++++++- 8 files changed, 139 insertions(+), 14 deletions(-) diff --git a/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/_shared_macros.j2 b/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/_shared_macros.j2 index b055b9ca31..ced2529f18 100644 --- a/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/_shared_macros.j2 +++ b/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/_shared_macros.j2 @@ -171,16 +171,23 @@ def _get_http_options(): timeout, transcoded_request, body=None): - + uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = {{ await_prefix }}getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), {% if body_spec %} data=body, {% endif %} diff --git a/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/transports/rest.py.j2 b/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/transports/rest.py.j2 index a55ced7c08..f5f57b0fe9 100644 --- a/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/transports/rest.py.j2 +++ b/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/transports/rest.py.j2 @@ -33,11 +33,13 @@ from google.iam.v1 import policy_pb2 # type: ignore from google.cloud.location import locations_pb2 # type: ignore {% endif %} -from requests import __version__ as requests_version import dataclasses from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union +from urllib.parse import urlencode import warnings +from requests import __version__ as requests_version + {{ shared_macros.operations_mixin_imports(api, service, opts) }} from .rest_base import _Base{{ service.name }}RestTransport diff --git a/gapic/ads-templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 b/gapic/ads-templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 index ac385e285d..dda6f834d7 100644 --- a/gapic/ads-templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 +++ b/gapic/ads-templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 @@ -18,6 +18,8 @@ import grpc from grpc.experimental import aio {% if "rest" in opts.transport %} from collections.abc import Iterable +import urllib.parse + from google.protobuf import json_format import json {% endif %} @@ -45,6 +47,7 @@ from google.api_core import client_options from google.api_core import exceptions as core_exceptions from google.api_core import grpc_helpers from google.api_core import path_template +from google.api_core import rest_helpers from google.api_core import retry as retries {% if service.has_lro %} from google.api_core import future @@ -1451,8 +1454,12 @@ def test_{{ method_name }}_rest_required_fields(request_type={{ method.input.ide ('$alt', 'json;enum-encoding=int') {% endif %} ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_{{ method_name }}_rest_unset_required_fields(): @@ -1461,9 +1468,55 @@ def test_{{ method_name }}_rest_unset_required_fields(): unset_fields = transport.{{ method.transport_safe_name|snake_case }}._get_unset_required_fields({}) assert set(unset_fields) == (set(({% for param in method.query_params|sort %}"{{ param|camel_case }}", {% endfor %})) & set(({% for param in method.input.required_fields %}"{{param.name|camel_case}}", {% endfor %}))) - {% endif %}{# required_fields #} + +def test_{{ method_name }}_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.{{ service.rest_transport_name }}(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.{{ method.transport_safe_name|snake_case }}.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': '{{ method.http_options[0].method }}', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, '{{ method.http_options[0].method }}') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + {% if not method.client_streaming %} @pytest.mark.parametrize("null_interceptor", [True, False]) def test_{{ method_name }}_rest_interceptors(null_interceptor): 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 6db274e82f..31c3718584 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 @@ -164,16 +164,23 @@ def _get_http_options(): timeout, transcoded_request, body=None): - + uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = {{ await_prefix }}getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), {% if body_spec %} data=body, {% endif %} diff --git a/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 b/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 index 95efafb389..2a55f8fd87 100644 --- a/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 +++ b/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 @@ -27,11 +27,13 @@ from google.iam.v1 import policy_pb2 # type: ignore from google.cloud.location import locations_pb2 # type: ignore {% endif %} -from requests import __version__ as requests_version import dataclasses from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union +from urllib.parse import urlencode import warnings +from requests import __version__ as requests_version + {{ shared_macros.operations_mixin_imports(api, service, opts) }} from .rest_base import _Base{{ service.name }}RestTransport 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 1d6ec87374..2c758c2b8d 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 @@ -48,9 +48,10 @@ from google.iam.v1 import policy_pb2 # type: ignore from google.cloud.location import locations_pb2 # type: ignore {% endif %} -import json # type: ignore import dataclasses +import json # type: ignore from typing import Any, Dict, List, Callable, Tuple, Optional, Sequence, Union +from urllib.parse import urlencode {{ shared_macros.operations_mixin_imports(api, service, opts) }} diff --git a/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 b/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 index c0e92cd9d6..73aeb74a50 100644 --- a/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 +++ b/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 @@ -21,6 +21,8 @@ import grpc from grpc.experimental import aio {% if "rest" in opts.transport %} from collections.abc import Iterable, AsyncIterable +import urllib.parse + from google.protobuf import json_format {% endif %} import json @@ -72,6 +74,7 @@ from google.api_core import exceptions as core_exceptions from google.api_core import grpc_helpers from google.api_core import grpc_helpers_async from google.api_core import path_template +from google.api_core import rest_helpers from google.api_core import retry as retries {% if service.has_lro or service.has_extended_lro %} from google.api_core import future 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 f15326d670..9641c2addb 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 @@ -1200,8 +1200,12 @@ def test_{{ method_name }}_rest_required_fields(request_type={{ method.input.ide ('$alt', 'json;enum-encoding=int') {% endif %} ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_{{ method_name }}_rest_unset_required_fields(): @@ -1213,6 +1217,52 @@ def test_{{ method_name }}_rest_unset_required_fields(): {% endif %}{# required_fields #} +def test_{{ method_name }}_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.{{ service.rest_transport_name }}(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.{{ method.transport_safe_name|snake_case }}.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': '{{ method.http_options[0].method }}', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, '{{ method.http_options[0].method }}') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + {% if method.flattened_fields and not method.client_streaming %} def test_{{ method_name }}_rest_flattened(): client = {{ service.client_name }}( From c6bbe1a4afea18034139dd4bfb5d3bb836eaf5d7 Mon Sep 17 00:00:00 2001 From: taimo3810 Date: Sat, 17 Jan 2026 22:58:45 +0900 Subject: [PATCH 2/3] test: add REST URL query params encoding tests for mixin methods Add tests for mixin methods (ListLocations, GetLocation, SetIamPolicy, GetIamPolicy, TestIamPermissions, ListOperations, GetOperation, DeleteOperation, CancelOperation) to cover the urlencode code path with safe="$" in REST transport. This ensures 100% test coverage for the $alt parameter fix. --- .../%name_%version/%sub/_test_mixins.py.j2 | 343 ++++++++++++++++++ .../%name_%version/%sub/test_%service.py.j2 | 93 ++--- .../%name_%version/%sub/_test_mixins.py.j2 | 343 ++++++++++++++++++ .../gapic/%name_%version/%sub/test_macros.j2 | 93 ++--- 4 files changed, 780 insertions(+), 92 deletions(-) diff --git a/gapic/ads-templates/tests/unit/gapic/%name_%version/%sub/_test_mixins.py.j2 b/gapic/ads-templates/tests/unit/gapic/%name_%version/%sub/_test_mixins.py.j2 index d7f8bb7e68..abcfe570a4 100644 --- a/gapic/ads-templates/tests/unit/gapic/%name_%version/%sub/_test_mixins.py.j2 +++ b/gapic/ads-templates/tests/unit/gapic/%name_%version/%sub/_test_mixins.py.j2 @@ -997,3 +997,346 @@ def test_test_iam_permissions_from_dict(): {% endif %} {% endif %} + +{# REST transport tests for mixin URL query params encoding #} +{% if 'rest' in opts.transport %} + +{# Operations mixin REST URL encoding tests #} +{% if api.has_operations_mixin %} + +{% if "ListOperations" in api.mixin_api_methods %} +def test_list_operations_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string for mixin methods. + transport = transports.{{ service.rest_transport_name }}(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.list_operations.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test/operations', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url +{% endif %} + +{% if "GetOperation" in api.mixin_api_methods %} +def test_get_operation_rest_url_query_params_encoding(): + transport = transports.{{ service.rest_transport_name }}(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.get_operation.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test/operations/op1', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url +{% endif %} + +{% if "DeleteOperation" in api.mixin_api_methods %} +def test_delete_operation_rest_url_query_params_encoding(): + transport = transports.{{ service.rest_transport_name }}(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.delete_operation.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.delete.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test/operations/op1', + 'method': 'delete', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.delete.called + call_url = mock_session.delete.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url +{% endif %} + +{% if "CancelOperation" in api.mixin_api_methods %} +def test_cancel_operation_rest_url_query_params_encoding(): + transport = transports.{{ service.rest_transport_name }}(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.cancel_operation.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.post.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test/operations/op1:cancel', + 'method': 'post', + 'body': {}, + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + body={}, + ) + + assert mock_session.post.called + call_url = mock_session.post.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url +{% endif %} + +{% endif %} {# operations_mixin #} + +{# Location mixin REST URL encoding tests #} +{% if api.has_location_mixin %} + +{% if "ListLocations" in api.mixin_api_methods %} +def test_list_locations_rest_url_query_params_encoding(): + transport = transports.{{ service.rest_transport_name }}(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.list_locations.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/projects/p1/locations', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url +{% endif %} + +{% if "GetLocation" in api.mixin_api_methods %} +def test_get_location_rest_url_query_params_encoding(): + transport = transports.{{ service.rest_transport_name }}(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.get_location.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/projects/p1/locations/l1', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url +{% endif %} + +{% endif %} {# location_mixin #} + +{# IAM mixin REST URL encoding tests #} +{% if api.has_iam_mixin or opts.add_iam_methods %} + +{% if "SetIamPolicy" in api.mixin_api_methods or opts.add_iam_methods %} +def test_set_iam_policy_rest_url_query_params_encoding(): + transport = transports.{{ service.rest_transport_name }}(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.set_iam_policy.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.post.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/resource:setIamPolicy', + 'method': 'post', + 'body': {}, + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + body={}, + ) + + assert mock_session.post.called + call_url = mock_session.post.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url +{% endif %} + +{% if "GetIamPolicy" in api.mixin_api_methods or opts.add_iam_methods %} +def test_get_iam_policy_rest_url_query_params_encoding(): + transport = transports.{{ service.rest_transport_name }}(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.get_iam_policy.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/resource:getIamPolicy', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url +{% endif %} + +{% if "TestIamPermissions" in api.mixin_api_methods or opts.add_iam_methods %} +def test_test_iam_permissions_rest_url_query_params_encoding(): + transport = transports.{{ service.rest_transport_name }}(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.test_iam_permissions.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.post.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/resource:testIamPermissions', + 'method': 'post', + 'body': {}, + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + body={}, + ) + + assert mock_session.post.called + call_url = mock_session.post.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url +{% endif %} + +{% endif %} {# iam_mixin #} + +{% endif %} {# rest transport #} diff --git a/gapic/ads-templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 b/gapic/ads-templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 index dda6f834d7..9569470c47 100644 --- a/gapic/ads-templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 +++ b/gapic/ads-templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 @@ -1060,6 +1060,53 @@ def test_{{ method_name }}_raw_page_lro(): {% for method in service.methods.values() if 'rest' in opts.transport %}{% with method_name = method.name|snake_case + "_unary" if method.operation_service else method.client_method_name|snake_case %}{% if method.http_options %} {# TODO(kbandes): remove this if condition when client streaming are supported. #} {% if not method.client_streaming %} + +def test_{{ method_name }}_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.{{ service.rest_transport_name }}(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.{{ method.transport_safe_name|snake_case }}.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': '{{ method.http_options[0].method }}', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, '{{ method.http_options[0].method }}') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + @pytest.mark.parametrize("request_type", [ {{ method.input.ident }}, dict, @@ -1471,52 +1518,6 @@ def test_{{ method_name }}_rest_unset_required_fields(): {% endif %}{# required_fields #} -def test_{{ method_name }}_rest_url_query_params_encoding(): - # Verify that special characters like '$' are correctly preserved (not URL-encoded) - # when building the URL query string. This tests the urlencode call with safe="$". - transport = transports.{{ service.rest_transport_name }}(credentials=ga_credentials.AnonymousCredentials) - method_class = transport.{{ method.transport_safe_name|snake_case }}.__class__ - # Get the _get_response static method from the method class - get_response_fn = method_class._get_response - - mock_session = mock.Mock() - mock_response = mock.Mock() - mock_response.status_code = 200 - mock_session.get.return_value = mock_response - mock_session.post.return_value = mock_response - mock_session.put.return_value = mock_response - mock_session.patch.return_value = mock_response - mock_session.delete.return_value = mock_response - - # Mock flatten_query_params to return query params that include '$' character - with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: - mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] - - transcoded_request = { - 'uri': '/v1/test', - 'method': '{{ method.http_options[0].method }}', - } - - get_response_fn( - host='https://example.com', - metadata=[], - query_params={}, - session=mock_session, - timeout=None, - transcoded_request=transcoded_request, - ) - - # Verify the session method was called with the URL containing query params - session_method = getattr(mock_session, '{{ method.http_options[0].method }}') - assert session_method.called - - # The URL should contain '$alt' (not '%24alt') because safe="$" is used - call_url = session_method.call_args.args[0] - assert '$alt=json' in call_url - assert '%24alt' not in call_url - assert 'foo=bar' in call_url - - {% if not method.client_streaming %} @pytest.mark.parametrize("null_interceptor", [True, False]) def test_{{ method_name }}_rest_interceptors(null_interceptor): diff --git a/gapic/templates/tests/unit/gapic/%name_%version/%sub/_test_mixins.py.j2 b/gapic/templates/tests/unit/gapic/%name_%version/%sub/_test_mixins.py.j2 index 169807a961..db320f22fe 100644 --- a/gapic/templates/tests/unit/gapic/%name_%version/%sub/_test_mixins.py.j2 +++ b/gapic/templates/tests/unit/gapic/%name_%version/%sub/_test_mixins.py.j2 @@ -1829,3 +1829,346 @@ async def test_test_iam_permissions_from_dict_async(): call.assert_called() {% endif %} {% endif %} + +{# REST transport tests for mixin URL query params encoding #} +{% if 'rest' in opts.transport %} + +{# Operations mixin REST URL encoding tests #} +{% if api.has_operations_mixin %} + +{% if "ListOperations" in api.mixin_api_methods %} +def test_list_operations_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string for mixin methods. + transport = transports.{{ service.rest_transport_name }}(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.list_operations.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test/operations', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url +{% endif %} + +{% if "GetOperation" in api.mixin_api_methods %} +def test_get_operation_rest_url_query_params_encoding(): + transport = transports.{{ service.rest_transport_name }}(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.get_operation.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test/operations/op1', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url +{% endif %} + +{% if "DeleteOperation" in api.mixin_api_methods %} +def test_delete_operation_rest_url_query_params_encoding(): + transport = transports.{{ service.rest_transport_name }}(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.delete_operation.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.delete.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test/operations/op1', + 'method': 'delete', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.delete.called + call_url = mock_session.delete.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url +{% endif %} + +{% if "CancelOperation" in api.mixin_api_methods %} +def test_cancel_operation_rest_url_query_params_encoding(): + transport = transports.{{ service.rest_transport_name }}(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.cancel_operation.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.post.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test/operations/op1:cancel', + 'method': 'post', + 'body': {}, + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + body={}, + ) + + assert mock_session.post.called + call_url = mock_session.post.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url +{% endif %} + +{% endif %} {# operations_mixin #} + +{# Location mixin REST URL encoding tests #} +{% if api.has_location_mixin %} + +{% if "ListLocations" in api.mixin_api_methods %} +def test_list_locations_rest_url_query_params_encoding(): + transport = transports.{{ service.rest_transport_name }}(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.list_locations.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/projects/p1/locations', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url +{% endif %} + +{% if "GetLocation" in api.mixin_api_methods %} +def test_get_location_rest_url_query_params_encoding(): + transport = transports.{{ service.rest_transport_name }}(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.get_location.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/projects/p1/locations/l1', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url +{% endif %} + +{% endif %} {# location_mixin #} + +{# IAM mixin REST URL encoding tests #} +{% if api.has_iam_mixin or opts.add_iam_methods %} + +{% if "SetIamPolicy" in api.mixin_api_methods or opts.add_iam_methods %} +def test_set_iam_policy_rest_url_query_params_encoding(): + transport = transports.{{ service.rest_transport_name }}(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.set_iam_policy.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.post.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/resource:setIamPolicy', + 'method': 'post', + 'body': {}, + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + body={}, + ) + + assert mock_session.post.called + call_url = mock_session.post.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url +{% endif %} + +{% if "GetIamPolicy" in api.mixin_api_methods or opts.add_iam_methods %} +def test_get_iam_policy_rest_url_query_params_encoding(): + transport = transports.{{ service.rest_transport_name }}(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.get_iam_policy.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/resource:getIamPolicy', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url +{% endif %} + +{% if "TestIamPermissions" in api.mixin_api_methods or opts.add_iam_methods %} +def test_test_iam_permissions_rest_url_query_params_encoding(): + transport = transports.{{ service.rest_transport_name }}(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.test_iam_permissions.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.post.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/resource:testIamPermissions', + 'method': 'post', + 'body': {}, + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + body={}, + ) + + assert mock_session.post.called + call_url = mock_session.post.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url +{% endif %} + +{% endif %} {# iam_mixin #} + +{% endif %} {# rest transport #} 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 9641c2addb..b738ff3176 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 @@ -1004,6 +1004,53 @@ def test_{{ method_name }}_raw_page_lro(): {% with method_name = method.client_method_name|snake_case + "_unary" if method.extended_lro and not full_extended_lro else method.client_method_name|snake_case, method_output = method.extended_lro.operation_type if method.extended_lro and not full_extended_lro else method.output %}{% if method.http_options %} {# TODO(kbandes): remove this if condition when lro and client streaming are supported. #} {% if not method.client_streaming %} + +def test_{{ method_name }}_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.{{ service.rest_transport_name }}(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.{{ method.transport_safe_name|snake_case }}.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': '{{ method.http_options[0].method }}', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, '{{ method.http_options[0].method }}') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_{{ method_name }}_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -1217,52 +1264,6 @@ def test_{{ method_name }}_rest_unset_required_fields(): {% endif %}{# required_fields #} -def test_{{ method_name }}_rest_url_query_params_encoding(): - # Verify that special characters like '$' are correctly preserved (not URL-encoded) - # when building the URL query string. This tests the urlencode call with safe="$". - transport = transports.{{ service.rest_transport_name }}(credentials=ga_credentials.AnonymousCredentials) - method_class = transport.{{ method.transport_safe_name|snake_case }}.__class__ - # Get the _get_response static method from the method class - get_response_fn = method_class._get_response - - mock_session = mock.Mock() - mock_response = mock.Mock() - mock_response.status_code = 200 - mock_session.get.return_value = mock_response - mock_session.post.return_value = mock_response - mock_session.put.return_value = mock_response - mock_session.patch.return_value = mock_response - mock_session.delete.return_value = mock_response - - # Mock flatten_query_params to return query params that include '$' character - with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: - mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] - - transcoded_request = { - 'uri': '/v1/test', - 'method': '{{ method.http_options[0].method }}', - } - - get_response_fn( - host='https://example.com', - metadata=[], - query_params={}, - session=mock_session, - timeout=None, - transcoded_request=transcoded_request, - ) - - # Verify the session method was called with the URL containing query params - session_method = getattr(mock_session, '{{ method.http_options[0].method }}') - assert session_method.called - - # The URL should contain '$alt' (not '%24alt') because safe="$" is used - call_url = session_method.call_args.args[0] - assert '$alt=json' in call_url - assert '%24alt' not in call_url - assert 'foo=bar' in call_url - - {% if method.flattened_fields and not method.client_streaming %} def test_{{ method_name }}_rest_flattened(): client = {{ service.client_name }}( From 0ff4584105f19050925e3db8fdd1aec5d5bd6fa4 Mon Sep 17 00:00:00 2001 From: taimo3810 Date: Tue, 3 Feb 2026 22:01:39 +0900 Subject: [PATCH 3/3] test: add async REST URL query params encoding tests and update goldens Add async REST transport versions of URL query params encoding tests to achieve 100% code coverage for rest_asyncio.py. Update integration test golden files to reflect the $alt query parameter encoding fix. --- .../%name_%version/%sub/_test_mixins.py.j2 | 365 ++++ .../%name_%version/%sub/test_%service.py.j2 | 4 +- .../gapic/%name_%version/%sub/test_macros.j2 | 49 +- .../services/asset_service/transports/rest.py | 268 ++- .../unit/gapic/asset_v1/test_asset_service.py | 1279 +++++++++++++- .../iam_credentials/transports/rest.py | 48 +- .../credentials_v1/test_iam_credentials.py | 219 ++- .../services/eventarc/transports/rest.py | 301 +++- .../unit/gapic/eventarc_v1/test_eventarc.py | 1283 +++++++++++++- .../logging_v2/test_config_service_v2.py | 1 + .../logging_v2/test_logging_service_v2.py | 1 + .../logging_v2/test_metrics_service_v2.py | 1 + .../logging_v2/test_config_service_v2.py | 1 + .../logging_v2/test_logging_service_v2.py | 1 + .../logging_v2/test_metrics_service_v2.py | 1 + .../services/cloud_redis/transports/rest.py | 202 ++- .../cloud_redis/transports/rest_asyncio.py | 201 ++- .../unit/gapic/redis_v1/test_cloud_redis.py | 1516 ++++++++++++++++- .../services/cloud_redis/transports/rest.py | 136 +- .../cloud_redis/transports/rest_asyncio.py | 135 +- .../unit/gapic/redis_v1/test_cloud_redis.py | 922 +++++++++- 21 files changed, 6572 insertions(+), 362 deletions(-) mode change 100755 => 100644 tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/rest.py mode change 100755 => 100644 tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py mode change 100755 => 100644 tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/rest.py mode change 100755 => 100644 tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py mode change 100755 => 100644 tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/rest.py mode change 100755 => 100644 tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py mode change 100755 => 100644 tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py mode change 100755 => 100644 tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py mode change 100755 => 100644 tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py mode change 100755 => 100644 tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py mode change 100755 => 100644 tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py mode change 100755 => 100644 tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py mode change 100755 => 100644 tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest.py mode change 100755 => 100644 tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py mode change 100755 => 100644 tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py mode change 100755 => 100644 tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest.py mode change 100755 => 100644 tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py mode change 100755 => 100644 tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py diff --git a/gapic/templates/tests/unit/gapic/%name_%version/%sub/_test_mixins.py.j2 b/gapic/templates/tests/unit/gapic/%name_%version/%sub/_test_mixins.py.j2 index db320f22fe..938f96cf19 100644 --- a/gapic/templates/tests/unit/gapic/%name_%version/%sub/_test_mixins.py.j2 +++ b/gapic/templates/tests/unit/gapic/%name_%version/%sub/_test_mixins.py.j2 @@ -2171,4 +2171,369 @@ def test_test_iam_permissions_rest_url_query_params_encoding(): {% endif %} {# iam_mixin #} +{% if rest_async_io_enabled %} +{# Async REST URL encoding tests for mixin methods #} + +{% if api.has_operations_mixin %} + +{% if "ListOperations" in api.mixin_api_methods %} +@pytest.mark.asyncio +async def test_list_operations_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + transport = transports.Async{{ service.name }}RestTransport(credentials=async_anonymous_credentials()) + method_class = transport.list_operations.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test/operations', + 'method': 'get', + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url +{% endif %} + +{% if "GetOperation" in api.mixin_api_methods %} +@pytest.mark.asyncio +async def test_get_operation_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + transport = transports.Async{{ service.name }}RestTransport(credentials=async_anonymous_credentials()) + method_class = transport.get_operation.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test/operations/op1', + 'method': 'get', + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url +{% endif %} + +{% if "DeleteOperation" in api.mixin_api_methods %} +@pytest.mark.asyncio +async def test_delete_operation_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + transport = transports.Async{{ service.name }}RestTransport(credentials=async_anonymous_credentials()) + method_class = transport.delete_operation.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.delete.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test/operations/op1', + 'method': 'delete', + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.delete.called + call_url = mock_session.delete.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url +{% endif %} + +{% if "CancelOperation" in api.mixin_api_methods %} +@pytest.mark.asyncio +async def test_cancel_operation_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + transport = transports.Async{{ service.name }}RestTransport(credentials=async_anonymous_credentials()) + method_class = transport.cancel_operation.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.post.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test/operations/op1:cancel', + 'method': 'post', + 'body': {}, + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + body={}, + ) + + assert mock_session.post.called + call_url = mock_session.post.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url +{% endif %} + +{% endif %} {# operations_mixin #} + +{% if api.has_location_mixin %} + +{% if "ListLocations" in api.mixin_api_methods %} +@pytest.mark.asyncio +async def test_list_locations_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + transport = transports.Async{{ service.name }}RestTransport(credentials=async_anonymous_credentials()) + method_class = transport.list_locations.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/projects/p1/locations', + 'method': 'get', + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url +{% endif %} + +{% if "GetLocation" in api.mixin_api_methods %} +@pytest.mark.asyncio +async def test_get_location_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + transport = transports.Async{{ service.name }}RestTransport(credentials=async_anonymous_credentials()) + method_class = transport.get_location.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/projects/p1/locations/l1', + 'method': 'get', + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url +{% endif %} + +{% endif %} {# location_mixin #} + +{% if api.has_iam_mixin or opts.add_iam_methods %} + +{% if "SetIamPolicy" in api.mixin_api_methods or opts.add_iam_methods %} +@pytest.mark.asyncio +async def test_set_iam_policy_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + transport = transports.Async{{ service.name }}RestTransport(credentials=async_anonymous_credentials()) + method_class = transport.set_iam_policy.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.post.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/resource:setIamPolicy', + 'method': 'post', + 'body': {}, + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + body={}, + ) + + assert mock_session.post.called + call_url = mock_session.post.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url +{% endif %} + +{% if "GetIamPolicy" in api.mixin_api_methods or opts.add_iam_methods %} +@pytest.mark.asyncio +async def test_get_iam_policy_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + transport = transports.Async{{ service.name }}RestTransport(credentials=async_anonymous_credentials()) + method_class = transport.get_iam_policy.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/resource:getIamPolicy', + 'method': 'get', + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url +{% endif %} + +{% if "TestIamPermissions" in api.mixin_api_methods or opts.add_iam_methods %} +@pytest.mark.asyncio +async def test_test_iam_permissions_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + transport = transports.Async{{ service.name }}RestTransport(credentials=async_anonymous_credentials()) + method_class = transport.test_iam_permissions.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.post.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/resource:testIamPermissions', + 'method': 'post', + 'body': {}, + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + body={}, + ) + + assert mock_session.post.called + call_url = mock_session.post.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url +{% endif %} + +{% endif %} {# iam_mixin #} + +{% endif %} {# rest_async_io_enabled #} + {% endif %} {# rest transport #} diff --git a/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 b/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 index 73aeb74a50..8195ef44dd 100644 --- a/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 +++ b/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 @@ -1118,10 +1118,10 @@ def test_{{ service.client_name|snake_case }}_create_channel_credentials_file(cl {% for method in service.methods.values() if 'rest' in opts.transport %} {% if method.extended_lro %} -{{ test_macros.rest_required_tests(method, service, numeric_enums=opts.rest_numeric_enums, full_extended_lro=True) }} +{{ test_macros.rest_required_tests(method, service, numeric_enums=opts.rest_numeric_enums, full_extended_lro=True, rest_async_io_enabled=rest_async_io_enabled) }} {% endif %} -{{ test_macros.rest_required_tests(method, service, numeric_enums=opts.rest_numeric_enums) }} +{{ test_macros.rest_required_tests(method, service, numeric_enums=opts.rest_numeric_enums, rest_async_io_enabled=rest_async_io_enabled) }} {% endfor -%} {#- method in methods for rest #} 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 b738ff3176..54f5a34bc3 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 @@ -1000,7 +1000,7 @@ def test_{{ method_name }}_raw_page_lro(): {% endif %}{# method.paged_result_field #}{% endwith %} {% endmacro %} -{% macro rest_required_tests(method, service, numeric_enums=False, full_extended_lro=False) %} +{% macro rest_required_tests(method, service, numeric_enums=False, full_extended_lro=False, rest_async_io_enabled=False) %} {% with method_name = method.client_method_name|snake_case + "_unary" if method.extended_lro and not full_extended_lro else method.client_method_name|snake_case, method_output = method.extended_lro.operation_type if method.extended_lro and not full_extended_lro else method.output %}{% if method.http_options %} {# TODO(kbandes): remove this if condition when lro and client streaming are supported. #} {% if not method.client_streaming %} @@ -1050,6 +1050,53 @@ def test_{{ method_name }}_rest_url_query_params_encoding(): assert '%24alt' not in call_url assert 'foo=bar' in call_url +{% if rest_async_io_enabled %} + +@pytest.mark.asyncio +async def test_{{ method_name }}_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string for the async REST transport. + transport = transports.Async{{ service.name }}RestTransport(credentials=async_anonymous_credentials()) + method_class = transport.{{ method.transport_safe_name|snake_case }}.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': '{{ method.http_options[0].method }}', + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + session_method = getattr(mock_session, '{{ method.http_options[0].method }}') + assert session_method.called + + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + +{% endif %}{# if rest_async_io_enabled #} def test_{{ method_name }}_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, diff --git a/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/rest.py b/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/rest.py old mode 100755 new mode 100644 index b7f32b5fc7..2f10ea257b --- a/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/rest.py +++ b/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/rest.py @@ -28,11 +28,13 @@ from google.protobuf import json_format from google.api_core import operations_v1 -from requests import __version__ as requests_version import dataclasses from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union +from urllib.parse import urlencode import warnings +from requests import __version__ as requests_version + from google.cloud.asset_v1.types import asset_service from google.protobuf import empty_pb2 # type: ignore @@ -1194,11 +1196,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -1316,11 +1325,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -1441,11 +1457,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -1563,11 +1586,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -1685,11 +1715,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -1808,11 +1845,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -1931,11 +1975,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -2050,11 +2101,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -2173,11 +2231,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -2303,11 +2368,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -2427,11 +2499,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -2512,11 +2591,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -2597,11 +2683,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -2720,11 +2813,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -2847,11 +2947,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -2968,11 +3075,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -3087,11 +3201,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -3206,11 +3327,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -3325,11 +3453,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -3447,11 +3582,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -3566,11 +3708,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -3685,11 +3834,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -3815,11 +3971,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -4127,11 +4290,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response diff --git a/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py b/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py old mode 100755 new mode 100644 index bd17439996..0ec975f1ef --- a/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py +++ b/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py @@ -24,6 +24,8 @@ import grpc from grpc.experimental import aio from collections.abc import Iterable, AsyncIterable +import urllib.parse + from google.protobuf import json_format import json import math @@ -52,6 +54,7 @@ from google.api_core import operation_async # type: ignore from google.api_core import operations_v1 from google.api_core import path_template +from google.api_core import rest_helpers from google.api_core import retry as retries from google.auth import credentials as ga_credentials from google.auth.exceptions import MutualTLSChannelError @@ -9107,6 +9110,52 @@ async def test_analyze_org_policy_governed_assets_async_pages(): assert page_.raw_page.next_page_token == token +def test_export_assets_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.AssetServiceRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.export_assets.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'post', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'post') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_export_assets_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -9210,8 +9259,12 @@ def test_export_assets_rest_required_fields(request_type=asset_service.ExportAss expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_export_assets_rest_unset_required_fields(): @@ -9221,6 +9274,52 @@ def test_export_assets_rest_unset_required_fields(): assert set(unset_fields) == (set(()) & set(("parent", "outputConfig", ))) +def test_list_assets_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.AssetServiceRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.list_assets.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'get') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_list_assets_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -9324,8 +9423,12 @@ def test_list_assets_rest_required_fields(request_type=asset_service.ListAssetsR expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_list_assets_rest_unset_required_fields(): @@ -9451,6 +9554,52 @@ def test_list_assets_rest_pager(transport: str = 'rest'): assert page_.raw_page.next_page_token == token +def test_batch_get_assets_history_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.AssetServiceRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.batch_get_assets_history.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'get') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_batch_get_assets_history_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -9554,8 +9703,12 @@ def test_batch_get_assets_history_rest_required_fields(request_type=asset_servic expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_batch_get_assets_history_rest_unset_required_fields(): @@ -9565,6 +9718,52 @@ def test_batch_get_assets_history_rest_unset_required_fields(): assert set(unset_fields) == (set(("assetNames", "contentType", "readTimeWindow", "relationshipTypes", )) & set(("parent", ))) +def test_create_feed_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.AssetServiceRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.create_feed.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'post', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'post') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_create_feed_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -9671,8 +9870,12 @@ def test_create_feed_rest_required_fields(request_type=asset_service.CreateFeedR expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_create_feed_rest_unset_required_fields(): @@ -9736,6 +9939,52 @@ def test_create_feed_rest_flattened_error(transport: str = 'rest'): ) +def test_get_feed_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.AssetServiceRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.get_feed.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'get') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_get_feed_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -9837,8 +10086,12 @@ def test_get_feed_rest_required_fields(request_type=asset_service.GetFeedRequest expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_get_feed_rest_unset_required_fields(): @@ -9902,6 +10155,52 @@ def test_get_feed_rest_flattened_error(transport: str = 'rest'): ) +def test_list_feeds_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.AssetServiceRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.list_feeds.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'get') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_list_feeds_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -10003,8 +10302,12 @@ def test_list_feeds_rest_required_fields(request_type=asset_service.ListFeedsReq expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_list_feeds_rest_unset_required_fields(): @@ -10068,6 +10371,52 @@ def test_list_feeds_rest_flattened_error(transport: str = 'rest'): ) +def test_update_feed_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.AssetServiceRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.update_feed.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'patch', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'patch') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_update_feed_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -10165,8 +10514,12 @@ def test_update_feed_rest_required_fields(request_type=asset_service.UpdateFeedR expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_update_feed_rest_unset_required_fields(): @@ -10230,6 +10583,52 @@ def test_update_feed_rest_flattened_error(transport: str = 'rest'): ) +def test_delete_feed_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.AssetServiceRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.delete_feed.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'delete', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'delete') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_delete_feed_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -10328,8 +10727,12 @@ def test_delete_feed_rest_required_fields(request_type=asset_service.DeleteFeedR expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_delete_feed_rest_unset_required_fields(): @@ -10391,6 +10794,52 @@ def test_delete_feed_rest_flattened_error(transport: str = 'rest'): ) +def test_search_all_resources_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.AssetServiceRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.search_all_resources.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'get') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_search_all_resources_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -10494,8 +10943,12 @@ def test_search_all_resources_rest_required_fields(request_type=asset_service.Se expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_search_all_resources_rest_unset_required_fields(): @@ -10625,6 +11078,52 @@ def test_search_all_resources_rest_pager(transport: str = 'rest'): assert page_.raw_page.next_page_token == token +def test_search_all_iam_policies_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.AssetServiceRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.search_all_iam_policies.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'get') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_search_all_iam_policies_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -10728,8 +11227,12 @@ def test_search_all_iam_policies_rest_required_fields(request_type=asset_service expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_search_all_iam_policies_rest_unset_required_fields(): @@ -10857,6 +11360,52 @@ def test_search_all_iam_policies_rest_pager(transport: str = 'rest'): assert page_.raw_page.next_page_token == token +def test_analyze_iam_policy_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.AssetServiceRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.analyze_iam_policy.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'get') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_analyze_iam_policy_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -10955,8 +11504,12 @@ def test_analyze_iam_policy_rest_required_fields(request_type=asset_service.Anal expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_analyze_iam_policy_rest_unset_required_fields(): @@ -10966,6 +11519,52 @@ def test_analyze_iam_policy_rest_unset_required_fields(): assert set(unset_fields) == (set(("analysisQuery", "executionTimeout", "savedAnalysisQuery", )) & set(("analysisQuery", ))) +def test_analyze_iam_policy_longrunning_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.AssetServiceRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.analyze_iam_policy_longrunning.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'post', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'post') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_analyze_iam_policy_longrunning_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -11064,8 +11663,12 @@ def test_analyze_iam_policy_longrunning_rest_required_fields(request_type=asset_ expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_analyze_iam_policy_longrunning_rest_unset_required_fields(): @@ -11075,6 +11678,52 @@ def test_analyze_iam_policy_longrunning_rest_unset_required_fields(): assert set(unset_fields) == (set(()) & set(("analysisQuery", "outputConfig", ))) +def test_analyze_move_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.AssetServiceRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.analyze_move.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'get') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_analyze_move_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -11189,8 +11838,12 @@ def test_analyze_move_rest_required_fields(request_type=asset_service.AnalyzeMov "", ), ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_analyze_move_rest_unset_required_fields(): @@ -11200,6 +11853,52 @@ def test_analyze_move_rest_unset_required_fields(): assert set(unset_fields) == (set(("destinationParent", "view", )) & set(("resource", "destinationParent", ))) +def test_query_assets_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.AssetServiceRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.query_assets.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'post', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'post') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_query_assets_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -11302,8 +12001,12 @@ def test_query_assets_rest_required_fields(request_type=asset_service.QueryAsset expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_query_assets_rest_unset_required_fields(): @@ -11313,6 +12016,52 @@ def test_query_assets_rest_unset_required_fields(): assert set(unset_fields) == (set(()) & set(("parent", ))) +def test_create_saved_query_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.AssetServiceRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.create_saved_query.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'post', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'post') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_create_saved_query_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -11428,8 +12177,12 @@ def test_create_saved_query_rest_required_fields(request_type=asset_service.Crea "", ), ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_create_saved_query_rest_unset_required_fields(): @@ -11497,6 +12250,52 @@ def test_create_saved_query_rest_flattened_error(transport: str = 'rest'): ) +def test_get_saved_query_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.AssetServiceRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.get_saved_query.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'get') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_get_saved_query_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -11598,8 +12397,12 @@ def test_get_saved_query_rest_required_fields(request_type=asset_service.GetSave expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_get_saved_query_rest_unset_required_fields(): @@ -11663,6 +12466,52 @@ def test_get_saved_query_rest_flattened_error(transport: str = 'rest'): ) +def test_list_saved_queries_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.AssetServiceRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.list_saved_queries.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'get') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_list_saved_queries_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -11766,8 +12615,12 @@ def test_list_saved_queries_rest_required_fields(request_type=asset_service.List expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_list_saved_queries_rest_unset_required_fields(): @@ -11893,6 +12746,52 @@ def test_list_saved_queries_rest_pager(transport: str = 'rest'): assert page_.raw_page.next_page_token == token +def test_update_saved_query_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.AssetServiceRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.update_saved_query.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'patch', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'patch') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_update_saved_query_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -11992,8 +12891,12 @@ def test_update_saved_query_rest_required_fields(request_type=asset_service.Upda expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_update_saved_query_rest_unset_required_fields(): @@ -12059,6 +12962,52 @@ def test_update_saved_query_rest_flattened_error(transport: str = 'rest'): ) +def test_delete_saved_query_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.AssetServiceRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.delete_saved_query.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'delete', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'delete') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_delete_saved_query_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -12157,8 +13106,12 @@ def test_delete_saved_query_rest_required_fields(request_type=asset_service.Dele expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_delete_saved_query_rest_unset_required_fields(): @@ -12220,6 +13173,52 @@ def test_delete_saved_query_rest_flattened_error(transport: str = 'rest'): ) +def test_batch_get_effective_iam_policies_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.AssetServiceRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.batch_get_effective_iam_policies.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'get') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_batch_get_effective_iam_policies_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -12334,8 +13333,12 @@ def test_batch_get_effective_iam_policies_rest_required_fields(request_type=asse "", ), ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_batch_get_effective_iam_policies_rest_unset_required_fields(): @@ -12345,6 +13348,52 @@ def test_batch_get_effective_iam_policies_rest_unset_required_fields(): assert set(unset_fields) == (set(("names", )) & set(("scope", "names", ))) +def test_analyze_org_policies_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.AssetServiceRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.analyze_org_policies.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'get') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_analyze_org_policies_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -12459,8 +13508,12 @@ def test_analyze_org_policies_rest_required_fields(request_type=asset_service.An "", ), ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_analyze_org_policies_rest_unset_required_fields(): @@ -12590,6 +13643,52 @@ def test_analyze_org_policies_rest_pager(transport: str = 'rest'): assert page_.raw_page.next_page_token == token +def test_analyze_org_policy_governed_containers_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.AssetServiceRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.analyze_org_policy_governed_containers.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'get') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_analyze_org_policy_governed_containers_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -12704,8 +13803,12 @@ def test_analyze_org_policy_governed_containers_rest_required_fields(request_typ "", ), ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_analyze_org_policy_governed_containers_rest_unset_required_fields(): @@ -12835,6 +13938,52 @@ def test_analyze_org_policy_governed_containers_rest_pager(transport: str = 'res assert page_.raw_page.next_page_token == token +def test_analyze_org_policy_governed_assets_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.AssetServiceRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.analyze_org_policy_governed_assets.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'get') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_analyze_org_policy_governed_assets_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -12949,8 +14098,12 @@ def test_analyze_org_policy_governed_assets_rest_required_fields(request_type=as "", ), ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_analyze_org_policy_governed_assets_rest_unset_required_fields(): @@ -18386,6 +19539,40 @@ async def test_get_operation_from_dict_async(): call.assert_called() +def test_get_operation_rest_url_query_params_encoding(): + transport = transports.AssetServiceRestTransport(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.get_operation.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test/operations/op1', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_transport_close_grpc(): client = AssetServiceClient( credentials=ga_credentials.AnonymousCredentials(), diff --git a/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/rest.py b/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/rest.py old mode 100755 new mode 100644 index c0ad2e73ea..802b42f195 --- a/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/rest.py +++ b/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/rest.py @@ -27,11 +27,13 @@ from google.protobuf import json_format -from requests import __version__ as requests_version import dataclasses from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union +from urllib.parse import urlencode import warnings +from requests import __version__ as requests_version + from google.iam.credentials_v1.types import common @@ -375,11 +377,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -497,11 +506,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -619,11 +635,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -741,11 +764,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response diff --git a/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py b/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py old mode 100755 new mode 100644 index d9f2f0a4be..1d16048813 --- a/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py +++ b/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py @@ -24,6 +24,8 @@ import grpc from grpc.experimental import aio from collections.abc import Iterable, AsyncIterable +import urllib.parse + from google.protobuf import json_format import json import math @@ -48,6 +50,7 @@ from google.api_core import grpc_helpers from google.api_core import grpc_helpers_async from google.api_core import path_template +from google.api_core import rest_helpers from google.api_core import retry as retries from google.auth import credentials as ga_credentials from google.auth.exceptions import MutualTLSChannelError @@ -2299,6 +2302,52 @@ async def test_sign_jwt_flattened_error_async(): ) +def test_generate_access_token_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.IAMCredentialsRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.generate_access_token.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'post', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'post') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_generate_access_token_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -2405,8 +2454,12 @@ def test_generate_access_token_rest_required_fields(request_type=common.Generate expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_generate_access_token_rest_unset_required_fields(): @@ -2476,6 +2529,52 @@ def test_generate_access_token_rest_flattened_error(transport: str = 'rest'): ) +def test_generate_id_token_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.IAMCredentialsRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.generate_id_token.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'post', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'post') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_generate_id_token_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -2582,8 +2681,12 @@ def test_generate_id_token_rest_required_fields(request_type=common.GenerateIdTo expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_generate_id_token_rest_unset_required_fields(): @@ -2653,6 +2756,52 @@ def test_generate_id_token_rest_flattened_error(transport: str = 'rest'): ) +def test_sign_blob_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.IAMCredentialsRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.sign_blob.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'post', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'post') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_sign_blob_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -2759,8 +2908,12 @@ def test_sign_blob_rest_required_fields(request_type=common.SignBlobRequest): expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_sign_blob_rest_unset_required_fields(): @@ -2828,6 +2981,52 @@ def test_sign_blob_rest_flattened_error(transport: str = 'rest'): ) +def test_sign_jwt_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.IAMCredentialsRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.sign_jwt.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'post', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'post') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_sign_jwt_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -2934,8 +3133,12 @@ def test_sign_jwt_rest_required_fields(request_type=common.SignJwtRequest): expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_sign_jwt_rest_unset_required_fields(): diff --git a/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/rest.py b/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/rest.py old mode 100755 new mode 100644 index 1d4a69adf0..c3b55e5a9b --- a/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/rest.py +++ b/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/rest.py @@ -31,11 +31,13 @@ from google.iam.v1 import policy_pb2 # type: ignore from google.cloud.location import locations_pb2 # type: ignore -from requests import __version__ as requests_version import dataclasses from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union +from urllib.parse import urlencode import warnings +from requests import __version__ as requests_version + from google.cloud.eventarc_v1.types import channel from google.cloud.eventarc_v1.types import channel_connection @@ -1235,11 +1237,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -1359,11 +1368,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -1483,11 +1499,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -1607,11 +1630,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -1728,11 +1758,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -1849,11 +1886,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -1970,11 +2014,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -2098,11 +2149,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -2225,11 +2283,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -2353,11 +2418,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -2475,11 +2547,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -2597,11 +2676,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -2719,11 +2805,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -2839,11 +2932,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -2959,11 +3059,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -3079,11 +3186,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -3203,11 +3317,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -3335,11 +3456,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -3607,11 +3735,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -3725,11 +3860,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -3843,11 +3985,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -3961,11 +4110,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -4082,11 +4238,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -4203,11 +4366,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -4298,11 +4468,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -4390,11 +4567,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -4508,11 +4692,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response diff --git a/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py b/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py old mode 100755 new mode 100644 index 88e728b9ca..fb3fb4a9d2 --- a/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py +++ b/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py @@ -24,6 +24,8 @@ import grpc from grpc.experimental import aio from collections.abc import Iterable, AsyncIterable +import urllib.parse + from google.protobuf import json_format import json import math @@ -52,6 +54,7 @@ from google.api_core import operation_async # type: ignore from google.api_core import operations_v1 from google.api_core import path_template +from google.api_core import rest_helpers from google.api_core import retry as retries from google.auth import credentials as ga_credentials from google.auth.exceptions import MutualTLSChannelError @@ -7592,6 +7595,52 @@ async def test_update_google_channel_config_flattened_error_async(): ) +def test_get_trigger_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.EventarcRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.get_trigger.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'get') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_get_trigger_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -7693,8 +7742,12 @@ def test_get_trigger_rest_required_fields(request_type=eventarc.GetTriggerReques expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_get_trigger_rest_unset_required_fields(): @@ -7758,6 +7811,52 @@ def test_get_trigger_rest_flattened_error(transport: str = 'rest'): ) +def test_list_triggers_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.EventarcRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.list_triggers.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'get') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_list_triggers_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -7861,8 +7960,12 @@ def test_list_triggers_rest_required_fields(request_type=eventarc.ListTriggersRe expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_list_triggers_rest_unset_required_fields(): @@ -7988,6 +8091,52 @@ def test_list_triggers_rest_pager(transport: str = 'rest'): assert page_.raw_page.next_page_token == token +def test_create_trigger_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.EventarcRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.create_trigger.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'post', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'post') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_create_trigger_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -8115,8 +8264,12 @@ def test_create_trigger_rest_required_fields(request_type=eventarc.CreateTrigger str(False).lower(), ), ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_create_trigger_rest_unset_required_fields(): @@ -8182,6 +8335,52 @@ def test_create_trigger_rest_flattened_error(transport: str = 'rest'): ) +def test_update_trigger_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.EventarcRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.update_trigger.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'patch', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'patch') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_update_trigger_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -8294,8 +8493,12 @@ def test_update_trigger_rest_required_fields(request_type=eventarc.UpdateTrigger str(False).lower(), ), ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_update_trigger_rest_unset_required_fields(): @@ -8361,6 +8564,52 @@ def test_update_trigger_rest_flattened_error(transport: str = 'rest'): ) +def test_delete_trigger_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.EventarcRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.delete_trigger.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'delete', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'delete') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_delete_trigger_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -8476,8 +8725,12 @@ def test_delete_trigger_rest_required_fields(request_type=eventarc.DeleteTrigger str(False).lower(), ), ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_delete_trigger_rest_unset_required_fields(): @@ -8541,6 +8794,52 @@ def test_delete_trigger_rest_flattened_error(transport: str = 'rest'): ) +def test_get_channel_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.EventarcRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.get_channel.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'get') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_get_channel_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -8642,8 +8941,12 @@ def test_get_channel_rest_required_fields(request_type=eventarc.GetChannelReques expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_get_channel_rest_unset_required_fields(): @@ -8707,6 +9010,52 @@ def test_get_channel_rest_flattened_error(transport: str = 'rest'): ) +def test_list_channels_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.EventarcRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.list_channels.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'get') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_list_channels_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -8810,8 +9159,12 @@ def test_list_channels_rest_required_fields(request_type=eventarc.ListChannelsRe expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_list_channels_rest_unset_required_fields(): @@ -8937,6 +9290,52 @@ def test_list_channels_rest_pager(transport: str = 'rest'): assert page_.raw_page.next_page_token == token +def test_create_channel_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.EventarcRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.create_channel_.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'post', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'post') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_create_channel_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -9064,8 +9463,12 @@ def test_create_channel_rest_required_fields(request_type=eventarc.CreateChannel str(False).lower(), ), ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_create_channel_rest_unset_required_fields(): @@ -9131,6 +9534,52 @@ def test_create_channel_rest_flattened_error(transport: str = 'rest'): ) +def test_update_channel_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.EventarcRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.update_channel.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'patch', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'patch') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_update_channel_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -9243,8 +9692,12 @@ def test_update_channel_rest_required_fields(request_type=eventarc.UpdateChannel str(False).lower(), ), ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_update_channel_rest_unset_required_fields(): @@ -9308,6 +9761,52 @@ def test_update_channel_rest_flattened_error(transport: str = 'rest'): ) +def test_delete_channel_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.EventarcRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.delete_channel.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'delete', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'delete') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_delete_channel_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -9423,8 +9922,12 @@ def test_delete_channel_rest_required_fields(request_type=eventarc.DeleteChannel str(False).lower(), ), ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_delete_channel_rest_unset_required_fields(): @@ -9486,6 +9989,52 @@ def test_delete_channel_rest_flattened_error(transport: str = 'rest'): ) +def test_get_provider_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.EventarcRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.get_provider.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'get') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_get_provider_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -9587,8 +10136,12 @@ def test_get_provider_rest_required_fields(request_type=eventarc.GetProviderRequ expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_get_provider_rest_unset_required_fields(): @@ -9652,6 +10205,52 @@ def test_get_provider_rest_flattened_error(transport: str = 'rest'): ) +def test_list_providers_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.EventarcRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.list_providers.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'get') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_list_providers_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -9755,8 +10354,12 @@ def test_list_providers_rest_required_fields(request_type=eventarc.ListProviders expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_list_providers_rest_unset_required_fields(): @@ -9882,6 +10485,52 @@ def test_list_providers_rest_pager(transport: str = 'rest'): assert page_.raw_page.next_page_token == token +def test_get_channel_connection_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.EventarcRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.get_channel_connection.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'get') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_get_channel_connection_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -9983,8 +10632,12 @@ def test_get_channel_connection_rest_required_fields(request_type=eventarc.GetCh expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_get_channel_connection_rest_unset_required_fields(): @@ -10048,6 +10701,52 @@ def test_get_channel_connection_rest_flattened_error(transport: str = 'rest'): ) +def test_list_channel_connections_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.EventarcRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.list_channel_connections.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'get') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_list_channel_connections_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -10151,8 +10850,12 @@ def test_list_channel_connections_rest_required_fields(request_type=eventarc.Lis expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_list_channel_connections_rest_unset_required_fields(): @@ -10278,6 +10981,52 @@ def test_list_channel_connections_rest_pager(transport: str = 'rest'): assert page_.raw_page.next_page_token == token +def test_create_channel_connection_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.EventarcRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.create_channel_connection.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'post', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'post') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_create_channel_connection_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -10394,8 +11143,12 @@ def test_create_channel_connection_rest_required_fields(request_type=eventarc.Cr "", ), ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_create_channel_connection_rest_unset_required_fields(): @@ -10461,6 +11214,52 @@ def test_create_channel_connection_rest_flattened_error(transport: str = 'rest') ) +def test_delete_channel_connection_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.EventarcRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.delete_channel_connection.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'delete', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'delete') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_delete_channel_connection_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -10563,8 +11362,12 @@ def test_delete_channel_connection_rest_required_fields(request_type=eventarc.De expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_delete_channel_connection_rest_unset_required_fields(): @@ -10626,6 +11429,52 @@ def test_delete_channel_connection_rest_flattened_error(transport: str = 'rest') ) +def test_get_google_channel_config_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.EventarcRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.get_google_channel_config.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'get') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_get_google_channel_config_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -10727,8 +11576,12 @@ def test_get_google_channel_config_rest_required_fields(request_type=eventarc.Ge expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_get_google_channel_config_rest_unset_required_fields(): @@ -10792,6 +11645,52 @@ def test_get_google_channel_config_rest_flattened_error(transport: str = 'rest') ) +def test_update_google_channel_config_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.EventarcRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.update_google_channel_config.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'patch', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'patch') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_update_google_channel_config_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -10891,8 +11790,12 @@ def test_update_google_channel_config_rest_required_fields(request_type=eventarc expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_update_google_channel_config_rest_unset_required_fields(): @@ -17237,6 +18140,314 @@ async def test_test_iam_permissions_from_dict_async(): call.assert_called() +def test_list_operations_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string for mixin methods. + transport = transports.EventarcRestTransport(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.list_operations.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test/operations', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + +def test_get_operation_rest_url_query_params_encoding(): + transport = transports.EventarcRestTransport(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.get_operation.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test/operations/op1', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + +def test_delete_operation_rest_url_query_params_encoding(): + transport = transports.EventarcRestTransport(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.delete_operation.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.delete.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test/operations/op1', + 'method': 'delete', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.delete.called + call_url = mock_session.delete.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + +def test_cancel_operation_rest_url_query_params_encoding(): + transport = transports.EventarcRestTransport(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.cancel_operation.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.post.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test/operations/op1:cancel', + 'method': 'post', + 'body': {}, + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + body={}, + ) + + assert mock_session.post.called + call_url = mock_session.post.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + +def test_list_locations_rest_url_query_params_encoding(): + transport = transports.EventarcRestTransport(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.list_locations.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/projects/p1/locations', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + +def test_get_location_rest_url_query_params_encoding(): + transport = transports.EventarcRestTransport(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.get_location.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/projects/p1/locations/l1', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + +def test_set_iam_policy_rest_url_query_params_encoding(): + transport = transports.EventarcRestTransport(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.set_iam_policy.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.post.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/resource:setIamPolicy', + 'method': 'post', + 'body': {}, + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + body={}, + ) + + assert mock_session.post.called + call_url = mock_session.post.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + +def test_get_iam_policy_rest_url_query_params_encoding(): + transport = transports.EventarcRestTransport(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.get_iam_policy.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/resource:getIamPolicy', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + +def test_test_iam_permissions_rest_url_query_params_encoding(): + transport = transports.EventarcRestTransport(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.test_iam_permissions.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.post.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/resource:testIamPermissions', + 'method': 'post', + 'body': {}, + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + body={}, + ) + + assert mock_session.post.called + call_url = mock_session.post.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_transport_close_grpc(): client = EventarcClient( credentials=ga_credentials.AnonymousCredentials(), diff --git a/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py b/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py old mode 100755 new mode 100644 index 9df459055f..18ada598a2 --- a/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py +++ b/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py @@ -46,6 +46,7 @@ from google.api_core import operation_async # type: ignore from google.api_core import operations_v1 from google.api_core import path_template +from google.api_core import rest_helpers from google.api_core import retry as retries from google.auth import credentials as ga_credentials from google.auth.exceptions import MutualTLSChannelError diff --git a/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py b/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py old mode 100755 new mode 100644 index 614126cfdb..1dce1bdcc7 --- a/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py +++ b/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py @@ -43,6 +43,7 @@ from google.api_core import grpc_helpers from google.api_core import grpc_helpers_async from google.api_core import path_template +from google.api_core import rest_helpers from google.api_core import retry as retries from google.auth import credentials as ga_credentials from google.auth.exceptions import MutualTLSChannelError diff --git a/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py b/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py old mode 100755 new mode 100644 index 027c8ab372..c59e55a178 --- a/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py +++ b/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py @@ -46,6 +46,7 @@ from google.api_core import grpc_helpers from google.api_core import grpc_helpers_async from google.api_core import path_template +from google.api_core import rest_helpers from google.api_core import retry as retries from google.auth import credentials as ga_credentials from google.auth.exceptions import MutualTLSChannelError diff --git a/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py b/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py old mode 100755 new mode 100644 index ca28dea89c..a03bf61257 --- a/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py +++ b/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py @@ -46,6 +46,7 @@ from google.api_core import operation_async # type: ignore from google.api_core import operations_v1 from google.api_core import path_template +from google.api_core import rest_helpers from google.api_core import retry as retries from google.auth import credentials as ga_credentials from google.auth.exceptions import MutualTLSChannelError diff --git a/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py b/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py old mode 100755 new mode 100644 index 614126cfdb..1dce1bdcc7 --- a/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py +++ b/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py @@ -43,6 +43,7 @@ from google.api_core import grpc_helpers from google.api_core import grpc_helpers_async from google.api_core import path_template +from google.api_core import rest_helpers from google.api_core import retry as retries from google.auth import credentials as ga_credentials from google.auth.exceptions import MutualTLSChannelError diff --git a/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py b/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py old mode 100755 new mode 100644 index d843efe3c1..98f115cfd4 --- a/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py +++ b/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py @@ -46,6 +46,7 @@ from google.api_core import grpc_helpers from google.api_core import grpc_helpers_async from google.api_core import path_template +from google.api_core import rest_helpers from google.api_core import retry as retries from google.auth import credentials as ga_credentials from google.auth.exceptions import MutualTLSChannelError diff --git a/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest.py b/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest.py old mode 100755 new mode 100644 index e9a1c7a484..81ccab462e --- a/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest.py +++ b/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest.py @@ -29,11 +29,13 @@ from google.api_core import operations_v1 from google.cloud.location import locations_pb2 # type: ignore -from requests import __version__ as requests_version import dataclasses from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union +from urllib.parse import urlencode import warnings +from requests import __version__ as requests_version + from google.cloud.redis_v1.types import cloud_redis from google.longrunning import operations_pb2 # type: ignore @@ -901,11 +903,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -1025,11 +1034,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -1146,11 +1162,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -1270,11 +1293,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -1394,11 +1424,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -1514,11 +1551,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -1634,11 +1678,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -1758,11 +1809,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -1880,11 +1938,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -2004,11 +2069,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -2128,11 +2200,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -2344,11 +2423,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -2462,11 +2548,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -2580,11 +2673,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -2672,11 +2772,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -2764,11 +2871,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -2882,11 +2996,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -3000,11 +3121,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response 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 old mode 100755 new mode 100644 index 832be441c1..61c4efcb53 --- 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 @@ -38,9 +38,10 @@ from google.api_core import operations_v1 from google.cloud.location import locations_pb2 # type: ignore -import json # type: ignore import dataclasses +import json # type: ignore from typing import Any, Dict, List, Callable, Tuple, Optional, Sequence, Union +from urllib.parse import urlencode from google.cloud.redis_v1.types import cloud_redis @@ -930,11 +931,18 @@ async def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -1060,11 +1068,18 @@ async def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -1187,11 +1202,18 @@ async def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -1317,11 +1339,18 @@ async def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -1447,11 +1476,18 @@ async def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -1571,11 +1607,18 @@ async def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -1695,11 +1738,18 @@ async def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -1825,11 +1875,18 @@ async def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -1951,11 +2008,18 @@ async def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -2081,11 +2145,18 @@ async def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -2211,11 +2282,18 @@ async def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -2467,11 +2545,18 @@ async def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -2589,11 +2674,18 @@ async def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -2711,11 +2803,18 @@ async def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -2807,11 +2906,18 @@ async def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -2903,11 +3009,18 @@ async def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -3025,11 +3138,18 @@ async def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -3147,11 +3267,18 @@ async def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response 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 old mode 100755 new mode 100644 index ae66b5a92c..b628a23545 --- 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 @@ -24,6 +24,8 @@ import grpc from grpc.experimental import aio from collections.abc import Iterable, AsyncIterable +import urllib.parse + from google.protobuf import json_format import json import math @@ -59,6 +61,7 @@ from google.api_core import operation_async # type: ignore from google.api_core import operations_v1 from google.api_core import path_template +from google.api_core import rest_helpers from google.api_core import retry as retries from google.auth import credentials as ga_credentials from google.auth.exceptions import MutualTLSChannelError @@ -4830,6 +4833,97 @@ async def test_reschedule_maintenance_flattened_error_async(): ) +def test_list_instances_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.CloudRedisRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.list_instances.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'get') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + +@pytest.mark.asyncio +async def test_list_instances_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string for the async REST transport. + transport = transports.AsyncCloudRedisRestTransport(credentials=async_anonymous_credentials()) + method_class = transport.list_instances.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'get', + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + session_method = getattr(mock_session, 'get') + assert session_method.called + + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_list_instances_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -4933,8 +5027,12 @@ def test_list_instances_rest_required_fields(request_type=cloud_redis.ListInstan expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_list_instances_rest_unset_required_fields(): @@ -5060,6 +5158,97 @@ def test_list_instances_rest_pager(transport: str = 'rest'): assert page_.raw_page.next_page_token == token +def test_get_instance_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.CloudRedisRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.get_instance.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'get') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + +@pytest.mark.asyncio +async def test_get_instance_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string for the async REST transport. + transport = transports.AsyncCloudRedisRestTransport(credentials=async_anonymous_credentials()) + method_class = transport.get_instance.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'get', + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + session_method = getattr(mock_session, 'get') + assert session_method.called + + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_get_instance_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -5161,8 +5350,12 @@ def test_get_instance_rest_required_fields(request_type=cloud_redis.GetInstanceR expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_get_instance_rest_unset_required_fields(): @@ -5226,6 +5419,97 @@ def test_get_instance_rest_flattened_error(transport: str = 'rest'): ) +def test_get_instance_auth_string_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.CloudRedisRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.get_instance_auth_string.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'get') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + +@pytest.mark.asyncio +async def test_get_instance_auth_string_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string for the async REST transport. + transport = transports.AsyncCloudRedisRestTransport(credentials=async_anonymous_credentials()) + method_class = transport.get_instance_auth_string.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'get', + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + session_method = getattr(mock_session, 'get') + assert session_method.called + + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_get_instance_auth_string_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -5327,8 +5611,12 @@ def test_get_instance_auth_string_rest_required_fields(request_type=cloud_redis. expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_get_instance_auth_string_rest_unset_required_fields(): @@ -5392,6 +5680,97 @@ def test_get_instance_auth_string_rest_flattened_error(transport: str = 'rest'): ) +def test_create_instance_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.CloudRedisRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.create_instance.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'post', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'post') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + +@pytest.mark.asyncio +async def test_create_instance_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string for the async REST transport. + transport = transports.AsyncCloudRedisRestTransport(credentials=async_anonymous_credentials()) + method_class = transport.create_instance.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'post', + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + session_method = getattr(mock_session, 'post') + assert session_method.called + + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_create_instance_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -5508,8 +5887,12 @@ def test_create_instance_rest_required_fields(request_type=cloud_redis.CreateIns "", ), ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_create_instance_rest_unset_required_fields(): @@ -5575,6 +5958,97 @@ def test_create_instance_rest_flattened_error(transport: str = 'rest'): ) +def test_update_instance_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.CloudRedisRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.update_instance.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'patch', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'patch') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + +@pytest.mark.asyncio +async def test_update_instance_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string for the async REST transport. + transport = transports.AsyncCloudRedisRestTransport(credentials=async_anonymous_credentials()) + method_class = transport.update_instance.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'patch', + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + session_method = getattr(mock_session, 'patch') + assert session_method.called + + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_update_instance_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -5675,8 +6149,12 @@ def test_update_instance_rest_required_fields(request_type=cloud_redis.UpdateIns expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_update_instance_rest_unset_required_fields(): @@ -5740,6 +6218,97 @@ def test_update_instance_rest_flattened_error(transport: str = 'rest'): ) +def test_upgrade_instance_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.CloudRedisRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.upgrade_instance.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'post', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'post') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + +@pytest.mark.asyncio +async def test_upgrade_instance_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string for the async REST transport. + transport = transports.AsyncCloudRedisRestTransport(credentials=async_anonymous_credentials()) + method_class = transport.upgrade_instance.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'post', + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + session_method = getattr(mock_session, 'post') + assert session_method.called + + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_upgrade_instance_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -5847,8 +6416,12 @@ def test_upgrade_instance_rest_required_fields(request_type=cloud_redis.UpgradeI expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_upgrade_instance_rest_unset_required_fields(): @@ -5912,6 +6485,97 @@ def test_upgrade_instance_rest_flattened_error(transport: str = 'rest'): ) +def test_import_instance_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.CloudRedisRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.import_instance.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'post', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'post') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + +@pytest.mark.asyncio +async def test_import_instance_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string for the async REST transport. + transport = transports.AsyncCloudRedisRestTransport(credentials=async_anonymous_credentials()) + method_class = transport.import_instance.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'post', + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + session_method = getattr(mock_session, 'post') + assert session_method.called + + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_import_instance_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -6015,8 +6679,12 @@ def test_import_instance_rest_required_fields(request_type=cloud_redis.ImportIns expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_import_instance_rest_unset_required_fields(): @@ -6080,6 +6748,97 @@ def test_import_instance_rest_flattened_error(transport: str = 'rest'): ) +def test_export_instance_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.CloudRedisRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.export_instance.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'post', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'post') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + +@pytest.mark.asyncio +async def test_export_instance_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string for the async REST transport. + transport = transports.AsyncCloudRedisRestTransport(credentials=async_anonymous_credentials()) + method_class = transport.export_instance.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'post', + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + session_method = getattr(mock_session, 'post') + assert session_method.called + + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_export_instance_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -6183,8 +6942,12 @@ def test_export_instance_rest_required_fields(request_type=cloud_redis.ExportIns expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_export_instance_rest_unset_required_fields(): @@ -6248,6 +7011,97 @@ def test_export_instance_rest_flattened_error(transport: str = 'rest'): ) +def test_failover_instance_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.CloudRedisRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.failover_instance.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'post', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'post') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + +@pytest.mark.asyncio +async def test_failover_instance_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string for the async REST transport. + transport = transports.AsyncCloudRedisRestTransport(credentials=async_anonymous_credentials()) + method_class = transport.failover_instance.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'post', + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + session_method = getattr(mock_session, 'post') + assert session_method.called + + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_failover_instance_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -6351,8 +7205,12 @@ def test_failover_instance_rest_required_fields(request_type=cloud_redis.Failove expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_failover_instance_rest_unset_required_fields(): @@ -6416,6 +7274,97 @@ def test_failover_instance_rest_flattened_error(transport: str = 'rest'): ) +def test_delete_instance_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.CloudRedisRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.delete_instance.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'delete', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'delete') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + +@pytest.mark.asyncio +async def test_delete_instance_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string for the async REST transport. + transport = transports.AsyncCloudRedisRestTransport(credentials=async_anonymous_credentials()) + method_class = transport.delete_instance.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'delete', + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + session_method = getattr(mock_session, 'delete') + assert session_method.called + + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_delete_instance_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -6518,8 +7467,12 @@ def test_delete_instance_rest_required_fields(request_type=cloud_redis.DeleteIns expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_delete_instance_rest_unset_required_fields(): @@ -6581,6 +7534,97 @@ def test_delete_instance_rest_flattened_error(transport: str = 'rest'): ) +def test_reschedule_maintenance_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.CloudRedisRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.reschedule_maintenance.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'post', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'post') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + +@pytest.mark.asyncio +async def test_reschedule_maintenance_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string for the async REST transport. + transport = transports.AsyncCloudRedisRestTransport(credentials=async_anonymous_credentials()) + method_class = transport.reschedule_maintenance.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'post', + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + session_method = getattr(mock_session, 'post') + assert session_method.called + + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_reschedule_maintenance_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -6684,8 +7728,12 @@ def test_reschedule_maintenance_rest_required_fields(request_type=cloud_redis.Re expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_reschedule_maintenance_rest_unset_required_fields(): @@ -12995,6 +14043,430 @@ async def test_get_location_from_dict_async(): call.assert_called() +def test_list_operations_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string for mixin methods. + transport = transports.CloudRedisRestTransport(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.list_operations.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test/operations', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + +def test_get_operation_rest_url_query_params_encoding(): + transport = transports.CloudRedisRestTransport(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.get_operation.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test/operations/op1', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + +def test_delete_operation_rest_url_query_params_encoding(): + transport = transports.CloudRedisRestTransport(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.delete_operation.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.delete.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test/operations/op1', + 'method': 'delete', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.delete.called + call_url = mock_session.delete.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + +def test_cancel_operation_rest_url_query_params_encoding(): + transport = transports.CloudRedisRestTransport(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.cancel_operation.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.post.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test/operations/op1:cancel', + 'method': 'post', + 'body': {}, + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + body={}, + ) + + assert mock_session.post.called + call_url = mock_session.post.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + +def test_list_locations_rest_url_query_params_encoding(): + transport = transports.CloudRedisRestTransport(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.list_locations.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/projects/p1/locations', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + +def test_get_location_rest_url_query_params_encoding(): + transport = transports.CloudRedisRestTransport(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.get_location.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/projects/p1/locations/l1', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + +@pytest.mark.asyncio +async def test_list_operations_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + transport = transports.AsyncCloudRedisRestTransport(credentials=async_anonymous_credentials()) + method_class = transport.list_operations.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test/operations', + 'method': 'get', + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + +@pytest.mark.asyncio +async def test_get_operation_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + transport = transports.AsyncCloudRedisRestTransport(credentials=async_anonymous_credentials()) + method_class = transport.get_operation.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test/operations/op1', + 'method': 'get', + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + +@pytest.mark.asyncio +async def test_delete_operation_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + transport = transports.AsyncCloudRedisRestTransport(credentials=async_anonymous_credentials()) + method_class = transport.delete_operation.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.delete.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test/operations/op1', + 'method': 'delete', + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.delete.called + call_url = mock_session.delete.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + +@pytest.mark.asyncio +async def test_cancel_operation_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + transport = transports.AsyncCloudRedisRestTransport(credentials=async_anonymous_credentials()) + method_class = transport.cancel_operation.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.post.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test/operations/op1:cancel', + 'method': 'post', + 'body': {}, + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + body={}, + ) + + assert mock_session.post.called + call_url = mock_session.post.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + +@pytest.mark.asyncio +async def test_list_locations_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + transport = transports.AsyncCloudRedisRestTransport(credentials=async_anonymous_credentials()) + method_class = transport.list_locations.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/projects/p1/locations', + 'method': 'get', + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + +@pytest.mark.asyncio +async def test_get_location_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + transport = transports.AsyncCloudRedisRestTransport(credentials=async_anonymous_credentials()) + method_class = transport.get_location.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/projects/p1/locations/l1', + 'method': 'get', + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_transport_close_grpc(): client = CloudRedisClient( credentials=ga_credentials.AnonymousCredentials(), diff --git a/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest.py b/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest.py old mode 100755 new mode 100644 index 7e43d25df4..02a5d6f60e --- a/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest.py +++ b/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest.py @@ -29,11 +29,13 @@ from google.api_core import operations_v1 from google.cloud.location import locations_pb2 # type: ignore -from requests import __version__ as requests_version import dataclasses from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union +from urllib.parse import urlencode import warnings +from requests import __version__ as requests_version + from google.cloud.redis_v1.types import cloud_redis from google.longrunning import operations_pb2 # type: ignore @@ -637,11 +639,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -761,11 +770,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -882,11 +898,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -1002,11 +1025,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -1124,11 +1154,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -1292,11 +1329,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -1410,11 +1454,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -1528,11 +1579,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -1620,11 +1678,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -1712,11 +1777,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -1830,11 +1902,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -1948,11 +2027,18 @@ def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response diff --git a/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py b/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py old mode 100755 new mode 100644 index fdb993eada..fe917e762f --- a/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py +++ b/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py @@ -38,9 +38,10 @@ from google.api_core import operations_v1 from google.cloud.location import locations_pb2 # type: ignore -import json # type: ignore import dataclasses +import json # type: ignore from typing import Any, Dict, List, Callable, Tuple, Optional, Sequence, Union +from urllib.parse import urlencode from google.cloud.redis_v1.types import cloud_redis @@ -636,11 +637,18 @@ async def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -766,11 +774,18 @@ async def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -893,11 +908,18 @@ async def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -1017,11 +1039,18 @@ async def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -1143,11 +1172,18 @@ async def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response @@ -1363,11 +1399,18 @@ async def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -1485,11 +1528,18 @@ async def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -1607,11 +1657,18 @@ async def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -1703,11 +1760,18 @@ async def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -1799,11 +1863,18 @@ async def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -1921,11 +1992,18 @@ async def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), ) return response @@ -2043,11 +2121,18 @@ async def _get_response( method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' + # Build query string manually to avoid URL-encoding special characters like '$'. + # The `requests` library encodes '$' as '%24' when using the `params` argument, + # which causes API errors for parameters like '$alt'. See: + # https://github.com/googleapis/gapic-generator-python/issues/2514 + _query_params = rest_helpers.flatten_query_params(query_params, strict=True) + _request_url = "{host}{uri}".format(host=host, uri=uri) + if _query_params: + _request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), + _request_url, timeout=timeout, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), data=body, ) return response diff --git a/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py b/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py old mode 100755 new mode 100644 index 44403c6627..77598f9439 --- a/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py +++ b/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py @@ -24,6 +24,8 @@ import grpc from grpc.experimental import aio from collections.abc import Iterable, AsyncIterable +import urllib.parse + from google.protobuf import json_format import json import math @@ -59,6 +61,7 @@ from google.api_core import operation_async # type: ignore from google.api_core import operations_v1 from google.api_core import path_template +from google.api_core import rest_helpers from google.api_core import retry as retries from google.auth import credentials as ga_credentials from google.auth.exceptions import MutualTLSChannelError @@ -2878,6 +2881,97 @@ async def test_delete_instance_flattened_error_async(): ) +def test_list_instances_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.CloudRedisRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.list_instances.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'get') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + +@pytest.mark.asyncio +async def test_list_instances_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string for the async REST transport. + transport = transports.AsyncCloudRedisRestTransport(credentials=async_anonymous_credentials()) + method_class = transport.list_instances.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'get', + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + session_method = getattr(mock_session, 'get') + assert session_method.called + + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_list_instances_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -2981,8 +3075,12 @@ def test_list_instances_rest_required_fields(request_type=cloud_redis.ListInstan expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_list_instances_rest_unset_required_fields(): @@ -3108,6 +3206,97 @@ def test_list_instances_rest_pager(transport: str = 'rest'): assert page_.raw_page.next_page_token == token +def test_get_instance_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.CloudRedisRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.get_instance.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'get') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + +@pytest.mark.asyncio +async def test_get_instance_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string for the async REST transport. + transport = transports.AsyncCloudRedisRestTransport(credentials=async_anonymous_credentials()) + method_class = transport.get_instance.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'get', + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + session_method = getattr(mock_session, 'get') + assert session_method.called + + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_get_instance_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -3209,8 +3398,12 @@ def test_get_instance_rest_required_fields(request_type=cloud_redis.GetInstanceR expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_get_instance_rest_unset_required_fields(): @@ -3274,6 +3467,97 @@ def test_get_instance_rest_flattened_error(transport: str = 'rest'): ) +def test_create_instance_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.CloudRedisRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.create_instance.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'post', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'post') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + +@pytest.mark.asyncio +async def test_create_instance_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string for the async REST transport. + transport = transports.AsyncCloudRedisRestTransport(credentials=async_anonymous_credentials()) + method_class = transport.create_instance.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'post', + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + session_method = getattr(mock_session, 'post') + assert session_method.called + + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_create_instance_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -3390,8 +3674,12 @@ def test_create_instance_rest_required_fields(request_type=cloud_redis.CreateIns "", ), ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_create_instance_rest_unset_required_fields(): @@ -3457,6 +3745,97 @@ def test_create_instance_rest_flattened_error(transport: str = 'rest'): ) +def test_update_instance_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.CloudRedisRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.update_instance.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'patch', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'patch') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + +@pytest.mark.asyncio +async def test_update_instance_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string for the async REST transport. + transport = transports.AsyncCloudRedisRestTransport(credentials=async_anonymous_credentials()) + method_class = transport.update_instance.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'patch', + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + session_method = getattr(mock_session, 'patch') + assert session_method.called + + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_update_instance_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -3557,8 +3936,12 @@ def test_update_instance_rest_required_fields(request_type=cloud_redis.UpdateIns expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_update_instance_rest_unset_required_fields(): @@ -3622,6 +4005,97 @@ def test_update_instance_rest_flattened_error(transport: str = 'rest'): ) +def test_delete_instance_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string. This tests the urlencode call with safe="$". + transport = transports.CloudRedisRestTransport(credentials=ga_credentials.AnonymousCredentials) + method_class = transport.delete_instance.__class__ + # Get the _get_response static method from the method class + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + # Mock flatten_query_params to return query params that include '$' character + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'delete', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + # Verify the session method was called with the URL containing query params + session_method = getattr(mock_session, 'delete') + assert session_method.called + + # The URL should contain '$alt' (not '%24alt') because safe="$" is used + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + +@pytest.mark.asyncio +async def test_delete_instance_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string for the async REST transport. + transport = transports.AsyncCloudRedisRestTransport(credentials=async_anonymous_credentials()) + method_class = transport.delete_instance.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + mock_session.post.return_value = mock_response + mock_session.put.return_value = mock_response + mock_session.patch.return_value = mock_response + mock_session.delete.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test', + 'method': 'delete', + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + session_method = getattr(mock_session, 'delete') + assert session_method.called + + call_url = session_method.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_delete_instance_rest_use_cached_wrapped_rpc(): # Clients should use _prep_wrapped_messages to create cached wrapped rpcs, # instead of constructing them on each call @@ -3724,8 +4198,12 @@ def test_delete_instance_rest_required_fields(request_type=cloud_redis.DeleteIns expected_params = [ ] - actual_params = req.call_args.kwargs['params'] - assert expected_params == actual_params + # Verify query params are correctly included in the URL + # Session.request is called as request(method, url, ...), so url is args[1] + actual_url = req.call_args.args[1] + parsed_url = urllib.parse.urlparse(actual_url) + actual_params = urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True) + assert set(expected_params).issubset(set(actual_params)) def test_delete_instance_rest_unset_required_fields(): @@ -8161,6 +8639,430 @@ async def test_get_location_from_dict_async(): call.assert_called() +def test_list_operations_rest_url_query_params_encoding(): + # Verify that special characters like '$' are correctly preserved (not URL-encoded) + # when building the URL query string for mixin methods. + transport = transports.CloudRedisRestTransport(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.list_operations.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test/operations', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + +def test_get_operation_rest_url_query_params_encoding(): + transport = transports.CloudRedisRestTransport(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.get_operation.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test/operations/op1', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + +def test_delete_operation_rest_url_query_params_encoding(): + transport = transports.CloudRedisRestTransport(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.delete_operation.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.delete.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test/operations/op1', + 'method': 'delete', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.delete.called + call_url = mock_session.delete.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + +def test_cancel_operation_rest_url_query_params_encoding(): + transport = transports.CloudRedisRestTransport(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.cancel_operation.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.post.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test/operations/op1:cancel', + 'method': 'post', + 'body': {}, + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + body={}, + ) + + assert mock_session.post.called + call_url = mock_session.post.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + +def test_list_locations_rest_url_query_params_encoding(): + transport = transports.CloudRedisRestTransport(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.list_locations.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/projects/p1/locations', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + +def test_get_location_rest_url_query_params_encoding(): + transport = transports.CloudRedisRestTransport(credentials=ga_credentials.AnonymousCredentials()) + method_class = transport.get_location.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.Mock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/projects/p1/locations/l1', + 'method': 'get', + } + + get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + +@pytest.mark.asyncio +async def test_list_operations_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + transport = transports.AsyncCloudRedisRestTransport(credentials=async_anonymous_credentials()) + method_class = transport.list_operations.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test/operations', + 'method': 'get', + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + +@pytest.mark.asyncio +async def test_get_operation_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + transport = transports.AsyncCloudRedisRestTransport(credentials=async_anonymous_credentials()) + method_class = transport.get_operation.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test/operations/op1', + 'method': 'get', + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + +@pytest.mark.asyncio +async def test_delete_operation_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + transport = transports.AsyncCloudRedisRestTransport(credentials=async_anonymous_credentials()) + method_class = transport.delete_operation.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.delete.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test/operations/op1', + 'method': 'delete', + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.delete.called + call_url = mock_session.delete.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + +@pytest.mark.asyncio +async def test_cancel_operation_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + transport = transports.AsyncCloudRedisRestTransport(credentials=async_anonymous_credentials()) + method_class = transport.cancel_operation.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.post.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/test/operations/op1:cancel', + 'method': 'post', + 'body': {}, + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + body={}, + ) + + assert mock_session.post.called + call_url = mock_session.post.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + +@pytest.mark.asyncio +async def test_list_locations_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + transport = transports.AsyncCloudRedisRestTransport(credentials=async_anonymous_credentials()) + method_class = transport.list_locations.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/projects/p1/locations', + 'method': 'get', + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + +@pytest.mark.asyncio +async def test_get_location_rest_asyncio_url_query_params_encoding(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + transport = transports.AsyncCloudRedisRestTransport(credentials=async_anonymous_credentials()) + method_class = transport.get_location.__class__ + get_response_fn = method_class._get_response + + mock_session = mock.AsyncMock() + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_session.get.return_value = mock_response + + with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten: + mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')] + + transcoded_request = { + 'uri': '/v1/projects/p1/locations/l1', + 'method': 'get', + } + + await get_response_fn( + host='https://example.com', + metadata=[], + query_params={}, + session=mock_session, + timeout=None, + transcoded_request=transcoded_request, + ) + + assert mock_session.get.called + call_url = mock_session.get.call_args.args[0] + assert '$alt=json' in call_url + assert '%24alt' not in call_url + assert 'foo=bar' in call_url + + def test_transport_close_grpc(): client = CloudRedisClient( credentials=ga_credentials.AnonymousCredentials(),