Skip to content

Commit b996272

Browse files
committed
test: add coverage for URL query param forwarding in form-encoded and multipart REST requests
Verify that URL-embedded query parameters (e.g. ?token=abc) are passed via httpx params= for both application/x-www-form-urlencoded and multipart/form-data branches, keeping them on the query string rather than merging into the request body. Signed-off-by: Madhav Kandukuri <madhav165@gmail.com>
1 parent 8d05de0 commit b996272

File tree

1 file changed

+74
-0
lines changed

1 file changed

+74
-0
lines changed

tests/unit/mcpgateway/services/test_tool_service.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2284,6 +2284,80 @@ async def test_invoke_tool_rest_post_form_nested_values(self, tool_service, mock
22842284
assert data["nothing"] == ""
22852285
assert json.loads(data["nested"]) == {"key": "val"}
22862286

2287+
@pytest.mark.asyncio
2288+
async def test_invoke_tool_rest_post_form_urlencoded_with_url_query_params(self, tool_service, mock_tool, mock_global_config_obj, test_db):
2289+
"""Form-urlencoded POST with URL query params should forward them via params= (query string), not in the form body."""
2290+
mock_tool.integration_type = "REST"
2291+
mock_tool.request_type = "POST"
2292+
mock_tool.jsonpath_filter = ""
2293+
mock_tool.auth_value = None
2294+
mock_tool.url = "http://example.com/api/submit?token=abc123&version=v2"
2295+
mock_tool.headers = {"Content-Type": "application/x-www-form-urlencoded"}
2296+
2297+
setup_db_execute_mock(test_db, mock_tool, mock_global_config_obj)
2298+
2299+
mock_response = AsyncMock()
2300+
mock_response.raise_for_status = Mock()
2301+
mock_response.status_code = 200
2302+
mock_response.json = Mock(return_value={"ok": True})
2303+
tool_service._http_client.request.return_value = mock_response
2304+
2305+
mock_metrics_buffer = Mock()
2306+
mock_metrics_buffer.record_tool_metric = Mock()
2307+
with (
2308+
patch("mcpgateway.services.tool_service.metrics_buffer", mock_metrics_buffer),
2309+
patch("mcpgateway.services.tool_service.decode_auth", return_value={}),
2310+
patch("mcpgateway.services.tool_service.extract_using_jq", return_value={"ok": True}),
2311+
):
2312+
await tool_service.invoke_tool(test_db, "test_tool", {"name": "test"}, request_headers=None)
2313+
2314+
call_kwargs = tool_service._http_client.request.call_args
2315+
# Body should only contain the user-supplied payload (form-encoded)
2316+
assert call_kwargs.kwargs.get("data") == {"name": "test"}
2317+
# URL query params should be forwarded via params= (on the query string)
2318+
assert call_kwargs.kwargs.get("params") == {"token": "abc123", "version": "v2"}
2319+
# URL should have query string stripped
2320+
assert call_kwargs.args[1] == "http://example.com/api/submit"
2321+
2322+
@pytest.mark.asyncio
2323+
async def test_invoke_tool_rest_post_multipart_with_url_query_params(self, tool_service, mock_tool, mock_global_config_obj, test_db):
2324+
"""Multipart POST with URL query params should forward them via params= (query string), not in the multipart body."""
2325+
mock_tool.integration_type = "REST"
2326+
mock_tool.request_type = "POST"
2327+
mock_tool.jsonpath_filter = ""
2328+
mock_tool.auth_value = None
2329+
mock_tool.url = "http://example.com/api/upload?token=secret"
2330+
mock_tool.headers = {"Content-Type": "multipart/form-data", "X-Custom": "keep-me"}
2331+
2332+
setup_db_execute_mock(test_db, mock_tool, mock_global_config_obj)
2333+
2334+
mock_response = AsyncMock()
2335+
mock_response.raise_for_status = Mock()
2336+
mock_response.status_code = 200
2337+
mock_response.json = Mock(return_value={"uploaded": True})
2338+
tool_service._http_client.request.return_value = mock_response
2339+
2340+
mock_metrics_buffer = Mock()
2341+
mock_metrics_buffer.record_tool_metric = Mock()
2342+
with (
2343+
patch("mcpgateway.services.tool_service.metrics_buffer", mock_metrics_buffer),
2344+
patch("mcpgateway.services.tool_service.decode_auth", return_value={}),
2345+
patch("mcpgateway.services.tool_service.extract_using_jq", return_value={"uploaded": True}),
2346+
):
2347+
await tool_service.invoke_tool(test_db, "test_tool", {"file_name": "doc.pdf"}, request_headers=None)
2348+
2349+
call_kwargs = tool_service._http_client.request.call_args
2350+
# Body should only contain user-supplied payload (multipart files=)
2351+
assert call_kwargs.kwargs.get("files") == {"file_name": (None, "doc.pdf")}
2352+
# URL query params should be forwarded via params=
2353+
assert call_kwargs.kwargs.get("params") == {"token": "secret"}
2354+
# URL should have query string stripped
2355+
assert call_kwargs.args[1] == "http://example.com/api/upload"
2356+
# Content-Type stripped, but other headers preserved
2357+
sent_headers = call_kwargs.kwargs.get("headers", {})
2358+
assert "Content-Type" not in sent_headers
2359+
assert sent_headers.get("X-Custom") == "keep-me"
2360+
22872361
@pytest.mark.asyncio
22882362
async def test_invoke_tool_rest_decrypts_encrypted_custom_headers(self, tool_service, mock_tool, mock_global_config_obj, test_db):
22892363
"""REST invocation should send decrypted values for encrypted custom headers."""

0 commit comments

Comments
 (0)