@@ -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