Skip to content
This repository was archived by the owner on Mar 26, 2026. It is now read-only.

Commit d35d541

Browse files
taimo3810claude
andcommitted
test: add unit tests for URL query params encoding to cover urlencode
Add test_*_rest_url_query_params_encoding() tests that verify the '$' character is preserved (not URL-encoded as '%24') when building URL query strings. This allows removing the # pragma: NO COVER markers from the urlencode calls in _shared_macros.j2 templates. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 9647046 commit d35d541

File tree

5 files changed

+97
-4
lines changed

5 files changed

+97
-4
lines changed

gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/_shared_macros.j2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ def _get_http_options():
183183
_query_params = rest_helpers.flatten_query_params(query_params, strict=True)
184184
_request_url = "{host}{uri}".format(host=host, uri=uri)
185185
if _query_params:
186-
_request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) # pragma: NO COVER
186+
_request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$"))
187187
response = {{ await_prefix }}getattr(session, method)(
188188
_request_url,
189189
timeout=timeout,

gapic/ads-templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ from google.api_core import client_options
4747
from google.api_core import exceptions as core_exceptions
4848
from google.api_core import grpc_helpers
4949
from google.api_core import path_template
50+
from google.api_core import rest_helpers
5051
from google.api_core import retry as retries
5152
{% if service.has_lro %}
5253
from google.api_core import future
@@ -1467,9 +1468,55 @@ def test_{{ method_name }}_rest_unset_required_fields():
14671468
unset_fields = transport.{{ method.transport_safe_name|snake_case }}._get_unset_required_fields({})
14681469
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 %})))
14691470

1470-
14711471
{% endif %}{# required_fields #}
14721472

1473+
1474+
def test_{{ method_name }}_rest_url_query_params_encoding():
1475+
# Verify that special characters like '$' are correctly preserved (not URL-encoded)
1476+
# when building the URL query string. This tests the urlencode call with safe="$".
1477+
transport = transports.{{ service.rest_transport_name }}(credentials=ga_credentials.AnonymousCredentials)
1478+
method_class = transport.{{ method.transport_safe_name|snake_case }}.__class__
1479+
# Get the _get_response method from the method class
1480+
get_response_fn = method_class._get_response.__func__
1481+
1482+
mock_session = mock.Mock()
1483+
mock_response = mock.Mock()
1484+
mock_response.status_code = 200
1485+
mock_session.get.return_value = mock_response
1486+
mock_session.post.return_value = mock_response
1487+
mock_session.put.return_value = mock_response
1488+
mock_session.patch.return_value = mock_response
1489+
mock_session.delete.return_value = mock_response
1490+
1491+
# Mock flatten_query_params to return query params that include '$' character
1492+
with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten:
1493+
mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')]
1494+
1495+
transcoded_request = {
1496+
'uri': '/v1/test',
1497+
'method': '{{ method.http_options[0].method }}',
1498+
}
1499+
1500+
get_response_fn(
1501+
host='https://example.com',
1502+
metadata=[],
1503+
query_params={},
1504+
session=mock_session,
1505+
timeout=None,
1506+
transcoded_request=transcoded_request,
1507+
)
1508+
1509+
# Verify the session method was called with the URL containing query params
1510+
session_method = getattr(mock_session, '{{ method.http_options[0].method }}')
1511+
assert session_method.called
1512+
1513+
# The URL should contain '$alt' (not '%24alt') because safe="$" is used
1514+
call_url = session_method.call_args.args[0]
1515+
assert '$alt=json' in call_url
1516+
assert '%24alt' not in call_url
1517+
assert 'foo=bar' in call_url
1518+
1519+
14731520
{% if not method.client_streaming %}
14741521
@pytest.mark.parametrize("null_interceptor", [True, False])
14751522
def test_{{ method_name }}_rest_interceptors(null_interceptor):

gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,15 +169,14 @@ def _get_http_options():
169169
method = transcoded_request['method']
170170
headers = dict(metadata)
171171
headers['Content-Type'] = 'application/json'
172-
# Coverage is insufficient for the `urlencode` call below. Use pragma to skip it.
173172
# Build query string manually to avoid URL-encoding special characters like '$'.
174173
# The `requests` library encodes '$' as '%24' when using the `params` argument,
175174
# which causes API errors for parameters like '$alt'. See:
176175
# https://github.com/googleapis/gapic-generator-python/issues/2514
177176
_query_params = rest_helpers.flatten_query_params(query_params, strict=True)
178177
_request_url = "{host}{uri}".format(host=host, uri=uri)
179178
if _query_params:
180-
_request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) # pragma: NO COVER
179+
_request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$"))
181180
response = {{ await_prefix }}getattr(session, method)(
182181
_request_url,
183182
timeout=timeout,

gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ from google.api_core import exceptions as core_exceptions
7474
from google.api_core import grpc_helpers
7575
from google.api_core import grpc_helpers_async
7676
from google.api_core import path_template
77+
from google.api_core import rest_helpers
7778
from google.api_core import retry as retries
7879
{% if service.has_lro or service.has_extended_lro %}
7980
from google.api_core import future

gapic/templates/tests/unit/gapic/%name_%version/%sub/test_macros.j2

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1217,6 +1217,52 @@ def test_{{ method_name }}_rest_unset_required_fields():
12171217
{% endif %}{# required_fields #}
12181218

12191219

1220+
def test_{{ method_name }}_rest_url_query_params_encoding():
1221+
# Verify that special characters like '$' are correctly preserved (not URL-encoded)
1222+
# when building the URL query string. This tests the urlencode call with safe="$".
1223+
transport = transports.{{ service.rest_transport_name }}(credentials=ga_credentials.AnonymousCredentials)
1224+
method_class = transport.{{ method.transport_safe_name|snake_case }}.__class__
1225+
# Get the _get_response method from the method class
1226+
get_response_fn = method_class._get_response.__func__
1227+
1228+
mock_session = mock.Mock()
1229+
mock_response = mock.Mock()
1230+
mock_response.status_code = 200
1231+
mock_session.get.return_value = mock_response
1232+
mock_session.post.return_value = mock_response
1233+
mock_session.put.return_value = mock_response
1234+
mock_session.patch.return_value = mock_response
1235+
mock_session.delete.return_value = mock_response
1236+
1237+
# Mock flatten_query_params to return query params that include '$' character
1238+
with mock.patch.object(rest_helpers, 'flatten_query_params') as mock_flatten:
1239+
mock_flatten.return_value = [('$alt', 'json;enum-encoding=int'), ('foo', 'bar')]
1240+
1241+
transcoded_request = {
1242+
'uri': '/v1/test',
1243+
'method': '{{ method.http_options[0].method }}',
1244+
}
1245+
1246+
get_response_fn(
1247+
host='https://example.com',
1248+
metadata=[],
1249+
query_params={},
1250+
session=mock_session,
1251+
timeout=None,
1252+
transcoded_request=transcoded_request,
1253+
)
1254+
1255+
# Verify the session method was called with the URL containing query params
1256+
session_method = getattr(mock_session, '{{ method.http_options[0].method }}')
1257+
assert session_method.called
1258+
1259+
# The URL should contain '$alt' (not '%24alt') because safe="$" is used
1260+
call_url = session_method.call_args.args[0]
1261+
assert '$alt=json' in call_url
1262+
assert '%24alt' not in call_url
1263+
assert 'foo=bar' in call_url
1264+
1265+
12201266
{% if method.flattened_fields and not method.client_streaming %}
12211267
def test_{{ method_name }}_rest_flattened():
12221268
client = {{ service.client_name }}(

0 commit comments

Comments
 (0)