feat: introduce trigger functionality (#27644)
Signed-off-by: lyzno1 <yuanyouhuilyz@gmail.com> Co-authored-by: Stream <Stream_2@qq.com> Co-authored-by: lyzno1 <92089059+lyzno1@users.noreply.github.com> Co-authored-by: zhsama <torvalds@linux.do> Co-authored-by: Harry <xh001x@hotmail.com> Co-authored-by: lyzno1 <yuanyouhuilyz@gmail.com> Co-authored-by: yessenia <yessenia.contact@gmail.com> Co-authored-by: hjlarry <hjlarry@163.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: WTW0313 <twwu@dify.ai> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,569 @@
|
||||
import json
|
||||
from io import BytesIO
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from faker import Faker
|
||||
from flask import Flask
|
||||
from werkzeug.datastructures import FileStorage
|
||||
|
||||
from models.enums import AppTriggerStatus, AppTriggerType
|
||||
from models.model import App
|
||||
from models.trigger import AppTrigger, WorkflowWebhookTrigger
|
||||
from models.workflow import Workflow
|
||||
from services.account_service import AccountService, TenantService
|
||||
from services.trigger.webhook_service import WebhookService
|
||||
|
||||
|
||||
class TestWebhookService:
|
||||
"""Integration tests for WebhookService using testcontainers."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_external_dependencies(self):
|
||||
"""Mock external service dependencies."""
|
||||
with (
|
||||
patch("services.trigger.webhook_service.AsyncWorkflowService") as mock_async_service,
|
||||
patch("services.trigger.webhook_service.ToolFileManager") as mock_tool_file_manager,
|
||||
patch("services.trigger.webhook_service.file_factory") as mock_file_factory,
|
||||
patch("services.account_service.FeatureService") as mock_feature_service,
|
||||
):
|
||||
# Mock ToolFileManager
|
||||
mock_tool_file_instance = MagicMock()
|
||||
mock_tool_file_manager.return_value = mock_tool_file_instance
|
||||
|
||||
# Mock file creation
|
||||
mock_tool_file = MagicMock()
|
||||
mock_tool_file.id = "test_file_id"
|
||||
mock_tool_file_instance.create_file_by_raw.return_value = mock_tool_file
|
||||
|
||||
# Mock file factory
|
||||
mock_file_obj = MagicMock()
|
||||
mock_file_factory.build_from_mapping.return_value = mock_file_obj
|
||||
|
||||
# Mock feature service
|
||||
mock_feature_service.get_system_features.return_value.is_allow_register = True
|
||||
mock_feature_service.get_system_features.return_value.is_allow_create_workspace = True
|
||||
|
||||
yield {
|
||||
"async_service": mock_async_service,
|
||||
"tool_file_manager": mock_tool_file_manager,
|
||||
"file_factory": mock_file_factory,
|
||||
"tool_file": mock_tool_file,
|
||||
"file_obj": mock_file_obj,
|
||||
"feature_service": mock_feature_service,
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def test_data(self, db_session_with_containers, mock_external_dependencies):
|
||||
"""Create test data for webhook service tests."""
|
||||
fake = Faker()
|
||||
|
||||
# Create account and tenant
|
||||
account = AccountService.create_account(
|
||||
email=fake.email(),
|
||||
name=fake.name(),
|
||||
interface_language="en-US",
|
||||
password=fake.password(length=12),
|
||||
)
|
||||
TenantService.create_owner_tenant_if_not_exist(account, name=fake.company())
|
||||
tenant = account.current_tenant
|
||||
|
||||
# Create app
|
||||
app = App(
|
||||
tenant_id=tenant.id,
|
||||
name=fake.company(),
|
||||
description=fake.text(),
|
||||
mode="workflow",
|
||||
icon="",
|
||||
icon_background="",
|
||||
enable_site=True,
|
||||
enable_api=True,
|
||||
)
|
||||
db_session_with_containers.add(app)
|
||||
db_session_with_containers.flush()
|
||||
|
||||
# Create workflow
|
||||
workflow_data = {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "webhook_node",
|
||||
"type": "webhook",
|
||||
"data": {
|
||||
"title": "Test Webhook",
|
||||
"method": "post",
|
||||
"content_type": "application/json",
|
||||
"headers": [
|
||||
{"name": "Authorization", "required": True},
|
||||
{"name": "Content-Type", "required": False},
|
||||
],
|
||||
"params": [{"name": "version", "required": True}, {"name": "format", "required": False}],
|
||||
"body": [
|
||||
{"name": "message", "type": "string", "required": True},
|
||||
{"name": "count", "type": "number", "required": False},
|
||||
{"name": "upload", "type": "file", "required": False},
|
||||
],
|
||||
"status_code": 200,
|
||||
"response_body": '{"status": "success"}',
|
||||
"timeout": 30,
|
||||
},
|
||||
}
|
||||
],
|
||||
"edges": [],
|
||||
}
|
||||
|
||||
workflow = Workflow(
|
||||
tenant_id=tenant.id,
|
||||
app_id=app.id,
|
||||
type="workflow",
|
||||
graph=json.dumps(workflow_data),
|
||||
features=json.dumps({}),
|
||||
created_by=account.id,
|
||||
environment_variables=[],
|
||||
conversation_variables=[],
|
||||
version="1.0",
|
||||
)
|
||||
db_session_with_containers.add(workflow)
|
||||
db_session_with_containers.flush()
|
||||
|
||||
# Create webhook trigger
|
||||
webhook_id = fake.uuid4()[:16]
|
||||
webhook_trigger = WorkflowWebhookTrigger(
|
||||
app_id=app.id,
|
||||
node_id="webhook_node",
|
||||
tenant_id=tenant.id,
|
||||
webhook_id=webhook_id,
|
||||
created_by=account.id,
|
||||
)
|
||||
db_session_with_containers.add(webhook_trigger)
|
||||
db_session_with_containers.flush()
|
||||
|
||||
# Create app trigger (required for non-debug mode)
|
||||
app_trigger = AppTrigger(
|
||||
tenant_id=tenant.id,
|
||||
app_id=app.id,
|
||||
node_id="webhook_node",
|
||||
trigger_type=AppTriggerType.TRIGGER_WEBHOOK,
|
||||
title="Test Webhook",
|
||||
status=AppTriggerStatus.ENABLED,
|
||||
)
|
||||
db_session_with_containers.add(app_trigger)
|
||||
db_session_with_containers.commit()
|
||||
|
||||
return {
|
||||
"tenant": tenant,
|
||||
"account": account,
|
||||
"app": app,
|
||||
"workflow": workflow,
|
||||
"webhook_trigger": webhook_trigger,
|
||||
"webhook_id": webhook_id,
|
||||
"app_trigger": app_trigger,
|
||||
}
|
||||
|
||||
def test_get_webhook_trigger_and_workflow_success(self, test_data, flask_app_with_containers):
|
||||
"""Test successful retrieval of webhook trigger and workflow."""
|
||||
webhook_id = test_data["webhook_id"]
|
||||
|
||||
with flask_app_with_containers.app_context():
|
||||
webhook_trigger, workflow, node_config = WebhookService.get_webhook_trigger_and_workflow(webhook_id)
|
||||
|
||||
assert webhook_trigger is not None
|
||||
assert webhook_trigger.webhook_id == webhook_id
|
||||
assert workflow is not None
|
||||
assert workflow.app_id == test_data["app"].id
|
||||
assert node_config is not None
|
||||
assert node_config["id"] == "webhook_node"
|
||||
assert node_config["data"]["title"] == "Test Webhook"
|
||||
|
||||
def test_get_webhook_trigger_and_workflow_not_found(self, flask_app_with_containers):
|
||||
"""Test webhook trigger not found scenario."""
|
||||
with flask_app_with_containers.app_context():
|
||||
with pytest.raises(ValueError, match="Webhook not found"):
|
||||
WebhookService.get_webhook_trigger_and_workflow("nonexistent_webhook")
|
||||
|
||||
def test_extract_webhook_data_json(self):
|
||||
"""Test webhook data extraction from JSON request."""
|
||||
app = Flask(__name__)
|
||||
|
||||
with app.test_request_context(
|
||||
"/webhook",
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/json", "Authorization": "Bearer token"},
|
||||
query_string="version=1&format=json",
|
||||
json={"message": "hello", "count": 42},
|
||||
):
|
||||
webhook_trigger = MagicMock()
|
||||
webhook_data = WebhookService.extract_webhook_data(webhook_trigger)
|
||||
|
||||
assert webhook_data["method"] == "POST"
|
||||
assert webhook_data["headers"]["Authorization"] == "Bearer token"
|
||||
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_form_urlencoded(self):
|
||||
"""Test webhook data extraction from form URL encoded request."""
|
||||
app = Flask(__name__)
|
||||
|
||||
with app.test_request_context(
|
||||
"/webhook",
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
data={"username": "test", "password": "secret"},
|
||||
):
|
||||
webhook_trigger = MagicMock()
|
||||
webhook_data = WebhookService.extract_webhook_data(webhook_trigger)
|
||||
|
||||
assert webhook_data["method"] == "POST"
|
||||
assert webhook_data["body"]["username"] == "test"
|
||||
assert webhook_data["body"]["password"] == "secret"
|
||||
|
||||
def test_extract_webhook_data_multipart_with_files(self, mock_external_dependencies):
|
||||
"""Test webhook data extraction from multipart form with files."""
|
||||
app = Flask(__name__)
|
||||
|
||||
# Create a mock file
|
||||
file_content = b"test file content"
|
||||
file_storage = FileStorage(stream=BytesIO(file_content), filename="test.txt", content_type="text/plain")
|
||||
|
||||
with app.test_request_context(
|
||||
"/webhook",
|
||||
method="POST",
|
||||
headers={"Content-Type": "multipart/form-data"},
|
||||
data={"message": "test", "upload": file_storage},
|
||||
):
|
||||
webhook_trigger = MagicMock()
|
||||
webhook_trigger.tenant_id = "test_tenant"
|
||||
|
||||
webhook_data = WebhookService.extract_webhook_data(webhook_trigger)
|
||||
|
||||
assert webhook_data["method"] == "POST"
|
||||
assert webhook_data["body"]["message"] == "test"
|
||||
assert "upload" in webhook_data["files"]
|
||||
|
||||
# Verify file processing was called
|
||||
mock_external_dependencies["tool_file_manager"].assert_called_once()
|
||||
mock_external_dependencies["file_factory"].build_from_mapping.assert_called_once()
|
||||
|
||||
def test_extract_webhook_data_raw_text(self):
|
||||
"""Test webhook data extraction from raw text request."""
|
||||
app = Flask(__name__)
|
||||
|
||||
with app.test_request_context(
|
||||
"/webhook", method="POST", headers={"Content-Type": "text/plain"}, data="raw text content"
|
||||
):
|
||||
webhook_trigger = MagicMock()
|
||||
webhook_data = WebhookService.extract_webhook_data(webhook_trigger)
|
||||
|
||||
assert webhook_data["method"] == "POST"
|
||||
assert webhook_data["body"]["raw"] == "raw text content"
|
||||
|
||||
def test_extract_and_validate_webhook_request_success(self):
|
||||
"""Test successful webhook request validation and type conversion."""
|
||||
app = Flask(__name__)
|
||||
|
||||
with app.test_request_context(
|
||||
"/webhook",
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/json", "Authorization": "Bearer token"},
|
||||
query_string="version=1",
|
||||
json={"message": "hello"},
|
||||
):
|
||||
webhook_trigger = MagicMock()
|
||||
node_config = {
|
||||
"data": {
|
||||
"method": "post",
|
||||
"content_type": "application/json",
|
||||
"headers": [
|
||||
{"name": "Authorization", "required": True},
|
||||
{"name": "Content-Type", "required": False},
|
||||
],
|
||||
"params": [{"name": "version", "required": True}],
|
||||
"body": [{"name": "message", "type": "string", "required": True}],
|
||||
}
|
||||
}
|
||||
|
||||
result = WebhookService.extract_and_validate_webhook_data(webhook_trigger, node_config)
|
||||
|
||||
assert result["headers"]["Authorization"] == "Bearer token"
|
||||
assert result["query_params"]["version"] == "1"
|
||||
assert result["body"]["message"] == "hello"
|
||||
|
||||
def test_extract_and_validate_webhook_request_method_mismatch(self):
|
||||
"""Test webhook validation with HTTP method mismatch."""
|
||||
app = Flask(__name__)
|
||||
|
||||
with app.test_request_context(
|
||||
"/webhook",
|
||||
method="GET",
|
||||
headers={"Content-Type": "application/json"},
|
||||
):
|
||||
webhook_trigger = MagicMock()
|
||||
node_config = {"data": {"method": "post", "content_type": "application/json"}}
|
||||
|
||||
with pytest.raises(ValueError, match="HTTP method mismatch"):
|
||||
WebhookService.extract_and_validate_webhook_data(webhook_trigger, node_config)
|
||||
|
||||
def test_extract_and_validate_webhook_request_missing_required_header(self):
|
||||
"""Test webhook validation with missing required header."""
|
||||
app = Flask(__name__)
|
||||
|
||||
with app.test_request_context(
|
||||
"/webhook",
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/json"},
|
||||
):
|
||||
webhook_trigger = MagicMock()
|
||||
node_config = {
|
||||
"data": {
|
||||
"method": "post",
|
||||
"content_type": "application/json",
|
||||
"headers": [{"name": "Authorization", "required": True}],
|
||||
}
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match="Required header missing: Authorization"):
|
||||
WebhookService.extract_and_validate_webhook_data(webhook_trigger, node_config)
|
||||
|
||||
def test_extract_and_validate_webhook_request_case_insensitive_headers(self):
|
||||
"""Test webhook validation with case-insensitive header matching."""
|
||||
app = Flask(__name__)
|
||||
|
||||
with app.test_request_context(
|
||||
"/webhook",
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/json", "authorization": "Bearer token"},
|
||||
json={"message": "hello"},
|
||||
):
|
||||
webhook_trigger = MagicMock()
|
||||
node_config = {
|
||||
"data": {
|
||||
"method": "post",
|
||||
"content_type": "application/json",
|
||||
"headers": [{"name": "Authorization", "required": True}],
|
||||
"body": [{"name": "message", "type": "string", "required": True}],
|
||||
}
|
||||
}
|
||||
|
||||
result = WebhookService.extract_and_validate_webhook_data(webhook_trigger, node_config)
|
||||
|
||||
assert result["headers"].get("Authorization") == "Bearer token"
|
||||
|
||||
def test_extract_and_validate_webhook_request_missing_required_param(self):
|
||||
"""Test webhook validation with missing required query parameter."""
|
||||
app = Flask(__name__)
|
||||
|
||||
with app.test_request_context(
|
||||
"/webhook",
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/json"},
|
||||
json={"message": "hello"},
|
||||
):
|
||||
webhook_trigger = MagicMock()
|
||||
node_config = {
|
||||
"data": {
|
||||
"method": "post",
|
||||
"content_type": "application/json",
|
||||
"params": [{"name": "version", "required": True}],
|
||||
"body": [{"name": "message", "type": "string", "required": True}],
|
||||
}
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match="Required parameter missing: version"):
|
||||
WebhookService.extract_and_validate_webhook_data(webhook_trigger, node_config)
|
||||
|
||||
def test_extract_and_validate_webhook_request_missing_required_body_param(self):
|
||||
"""Test webhook validation with missing required body parameter."""
|
||||
app = Flask(__name__)
|
||||
|
||||
with app.test_request_context(
|
||||
"/webhook",
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/json"},
|
||||
json={},
|
||||
):
|
||||
webhook_trigger = MagicMock()
|
||||
node_config = {
|
||||
"data": {
|
||||
"method": "post",
|
||||
"content_type": "application/json",
|
||||
"body": [{"name": "message", "type": "string", "required": True}],
|
||||
}
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match="Required body parameter missing: message"):
|
||||
WebhookService.extract_and_validate_webhook_data(webhook_trigger, node_config)
|
||||
|
||||
def test_extract_and_validate_webhook_request_missing_required_file(self):
|
||||
"""Test webhook validation when required file is missing from multipart request."""
|
||||
app = Flask(__name__)
|
||||
|
||||
with app.test_request_context(
|
||||
"/webhook",
|
||||
method="POST",
|
||||
data={"note": "test"},
|
||||
content_type="multipart/form-data",
|
||||
):
|
||||
webhook_trigger = MagicMock()
|
||||
webhook_trigger.tenant_id = "tenant"
|
||||
webhook_trigger.created_by = "user"
|
||||
node_config = {
|
||||
"data": {
|
||||
"method": "post",
|
||||
"content_type": "multipart/form-data",
|
||||
"body": [{"name": "upload", "type": "file", "required": True}],
|
||||
}
|
||||
}
|
||||
|
||||
result = WebhookService.extract_and_validate_webhook_data(webhook_trigger, node_config)
|
||||
|
||||
assert result["files"] == {}
|
||||
|
||||
def test_trigger_workflow_execution_success(self, test_data, mock_external_dependencies, flask_app_with_containers):
|
||||
"""Test successful workflow execution trigger."""
|
||||
webhook_data = {
|
||||
"method": "POST",
|
||||
"headers": {"Authorization": "Bearer token"},
|
||||
"query_params": {"version": "1"},
|
||||
"body": {"message": "hello"},
|
||||
"files": {},
|
||||
}
|
||||
|
||||
with flask_app_with_containers.app_context():
|
||||
# Mock tenant owner lookup to return the test account
|
||||
with patch("services.trigger.webhook_service.select") as mock_select:
|
||||
mock_query = MagicMock()
|
||||
mock_select.return_value.join.return_value.where.return_value = mock_query
|
||||
|
||||
# Mock the session to return our test account
|
||||
with patch("services.trigger.webhook_service.Session") as mock_session:
|
||||
mock_session_instance = MagicMock()
|
||||
mock_session.return_value.__enter__.return_value = mock_session_instance
|
||||
mock_session_instance.scalar.return_value = test_data["account"]
|
||||
|
||||
# Should not raise any exceptions
|
||||
WebhookService.trigger_workflow_execution(
|
||||
test_data["webhook_trigger"], webhook_data, test_data["workflow"]
|
||||
)
|
||||
|
||||
# Verify AsyncWorkflowService was called
|
||||
mock_external_dependencies["async_service"].trigger_workflow_async.assert_called_once()
|
||||
|
||||
def test_trigger_workflow_execution_end_user_service_failure(
|
||||
self, test_data, mock_external_dependencies, flask_app_with_containers
|
||||
):
|
||||
"""Test workflow execution trigger when EndUserService fails."""
|
||||
webhook_data = {"method": "POST", "headers": {}, "query_params": {}, "body": {}, "files": {}}
|
||||
|
||||
with flask_app_with_containers.app_context():
|
||||
# Mock EndUserService to raise an exception
|
||||
with patch(
|
||||
"services.trigger.webhook_service.EndUserService.get_or_create_end_user_by_type"
|
||||
) as mock_end_user:
|
||||
mock_end_user.side_effect = ValueError("Failed to create end user")
|
||||
|
||||
with pytest.raises(ValueError, match="Failed to create end user"):
|
||||
WebhookService.trigger_workflow_execution(
|
||||
test_data["webhook_trigger"], webhook_data, test_data["workflow"]
|
||||
)
|
||||
|
||||
def test_generate_webhook_response_default(self):
|
||||
"""Test webhook response generation with default values."""
|
||||
node_config = {"data": {}}
|
||||
|
||||
response_data, status_code = WebhookService.generate_webhook_response(node_config)
|
||||
|
||||
assert status_code == 200
|
||||
assert response_data["status"] == "success"
|
||||
assert "Webhook processed successfully" in response_data["message"]
|
||||
|
||||
def test_generate_webhook_response_custom_json(self):
|
||||
"""Test webhook response generation with custom JSON response."""
|
||||
node_config = {"data": {"status_code": 201, "response_body": '{"result": "created", "id": 123}'}}
|
||||
|
||||
response_data, status_code = WebhookService.generate_webhook_response(node_config)
|
||||
|
||||
assert status_code == 201
|
||||
assert response_data["result"] == "created"
|
||||
assert response_data["id"] == 123
|
||||
|
||||
def test_generate_webhook_response_custom_text(self):
|
||||
"""Test webhook response generation with custom text response."""
|
||||
node_config = {"data": {"status_code": 202, "response_body": "Request accepted for processing"}}
|
||||
|
||||
response_data, status_code = WebhookService.generate_webhook_response(node_config)
|
||||
|
||||
assert status_code == 202
|
||||
assert response_data["message"] == "Request accepted for processing"
|
||||
|
||||
def test_generate_webhook_response_invalid_json(self):
|
||||
"""Test webhook response generation with invalid JSON response."""
|
||||
node_config = {"data": {"status_code": 400, "response_body": '{"invalid": json}'}}
|
||||
|
||||
response_data, status_code = WebhookService.generate_webhook_response(node_config)
|
||||
|
||||
assert status_code == 400
|
||||
assert response_data["message"] == '{"invalid": json}'
|
||||
|
||||
def test_process_file_uploads_success(self, mock_external_dependencies):
|
||||
"""Test successful file upload processing."""
|
||||
# Create mock files
|
||||
files = {
|
||||
"file1": MagicMock(filename="test1.txt", content_type="text/plain"),
|
||||
"file2": MagicMock(filename="test2.jpg", content_type="image/jpeg"),
|
||||
}
|
||||
|
||||
# Mock file reads
|
||||
files["file1"].read.return_value = b"content1"
|
||||
files["file2"].read.return_value = b"content2"
|
||||
|
||||
webhook_trigger = MagicMock()
|
||||
webhook_trigger.tenant_id = "test_tenant"
|
||||
|
||||
result = WebhookService._process_file_uploads(files, webhook_trigger)
|
||||
|
||||
assert len(result) == 2
|
||||
assert "file1" in result
|
||||
assert "file2" in result
|
||||
|
||||
# Verify file processing was called for each file
|
||||
assert mock_external_dependencies["tool_file_manager"].call_count == 2
|
||||
assert mock_external_dependencies["file_factory"].build_from_mapping.call_count == 2
|
||||
|
||||
def test_process_file_uploads_with_errors(self, mock_external_dependencies):
|
||||
"""Test file upload processing with errors."""
|
||||
# Create mock files, one will fail
|
||||
files = {
|
||||
"good_file": MagicMock(filename="test.txt", content_type="text/plain"),
|
||||
"bad_file": MagicMock(filename="test.bad", content_type="text/plain"),
|
||||
}
|
||||
|
||||
files["good_file"].read.return_value = b"content"
|
||||
files["bad_file"].read.side_effect = Exception("Read error")
|
||||
|
||||
webhook_trigger = MagicMock()
|
||||
webhook_trigger.tenant_id = "test_tenant"
|
||||
|
||||
result = WebhookService._process_file_uploads(files, webhook_trigger)
|
||||
|
||||
# Should process the good file and skip the bad one
|
||||
assert len(result) == 1
|
||||
assert "good_file" in result
|
||||
assert "bad_file" not in result
|
||||
|
||||
def test_process_file_uploads_empty_filename(self, mock_external_dependencies):
|
||||
"""Test file upload processing with empty filename."""
|
||||
files = {
|
||||
"no_filename": MagicMock(filename="", content_type="text/plain"),
|
||||
"none_filename": MagicMock(filename=None, content_type="text/plain"),
|
||||
}
|
||||
|
||||
webhook_trigger = MagicMock()
|
||||
webhook_trigger.tenant_id = "test_tenant"
|
||||
|
||||
result = WebhookService._process_file_uploads(files, webhook_trigger)
|
||||
|
||||
# Should skip files without filenames
|
||||
assert len(result) == 0
|
||||
mock_external_dependencies["tool_file_manager"].assert_not_called()
|
||||
@@ -584,7 +584,16 @@ class TestWorkflowService:
|
||||
account = self._create_test_account(db_session_with_containers, fake)
|
||||
app = self._create_test_app(db_session_with_containers, fake)
|
||||
|
||||
graph = {"nodes": [{"id": "start", "type": "start"}], "edges": []}
|
||||
graph = {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "start",
|
||||
"type": "start",
|
||||
"data": {"type": "start", "title": "Start"},
|
||||
}
|
||||
],
|
||||
"edges": [],
|
||||
}
|
||||
features = {"features": ["feature1", "feature2"]}
|
||||
# Don't pre-calculate hash, let the service generate it
|
||||
unique_hash = None
|
||||
@@ -632,7 +641,25 @@ class TestWorkflowService:
|
||||
# Get the actual hash that was generated
|
||||
original_hash = existing_workflow.unique_hash
|
||||
|
||||
new_graph = {"nodes": [{"id": "start", "type": "start"}, {"id": "end", "type": "end"}], "edges": []}
|
||||
new_graph = {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "start",
|
||||
"type": "start",
|
||||
"data": {"type": "start", "title": "Start"},
|
||||
},
|
||||
{
|
||||
"id": "end",
|
||||
"type": "end",
|
||||
"data": {
|
||||
"type": "end",
|
||||
"title": "End",
|
||||
"outputs": [{"variable": "output", "value_selector": ["start", "text"]}],
|
||||
},
|
||||
},
|
||||
],
|
||||
"edges": [],
|
||||
}
|
||||
new_features = {"features": ["feature1", "feature2", "feature3"]}
|
||||
|
||||
environment_variables = []
|
||||
@@ -679,7 +706,16 @@ class TestWorkflowService:
|
||||
# Get the actual hash that was generated
|
||||
original_hash = existing_workflow.unique_hash
|
||||
|
||||
new_graph = {"nodes": [{"id": "start", "type": "start"}], "edges": []}
|
||||
new_graph = {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "start",
|
||||
"type": "start",
|
||||
"data": {"type": "start", "title": "Start"},
|
||||
}
|
||||
],
|
||||
"edges": [],
|
||||
}
|
||||
new_features = {"features": ["feature1"]}
|
||||
# Use a different hash to trigger the error
|
||||
mismatched_hash = "different_hash_12345"
|
||||
|
||||
@@ -8,6 +8,7 @@ from core.tools.entities.common_entities import I18nObject
|
||||
from core.tools.entities.tool_entities import ToolProviderType
|
||||
from libs.uuid_utils import uuidv7
|
||||
from models.tools import ApiToolProvider, BuiltinToolProvider, MCPToolProvider, WorkflowToolProvider
|
||||
from services.plugin.plugin_service import PluginService
|
||||
from services.tools.tools_transform_service import ToolTransformService
|
||||
|
||||
|
||||
@@ -17,15 +18,14 @@ class TestToolTransformService:
|
||||
@pytest.fixture
|
||||
def mock_external_service_dependencies(self):
|
||||
"""Mock setup for external service dependencies."""
|
||||
with (
|
||||
patch("services.tools.tools_transform_service.dify_config") as mock_dify_config,
|
||||
):
|
||||
# Setup default mock returns
|
||||
mock_dify_config.CONSOLE_API_URL = "https://console.example.com"
|
||||
with patch("services.tools.tools_transform_service.dify_config") as mock_dify_config:
|
||||
with patch("services.plugin.plugin_service.dify_config", new=mock_dify_config):
|
||||
# Setup default mock returns
|
||||
mock_dify_config.CONSOLE_API_URL = "https://console.example.com"
|
||||
|
||||
yield {
|
||||
"dify_config": mock_dify_config,
|
||||
}
|
||||
yield {
|
||||
"dify_config": mock_dify_config,
|
||||
}
|
||||
|
||||
def _create_test_tool_provider(
|
||||
self, db_session_with_containers, mock_external_service_dependencies, provider_type="api"
|
||||
@@ -113,13 +113,13 @@ class TestToolTransformService:
|
||||
filename = "test_icon.png"
|
||||
|
||||
# Act: Execute the method under test
|
||||
result = ToolTransformService.get_plugin_icon_url(tenant_id, filename)
|
||||
result = PluginService.get_plugin_icon_url(str(tenant_id), filename)
|
||||
|
||||
# Assert: Verify the expected outcomes
|
||||
assert result is not None
|
||||
assert isinstance(result, str)
|
||||
assert "console/api/workspaces/current/plugin/icon" in result
|
||||
assert tenant_id in result
|
||||
assert str(tenant_id) in result
|
||||
assert filename in result
|
||||
assert result.startswith("https://console.example.com")
|
||||
|
||||
@@ -144,13 +144,13 @@ class TestToolTransformService:
|
||||
filename = "test_icon.png"
|
||||
|
||||
# Act: Execute the method under test
|
||||
result = ToolTransformService.get_plugin_icon_url(tenant_id, filename)
|
||||
result = PluginService.get_plugin_icon_url(str(tenant_id), filename)
|
||||
|
||||
# Assert: Verify the expected outcomes
|
||||
assert result is not None
|
||||
assert isinstance(result, str)
|
||||
assert result.startswith("/console/api/workspaces/current/plugin/icon")
|
||||
assert tenant_id in result
|
||||
assert str(tenant_id) in result
|
||||
assert filename in result
|
||||
|
||||
# Verify URL structure
|
||||
@@ -334,7 +334,7 @@ class TestToolTransformService:
|
||||
provider = {"type": ToolProviderType.BUILT_IN, "name": fake.company(), "icon": "🔧"}
|
||||
|
||||
# Act: Execute the method under test
|
||||
ToolTransformService.repack_provider(tenant_id, provider)
|
||||
ToolTransformService.repack_provider(str(tenant_id), provider)
|
||||
|
||||
# Assert: Verify the expected outcomes
|
||||
assert "icon" in provider
|
||||
@@ -358,7 +358,7 @@ class TestToolTransformService:
|
||||
|
||||
# Create provider entity with plugin_id
|
||||
provider = ToolProviderApiEntity(
|
||||
id=fake.uuid4(),
|
||||
id=str(fake.uuid4()),
|
||||
author=fake.name(),
|
||||
name=fake.company(),
|
||||
description=I18nObject(en_US=fake.text(max_nb_chars=100)),
|
||||
@@ -380,14 +380,14 @@ class TestToolTransformService:
|
||||
assert provider.icon is not None
|
||||
assert isinstance(provider.icon, str)
|
||||
assert "console/api/workspaces/current/plugin/icon" in provider.icon
|
||||
assert tenant_id in provider.icon
|
||||
assert str(tenant_id) in provider.icon
|
||||
assert "test_icon.png" in provider.icon
|
||||
|
||||
# Verify dark icon handling
|
||||
assert provider.icon_dark is not None
|
||||
assert isinstance(provider.icon_dark, str)
|
||||
assert "console/api/workspaces/current/plugin/icon" in provider.icon_dark
|
||||
assert tenant_id in provider.icon_dark
|
||||
assert str(tenant_id) in provider.icon_dark
|
||||
assert "test_icon_dark.png" in provider.icon_dark
|
||||
|
||||
def test_repack_provider_entity_no_plugin_success(
|
||||
@@ -423,7 +423,7 @@ class TestToolTransformService:
|
||||
)
|
||||
|
||||
# Act: Execute the method under test
|
||||
ToolTransformService.repack_provider(tenant_id, provider)
|
||||
ToolTransformService.repack_provider(str(tenant_id), provider)
|
||||
|
||||
# Assert: Verify the expected outcomes
|
||||
assert provider.icon is not None
|
||||
@@ -521,7 +521,7 @@ class TestToolTransformService:
|
||||
with patch("services.tools.tools_transform_service.create_provider_encrypter") as mock_encrypter:
|
||||
mock_encrypter_instance = Mock()
|
||||
mock_encrypter_instance.decrypt.return_value = {"api_key": "decrypted_key"}
|
||||
mock_encrypter_instance.mask_tool_credentials.return_value = {"api_key": ""}
|
||||
mock_encrypter_instance.mask_plugin_credentials.return_value = {"api_key": ""}
|
||||
mock_encrypter.return_value = (mock_encrypter_instance, None)
|
||||
|
||||
# Act: Execute the method under test
|
||||
|
||||
Reference in New Issue
Block a user