From 080cdda4fab5078c79712550e615674388ffc199 Mon Sep 17 00:00:00 2001 From: hjlarry Date: Tue, 30 Sep 2025 21:20:45 +0800 Subject: [PATCH] query param of webhook backend support --- api/core/trigger/trigger_manager.py | 2 +- api/services/webhook_service.py | 60 ++++++++- .../services/test_webhook_service.py | 118 ++++++++++++++++-- 3 files changed, 164 insertions(+), 16 deletions(-) diff --git a/api/core/trigger/trigger_manager.py b/api/core/trigger/trigger_manager.py index 1e856375ce..ff56601e50 100644 --- a/api/core/trigger/trigger_manager.py +++ b/api/core/trigger/trigger_manager.py @@ -5,7 +5,7 @@ Trigger Manager for loading and managing trigger providers and triggers import logging from collections.abc import Mapping from threading import Lock -from typing import Any, Optional +from typing import Any from flask import Request diff --git a/api/services/webhook_service.py b/api/services/webhook_service.py index f2e1f89f04..cf7216a48f 100644 --- a/api/services/webhook_service.py +++ b/api/services/webhook_service.py @@ -90,7 +90,7 @@ class WebhookService: data = { "method": request.method, "headers": dict(request.headers), - "query_params": dict(request.args), + "query_params": cls._extract_query_params(), "body": {}, "files": {}, } @@ -120,6 +120,43 @@ class WebhookService: return data + @classmethod + def _extract_query_params(cls) -> dict[str, Any]: + """Extract query parameters preserving multi-value entries.""" + if not request.args: + return {} + + query_params: dict[str, Any] = {} + for key, value in request.args.items(): + query_params[key] = cls._convert_query_param_value(value) + + return query_params + + @classmethod + def _convert_query_param_value(cls, value: str) -> Any: + """Convert query parameter strings to numbers or booleans when applicable.""" + lower_value = value.lower() + bool_map = { + "true": True, + "false": False, + "yes": True, + "no": False, + } + + if lower_value in bool_map: + return bool_map[lower_value] + + if cls._can_convert_to_number(value): + try: + numeric_value = float(value) + if numeric_value.is_integer(): + return int(numeric_value) + return numeric_value + except ValueError: + return value + + return value + @classmethod def _validate_content_length(cls) -> None: """Validate request content length against maximum allowed size.""" @@ -285,11 +322,24 @@ class WebhookService: return cls._validation_error(f"Required header missing: {header_name}") # Validate required query parameters + query_params = webhook_data.get("query_params", {}) for param in node_data.get("params", []): - if param.get("required", False): - param_name = param.get("name", "") - if param_name not in webhook_data["query_params"]: - return cls._validation_error(f"Required query parameter missing: {param_name}") + param_name = param.get("name", "") + param_type = param.get("type", SegmentType.STRING) + is_required = param.get("required", False) + + param_exists = param_name in query_params + if is_required and not param_exists: + return cls._validation_error(f"Required query parameter missing: {param_name}") + + if not param_exists: + continue + + if param_exists and param_type != SegmentType.STRING: + param_value = query_params[param_name] + validation_result = cls._validate_form_parameter_type(param_name, param_value, param_type) + if not validation_result["valid"]: + return validation_result return {"valid": True} diff --git a/api/tests/unit_tests/services/test_webhook_service.py b/api/tests/unit_tests/services/test_webhook_service.py index dbb9092ac5..20bfcca6cc 100644 --- a/api/tests/unit_tests/services/test_webhook_service.py +++ b/api/tests/unit_tests/services/test_webhook_service.py @@ -26,12 +26,30 @@ class TestWebhookServiceUnit: assert webhook_data["method"] == "POST" assert webhook_data["headers"]["Authorization"] == "Bearer token" - assert webhook_data["query_params"]["version"] == "1" + assert webhook_data["query_params"]["version"] == 1 assert webhook_data["query_params"]["format"] == "json" assert webhook_data["body"]["message"] == "hello" assert webhook_data["body"]["count"] == 42 assert webhook_data["files"] == {} + def test_extract_webhook_data_query_params_remain_strings(self): + """Query parameters remain raw strings during extraction.""" + app = Flask(__name__) + + with app.test_request_context( + "/webhook", + method="GET", + headers={"Content-Type": "application/json"}, + query_string="count=42&threshold=3.14&enabled=true¬e=text", + ): + webhook_trigger = MagicMock() + webhook_data = WebhookService.extract_webhook_data(webhook_trigger) + + assert webhook_data["query_params"]["count"] == 42 + assert webhook_data["query_params"]["threshold"] == 3.14 + assert webhook_data["query_params"]["enabled"] == True + assert webhook_data["query_params"]["note"] == "text" + def test_extract_webhook_data_form_urlencoded(self): """Test webhook data extraction from form URL encoded request.""" app = Flask(__name__) @@ -182,6 +200,92 @@ class TestWebhookServiceUnit: assert result["valid"] is False assert "Required query parameter missing: version" in result["error"] + def test_validate_webhook_request_query_param_number_type(self): + """Numeric query parameters should validate with numeric types.""" + webhook_data = { + "method": "POST", + "headers": {}, + "query_params": {"count": "42"}, + "body": {}, + "files": {}, + } + + node_config = { + "data": { + "method": "post", + "params": [{"name": "count", "required": True, "type": "number"}], + } + } + + result = WebhookService.validate_webhook_request(webhook_data, node_config) + + assert result["valid"] is True + + def test_validate_webhook_request_query_param_number_type_invalid(self): + """Numeric query parameter validation should fail for non-numeric values.""" + webhook_data = { + "method": "POST", + "headers": {}, + "query_params": {"count": "forty-two"}, + "body": {}, + "files": {}, + } + + node_config = { + "data": { + "method": "post", + "params": [{"name": "count", "required": True, "type": "number"}], + } + } + + result = WebhookService.validate_webhook_request(webhook_data, node_config) + + assert result["valid"] is False + assert "must be a valid number" in result["error"] + + def test_validate_webhook_request_query_param_boolean_type(self): + """Boolean query parameters should validate with supported boolean strings.""" + webhook_data = { + "method": "POST", + "headers": {}, + "query_params": {"enabled": "true"}, + "body": {}, + "files": {}, + } + + node_config = { + "data": { + "method": "post", + "params": [{"name": "enabled", "required": True, "type": "boolean"}], + } + } + + result = WebhookService.validate_webhook_request(webhook_data, node_config) + + assert result["valid"] is True + + def test_validate_webhook_request_query_param_string_type_preserved(self): + """String typed query parameters remain as strings even if boolean-like.""" + webhook_data = { + "method": "POST", + "headers": {}, + "query_params": {"flag": "true"}, + "body": {}, + "files": {}, + } + + node_config = { + "data": { + "method": "post", + "params": [{"name": "flag", "required": True, "type": "string"}], + } + } + + result = WebhookService.validate_webhook_request(webhook_data, node_config) + + assert result["valid"] is True + assert webhook_data["query_params"]["flag"] == "true" + def test_validate_webhook_request_missing_required_body_param(self): """Test webhook validation with missing required body parameter.""" webhook_data = {"method": "POST", "headers": {}, "query_params": {}, "body": {}, "files": {}} @@ -515,7 +619,7 @@ class TestWebhookServiceUnit: assert "must be an array of numbers" in result["error"] def test_validate_json_parameter_type_array_bool(self): - """Test JSON parameter type validation for array[bool] type.""" + """Test JSON parameter type validation for array[boolean] type.""" # Valid array of booleans result = WebhookService._validate_json_parameter_type("flags", [True, False, True], "array[boolean]") assert result["valid"] is True @@ -540,12 +644,6 @@ class TestWebhookServiceUnit: assert result["valid"] is False assert "must be an array of objects" in result["error"] - def test_validate_json_parameter_type_unknown_type(self): - """Test JSON parameter type validation for unknown type.""" - # Unknown type should return valid and log warning - result = WebhookService._validate_json_parameter_type("data", "anything", "unknown_type") - assert result["valid"] is True - def test_validate_webhook_request_json_type_validation(self): """Test webhook validation with JSON parameter type validation.""" # Test valid JSON types @@ -573,11 +671,11 @@ class TestWebhookServiceUnit: "body": [ {"name": "name", "type": "string", "required": True}, {"name": "age", "type": "number", "required": True}, - {"name": "active", "type": "bool", "required": True}, + {"name": "active", "type": "boolean", "required": True}, {"name": "profile", "type": "object", "required": True}, {"name": "tags", "type": "array[string]", "required": True}, {"name": "scores", "type": "array[number]", "required": True}, - {"name": "flags", "type": "array[bool]", "required": True}, + {"name": "flags", "type": "array[boolean]", "required": True}, {"name": "items", "type": "array[object]", "required": True}, ], }