mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 12:59:18 +08:00
Adapt the backend Graphon integration to the v0.3.0 breaking changes. Migrate provider factory and runtime usage, switch workflow node construction to the new data payload API, and refresh backend tests for the updated VariablePool and node behaviors.
745 lines
24 KiB
Python
745 lines
24 KiB
Python
import time
|
|
import uuid
|
|
from urllib.parse import urlencode
|
|
|
|
import pytest
|
|
|
|
from configs import dify_config
|
|
from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom
|
|
from core.helper.ssrf_proxy import ssrf_proxy
|
|
from core.tools.tool_file_manager import ToolFileManager
|
|
from core.workflow.node_factory import DifyNodeFactory
|
|
from core.workflow.node_runtime import DifyFileReferenceFactory
|
|
from core.workflow.system_variables import build_system_variables
|
|
from graphon.enums import WorkflowNodeExecutionStatus
|
|
from graphon.file.file_manager import file_manager
|
|
from graphon.graph import Graph
|
|
from graphon.nodes.http_request import HttpRequestNode, HttpRequestNodeConfig, HttpRequestNodeData
|
|
from graphon.runtime import GraphRuntimeState, VariablePool
|
|
from tests.workflow_test_utils import build_test_graph_init_params
|
|
|
|
pytest_plugins = ("tests.integration_tests.workflow.nodes.__mock.http",)
|
|
|
|
HTTP_REQUEST_CONFIG = HttpRequestNodeConfig(
|
|
max_connect_timeout=dify_config.HTTP_REQUEST_MAX_CONNECT_TIMEOUT,
|
|
max_read_timeout=dify_config.HTTP_REQUEST_MAX_READ_TIMEOUT,
|
|
max_write_timeout=dify_config.HTTP_REQUEST_MAX_WRITE_TIMEOUT,
|
|
max_binary_size=dify_config.HTTP_REQUEST_NODE_MAX_BINARY_SIZE,
|
|
max_text_size=dify_config.HTTP_REQUEST_NODE_MAX_TEXT_SIZE,
|
|
ssl_verify=dify_config.HTTP_REQUEST_NODE_SSL_VERIFY,
|
|
ssrf_default_max_retries=dify_config.SSRF_DEFAULT_MAX_RETRIES,
|
|
)
|
|
|
|
|
|
def init_http_node(config: dict):
|
|
graph_config = {
|
|
"edges": [
|
|
{
|
|
"id": "start-source-next-target",
|
|
"source": "start",
|
|
"target": "1",
|
|
},
|
|
],
|
|
"nodes": [{"data": {"type": "start", "title": "Start"}, "id": "start"}, config],
|
|
}
|
|
|
|
init_params = build_test_graph_init_params(
|
|
workflow_id="1",
|
|
graph_config=graph_config,
|
|
tenant_id="1",
|
|
app_id="1",
|
|
user_id="1",
|
|
user_from=UserFrom.ACCOUNT,
|
|
invoke_from=InvokeFrom.DEBUGGER,
|
|
call_depth=0,
|
|
)
|
|
|
|
# construct variable pool
|
|
variable_pool = VariablePool(
|
|
system_variables=build_system_variables(user_id="aaa", files=[]),
|
|
user_inputs={},
|
|
environment_variables=[],
|
|
conversation_variables=[],
|
|
)
|
|
variable_pool.add(["a", "args1"], 1)
|
|
variable_pool.add(["a", "args2"], 2)
|
|
|
|
graph_runtime_state = GraphRuntimeState(variable_pool=variable_pool, start_at=time.perf_counter())
|
|
|
|
# Create node factory
|
|
node_factory = DifyNodeFactory(
|
|
graph_init_params=init_params,
|
|
graph_runtime_state=graph_runtime_state,
|
|
)
|
|
|
|
graph = Graph.init(graph_config=graph_config, node_factory=node_factory, root_node_id="start")
|
|
|
|
node = HttpRequestNode(
|
|
node_id=str(uuid.uuid4()),
|
|
data=HttpRequestNodeData.model_validate(config["data"]),
|
|
graph_init_params=init_params,
|
|
graph_runtime_state=graph_runtime_state,
|
|
http_request_config=HTTP_REQUEST_CONFIG,
|
|
http_client=ssrf_proxy,
|
|
tool_file_manager_factory=ToolFileManager,
|
|
file_manager=file_manager,
|
|
file_reference_factory=DifyFileReferenceFactory(init_params.run_context),
|
|
)
|
|
|
|
return node
|
|
|
|
|
|
@pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
|
|
def test_get(setup_http_mock):
|
|
node = init_http_node(
|
|
config={
|
|
"id": "1",
|
|
"data": {
|
|
"type": "http-request",
|
|
"title": "http",
|
|
"desc": "",
|
|
"method": "get",
|
|
"url": "http://example.com",
|
|
"authorization": {
|
|
"type": "api-key",
|
|
"config": {
|
|
"type": "basic",
|
|
"api_key": "ak-xxx",
|
|
"header": "api-key",
|
|
},
|
|
},
|
|
"headers": "X-Header:123",
|
|
"params": "A:b",
|
|
"body": None,
|
|
},
|
|
}
|
|
)
|
|
|
|
result = node._run()
|
|
assert result.process_data is not None
|
|
data = result.process_data.get("request", "")
|
|
|
|
assert "?A=b" in data
|
|
assert "X-Header: 123" in data
|
|
|
|
|
|
@pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
|
|
def test_no_auth(setup_http_mock):
|
|
node = init_http_node(
|
|
config={
|
|
"id": "1",
|
|
"data": {
|
|
"type": "http-request",
|
|
"title": "http",
|
|
"desc": "",
|
|
"method": "get",
|
|
"url": "http://example.com",
|
|
"authorization": {
|
|
"type": "no-auth",
|
|
"config": None,
|
|
},
|
|
"headers": "X-Header:123",
|
|
"params": "A:b",
|
|
"body": None,
|
|
},
|
|
}
|
|
)
|
|
|
|
result = node._run()
|
|
assert result.process_data is not None
|
|
data = result.process_data.get("request", "")
|
|
|
|
assert "?A=b" in data
|
|
assert "X-Header: 123" in data
|
|
|
|
|
|
@pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
|
|
def test_custom_authorization_header(setup_http_mock):
|
|
node = init_http_node(
|
|
config={
|
|
"id": "1",
|
|
"data": {
|
|
"type": "http-request",
|
|
"title": "http",
|
|
"desc": "",
|
|
"method": "get",
|
|
"url": "http://example.com",
|
|
"authorization": {
|
|
"type": "api-key",
|
|
"config": {
|
|
"type": "custom",
|
|
"api_key": "Auth",
|
|
"header": "X-Auth",
|
|
},
|
|
},
|
|
"headers": "X-Header:123",
|
|
"params": "A:b",
|
|
"body": None,
|
|
},
|
|
}
|
|
)
|
|
|
|
result = node._run()
|
|
assert result.process_data is not None
|
|
data = result.process_data.get("request", "")
|
|
|
|
assert "?A=b" in data
|
|
assert "X-Header: 123" in data
|
|
# Custom authorization header should be set (may be masked)
|
|
assert "X-Auth:" in data
|
|
|
|
|
|
@pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
|
|
def test_custom_auth_with_empty_api_key_raises_error(setup_http_mock):
|
|
"""Test: In custom authentication mode, when the api_key is empty, AuthorizationConfigError should be raised."""
|
|
from core.workflow.system_variables import build_system_variables
|
|
from graphon.enums import BuiltinNodeTypes
|
|
from graphon.nodes.http_request.entities import (
|
|
HttpRequestNodeAuthorization,
|
|
HttpRequestNodeData,
|
|
HttpRequestNodeTimeout,
|
|
)
|
|
from graphon.nodes.http_request.exc import AuthorizationConfigError
|
|
from graphon.nodes.http_request.executor import Executor
|
|
from graphon.runtime import VariablePool
|
|
|
|
# Create variable pool
|
|
variable_pool = VariablePool(
|
|
system_variables=build_system_variables(user_id="test", files=[]),
|
|
user_inputs={},
|
|
environment_variables=[],
|
|
conversation_variables=[],
|
|
)
|
|
|
|
# Create node data with custom auth and empty api_key
|
|
node_data = HttpRequestNodeData(
|
|
type=BuiltinNodeTypes.HTTP_REQUEST,
|
|
title="http",
|
|
desc="",
|
|
url="http://example.com",
|
|
method="get",
|
|
authorization=HttpRequestNodeAuthorization(
|
|
type="api-key",
|
|
config={
|
|
"type": "custom",
|
|
"api_key": "", # Empty api_key
|
|
"header": "X-Custom-Auth",
|
|
},
|
|
),
|
|
headers="",
|
|
params="",
|
|
body=None,
|
|
ssl_verify=True,
|
|
)
|
|
|
|
# Create executor should raise AuthorizationConfigError
|
|
with pytest.raises(AuthorizationConfigError, match="API key is required"):
|
|
Executor(
|
|
node_data=node_data,
|
|
timeout=HttpRequestNodeTimeout(connect=10, read=30, write=10),
|
|
http_request_config=HTTP_REQUEST_CONFIG,
|
|
variable_pool=variable_pool,
|
|
http_client=ssrf_proxy,
|
|
file_manager=file_manager,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
|
|
def test_bearer_authorization_with_custom_header_ignored(setup_http_mock):
|
|
"""
|
|
Test that when switching from custom to bearer authorization,
|
|
the custom header settings don't interfere with bearer token.
|
|
This test verifies the fix for issue #23554.
|
|
"""
|
|
node = init_http_node(
|
|
config={
|
|
"id": "1",
|
|
"data": {
|
|
"type": "http-request",
|
|
"title": "http",
|
|
"desc": "",
|
|
"method": "get",
|
|
"url": "http://example.com",
|
|
"authorization": {
|
|
"type": "api-key",
|
|
"config": {
|
|
"type": "bearer",
|
|
"api_key": "test-token",
|
|
"header": "", # Empty header - should default to Authorization
|
|
},
|
|
},
|
|
"headers": "",
|
|
"params": "",
|
|
"body": None,
|
|
},
|
|
}
|
|
)
|
|
|
|
result = node._run()
|
|
assert result.process_data is not None
|
|
data = result.process_data.get("request", "")
|
|
|
|
# In bearer mode, should use Authorization header (value is masked with *)
|
|
assert "Authorization: " in data
|
|
# Should contain masked Bearer token
|
|
assert "*" in data
|
|
|
|
|
|
@pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
|
|
def test_basic_authorization_with_custom_header_ignored(setup_http_mock):
|
|
"""
|
|
Test that when switching from custom to basic authorization,
|
|
the custom header settings don't interfere with basic auth.
|
|
This test verifies the fix for issue #23554.
|
|
"""
|
|
node = init_http_node(
|
|
config={
|
|
"id": "1",
|
|
"data": {
|
|
"type": "http-request",
|
|
"title": "http",
|
|
"desc": "",
|
|
"method": "get",
|
|
"url": "http://example.com",
|
|
"authorization": {
|
|
"type": "api-key",
|
|
"config": {
|
|
"type": "basic",
|
|
"api_key": "user:pass",
|
|
"header": "", # Empty header - should default to Authorization
|
|
},
|
|
},
|
|
"headers": "",
|
|
"params": "",
|
|
"body": None,
|
|
},
|
|
}
|
|
)
|
|
|
|
result = node._run()
|
|
assert result.process_data is not None
|
|
data = result.process_data.get("request", "")
|
|
|
|
# In basic mode, should use Authorization header (value is masked with *)
|
|
assert "Authorization: " in data
|
|
# Should contain masked Basic credentials
|
|
assert "*" in data
|
|
|
|
|
|
@pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
|
|
def test_custom_authorization_with_empty_api_key(setup_http_mock):
|
|
"""
|
|
Test that custom authorization raises error when api_key is empty.
|
|
This test verifies the fix for issue #21830.
|
|
"""
|
|
|
|
node = init_http_node(
|
|
config={
|
|
"id": "1",
|
|
"data": {
|
|
"type": "http-request",
|
|
"title": "http",
|
|
"desc": "",
|
|
"method": "get",
|
|
"url": "http://example.com",
|
|
"authorization": {
|
|
"type": "api-key",
|
|
"config": {
|
|
"type": "custom",
|
|
"api_key": "", # Empty api_key
|
|
"header": "X-Custom-Auth",
|
|
},
|
|
},
|
|
"headers": "",
|
|
"params": "",
|
|
"body": None,
|
|
},
|
|
}
|
|
)
|
|
|
|
result = node._run()
|
|
# Should fail with AuthorizationConfigError
|
|
assert result.status == WorkflowNodeExecutionStatus.FAILED
|
|
assert "API key is required" in result.error
|
|
assert result.error_type == "AuthorizationConfigError"
|
|
|
|
|
|
@pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
|
|
def test_template(setup_http_mock):
|
|
node = init_http_node(
|
|
config={
|
|
"id": "1",
|
|
"data": {
|
|
"type": "http-request",
|
|
"title": "http",
|
|
"desc": "",
|
|
"method": "get",
|
|
"url": "http://example.com/{{#a.args2#}}",
|
|
"authorization": {
|
|
"type": "api-key",
|
|
"config": {
|
|
"type": "basic",
|
|
"api_key": "ak-xxx",
|
|
"header": "api-key",
|
|
},
|
|
},
|
|
"headers": "X-Header:123\nX-Header2:{{#a.args2#}}",
|
|
"params": "A:b\nTemplate:{{#a.args2#}}",
|
|
"body": None,
|
|
},
|
|
}
|
|
)
|
|
|
|
result = node._run()
|
|
assert result.process_data is not None
|
|
data = result.process_data.get("request", "")
|
|
|
|
assert "?A=b" in data
|
|
assert "Template=2" in data
|
|
assert "X-Header: 123" in data
|
|
assert "X-Header2: 2" in data
|
|
|
|
|
|
@pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
|
|
def test_json(setup_http_mock):
|
|
node = init_http_node(
|
|
config={
|
|
"id": "1",
|
|
"data": {
|
|
"type": "http-request",
|
|
"title": "http",
|
|
"desc": "",
|
|
"method": "post",
|
|
"url": "http://example.com",
|
|
"authorization": {
|
|
"type": "api-key",
|
|
"config": {
|
|
"type": "basic",
|
|
"api_key": "ak-xxx",
|
|
"header": "api-key",
|
|
},
|
|
},
|
|
"headers": "X-Header:123",
|
|
"params": "A:b",
|
|
"body": {
|
|
"type": "json",
|
|
"data": [
|
|
{
|
|
"key": "",
|
|
"type": "text",
|
|
"value": '{"a": "{{#a.args1#}}"}',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
}
|
|
)
|
|
|
|
result = node._run()
|
|
assert result.process_data is not None
|
|
data = result.process_data.get("request", "")
|
|
|
|
assert '{"a": "1"}' in data
|
|
assert "X-Header: 123" in data
|
|
|
|
|
|
@pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
|
|
def test_x_www_form_urlencoded(setup_http_mock):
|
|
node = init_http_node(
|
|
config={
|
|
"id": "1",
|
|
"data": {
|
|
"type": "http-request",
|
|
"title": "http",
|
|
"desc": "",
|
|
"method": "post",
|
|
"url": "http://example.com",
|
|
"authorization": {
|
|
"type": "api-key",
|
|
"config": {
|
|
"type": "basic",
|
|
"api_key": "ak-xxx",
|
|
"header": "api-key",
|
|
},
|
|
},
|
|
"headers": "X-Header:123",
|
|
"params": "A:b",
|
|
"body": {
|
|
"type": "x-www-form-urlencoded",
|
|
"data": [
|
|
{
|
|
"key": "a",
|
|
"type": "text",
|
|
"value": "{{#a.args1#}}",
|
|
},
|
|
{
|
|
"key": "b",
|
|
"type": "text",
|
|
"value": "{{#a.args2#}}",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
}
|
|
)
|
|
|
|
result = node._run()
|
|
assert result.process_data is not None
|
|
data = result.process_data.get("request", "")
|
|
|
|
assert "a=1&b=2" in data
|
|
assert "X-Header: 123" in data
|
|
|
|
|
|
@pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
|
|
def test_form_data(setup_http_mock):
|
|
node = init_http_node(
|
|
config={
|
|
"id": "1",
|
|
"data": {
|
|
"type": "http-request",
|
|
"title": "http",
|
|
"desc": "",
|
|
"method": "post",
|
|
"url": "http://example.com",
|
|
"authorization": {
|
|
"type": "api-key",
|
|
"config": {
|
|
"type": "basic",
|
|
"api_key": "ak-xxx",
|
|
"header": "api-key",
|
|
},
|
|
},
|
|
"headers": "X-Header:123",
|
|
"params": "A:b",
|
|
"body": {
|
|
"type": "form-data",
|
|
"data": [
|
|
{
|
|
"key": "a",
|
|
"type": "text",
|
|
"value": "{{#a.args1#}}",
|
|
},
|
|
{
|
|
"key": "b",
|
|
"type": "text",
|
|
"value": "{{#a.args2#}}",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
}
|
|
)
|
|
|
|
result = node._run()
|
|
assert result.process_data is not None
|
|
data = result.process_data.get("request", "")
|
|
|
|
assert 'form-data; name="a"' in data
|
|
assert "1" in data
|
|
assert 'form-data; name="b"' in data
|
|
assert "2" in data
|
|
assert "X-Header: 123" in data
|
|
|
|
|
|
@pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
|
|
def test_none_data(setup_http_mock):
|
|
node = init_http_node(
|
|
config={
|
|
"id": "1",
|
|
"data": {
|
|
"type": "http-request",
|
|
"title": "http",
|
|
"desc": "",
|
|
"method": "post",
|
|
"url": "http://example.com",
|
|
"authorization": {
|
|
"type": "api-key",
|
|
"config": {
|
|
"type": "basic",
|
|
"api_key": "ak-xxx",
|
|
"header": "api-key",
|
|
},
|
|
},
|
|
"headers": "X-Header:123",
|
|
"params": "A:b",
|
|
"body": {"type": "none", "data": []},
|
|
},
|
|
}
|
|
)
|
|
|
|
result = node._run()
|
|
assert result.process_data is not None
|
|
data = result.process_data.get("request", "")
|
|
|
|
assert "X-Header: 123" in data
|
|
assert "123123123" not in data
|
|
|
|
|
|
@pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
|
|
def test_mock_404(setup_http_mock):
|
|
node = init_http_node(
|
|
config={
|
|
"id": "1",
|
|
"data": {
|
|
"type": "http-request",
|
|
"title": "http",
|
|
"desc": "",
|
|
"method": "get",
|
|
"url": "http://404.com",
|
|
"authorization": {
|
|
"type": "no-auth",
|
|
"config": None,
|
|
},
|
|
"body": None,
|
|
"params": "",
|
|
"headers": "X-Header:123",
|
|
},
|
|
}
|
|
)
|
|
|
|
result = node._run()
|
|
assert result.outputs is not None
|
|
resp = result.outputs
|
|
|
|
assert resp.get("status_code") == 404
|
|
assert "Not Found" in resp.get("body", "")
|
|
|
|
|
|
@pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
|
|
def test_multi_colons_parse(setup_http_mock):
|
|
node = init_http_node(
|
|
config={
|
|
"id": "1",
|
|
"data": {
|
|
"type": "http-request",
|
|
"title": "http",
|
|
"desc": "",
|
|
"method": "get",
|
|
"url": "http://example.com",
|
|
"authorization": {
|
|
"type": "no-auth",
|
|
"config": None,
|
|
},
|
|
"params": "Referer:http://example1.com\nRedirect:http://example2.com",
|
|
"headers": "Referer:http://example3.com\nRedirect:http://example4.com",
|
|
"body": {
|
|
"type": "form-data",
|
|
"data": [
|
|
{
|
|
"key": "Referer",
|
|
"type": "text",
|
|
"value": "http://example5.com",
|
|
},
|
|
{
|
|
"key": "Redirect",
|
|
"type": "text",
|
|
"value": "http://example6.com",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
}
|
|
)
|
|
|
|
result = node._run()
|
|
assert result.process_data is not None
|
|
assert result.outputs is not None
|
|
|
|
assert urlencode({"Redirect": "http://example2.com"}) in result.process_data.get("request", "")
|
|
assert 'form-data; name="Redirect"\r\n\r\nhttp://example6.com' in result.process_data.get("request", "")
|
|
# resp = result.outputs
|
|
# assert "http://example3.com" == resp.get("headers", {}).get("referer")
|
|
|
|
|
|
@pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
|
|
def test_nested_object_variable_selector(setup_http_mock):
|
|
"""Test variable selector functionality with nested object properties."""
|
|
# Create independent test setup without affecting other tests
|
|
graph_config = {
|
|
"edges": [
|
|
{
|
|
"id": "start-source-next-target",
|
|
"source": "start",
|
|
"target": "1",
|
|
},
|
|
],
|
|
"nodes": [
|
|
{"data": {"type": "start", "title": "Start"}, "id": "start"},
|
|
{
|
|
"id": "1",
|
|
"data": {
|
|
"type": "http-request",
|
|
"title": "http",
|
|
"desc": "",
|
|
"method": "get",
|
|
"url": "http://example.com/{{#a.args2#}}/{{#a.args3.nested#}}",
|
|
"authorization": {
|
|
"type": "api-key",
|
|
"config": {
|
|
"type": "basic",
|
|
"api_key": "ak-xxx",
|
|
"header": "api-key",
|
|
},
|
|
},
|
|
"headers": "X-Header:{{#a.args3.nested#}}",
|
|
"params": "nested_param:{{#a.args3.nested#}}",
|
|
"body": None,
|
|
},
|
|
},
|
|
],
|
|
}
|
|
|
|
init_params = build_test_graph_init_params(
|
|
workflow_id="1",
|
|
graph_config=graph_config,
|
|
tenant_id="1",
|
|
app_id="1",
|
|
user_id="1",
|
|
user_from=UserFrom.ACCOUNT,
|
|
invoke_from=InvokeFrom.DEBUGGER,
|
|
call_depth=0,
|
|
)
|
|
|
|
# Create independent variable pool for this test only
|
|
variable_pool = VariablePool(
|
|
system_variables=build_system_variables(user_id="aaa", files=[]),
|
|
user_inputs={},
|
|
environment_variables=[],
|
|
conversation_variables=[],
|
|
)
|
|
variable_pool.add(["a", "args1"], 1)
|
|
variable_pool.add(["a", "args2"], 2)
|
|
variable_pool.add(["a", "args3"], {"nested": "nested_value"}) # Only for this test
|
|
|
|
graph_runtime_state = GraphRuntimeState(variable_pool=variable_pool, start_at=time.perf_counter())
|
|
|
|
# Create node factory
|
|
node_factory = DifyNodeFactory(
|
|
graph_init_params=init_params,
|
|
graph_runtime_state=graph_runtime_state,
|
|
)
|
|
|
|
graph = Graph.init(graph_config=graph_config, node_factory=node_factory, root_node_id="start")
|
|
|
|
node = HttpRequestNode(
|
|
node_id=str(uuid.uuid4()),
|
|
data=HttpRequestNodeData.model_validate(graph_config["nodes"][1]["data"]),
|
|
graph_init_params=init_params,
|
|
graph_runtime_state=graph_runtime_state,
|
|
http_request_config=HTTP_REQUEST_CONFIG,
|
|
http_client=ssrf_proxy,
|
|
tool_file_manager_factory=ToolFileManager,
|
|
file_manager=file_manager,
|
|
file_reference_factory=DifyFileReferenceFactory(init_params.run_context),
|
|
)
|
|
|
|
result = node._run()
|
|
assert result.process_data is not None
|
|
data = result.process_data.get("request", "")
|
|
|
|
# Verify nested object property is correctly resolved
|
|
assert "/2/nested_value" in data # URL path should contain resolved nested value
|
|
assert "X-Header: nested_value" in data # Header should contain nested value
|
|
assert "nested_param=nested_value" in data # Param should contain nested value
|