diff --git a/end_to_end_tests/functional_tests/generated_code_execution/test_path_parameters.py b/end_to_end_tests/functional_tests/generated_code_execution/test_path_parameters.py new file mode 100644 index 000000000..694788d96 --- /dev/null +++ b/end_to_end_tests/functional_tests/generated_code_execution/test_path_parameters.py @@ -0,0 +1,151 @@ +from unittest.mock import MagicMock + +import httpx +import pytest + +from end_to_end_tests.functional_tests.helpers import ( + with_generated_client_fixture, + with_generated_code_import, +) + + +@with_generated_client_fixture( +""" +paths: + "/items/{item_id}/details/{detail_id}": + get: + operationId: getItemDetail + parameters: + - name: item_id + in: path + required: true + schema: + type: string + - name: detail_id + in: path + required: true + schema: + type: string + responses: + "200": + description: Success + content: + application/json: + schema: + type: object + properties: + id: + type: string +""") +@with_generated_code_import(".api.default.get_item_detail.sync_detailed") +@with_generated_code_import(".client.Client") +class TestPathParameterEncoding: + """Test that path parameters are properly URL-encoded""" + + def test_path_params_with_normal_chars_work(self, sync_detailed, Client): + """Test that normal alphanumeric path parameters still work correctly""" + mock_httpx_client = MagicMock(spec=httpx.Client) + mock_response = MagicMock(spec=httpx.Response) + mock_response.status_code = 200 + mock_response.json.return_value = {"id": "test"} + mock_response.content = b'{"id": "test"}' + mock_response.headers = {} + mock_httpx_client.request.return_value = mock_response + + client = Client(base_url="https://api.example.com") + client.set_httpx_client(mock_httpx_client) + + sync_detailed( + item_id="item123", + detail_id="detail456", + client=client, + ) + + mock_httpx_client.request.assert_called_once() + call_kwargs = mock_httpx_client.request.call_args[1] + + # Normal characters should remain unchanged + expected_url = "/items/item123/details/detail456" + assert call_kwargs["url"] == expected_url + + def test_path_params_with_reserved_chars_are_encoded(self, sync_detailed, Client): + """Test that path parameters with reserved characters are properly URL-encoded""" + # Create a mock httpx client + mock_httpx_client = MagicMock(spec=httpx.Client) + mock_response = MagicMock(spec=httpx.Response) + mock_response.status_code = 200 + mock_response.json.return_value = {"id": "test"} + mock_response.content = b'{"id": "test"}' + mock_response.headers = {} + mock_httpx_client.request.return_value = mock_response + + # Create a client with the mock httpx client + client = Client(base_url="https://api.example.com") + client.set_httpx_client(mock_httpx_client) + + # Call the endpoint with path parameters containing reserved characters + sync_detailed( + item_id="item/with/slashes", + detail_id="detail?with=query&chars", + client=client, + ) + + # Verify the request was made with properly encoded URL + mock_httpx_client.request.assert_called_once() + call_kwargs = mock_httpx_client.request.call_args[1] + + # The URL should have encoded slashes and query characters + expected_url = "/items/item%2Fwith%2Fslashes/details/detail%3Fwith%3Dquery%26chars" + assert call_kwargs["url"] == expected_url + + def test_path_params_with_spaces_are_encoded(self, sync_detailed, Client): + """Test that path parameters with spaces are properly URL-encoded""" + mock_httpx_client = MagicMock(spec=httpx.Client) + mock_response = MagicMock(spec=httpx.Response) + mock_response.status_code = 200 + mock_response.json.return_value = {"id": "test"} + mock_response.content = b'{"id": "test"}' + mock_response.headers = {} + mock_httpx_client.request.return_value = mock_response + + client = Client(base_url="https://api.example.com") + client.set_httpx_client(mock_httpx_client) + + sync_detailed( + item_id="item with spaces", + detail_id="detail with spaces", + client=client, + ) + + mock_httpx_client.request.assert_called_once() + call_kwargs = mock_httpx_client.request.call_args[1] + + # Spaces should be encoded as %20 + expected_url = "/items/item%20with%20spaces/details/detail%20with%20spaces" + assert call_kwargs["url"] == expected_url + + def test_path_params_with_hash_are_encoded(self, sync_detailed, Client): + """Test that path parameters with hash/fragment characters are properly URL-encoded""" + mock_httpx_client = MagicMock(spec=httpx.Client) + mock_response = MagicMock(spec=httpx.Response) + mock_response.status_code = 200 + mock_response.json.return_value = {"id": "test"} + mock_response.content = b'{"id": "test"}' + mock_response.headers = {} + mock_httpx_client.request.return_value = mock_response + + client = Client(base_url="https://api.example.com") + client.set_httpx_client(mock_httpx_client) + + sync_detailed( + item_id="item#1", + detail_id="detail#id", + client=client, + ) + + mock_httpx_client.request.assert_called_once() + call_kwargs = mock_httpx_client.request.call_args[1] + + # Hash should be encoded as %23 + expected_url = "/items/item%231/details/detail%23id" + assert call_kwargs["url"] == expected_url diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/naming/hyphen_in_path.py b/end_to_end_tests/golden-record/my_test_api_client/api/naming/hyphen_in_path.py index 06cf0a0ad..a31e2e093 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/naming/hyphen_in_path.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/naming/hyphen_in_path.py @@ -1,5 +1,6 @@ from http import HTTPStatus from typing import Any +from urllib.parse import quote import httpx @@ -13,7 +14,9 @@ def _get_kwargs( ) -> dict[str, Any]: _kwargs: dict[str, Any] = { "method": "get", - "url": f"/naming/{hyphen_in_path}", + "url": "/naming/{hyphen_in_path}".format( + hyphen_in_path=quote(str(hyphen_in_path), safe=""), + ), } return _kwargs diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/parameter_references/get_parameter_references_path_param.py b/end_to_end_tests/golden-record/my_test_api_client/api/parameter_references/get_parameter_references_path_param.py index d2b6cdb49..801929db9 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/parameter_references/get_parameter_references_path_param.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/parameter_references/get_parameter_references_path_param.py @@ -1,5 +1,6 @@ from http import HTTPStatus from typing import Any +from urllib.parse import quote import httpx @@ -34,7 +35,9 @@ def _get_kwargs( _kwargs: dict[str, Any] = { "method": "get", - "url": f"/parameter-references/{path_param}", + "url": "/parameter-references/{path_param}".format( + path_param=quote(str(path_param), safe=""), + ), "params": params, "cookies": cookies, } diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/parameters/delete_common_parameters_overriding_param.py b/end_to_end_tests/golden-record/my_test_api_client/api/parameters/delete_common_parameters_overriding_param.py index a243499a5..1415a8cb4 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/parameters/delete_common_parameters_overriding_param.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/parameters/delete_common_parameters_overriding_param.py @@ -1,5 +1,6 @@ from http import HTTPStatus from typing import Any +from urllib.parse import quote import httpx @@ -21,7 +22,9 @@ def _get_kwargs( _kwargs: dict[str, Any] = { "method": "delete", - "url": f"/common_parameters_overriding/{param_path}", + "url": "/common_parameters_overriding/{param_path}".format( + param_path=quote(str(param_path), safe=""), + ), "params": params, } diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/parameters/get_common_parameters_overriding_param.py b/end_to_end_tests/golden-record/my_test_api_client/api/parameters/get_common_parameters_overriding_param.py index 38f854e2d..4f2c5056f 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/parameters/get_common_parameters_overriding_param.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/parameters/get_common_parameters_overriding_param.py @@ -1,5 +1,6 @@ from http import HTTPStatus from typing import Any +from urllib.parse import quote import httpx @@ -21,7 +22,9 @@ def _get_kwargs( _kwargs: dict[str, Any] = { "method": "get", - "url": f"/common_parameters_overriding/{param_path}", + "url": "/common_parameters_overriding/{param_path}".format( + param_path=quote(str(param_path), safe=""), + ), "params": params, } diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/parameters/get_same_name_multiple_locations_param.py b/end_to_end_tests/golden-record/my_test_api_client/api/parameters/get_same_name_multiple_locations_param.py index 3ab6c1902..dd11f68ca 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/parameters/get_same_name_multiple_locations_param.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/parameters/get_same_name_multiple_locations_param.py @@ -1,5 +1,6 @@ from http import HTTPStatus from typing import Any +from urllib.parse import quote import httpx @@ -31,7 +32,9 @@ def _get_kwargs( _kwargs: dict[str, Any] = { "method": "get", - "url": f"/same-name-multiple-locations/{param_path}", + "url": "/same-name-multiple-locations/{param_path}".format( + param_path=quote(str(param_path), safe=""), + ), "params": params, "cookies": cookies, } diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/parameters/multiple_path_parameters.py b/end_to_end_tests/golden-record/my_test_api_client/api/parameters/multiple_path_parameters.py index 5892a82de..3b11674f5 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/parameters/multiple_path_parameters.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/parameters/multiple_path_parameters.py @@ -1,5 +1,6 @@ from http import HTTPStatus from typing import Any +from urllib.parse import quote import httpx @@ -16,7 +17,12 @@ def _get_kwargs( ) -> dict[str, Any]: _kwargs: dict[str, Any] = { "method": "get", - "url": f"/multiple-path-parameters/{param4}/something/{param2}/{param1}/{param3}", + "url": "/multiple-path-parameters/{param4}/something/{param2}/{param1}/{param3}".format( + param4=quote(str(param4), safe=""), + param2=quote(str(param2), safe=""), + param1=quote(str(param1), safe=""), + param3=quote(str(param3), safe=""), + ), } return _kwargs diff --git a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/const/post_const_path.py b/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/const/post_const_path.py index fea660b2b..bf3472121 100644 --- a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/const/post_const_path.py +++ b/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/const/post_const_path.py @@ -1,5 +1,6 @@ from http import HTTPStatus from typing import Any, Literal, cast +from urllib.parse import quote import httpx @@ -28,7 +29,9 @@ def _get_kwargs( _kwargs: dict[str, Any] = { "method": "post", - "url": f"/const/{path}", + "url": "/const/{path}".format( + path=quote(str(path), safe=""), + ), "params": params, } diff --git a/openapi_python_client/templates/endpoint_module.py.jinja b/openapi_python_client/templates/endpoint_module.py.jinja index 96397c26d..27213ecf4 100644 --- a/openapi_python_client/templates/endpoint_module.py.jinja +++ b/openapi_python_client/templates/endpoint_module.py.jinja @@ -1,5 +1,6 @@ from http import HTTPStatus from typing import Any, cast +from urllib.parse import quote import httpx @@ -31,7 +32,7 @@ def _get_kwargs( {% if endpoint.path_parameters %} "url": "{{ endpoint.path }}".format( {%- for parameter in endpoint.path_parameters -%} - {{parameter.python_name}}={{parameter.python_name}}, + {{parameter.python_name}}=quote(str({{parameter.python_name}}), safe=""), {%- endfor -%} ), {% else %}