@@ -1268,6 +1268,162 @@ async def test_call_without_header_provider(
12681268
12691269 assert result == {"result" : "success" }
12701270
1271+ def test_prepare_request_params_extracts_embedded_query_params (
1272+ self , sample_auth_credential , sample_auth_scheme
1273+ ):
1274+ """Test that query params embedded in the URL path are extracted.
1275+
1276+ ApplicationIntegrationToolset embeds query params and fragments directly
1277+ in the OpenAPI path (e.g. '...execute?triggerId=api_trigger/Name#action').
1278+ These must be moved into the explicit query_params dict so httpx does not
1279+ strip them when it replaces the URL query string with the `params` arg.
1280+ Regression test for https://github.com/google/adk-python/issues/4555.
1281+ """
1282+ integration_path = (
1283+ "/v2/projects/my-proj/locations/us-central1"
1284+ "/integrations/ExecuteConnection:execute"
1285+ "?triggerId=api_trigger/ExecuteConnection"
1286+ "#POST_files"
1287+ )
1288+ endpoint = OperationEndpoint (
1289+ base_url = "https://integrations.googleapis.com" ,
1290+ path = integration_path ,
1291+ method = "POST" ,
1292+ )
1293+ operation = Operation (operationId = "test_op" )
1294+ tool = RestApiTool (
1295+ name = "test_tool" ,
1296+ description = "test" ,
1297+ endpoint = endpoint ,
1298+ operation = operation ,
1299+ auth_credential = sample_auth_credential ,
1300+ auth_scheme = sample_auth_scheme ,
1301+ )
1302+
1303+ request_params = tool ._prepare_request_params ([], {})
1304+
1305+ # The embedded query param must appear in params
1306+ assert request_params ["params" ]["triggerId" ] == (
1307+ "api_trigger/ExecuteConnection"
1308+ )
1309+ # The URL must NOT contain the query string or fragment
1310+ assert "?" not in request_params ["url" ]
1311+ assert "#" not in request_params ["url" ]
1312+ assert request_params ["url" ] == (
1313+ "https://integrations.googleapis.com"
1314+ "/v2/projects/my-proj/locations/us-central1"
1315+ "/integrations/ExecuteConnection:execute"
1316+ )
1317+
1318+ def test_prepare_request_params_merges_embedded_and_explicit_query_params (
1319+ self , sample_auth_credential , sample_auth_scheme
1320+ ):
1321+ """Embedded URL query params merge with explicitly defined query params."""
1322+ endpoint = OperationEndpoint (
1323+ base_url = "https://example.com" ,
1324+ path = "/api?embedded_key=embedded_val" ,
1325+ method = "GET" ,
1326+ )
1327+ operation = Operation (operationId = "test_op" )
1328+ tool = RestApiTool (
1329+ name = "test_tool" ,
1330+ description = "test" ,
1331+ endpoint = endpoint ,
1332+ operation = operation ,
1333+ auth_credential = sample_auth_credential ,
1334+ auth_scheme = sample_auth_scheme ,
1335+ )
1336+ params = [
1337+ ApiParameter (
1338+ original_name = "explicit_key" ,
1339+ py_name = "explicit_key" ,
1340+ param_location = "query" ,
1341+ param_schema = OpenAPISchema (type = "string" ),
1342+ ),
1343+ ]
1344+ kwargs = {"explicit_key" : "explicit_val" }
1345+
1346+ request_params = tool ._prepare_request_params (params , kwargs )
1347+
1348+ assert request_params ["params" ]["embedded_key" ] == "embedded_val"
1349+ assert request_params ["params" ]["explicit_key" ] == "explicit_val"
1350+ assert "?" not in request_params ["url" ]
1351+
1352+ def test_prepare_request_params_explicit_query_param_takes_precedence (
1353+ self , sample_auth_credential , sample_auth_scheme
1354+ ):
1355+ """Explicitly defined query params take precedence over embedded ones."""
1356+ endpoint = OperationEndpoint (
1357+ base_url = "https://example.com" ,
1358+ path = "/api?key=embedded" ,
1359+ method = "GET" ,
1360+ )
1361+ operation = Operation (operationId = "test_op" )
1362+ tool = RestApiTool (
1363+ name = "test_tool" ,
1364+ description = "test" ,
1365+ endpoint = endpoint ,
1366+ operation = operation ,
1367+ auth_credential = sample_auth_credential ,
1368+ auth_scheme = sample_auth_scheme ,
1369+ )
1370+ params = [
1371+ ApiParameter (
1372+ original_name = "key" ,
1373+ py_name = "key" ,
1374+ param_location = "query" ,
1375+ param_schema = OpenAPISchema (type = "string" ),
1376+ ),
1377+ ]
1378+ kwargs = {"key" : "explicit" }
1379+
1380+ request_params = tool ._prepare_request_params (params , kwargs )
1381+
1382+ # Explicit value wins over the embedded one
1383+ assert request_params ["params" ]["key" ] == "explicit"
1384+
1385+ def test_prepare_request_params_strips_fragment_only (
1386+ self , sample_auth_credential , sample_auth_scheme
1387+ ):
1388+ """Fragment-only paths (no query string) are also cleaned."""
1389+ endpoint = OperationEndpoint (
1390+ base_url = "https://example.com" ,
1391+ path = "/api#fragment" ,
1392+ method = "GET" ,
1393+ )
1394+ operation = Operation (operationId = "test_op" )
1395+ tool = RestApiTool (
1396+ name = "test_tool" ,
1397+ description = "test" ,
1398+ endpoint = endpoint ,
1399+ operation = operation ,
1400+ auth_credential = sample_auth_credential ,
1401+ auth_scheme = sample_auth_scheme ,
1402+ )
1403+
1404+ request_params = tool ._prepare_request_params ([], {})
1405+
1406+ assert "#" not in request_params ["url" ]
1407+ assert request_params ["url" ] == "https://example.com/api"
1408+
1409+ def test_prepare_request_params_plain_url_unchanged (
1410+ self , sample_endpoint , sample_auth_credential , sample_auth_scheme
1411+ ):
1412+ """URLs without embedded query or fragment are not modified."""
1413+ operation = Operation (operationId = "test_op" )
1414+ tool = RestApiTool (
1415+ name = "test_tool" ,
1416+ description = "test" ,
1417+ endpoint = sample_endpoint ,
1418+ operation = operation ,
1419+ auth_credential = sample_auth_credential ,
1420+ auth_scheme = sample_auth_scheme ,
1421+ )
1422+
1423+ request_params = tool ._prepare_request_params ([], {})
1424+
1425+ assert request_params ["url" ] == "https://example.com/test"
1426+
12711427
12721428def test_snake_to_lower_camel ():
12731429 assert snake_to_lower_camel ("single" ) == "single"
0 commit comments