mirror of https://github.com/langgenius/dify.git
refactor webhook service
This commit is contained in:
parent
9114881623
commit
604651873e
|
|
@ -12,14 +12,17 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
def _prepare_webhook_execution(webhook_id: str):
|
||||
"""Fetch trigger context, extract request data, and validate payload."""
|
||||
"""Fetch trigger context, extract request data, and validate payload using unified processing."""
|
||||
webhook_trigger, workflow, node_config = WebhookService.get_webhook_trigger_and_workflow(webhook_id)
|
||||
webhook_data = WebhookService.extract_webhook_data(webhook_trigger)
|
||||
validation_result = WebhookService.validate_webhook_request(webhook_data, node_config)
|
||||
if not validation_result["valid"]:
|
||||
return webhook_trigger, workflow, node_config, webhook_data, validation_result["error"]
|
||||
|
||||
return webhook_trigger, workflow, node_config, webhook_data, None
|
||||
try:
|
||||
# Use new unified extraction and validation
|
||||
webhook_data = WebhookService.extract_and_validate_webhook_data(webhook_trigger, node_config)
|
||||
return webhook_trigger, workflow, node_config, webhook_data, None
|
||||
except ValueError as e:
|
||||
# Fall back to raw extraction for error reporting
|
||||
webhook_data = WebhookService.extract_webhook_data(webhook_trigger)
|
||||
return webhook_trigger, workflow, node_config, webhook_data, str(e)
|
||||
|
||||
|
||||
@bp.route("/webhook/<string:webhook_id>", methods=["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"])
|
||||
|
|
@ -55,7 +58,7 @@ def handle_webhook(webhook_id: str):
|
|||
def handle_webhook_debug(webhook_id: str):
|
||||
"""Handle webhook debug calls without triggering production workflow execution."""
|
||||
try:
|
||||
webhook_trigger, workflow, node_config, webhook_data, error = _prepare_webhook_execution(webhook_id)
|
||||
webhook_trigger, _, node_config, webhook_data, error = _prepare_webhook_execution(webhook_id)
|
||||
if error:
|
||||
return jsonify({"error": "Bad Request", "message": error}), 400
|
||||
|
||||
|
|
|
|||
|
|
@ -82,15 +82,34 @@ class WebhookService:
|
|||
|
||||
return webhook_trigger, workflow, node_config
|
||||
|
||||
@classmethod
|
||||
def extract_and_validate_webhook_data(
|
||||
cls, webhook_trigger: WorkflowWebhookTrigger, node_config: Mapping[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""Extract and validate webhook data in a single unified process."""
|
||||
# Extract raw data first
|
||||
raw_data = cls.extract_webhook_data(webhook_trigger)
|
||||
|
||||
# Validate HTTP metadata (method, content-type)
|
||||
node_data = node_config.get("data", {})
|
||||
validation_result = cls._validate_http_metadata(raw_data, node_data)
|
||||
if not validation_result["valid"]:
|
||||
raise ValueError(validation_result["error"])
|
||||
|
||||
# Process and validate data according to configuration
|
||||
processed_data = cls._process_and_validate_data(raw_data, node_data)
|
||||
|
||||
return processed_data
|
||||
|
||||
@classmethod
|
||||
def extract_webhook_data(cls, webhook_trigger: WorkflowWebhookTrigger) -> dict[str, Any]:
|
||||
"""Extract and process data from incoming webhook request."""
|
||||
"""Extract raw data from incoming webhook request without type conversion."""
|
||||
cls._validate_content_length()
|
||||
|
||||
data = {
|
||||
"method": request.method,
|
||||
"headers": dict(request.headers),
|
||||
"query_params": cls._extract_query_params(),
|
||||
"query_params": dict(request.args),
|
||||
"body": {},
|
||||
"files": {},
|
||||
}
|
||||
|
|
@ -121,41 +140,25 @@ 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 {}
|
||||
def _process_and_validate_data(cls, raw_data: dict[str, Any], node_data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Process and validate webhook data according to node configuration."""
|
||||
result = raw_data.copy()
|
||||
|
||||
query_params: dict[str, Any] = {}
|
||||
for key, value in request.args.items():
|
||||
query_params[key] = cls._convert_query_param_value(value)
|
||||
# Validate and process headers
|
||||
cls._validate_required_headers(raw_data["headers"], node_data.get("headers", []))
|
||||
|
||||
return query_params
|
||||
# Process query parameters with type conversion and validation
|
||||
result["query_params"] = cls._process_parameters(
|
||||
raw_data["query_params"], node_data.get("params", []), is_form_data=True
|
||||
)
|
||||
|
||||
@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,
|
||||
}
|
||||
# Process body parameters based on content type
|
||||
configured_content_type = node_data.get("content_type", "application/json").lower()
|
||||
result["body"] = cls._process_body_parameters(
|
||||
raw_data["body"], node_data.get("body", []), configured_content_type
|
||||
)
|
||||
|
||||
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
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def _validate_content_length(cls) -> None:
|
||||
|
|
@ -260,26 +263,156 @@ class WebhookService:
|
|||
)
|
||||
|
||||
@classmethod
|
||||
def validate_webhook_request(cls, webhook_data: dict[str, Any], node_config: Mapping[str, Any]) -> dict[str, Any]:
|
||||
"""Validate webhook request against node configuration."""
|
||||
if node_config is None:
|
||||
return cls._validation_error("Validation failed: Invalid node configuration")
|
||||
def _process_parameters(
|
||||
cls, raw_params: dict[str, str], param_configs: list, is_form_data: bool = False
|
||||
) -> dict[str, Any]:
|
||||
"""Process parameters with unified validation and type conversion."""
|
||||
processed = {}
|
||||
configured_params = {config.get("name", ""): config for config in param_configs}
|
||||
|
||||
node_data = node_config.get("data", {})
|
||||
# Process configured parameters
|
||||
for param_config in param_configs:
|
||||
name = param_config.get("name", "")
|
||||
param_type = param_config.get("type", SegmentType.STRING)
|
||||
required = param_config.get("required", False)
|
||||
|
||||
# Early validation of HTTP method and content-type
|
||||
validation_result = cls._validate_http_metadata(webhook_data, node_data)
|
||||
if not validation_result["valid"]:
|
||||
return validation_result
|
||||
# Check required parameters
|
||||
if required and name not in raw_params:
|
||||
raise ValueError(f"Required parameter missing: {name}")
|
||||
|
||||
# Validate headers and query params
|
||||
validation_result = cls._validate_headers_and_params(webhook_data, node_data)
|
||||
if not validation_result["valid"]:
|
||||
return validation_result
|
||||
if name in raw_params:
|
||||
raw_value = raw_params[name]
|
||||
processed[name] = cls._validate_and_convert_value(name, raw_value, param_type, is_form_data)
|
||||
|
||||
# Validate body based on content type
|
||||
configured_content_type = node_data.get("content_type", "application/json").lower()
|
||||
return cls._validate_body_by_content_type(webhook_data, node_data, configured_content_type)
|
||||
# Include unconfigured parameters as strings
|
||||
for name, value in raw_params.items():
|
||||
if name not in configured_params:
|
||||
processed[name] = value
|
||||
|
||||
return processed
|
||||
|
||||
@classmethod
|
||||
def _process_body_parameters(
|
||||
cls, raw_body: dict[str, Any], body_configs: list, content_type: str
|
||||
) -> dict[str, Any]:
|
||||
"""Process body parameters based on content type and configuration."""
|
||||
if content_type in ["text/plain", "application/octet-stream"]:
|
||||
# For text/plain and octet-stream, validate required content exists
|
||||
if body_configs and any(config.get("required", False) for config in body_configs):
|
||||
raw_content = raw_body.get("raw")
|
||||
if not raw_content:
|
||||
raise ValueError(f"Required body content missing for {content_type} request")
|
||||
return raw_body
|
||||
|
||||
# For structured data (JSON, form-data, etc.)
|
||||
processed = {}
|
||||
configured_params = {config.get("name", ""): config for config in body_configs}
|
||||
|
||||
for body_config in body_configs:
|
||||
name = body_config.get("name", "")
|
||||
param_type = body_config.get("type", SegmentType.STRING)
|
||||
required = body_config.get("required", False)
|
||||
|
||||
# Handle file parameters for multipart data
|
||||
if param_type == SegmentType.FILE and content_type == "multipart/form-data":
|
||||
# File validation is handled separately in extract phase
|
||||
continue
|
||||
|
||||
# Check required parameters
|
||||
if required and name not in raw_body:
|
||||
raise ValueError(f"Required body parameter missing: {name}")
|
||||
|
||||
if name in raw_body:
|
||||
raw_value = raw_body[name]
|
||||
is_form_data = content_type in ["application/x-www-form-urlencoded", "multipart/form-data"]
|
||||
processed[name] = cls._validate_and_convert_value(name, raw_value, param_type, is_form_data)
|
||||
|
||||
# Include unconfigured parameters
|
||||
for name, value in raw_body.items():
|
||||
if name not in configured_params:
|
||||
processed[name] = value
|
||||
|
||||
return processed
|
||||
|
||||
@classmethod
|
||||
def _validate_and_convert_value(cls, param_name: str, value: Any, param_type: str, is_form_data: bool) -> Any:
|
||||
"""Unified validation and type conversion for parameter values."""
|
||||
try:
|
||||
if is_form_data:
|
||||
# Form data comes as strings and needs conversion
|
||||
return cls._convert_form_value(param_name, value, param_type)
|
||||
else:
|
||||
# JSON data should already be in correct types, just validate
|
||||
return cls._validate_json_value(param_name, value, param_type)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Parameter '{param_name}' validation failed: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
def _convert_form_value(cls, param_name: str, value: str, param_type: str) -> Any:
|
||||
"""Convert form data string values to specified types."""
|
||||
if param_type == SegmentType.STRING:
|
||||
return value
|
||||
elif param_type == SegmentType.NUMBER:
|
||||
if not cls._can_convert_to_number(value):
|
||||
raise ValueError(f"Cannot convert '{value}' to number")
|
||||
numeric_value = float(value)
|
||||
return int(numeric_value) if numeric_value.is_integer() else numeric_value
|
||||
elif param_type == SegmentType.BOOLEAN:
|
||||
lower_value = value.lower()
|
||||
bool_map = {"true": True, "false": False, "1": True, "0": False, "yes": True, "no": False}
|
||||
if lower_value not in bool_map:
|
||||
raise ValueError(f"Cannot convert '{value}' to boolean")
|
||||
return bool_map[lower_value]
|
||||
else:
|
||||
raise ValueError(f"Unsupported type '{param_type}' for form data parameter '{param_name}'")
|
||||
|
||||
@classmethod
|
||||
def _validate_json_value(cls, param_name: str, value: Any, param_type: str) -> Any:
|
||||
"""Validate JSON values against expected types."""
|
||||
type_validators = {
|
||||
SegmentType.STRING: (lambda v: isinstance(v, str), "string"),
|
||||
SegmentType.NUMBER: (lambda v: isinstance(v, (int, float)), "number"),
|
||||
SegmentType.BOOLEAN: (lambda v: isinstance(v, bool), "boolean"),
|
||||
SegmentType.OBJECT: (lambda v: isinstance(v, dict), "object"),
|
||||
SegmentType.ARRAY_STRING: (
|
||||
lambda v: isinstance(v, list) and all(isinstance(item, str) for item in v),
|
||||
"array of strings",
|
||||
),
|
||||
SegmentType.ARRAY_NUMBER: (
|
||||
lambda v: isinstance(v, list) and all(isinstance(item, (int, float)) for item in v),
|
||||
"array of numbers",
|
||||
),
|
||||
SegmentType.ARRAY_BOOLEAN: (
|
||||
lambda v: isinstance(v, list) and all(isinstance(item, bool) for item in v),
|
||||
"array of booleans",
|
||||
),
|
||||
SegmentType.ARRAY_OBJECT: (
|
||||
lambda v: isinstance(v, list) and all(isinstance(item, dict) for item in v),
|
||||
"array of objects",
|
||||
),
|
||||
}
|
||||
|
||||
validator_info = type_validators.get(SegmentType(param_type))
|
||||
if not validator_info:
|
||||
logger.warning("Unknown parameter type: %s for parameter %s", param_type, param_name)
|
||||
return value
|
||||
|
||||
validator, expected_type = validator_info
|
||||
if not validator(value):
|
||||
actual_type = type(value).__name__
|
||||
raise ValueError(f"Expected {expected_type}, got {actual_type}")
|
||||
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def _validate_required_headers(cls, headers: dict[str, Any], header_configs: list) -> None:
|
||||
"""Validate required headers are present."""
|
||||
headers_lower = {k.lower(): v for k, v in headers.items()}
|
||||
for header_config in header_configs:
|
||||
if header_config.get("required", False):
|
||||
header_name = header_config.get("name", "")
|
||||
if header_name.lower() not in headers_lower:
|
||||
raise ValueError(f"Required header missing: {header_name}")
|
||||
|
||||
@classmethod
|
||||
def _validate_http_metadata(cls, webhook_data: dict[str, Any], node_data: dict[str, Any]) -> dict[str, Any]:
|
||||
|
|
@ -310,259 +443,11 @@ class WebhookService:
|
|||
# Extract the main content type (ignore parameters like boundary)
|
||||
return content_type.split(";")[0].strip()
|
||||
|
||||
@classmethod
|
||||
def _validate_headers_and_params(cls, webhook_data: dict[str, Any], node_data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate required headers and query parameters."""
|
||||
# Validate required headers (case-insensitive)
|
||||
webhook_headers_lower = {k.lower(): v for k, v in webhook_data["headers"].items()}
|
||||
for header in node_data.get("headers", []):
|
||||
if header.get("required", False):
|
||||
header_name = header.get("name", "")
|
||||
if header_name.lower() not in webhook_headers_lower:
|
||||
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", []):
|
||||
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}
|
||||
|
||||
@classmethod
|
||||
def _validate_body_by_content_type(
|
||||
cls, webhook_data: dict[str, Any], node_data: dict[str, Any], content_type: str
|
||||
) -> dict[str, Any]:
|
||||
"""Route body validation to appropriate validator based on content type."""
|
||||
validators = {
|
||||
"text/plain": cls._validate_text_plain_body,
|
||||
"application/octet-stream": cls._validate_octet_stream_body,
|
||||
"application/json": cls._validate_json_body,
|
||||
"application/x-www-form-urlencoded": cls._validate_form_urlencoded_body,
|
||||
"multipart/form-data": cls._validate_multipart_body,
|
||||
}
|
||||
|
||||
validator = validators.get(content_type)
|
||||
if not validator:
|
||||
raise ValueError(f"Unsupported Content-Type for validation: {content_type}")
|
||||
|
||||
return validator(webhook_data, node_data)
|
||||
|
||||
@classmethod
|
||||
def _validate_text_plain_body(cls, webhook_data: dict[str, Any], node_data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate text/plain body."""
|
||||
body_params = node_data.get("body", [])
|
||||
if body_params and any(param.get("required", False) for param in body_params):
|
||||
body_data = webhook_data.get("body", {})
|
||||
raw_content = body_data.get("raw", "")
|
||||
if not raw_content or not isinstance(raw_content, str):
|
||||
return cls._validation_error("Required body content missing for text/plain request")
|
||||
return {"valid": True}
|
||||
|
||||
@classmethod
|
||||
def _validate_octet_stream_body(cls, webhook_data: dict[str, Any], node_data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate application/octet-stream body."""
|
||||
body_params = node_data.get("body", [])
|
||||
if body_params and any(param.get("required", False) for param in body_params):
|
||||
body_data = webhook_data.get("body", {})
|
||||
raw_content = body_data.get("raw", "")
|
||||
if not raw_content or not isinstance(raw_content, bytes):
|
||||
return cls._validation_error("Required body content missing for application/octet-stream request")
|
||||
return {"valid": True}
|
||||
|
||||
@classmethod
|
||||
def _validate_json_body(cls, webhook_data: dict[str, Any], node_data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate application/json body."""
|
||||
body_params = node_data.get("body", [])
|
||||
body_data = webhook_data.get("body", {})
|
||||
|
||||
for body_param in body_params:
|
||||
param_name = body_param.get("name", "")
|
||||
param_type = body_param.get("type", SegmentType.STRING)
|
||||
is_required = body_param.get("required", False)
|
||||
|
||||
param_exists = param_name in body_data
|
||||
|
||||
if is_required and not param_exists:
|
||||
return cls._validation_error(f"Required body parameter missing: {param_name}")
|
||||
|
||||
if param_exists:
|
||||
param_value = body_data[param_name]
|
||||
validation_result = cls._validate_json_parameter_type(param_name, param_value, param_type)
|
||||
if not validation_result["valid"]:
|
||||
return validation_result
|
||||
|
||||
return {"valid": True}
|
||||
|
||||
@classmethod
|
||||
def _validate_form_urlencoded_body(cls, webhook_data: dict[str, Any], node_data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate application/x-www-form-urlencoded body."""
|
||||
body_params = node_data.get("body", [])
|
||||
body_data = webhook_data.get("body", {})
|
||||
|
||||
for body_param in body_params:
|
||||
param_name = body_param.get("name", "")
|
||||
param_type = body_param.get("type", SegmentType.STRING)
|
||||
is_required = body_param.get("required", False)
|
||||
|
||||
param_exists = param_name in body_data
|
||||
if is_required and not param_exists:
|
||||
return cls._validation_error(f"Required body parameter missing: {param_name}")
|
||||
|
||||
if param_exists and param_type != SegmentType.STRING:
|
||||
param_value = body_data[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}
|
||||
|
||||
@classmethod
|
||||
def _validate_multipart_body(cls, webhook_data: dict[str, Any], node_data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate multipart/form-data body."""
|
||||
body_params = node_data.get("body", [])
|
||||
body_data = webhook_data.get("body", {})
|
||||
|
||||
for body_param in body_params:
|
||||
param_name = body_param.get("name", "")
|
||||
param_type = body_param.get("type", SegmentType.STRING)
|
||||
is_required = body_param.get("required", False)
|
||||
|
||||
if param_type == SegmentType.FILE:
|
||||
file_obj = webhook_data.get("files", {}).get(param_name)
|
||||
if is_required and not file_obj:
|
||||
return cls._validation_error(f"Required file parameter missing: {param_name}")
|
||||
else:
|
||||
param_exists = param_name in body_data
|
||||
|
||||
if is_required and not param_exists:
|
||||
return cls._validation_error(f"Required body parameter missing: {param_name}")
|
||||
|
||||
if param_exists and param_type != SegmentType.STRING:
|
||||
param_value = body_data[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}
|
||||
|
||||
@classmethod
|
||||
def _validation_error(cls, error_message: str) -> dict[str, Any]:
|
||||
"""Create a standard validation error response."""
|
||||
return {"valid": False, "error": error_message}
|
||||
|
||||
@classmethod
|
||||
def _validate_json_parameter_type(cls, param_name: str, param_value: Any, param_type: str) -> dict[str, Any]:
|
||||
"""Validate JSON parameter type against expected type."""
|
||||
try:
|
||||
# Define type validators
|
||||
type_validators = {
|
||||
SegmentType.STRING: (lambda v: isinstance(v, str), "string"),
|
||||
SegmentType.NUMBER: (lambda v: isinstance(v, (int, float)), "number"),
|
||||
SegmentType.BOOLEAN: (lambda v: isinstance(v, bool), "boolean"),
|
||||
SegmentType.OBJECT: (lambda v: isinstance(v, dict), "object"),
|
||||
SegmentType.ARRAY_STRING: (
|
||||
lambda v: isinstance(v, list) and all(isinstance(item, str) for item in v),
|
||||
"array of strings",
|
||||
),
|
||||
SegmentType.ARRAY_NUMBER: (
|
||||
lambda v: isinstance(v, list) and all(isinstance(item, (int, float)) for item in v),
|
||||
"array of numbers",
|
||||
),
|
||||
SegmentType.ARRAY_BOOLEAN: (
|
||||
lambda v: isinstance(v, list) and all(isinstance(item, bool) for item in v),
|
||||
"array of booleans",
|
||||
),
|
||||
SegmentType.ARRAY_OBJECT: (
|
||||
lambda v: isinstance(v, list) and all(isinstance(item, dict) for item in v),
|
||||
"array of objects",
|
||||
),
|
||||
}
|
||||
|
||||
# Get validator for the type
|
||||
validator_info = type_validators.get(SegmentType(param_type))
|
||||
if not validator_info:
|
||||
logger.warning("Unknown parameter type: %s for parameter %s", param_type, param_name)
|
||||
return {"valid": True}
|
||||
|
||||
validator, expected_type = validator_info
|
||||
|
||||
# Validate the parameter
|
||||
if not validator(param_value):
|
||||
# Check if it's an array type first
|
||||
if param_type.startswith("array") and not isinstance(param_value, list):
|
||||
actual_type = type(param_value).__name__
|
||||
error_msg = f"Parameter '{param_name}' must be an array, got {actual_type}"
|
||||
else:
|
||||
actual_type = type(param_value).__name__
|
||||
# Format error message based on expected type
|
||||
if param_type.startswith("array"):
|
||||
error_msg = f"Parameter '{param_name}' must be an {expected_type}"
|
||||
elif expected_type in ["string", "number", "boolean"]:
|
||||
error_msg = f"Parameter '{param_name}' must be a {expected_type}, got {actual_type}"
|
||||
else:
|
||||
error_msg = f"Parameter '{param_name}' must be an {expected_type}, got {actual_type}"
|
||||
|
||||
return {"valid": False, "error": error_msg}
|
||||
|
||||
return {"valid": True}
|
||||
|
||||
except Exception:
|
||||
logger.exception("Type validation error for parameter %s", param_name)
|
||||
return {"valid": False, "error": f"Type validation failed for parameter '{param_name}'"}
|
||||
|
||||
@classmethod
|
||||
def _validate_form_parameter_type(cls, param_name: str, param_value: str, param_type: str) -> dict[str, Any]:
|
||||
"""Validate form parameter type against expected type. Form data are always strings but can be converted."""
|
||||
try:
|
||||
# Define form type converters and validators
|
||||
form_validators = {
|
||||
SegmentType.STRING: (lambda _: True, None), # String is always valid
|
||||
SegmentType.NUMBER: (lambda v: cls._can_convert_to_number(v), "a valid number"),
|
||||
SegmentType.BOOLEAN: (
|
||||
lambda v: v.lower() in ["true", "false", "1", "0", "yes", "no"],
|
||||
"a boolean value",
|
||||
),
|
||||
}
|
||||
|
||||
# Get validator for the type
|
||||
validator_info = form_validators.get(SegmentType(param_type))
|
||||
if not validator_info:
|
||||
# Unsupported type for form data
|
||||
return {
|
||||
"valid": False,
|
||||
"error": f"Parameter '{param_name}' type '{param_type}' is not supported for form data.",
|
||||
}
|
||||
|
||||
validator, expected_format = validator_info
|
||||
|
||||
# Validate the parameter
|
||||
if not validator(param_value):
|
||||
return {
|
||||
"valid": False,
|
||||
"error": f"Parameter '{param_name}' must be {expected_format}, got '{param_value}'",
|
||||
}
|
||||
|
||||
return {"valid": True}
|
||||
|
||||
except Exception:
|
||||
logger.exception("Form type validation error for parameter %s", param_name)
|
||||
return {"valid": False, "error": f"Form type validation failed for parameter '{param_name}'"}
|
||||
|
||||
@classmethod
|
||||
def _can_convert_to_number(cls, value: str) -> bool:
|
||||
"""Check if a string can be converted to a number."""
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from io import BytesIO
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
from werkzeug.datastructures import FileStorage
|
||||
|
||||
|
|
@ -26,14 +27,15 @@ class TestWebhookServiceUnit:
|
|||
|
||||
assert webhook_data["method"] == "POST"
|
||||
assert webhook_data["headers"]["Authorization"] == "Bearer token"
|
||||
assert webhook_data["query_params"]["version"] == 1
|
||||
# Query params are now extracted as raw strings
|
||||
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."""
|
||||
"""Query parameters should be extracted as raw strings without automatic conversion."""
|
||||
app = Flask(__name__)
|
||||
|
||||
with app.test_request_context(
|
||||
|
|
@ -45,9 +47,10 @@ class TestWebhookServiceUnit:
|
|||
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
|
||||
# After refactoring, raw extraction keeps query params as strings
|
||||
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):
|
||||
|
|
@ -120,275 +123,6 @@ class TestWebhookServiceUnit:
|
|||
assert webhook_data["method"] == "POST"
|
||||
assert webhook_data["body"] == {} # Should default to empty dict
|
||||
|
||||
def test_validate_webhook_request_success(self):
|
||||
"""Test successful webhook request validation."""
|
||||
webhook_data = {
|
||||
"method": "POST",
|
||||
"headers": {"Authorization": "Bearer token", "Content-Type": "application/json"},
|
||||
"query_params": {"version": "1"},
|
||||
"body": {"message": "hello"},
|
||||
"files": {},
|
||||
}
|
||||
|
||||
node_config = {
|
||||
"data": {
|
||||
"method": "post",
|
||||
"headers": [{"name": "Authorization", "required": True}, {"name": "Content-Type", "required": False}],
|
||||
"params": [{"name": "version", "required": True}],
|
||||
"body": [{"name": "message", "type": "string", "required": True}],
|
||||
}
|
||||
}
|
||||
|
||||
result = WebhookService.validate_webhook_request(webhook_data, node_config)
|
||||
|
||||
assert result["valid"] is True
|
||||
|
||||
def test_validate_webhook_request_method_mismatch(self):
|
||||
"""Test webhook validation with HTTP method mismatch."""
|
||||
webhook_data = {"method": "GET", "headers": {}, "query_params": {}, "body": {}, "files": {}}
|
||||
|
||||
node_config = {"data": {"method": "post"}}
|
||||
|
||||
result = WebhookService.validate_webhook_request(webhook_data, node_config)
|
||||
|
||||
assert result["valid"] is False
|
||||
assert "HTTP method mismatch" in result["error"]
|
||||
assert "Expected POST, got GET" in result["error"]
|
||||
|
||||
def test_validate_webhook_request_missing_required_header(self):
|
||||
"""Test webhook validation with missing required header."""
|
||||
webhook_data = {"method": "POST", "headers": {}, "query_params": {}, "body": {}, "files": {}}
|
||||
|
||||
node_config = {"data": {"method": "post", "headers": [{"name": "Authorization", "required": True}]}}
|
||||
|
||||
result = WebhookService.validate_webhook_request(webhook_data, node_config)
|
||||
|
||||
assert result["valid"] is False
|
||||
assert "Required header missing: Authorization" in result["error"]
|
||||
|
||||
def test_validate_webhook_request_case_insensitive_headers(self):
|
||||
"""Test webhook validation with case-insensitive header matching."""
|
||||
webhook_data = {
|
||||
"method": "POST",
|
||||
"headers": {"authorization": "Bearer token"}, # lowercase
|
||||
"query_params": {},
|
||||
"body": {},
|
||||
"files": {},
|
||||
}
|
||||
|
||||
node_config = {
|
||||
"data": {
|
||||
"method": "post",
|
||||
"headers": [
|
||||
{"name": "Authorization", "required": True} # Pascal case
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
result = WebhookService.validate_webhook_request(webhook_data, node_config)
|
||||
|
||||
assert result["valid"] is True
|
||||
|
||||
def test_validate_webhook_request_missing_required_param(self):
|
||||
"""Test webhook validation with missing required query parameter."""
|
||||
webhook_data = {"method": "POST", "headers": {}, "query_params": {}, "body": {}, "files": {}}
|
||||
|
||||
node_config = {"data": {"method": "post", "params": [{"name": "version", "required": True}]}}
|
||||
|
||||
result = WebhookService.validate_webhook_request(webhook_data, node_config)
|
||||
|
||||
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": {}}
|
||||
|
||||
node_config = {"data": {"method": "post", "body": [{"name": "message", "type": "string", "required": True}]}}
|
||||
|
||||
result = WebhookService.validate_webhook_request(webhook_data, node_config)
|
||||
|
||||
assert result["valid"] is False
|
||||
assert "Required body parameter missing: message" in result["error"]
|
||||
|
||||
def test_validate_webhook_request_missing_required_file(self):
|
||||
"""Test webhook validation with missing required file parameter."""
|
||||
webhook_data = {"method": "POST", "headers": {}, "query_params": {}, "body": {}, "files": {}}
|
||||
|
||||
node_config = {"data": {"method": "post", "body": [{"name": "upload", "type": "file", "required": True}]}}
|
||||
|
||||
result = WebhookService.validate_webhook_request(webhook_data, node_config)
|
||||
|
||||
assert result["valid"] is False
|
||||
assert "Required body parameter missing: upload" in result["error"]
|
||||
|
||||
def test_validate_webhook_request_text_plain_with_required_body(self):
|
||||
"""Test webhook validation for text/plain content type with required body content."""
|
||||
# Test case 1: text/plain with raw content - should pass
|
||||
webhook_data = {
|
||||
"method": "POST",
|
||||
"headers": {"content-type": "text/plain"},
|
||||
"query_params": {},
|
||||
"body": {"raw": "Hello World"},
|
||||
"files": {},
|
||||
}
|
||||
|
||||
node_config = {
|
||||
"data": {
|
||||
"method": "post",
|
||||
"content_type": "text/plain",
|
||||
"body": [{"name": "message", "type": "string", "required": True}],
|
||||
}
|
||||
}
|
||||
|
||||
result = WebhookService.validate_webhook_request(webhook_data, node_config)
|
||||
assert result["valid"] is True
|
||||
|
||||
# Test case 2: text/plain without raw content but required - should fail
|
||||
webhook_data_no_body = {
|
||||
"method": "POST",
|
||||
"headers": {"content-type": "text/plain"},
|
||||
"query_params": {},
|
||||
"body": {},
|
||||
"files": {},
|
||||
}
|
||||
|
||||
result = WebhookService.validate_webhook_request(webhook_data_no_body, node_config)
|
||||
assert result["valid"] is False
|
||||
assert "Required body content missing for text/plain request" in result["error"]
|
||||
|
||||
# Test case 3: text/plain with empty raw content but required - should fail
|
||||
webhook_data_empty_body = {
|
||||
"method": "POST",
|
||||
"headers": {"content-type": "text/plain"},
|
||||
"query_params": {},
|
||||
"body": {"raw": ""},
|
||||
"files": {},
|
||||
}
|
||||
|
||||
result = WebhookService.validate_webhook_request(webhook_data_empty_body, node_config)
|
||||
assert result["valid"] is False
|
||||
assert "Required body content missing for text/plain request" in result["error"]
|
||||
|
||||
def test_validate_webhook_request_text_plain_no_body_params(self):
|
||||
"""Test webhook validation for text/plain content type with no body params configured."""
|
||||
webhook_data = {
|
||||
"method": "POST",
|
||||
"headers": {"content-type": "text/plain"},
|
||||
"query_params": {},
|
||||
"body": {"raw": "Hello World"},
|
||||
"files": {},
|
||||
}
|
||||
|
||||
node_config = {
|
||||
"data": {
|
||||
"method": "post",
|
||||
"content_type": "text/plain",
|
||||
"body": [], # No body params configured
|
||||
}
|
||||
}
|
||||
|
||||
result = WebhookService.validate_webhook_request(webhook_data, node_config)
|
||||
assert result["valid"] is True
|
||||
|
||||
def test_validate_webhook_request_validation_exception(self):
|
||||
"""Test webhook validation with exception handling."""
|
||||
webhook_data = {"method": "POST", "headers": {}, "query_params": {}, "body": {}, "files": {}}
|
||||
|
||||
# Invalid node config that will cause an exception
|
||||
node_config = None
|
||||
|
||||
result = WebhookService.validate_webhook_request(webhook_data, node_config)
|
||||
|
||||
assert result["valid"] is False
|
||||
assert "Validation failed" in result["error"]
|
||||
|
||||
def test_generate_webhook_response_default(self):
|
||||
"""Test webhook response generation with default values."""
|
||||
node_config = {"data": {}}
|
||||
|
|
@ -540,199 +274,183 @@ class TestWebhookServiceUnit:
|
|||
# Should skip files without filenames
|
||||
assert len(result) == 0
|
||||
|
||||
def test_validate_json_parameter_type_string(self):
|
||||
"""Test JSON parameter type validation for string type."""
|
||||
def test_validate_json_value_string(self):
|
||||
"""Test JSON value validation for string type."""
|
||||
# Valid string
|
||||
result = WebhookService._validate_json_parameter_type("name", "hello", "string")
|
||||
assert result["valid"] is True
|
||||
result = WebhookService._validate_json_value("name", "hello", "string")
|
||||
assert result == "hello"
|
||||
|
||||
# Invalid string (number)
|
||||
result = WebhookService._validate_json_parameter_type("name", 123, "string")
|
||||
assert result["valid"] is False
|
||||
assert "must be a string, got int" in result["error"]
|
||||
# Invalid string (number) - should raise ValueError
|
||||
with pytest.raises(ValueError, match="Expected string, got int"):
|
||||
WebhookService._validate_json_value("name", 123, "string")
|
||||
|
||||
def test_validate_json_parameter_type_number(self):
|
||||
"""Test JSON parameter type validation for number type."""
|
||||
def test_validate_json_value_number(self):
|
||||
"""Test JSON value validation for number type."""
|
||||
# Valid integer
|
||||
result = WebhookService._validate_json_parameter_type("count", 42, "number")
|
||||
assert result["valid"] is True
|
||||
result = WebhookService._validate_json_value("count", 42, "number")
|
||||
assert result == 42
|
||||
|
||||
# Valid float
|
||||
result = WebhookService._validate_json_parameter_type("price", 19.99, "number")
|
||||
assert result["valid"] is True
|
||||
result = WebhookService._validate_json_value("price", 19.99, "number")
|
||||
assert result == 19.99
|
||||
|
||||
# Invalid number (string)
|
||||
result = WebhookService._validate_json_parameter_type("count", "42", "number")
|
||||
assert result["valid"] is False
|
||||
assert "must be a number, got str" in result["error"]
|
||||
# Invalid number (string) - should raise ValueError
|
||||
with pytest.raises(ValueError, match="Expected number, got str"):
|
||||
WebhookService._validate_json_value("count", "42", "number")
|
||||
|
||||
def test_validate_json_parameter_type_bool(self):
|
||||
"""Test JSON parameter type validation for boolean type."""
|
||||
def test_validate_json_value_bool(self):
|
||||
"""Test JSON value validation for boolean type."""
|
||||
# Valid boolean
|
||||
result = WebhookService._validate_json_parameter_type("enabled", True, "boolean")
|
||||
assert result["valid"] is True
|
||||
result = WebhookService._validate_json_value("enabled", True, "boolean")
|
||||
assert result is True
|
||||
|
||||
result = WebhookService._validate_json_parameter_type("enabled", False, "boolean")
|
||||
assert result["valid"] is True
|
||||
result = WebhookService._validate_json_value("enabled", False, "boolean")
|
||||
assert result is False
|
||||
|
||||
# Invalid boolean (string)
|
||||
result = WebhookService._validate_json_parameter_type("enabled", "true", "boolean")
|
||||
assert result["valid"] is False
|
||||
assert "must be a boolean, got str" in result["error"]
|
||||
# Invalid boolean (string) - should raise ValueError
|
||||
with pytest.raises(ValueError, match="Expected boolean, got str"):
|
||||
WebhookService._validate_json_value("enabled", "true", "boolean")
|
||||
|
||||
def test_validate_json_parameter_type_object(self):
|
||||
"""Test JSON parameter type validation for object type."""
|
||||
def test_validate_json_value_object(self):
|
||||
"""Test JSON value validation for object type."""
|
||||
# Valid object
|
||||
result = WebhookService._validate_json_parameter_type("user", {"name": "John", "age": 30}, "object")
|
||||
assert result["valid"] is True
|
||||
result = WebhookService._validate_json_value("user", {"name": "John", "age": 30}, "object")
|
||||
assert result == {"name": "John", "age": 30}
|
||||
|
||||
# Invalid object (string)
|
||||
result = WebhookService._validate_json_parameter_type("user", "not_an_object", "object")
|
||||
assert result["valid"] is False
|
||||
assert "must be an object, got str" in result["error"]
|
||||
# Invalid object (string) - should raise ValueError
|
||||
with pytest.raises(ValueError, match="Expected object, got str"):
|
||||
WebhookService._validate_json_value("user", "not_an_object", "object")
|
||||
|
||||
def test_validate_json_parameter_type_array_string(self):
|
||||
"""Test JSON parameter type validation for array[string] type."""
|
||||
def test_validate_json_value_array_string(self):
|
||||
"""Test JSON value validation for array[string] type."""
|
||||
# Valid array of strings
|
||||
result = WebhookService._validate_json_parameter_type("tags", ["tag1", "tag2", "tag3"], "array[string]")
|
||||
assert result["valid"] is True
|
||||
result = WebhookService._validate_json_value("tags", ["tag1", "tag2", "tag3"], "array[string]")
|
||||
assert result == ["tag1", "tag2", "tag3"]
|
||||
|
||||
# Invalid - not an array
|
||||
result = WebhookService._validate_json_parameter_type("tags", "not_an_array", "array[string]")
|
||||
assert result["valid"] is False
|
||||
assert "must be an array, got str" in result["error"]
|
||||
with pytest.raises(ValueError, match="Expected array of strings, got str"):
|
||||
WebhookService._validate_json_value("tags", "not_an_array", "array[string]")
|
||||
|
||||
# Invalid - array with non-strings
|
||||
result = WebhookService._validate_json_parameter_type("tags", ["tag1", 123, "tag3"], "array[string]")
|
||||
assert result["valid"] is False
|
||||
assert "must be an array of strings" in result["error"]
|
||||
with pytest.raises(ValueError, match="Expected array of strings, got list"):
|
||||
WebhookService._validate_json_value("tags", ["tag1", 123, "tag3"], "array[string]")
|
||||
|
||||
def test_validate_json_parameter_type_array_number(self):
|
||||
"""Test JSON parameter type validation for array[number] type."""
|
||||
def test_validate_json_value_array_number(self):
|
||||
"""Test JSON value validation for array[number] type."""
|
||||
# Valid array of numbers
|
||||
result = WebhookService._validate_json_parameter_type("scores", [1, 2.5, 3, 4.7], "array[number]")
|
||||
assert result["valid"] is True
|
||||
result = WebhookService._validate_json_value("scores", [1, 2.5, 3, 4.7], "array[number]")
|
||||
assert result == [1, 2.5, 3, 4.7]
|
||||
|
||||
# Invalid - array with non-numbers
|
||||
result = WebhookService._validate_json_parameter_type("scores", [1, "2", 3], "array[number]")
|
||||
assert result["valid"] is False
|
||||
assert "must be an array of numbers" in result["error"]
|
||||
with pytest.raises(ValueError, match="Expected array of numbers, got list"):
|
||||
WebhookService._validate_json_value("scores", [1, "2", 3], "array[number]")
|
||||
|
||||
def test_validate_json_parameter_type_array_bool(self):
|
||||
"""Test JSON parameter type validation for array[boolean] type."""
|
||||
def test_validate_json_value_array_bool(self):
|
||||
"""Test JSON value 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
|
||||
result = WebhookService._validate_json_value("flags", [True, False, True], "array[boolean]")
|
||||
assert result == [True, False, True]
|
||||
|
||||
# Invalid - array with non-booleans
|
||||
result = WebhookService._validate_json_parameter_type("flags", [True, "false", True], "array[boolean]")
|
||||
assert result["valid"] is False
|
||||
assert "must be an array of booleans" in result["error"]
|
||||
with pytest.raises(ValueError, match="Expected array of booleans, got list"):
|
||||
WebhookService._validate_json_value("flags", [True, "false", True], "array[boolean]")
|
||||
|
||||
def test_validate_json_parameter_type_array_object(self):
|
||||
"""Test JSON parameter type validation for array[object] type."""
|
||||
def test_validate_json_value_array_object(self):
|
||||
"""Test JSON value validation for array[object] type."""
|
||||
# Valid array of objects
|
||||
result = WebhookService._validate_json_parameter_type(
|
||||
"users", [{"name": "John"}, {"name": "Jane"}], "array[object]"
|
||||
)
|
||||
assert result["valid"] is True
|
||||
result = WebhookService._validate_json_value("users", [{"name": "John"}, {"name": "Jane"}], "array[object]")
|
||||
assert result == [{"name": "John"}, {"name": "Jane"}]
|
||||
|
||||
# Invalid - array with non-objects
|
||||
result = WebhookService._validate_json_parameter_type(
|
||||
"users", [{"name": "John"}, "not_object"], "array[object]"
|
||||
)
|
||||
assert result["valid"] is False
|
||||
assert "must be an array of objects" in result["error"]
|
||||
with pytest.raises(ValueError, match="Expected array of objects, got list"):
|
||||
WebhookService._validate_json_value("users", [{"name": "John"}, "not_object"], "array[object]")
|
||||
|
||||
def test_validate_webhook_request_json_type_validation(self):
|
||||
"""Test webhook validation with JSON parameter type validation."""
|
||||
# Test valid JSON types
|
||||
webhook_data = {
|
||||
"method": "POST",
|
||||
"headers": {"Content-Type": "application/json"},
|
||||
"query_params": {},
|
||||
"body": {
|
||||
"name": "John",
|
||||
"age": 30,
|
||||
"active": True,
|
||||
"profile": {"email": "john@example.com"},
|
||||
"tags": ["developer", "python"],
|
||||
"scores": [85, 92.5, 78],
|
||||
"flags": [True, False],
|
||||
"items": [{"id": 1}, {"id": 2}],
|
||||
},
|
||||
"files": {},
|
||||
}
|
||||
def test_convert_form_value_string(self):
|
||||
"""Test form value conversion for string type."""
|
||||
result = WebhookService._convert_form_value("test", "hello", "string")
|
||||
assert result == "hello"
|
||||
|
||||
node_config = {
|
||||
"data": {
|
||||
"method": "post",
|
||||
"content_type": "application/json",
|
||||
"body": [
|
||||
{"name": "name", "type": "string", "required": True},
|
||||
{"name": "age", "type": "number", "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[boolean]", "required": True},
|
||||
{"name": "items", "type": "array[object]", "required": True},
|
||||
],
|
||||
def test_convert_form_value_number(self):
|
||||
"""Test form value conversion for number type."""
|
||||
# Integer
|
||||
result = WebhookService._convert_form_value("count", "42", "number")
|
||||
assert result == 42
|
||||
|
||||
# Float
|
||||
result = WebhookService._convert_form_value("price", "19.99", "number")
|
||||
assert result == 19.99
|
||||
|
||||
# Invalid number
|
||||
with pytest.raises(ValueError, match="Cannot convert 'not_a_number' to number"):
|
||||
WebhookService._convert_form_value("count", "not_a_number", "number")
|
||||
|
||||
def test_convert_form_value_boolean(self):
|
||||
"""Test form value conversion for boolean type."""
|
||||
# True values
|
||||
assert WebhookService._convert_form_value("flag", "true", "boolean") is True
|
||||
assert WebhookService._convert_form_value("flag", "1", "boolean") is True
|
||||
assert WebhookService._convert_form_value("flag", "yes", "boolean") is True
|
||||
|
||||
# False values
|
||||
assert WebhookService._convert_form_value("flag", "false", "boolean") is False
|
||||
assert WebhookService._convert_form_value("flag", "0", "boolean") is False
|
||||
assert WebhookService._convert_form_value("flag", "no", "boolean") is False
|
||||
|
||||
# Invalid boolean
|
||||
with pytest.raises(ValueError, match="Cannot convert 'maybe' to boolean"):
|
||||
WebhookService._convert_form_value("flag", "maybe", "boolean")
|
||||
|
||||
def test_extract_and_validate_webhook_data_success(self):
|
||||
"""Test successful unified data extraction and validation."""
|
||||
app = Flask(__name__)
|
||||
|
||||
with app.test_request_context(
|
||||
"/webhook",
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/json"},
|
||||
query_string="count=42&enabled=true",
|
||||
json={"message": "hello", "age": 25},
|
||||
):
|
||||
webhook_trigger = MagicMock()
|
||||
node_config = {
|
||||
"data": {
|
||||
"method": "post",
|
||||
"content_type": "application/json",
|
||||
"params": [
|
||||
{"name": "count", "type": "number", "required": True},
|
||||
{"name": "enabled", "type": "boolean", "required": True},
|
||||
],
|
||||
"body": [
|
||||
{"name": "message", "type": "string", "required": True},
|
||||
{"name": "age", "type": "number", "required": True},
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result = WebhookService.validate_webhook_request(webhook_data, node_config)
|
||||
assert result["valid"] is True
|
||||
result = WebhookService.extract_and_validate_webhook_data(webhook_trigger, node_config)
|
||||
|
||||
def test_validate_webhook_request_json_type_validation_invalid(self):
|
||||
"""Test webhook validation with invalid JSON parameter types."""
|
||||
webhook_data = {
|
||||
"method": "POST",
|
||||
"headers": {"Content-Type": "application/json"},
|
||||
"query_params": {},
|
||||
"body": {
|
||||
"name": 123, # Should be string
|
||||
"age": "thirty", # Should be number
|
||||
},
|
||||
"files": {},
|
||||
}
|
||||
# Check that types are correctly converted
|
||||
assert result["query_params"]["count"] == 42 # Converted to int
|
||||
assert result["query_params"]["enabled"] is True # Converted to bool
|
||||
assert result["body"]["message"] == "hello" # Already string
|
||||
assert result["body"]["age"] == 25 # Already number
|
||||
|
||||
node_config = {
|
||||
"data": {
|
||||
"method": "post",
|
||||
"content_type": "application/json",
|
||||
"body": [
|
||||
{"name": "name", "type": "string", "required": True},
|
||||
{"name": "age", "type": "number", "required": True},
|
||||
],
|
||||
def test_extract_and_validate_webhook_data_validation_error(self):
|
||||
"""Test unified data extraction with validation error."""
|
||||
app = Flask(__name__)
|
||||
|
||||
with app.test_request_context(
|
||||
"/webhook",
|
||||
method="GET", # Wrong method
|
||||
headers={"Content-Type": "application/json"},
|
||||
):
|
||||
webhook_trigger = MagicMock()
|
||||
node_config = {
|
||||
"data": {
|
||||
"method": "post", # Expects POST
|
||||
"content_type": "application/json",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result = WebhookService.validate_webhook_request(webhook_data, node_config)
|
||||
assert result["valid"] is False
|
||||
assert "must be a string, got int" in result["error"]
|
||||
|
||||
def test_validate_webhook_request_non_json_skip_type_validation(self):
|
||||
"""Test that type validation is skipped for non-JSON content types."""
|
||||
webhook_data = {
|
||||
"method": "POST",
|
||||
"headers": {"Content-Type": "application/x-www-form-urlencoded"},
|
||||
"query_params": {},
|
||||
"body": {
|
||||
"name": 123, # Would be invalid for string if this was JSON
|
||||
},
|
||||
"files": {},
|
||||
}
|
||||
|
||||
node_config = {
|
||||
"data": {
|
||||
"method": "post",
|
||||
"content_type": "application/x-www-form-urlencoded",
|
||||
"body": [
|
||||
{"name": "name", "type": "string", "required": True},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
result = WebhookService.validate_webhook_request(webhook_data, node_config)
|
||||
assert result["valid"] is True # Should pass because type validation is only for JSON
|
||||
with pytest.raises(ValueError, match="HTTP method mismatch"):
|
||||
WebhookService.extract_and_validate_webhook_data(webhook_trigger, node_config)
|
||||
|
|
|
|||
Loading…
Reference in New Issue