This commit is contained in:
2025-12-01 17:21:38 +08:00
parent 32fee2b8ab
commit fab8c13cb3
7511 changed files with 996300 additions and 0 deletions

View File

@@ -0,0 +1,550 @@
from unittest.mock import patch
import pytest
from faker import Faker
from models import Account, Tenant
from models.tools import ApiToolProvider
from services.tools.api_tools_manage_service import ApiToolManageService
class TestApiToolManageService:
"""Integration tests for ApiToolManageService using testcontainers."""
@pytest.fixture
def mock_external_service_dependencies(self):
"""Mock setup for external service dependencies."""
with (
patch("services.tools.api_tools_manage_service.ToolLabelManager") as mock_tool_label_manager,
patch("services.tools.api_tools_manage_service.create_tool_provider_encrypter") as mock_encrypter,
patch("services.tools.api_tools_manage_service.ApiToolProviderController") as mock_provider_controller,
):
# Setup default mock returns
mock_tool_label_manager.update_tool_labels.return_value = None
mock_encrypter.return_value = (mock_encrypter, None)
mock_encrypter.encrypt.return_value = {"encrypted": "credentials"}
mock_provider_controller.from_db.return_value = mock_provider_controller
mock_provider_controller.load_bundled_tools.return_value = None
yield {
"tool_label_manager": mock_tool_label_manager,
"encrypter": mock_encrypter,
"provider_controller": mock_provider_controller,
}
def _create_test_account_and_tenant(self, db_session_with_containers, mock_external_service_dependencies):
"""
Helper method to create a test account and tenant for testing.
Args:
db_session_with_containers: Database session from testcontainers infrastructure
mock_external_service_dependencies: Mock dependencies
Returns:
tuple: (account, tenant) - Created account and tenant instances
"""
fake = Faker()
# Create account
account = Account(
email=fake.email(),
name=fake.name(),
interface_language="en-US",
status="active",
)
from extensions.ext_database import db
db.session.add(account)
db.session.commit()
# Create tenant for the account
tenant = Tenant(
name=fake.company(),
status="normal",
)
db.session.add(tenant)
db.session.commit()
# Create tenant-account join
from models.account import TenantAccountJoin, TenantAccountRole
join = TenantAccountJoin(
tenant_id=tenant.id,
account_id=account.id,
role=TenantAccountRole.OWNER,
current=True,
)
db.session.add(join)
db.session.commit()
# Set current tenant for account
account.current_tenant = tenant
return account, tenant
def _create_test_openapi_schema(self):
"""Helper method to create a test OpenAPI schema."""
return """
{
"openapi": "3.0.0",
"info": {
"title": "Test API",
"version": "1.0.0",
"description": "Test API for testing purposes"
},
"servers": [
{
"url": "https://api.example.com",
"description": "Production server"
}
],
"paths": {
"/test": {
"get": {
"operationId": "testOperation",
"summary": "Test operation",
"responses": {
"200": {
"description": "Success"
}
}
}
}
}
}
"""
def test_parser_api_schema_success(
self, flask_req_ctx_with_containers, db_session_with_containers, mock_external_service_dependencies
):
"""
Test successful parsing of API schema.
This test verifies:
- Proper schema parsing with valid OpenAPI schema
- Correct credentials schema generation
- Proper warning handling
- Return value structure
"""
# Arrange: Create test schema
schema = self._create_test_openapi_schema()
# Act: Parse the schema
result = ApiToolManageService.parser_api_schema(schema)
# Assert: Verify the result structure
assert result is not None
assert "schema_type" in result
assert "parameters_schema" in result
assert "credentials_schema" in result
assert "warning" in result
# Verify credentials schema structure
credentials_schema = result["credentials_schema"]
assert len(credentials_schema) == 3
# Check auth_type field
auth_type_field = next(field for field in credentials_schema if field["name"] == "auth_type")
assert auth_type_field["required"] is True
assert auth_type_field["default"] == "none"
assert len(auth_type_field["options"]) == 2
# Check api_key_header field
api_key_header_field = next(field for field in credentials_schema if field["name"] == "api_key_header")
assert api_key_header_field["required"] is False
assert api_key_header_field["default"] == "api_key"
# Check api_key_value field
api_key_value_field = next(field for field in credentials_schema if field["name"] == "api_key_value")
assert api_key_value_field["required"] is False
assert api_key_value_field["default"] == ""
def test_parser_api_schema_invalid_schema(
self, flask_req_ctx_with_containers, db_session_with_containers, mock_external_service_dependencies
):
"""
Test parsing of invalid API schema.
This test verifies:
- Proper error handling for invalid schemas
- Correct exception type and message
- Error propagation from underlying parser
"""
# Arrange: Create invalid schema
invalid_schema = "invalid json schema"
# Act & Assert: Verify proper error handling
with pytest.raises(ValueError) as exc_info:
ApiToolManageService.parser_api_schema(invalid_schema)
assert "invalid schema" in str(exc_info.value)
def test_parser_api_schema_malformed_json(
self, flask_req_ctx_with_containers, db_session_with_containers, mock_external_service_dependencies
):
"""
Test parsing of malformed JSON schema.
This test verifies:
- Proper error handling for malformed JSON
- Correct exception type and message
- Error propagation from JSON parsing
"""
# Arrange: Create malformed JSON schema
malformed_schema = '{"openapi": "3.0.0", "info": {"title": "Test", "version": "1.0.0"}, "paths": {}}'
# Act & Assert: Verify proper error handling
with pytest.raises(ValueError) as exc_info:
ApiToolManageService.parser_api_schema(malformed_schema)
assert "invalid schema" in str(exc_info.value)
def test_convert_schema_to_tool_bundles_success(
self, flask_req_ctx_with_containers, db_session_with_containers, mock_external_service_dependencies
):
"""
Test successful conversion of schema to tool bundles.
This test verifies:
- Proper schema conversion with valid OpenAPI schema
- Correct tool bundles generation
- Proper schema type detection
- Return value structure
"""
# Arrange: Create test schema
schema = self._create_test_openapi_schema()
# Act: Convert schema to tool bundles
tool_bundles, schema_type = ApiToolManageService.convert_schema_to_tool_bundles(schema)
# Assert: Verify the result structure
assert tool_bundles is not None
assert isinstance(tool_bundles, list)
assert len(tool_bundles) > 0
assert schema_type is not None
assert isinstance(schema_type, str)
# Verify tool bundle structure
tool_bundle = tool_bundles[0]
assert hasattr(tool_bundle, "operation_id")
assert tool_bundle.operation_id == "testOperation"
def test_convert_schema_to_tool_bundles_with_extra_info(
self, flask_req_ctx_with_containers, db_session_with_containers, mock_external_service_dependencies
):
"""
Test successful conversion of schema to tool bundles with extra info.
This test verifies:
- Proper schema conversion with extra info parameter
- Correct tool bundles generation
- Extra info handling
- Return value structure
"""
# Arrange: Create test schema and extra info
schema = self._create_test_openapi_schema()
extra_info = {"description": "Custom description", "version": "2.0.0"}
# Act: Convert schema to tool bundles with extra info
tool_bundles, schema_type = ApiToolManageService.convert_schema_to_tool_bundles(schema, extra_info)
# Assert: Verify the result structure
assert tool_bundles is not None
assert isinstance(tool_bundles, list)
assert len(tool_bundles) > 0
assert schema_type is not None
assert isinstance(schema_type, str)
def test_convert_schema_to_tool_bundles_invalid_schema(
self, flask_req_ctx_with_containers, db_session_with_containers, mock_external_service_dependencies
):
"""
Test conversion of invalid schema to tool bundles.
This test verifies:
- Proper error handling for invalid schemas
- Correct exception type and message
- Error propagation from underlying parser
"""
# Arrange: Create invalid schema
invalid_schema = "invalid schema content"
# Act & Assert: Verify proper error handling
with pytest.raises(ValueError) as exc_info:
ApiToolManageService.convert_schema_to_tool_bundles(invalid_schema)
assert "invalid schema" in str(exc_info.value)
def test_create_api_tool_provider_success(
self, flask_req_ctx_with_containers, db_session_with_containers, mock_external_service_dependencies
):
"""
Test successful creation of API tool provider.
This test verifies:
- Proper provider creation with valid parameters
- Correct database state after creation
- Proper relationship establishment
- External service integration
- Return value correctness
"""
# Arrange: Create test data
fake = Faker()
account, tenant = self._create_test_account_and_tenant(
db_session_with_containers, mock_external_service_dependencies
)
provider_name = fake.company()
icon = {"type": "emoji", "value": "🔧"}
credentials = {"auth_type": "none", "api_key_header": "X-API-Key", "api_key_value": ""}
schema_type = "openapi"
schema = self._create_test_openapi_schema()
privacy_policy = "https://example.com/privacy"
custom_disclaimer = "Custom disclaimer text"
labels = ["test", "api"]
# Act: Create API tool provider
result = ApiToolManageService.create_api_tool_provider(
user_id=account.id,
tenant_id=tenant.id,
provider_name=provider_name,
icon=icon,
credentials=credentials,
schema_type=schema_type,
schema=schema,
privacy_policy=privacy_policy,
custom_disclaimer=custom_disclaimer,
labels=labels,
)
# Assert: Verify the result
assert result == {"result": "success"}
# Verify database state
from extensions.ext_database import db
provider = (
db.session.query(ApiToolProvider)
.filter(ApiToolProvider.tenant_id == tenant.id, ApiToolProvider.name == provider_name)
.first()
)
assert provider is not None
assert provider.name == provider_name
assert provider.tenant_id == tenant.id
assert provider.user_id == account.id
assert provider.schema_type_str == schema_type
assert provider.privacy_policy == privacy_policy
assert provider.custom_disclaimer == custom_disclaimer
# Verify mock interactions
mock_external_service_dependencies["tool_label_manager"].update_tool_labels.assert_called_once()
mock_external_service_dependencies["encrypter"].assert_called_once()
mock_external_service_dependencies["provider_controller"].from_db.assert_called_once()
mock_external_service_dependencies["provider_controller"].load_bundled_tools.assert_called_once()
def test_create_api_tool_provider_duplicate_name(
self, flask_req_ctx_with_containers, db_session_with_containers, mock_external_service_dependencies
):
"""
Test creation of API tool provider with duplicate name.
This test verifies:
- Proper error handling for duplicate provider names
- Correct exception type and message
- Database constraint enforcement
"""
# Arrange: Create test data and existing provider
fake = Faker()
account, tenant = self._create_test_account_and_tenant(
db_session_with_containers, mock_external_service_dependencies
)
provider_name = fake.company()
icon = {"type": "emoji", "value": "🔧"}
credentials = {"auth_type": "none"}
schema_type = "openapi"
schema = self._create_test_openapi_schema()
privacy_policy = "https://example.com/privacy"
custom_disclaimer = "Custom disclaimer text"
labels = ["test"]
# Create first provider
ApiToolManageService.create_api_tool_provider(
user_id=account.id,
tenant_id=tenant.id,
provider_name=provider_name,
icon=icon,
credentials=credentials,
schema_type=schema_type,
schema=schema,
privacy_policy=privacy_policy,
custom_disclaimer=custom_disclaimer,
labels=labels,
)
# Act & Assert: Try to create duplicate provider
with pytest.raises(ValueError) as exc_info:
ApiToolManageService.create_api_tool_provider(
user_id=account.id,
tenant_id=tenant.id,
provider_name=provider_name,
icon=icon,
credentials=credentials,
schema_type=schema_type,
schema=schema,
privacy_policy=privacy_policy,
custom_disclaimer=custom_disclaimer,
labels=labels,
)
assert f"provider {provider_name} already exists" in str(exc_info.value)
def test_create_api_tool_provider_invalid_schema_type(
self, flask_req_ctx_with_containers, db_session_with_containers, mock_external_service_dependencies
):
"""
Test creation of API tool provider with invalid schema type.
This test verifies:
- Proper error handling for invalid schema types
- Correct exception type and message
- Schema type validation
"""
# Arrange: Create test data with invalid schema type
fake = Faker()
account, tenant = self._create_test_account_and_tenant(
db_session_with_containers, mock_external_service_dependencies
)
provider_name = fake.company()
icon = {"type": "emoji", "value": "🔧"}
credentials = {"auth_type": "none"}
schema_type = "invalid_type"
schema = self._create_test_openapi_schema()
privacy_policy = "https://example.com/privacy"
custom_disclaimer = "Custom disclaimer text"
labels = ["test"]
# Act & Assert: Try to create provider with invalid schema type
with pytest.raises(ValueError) as exc_info:
ApiToolManageService.create_api_tool_provider(
user_id=account.id,
tenant_id=tenant.id,
provider_name=provider_name,
icon=icon,
credentials=credentials,
schema_type=schema_type,
schema=schema,
privacy_policy=privacy_policy,
custom_disclaimer=custom_disclaimer,
labels=labels,
)
assert "invalid schema type" in str(exc_info.value)
def test_create_api_tool_provider_missing_auth_type(
self, flask_req_ctx_with_containers, db_session_with_containers, mock_external_service_dependencies
):
"""
Test creation of API tool provider with missing auth type.
This test verifies:
- Proper error handling for missing auth type
- Correct exception type and message
- Credentials validation
"""
# Arrange: Create test data with missing auth type
fake = Faker()
account, tenant = self._create_test_account_and_tenant(
db_session_with_containers, mock_external_service_dependencies
)
provider_name = fake.company()
icon = {"type": "emoji", "value": "🔧"}
credentials = {} # Missing auth_type
schema_type = "openapi"
schema = self._create_test_openapi_schema()
privacy_policy = "https://example.com/privacy"
custom_disclaimer = "Custom disclaimer text"
labels = ["test"]
# Act & Assert: Try to create provider with missing auth type
with pytest.raises(ValueError) as exc_info:
ApiToolManageService.create_api_tool_provider(
user_id=account.id,
tenant_id=tenant.id,
provider_name=provider_name,
icon=icon,
credentials=credentials,
schema_type=schema_type,
schema=schema,
privacy_policy=privacy_policy,
custom_disclaimer=custom_disclaimer,
labels=labels,
)
assert "auth_type is required" in str(exc_info.value)
def test_create_api_tool_provider_with_api_key_auth(
self, flask_req_ctx_with_containers, db_session_with_containers, mock_external_service_dependencies
):
"""
Test successful creation of API tool provider with API key authentication.
This test verifies:
- Proper provider creation with API key auth
- Correct credentials handling
- Proper authentication type processing
"""
# Arrange: Create test data with API key auth
fake = Faker()
account, tenant = self._create_test_account_and_tenant(
db_session_with_containers, mock_external_service_dependencies
)
provider_name = fake.company()
icon = {"type": "emoji", "value": "🔑"}
credentials = {"auth_type": "api_key", "api_key_header": "X-API-Key", "api_key_value": fake.uuid4()}
schema_type = "openapi"
schema = self._create_test_openapi_schema()
privacy_policy = "https://example.com/privacy"
custom_disclaimer = "Custom disclaimer text"
labels = ["api_key", "secure"]
# Act: Create API tool provider
result = ApiToolManageService.create_api_tool_provider(
user_id=account.id,
tenant_id=tenant.id,
provider_name=provider_name,
icon=icon,
credentials=credentials,
schema_type=schema_type,
schema=schema,
privacy_policy=privacy_policy,
custom_disclaimer=custom_disclaimer,
labels=labels,
)
# Assert: Verify the result
assert result == {"result": "success"}
# Verify database state
from extensions.ext_database import db
provider = (
db.session.query(ApiToolProvider)
.filter(ApiToolProvider.tenant_id == tenant.id, ApiToolProvider.name == provider_name)
.first()
)
assert provider is not None
assert provider.name == provider_name
assert provider.tenant_id == tenant.id
assert provider.user_id == account.id
assert provider.schema_type_str == schema_type
# Verify mock interactions
mock_external_service_dependencies["encrypter"].assert_called_once()
mock_external_service_dependencies["provider_controller"].from_db.assert_called_once()

View File

@@ -0,0 +1,788 @@
from unittest.mock import Mock, patch
import pytest
from faker import Faker
from core.tools.entities.api_entities import ToolProviderApiEntity
from core.tools.entities.common_entities import I18nObject
from core.tools.entities.tool_entities import ToolProviderType
from models.tools import ApiToolProvider, BuiltinToolProvider, MCPToolProvider, WorkflowToolProvider
from services.plugin.plugin_service import PluginService
from services.tools.tools_transform_service import ToolTransformService
class TestToolTransformService:
"""Integration tests for ToolTransformService using testcontainers."""
@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:
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,
}
def _create_test_tool_provider(
self, db_session_with_containers, mock_external_service_dependencies, provider_type="api"
):
"""
Helper method to create a test tool provider for testing.
Args:
db_session_with_containers: Database session from testcontainers infrastructure
mock_external_service_dependencies: Mock dependencies
provider_type: Type of provider to create
Returns:
Tool provider instance
"""
fake = Faker()
if provider_type == "api":
provider = ApiToolProvider(
name=fake.company(),
description=fake.text(max_nb_chars=100),
icon='{"background": "#FF6B6B", "content": "🔧"}',
icon_dark='{"background": "#252525", "content": "🔧"}',
tenant_id="test_tenant_id",
user_id="test_user_id",
credentials={"auth_type": "api_key_header", "api_key": "test_key"},
provider_type="api",
)
elif provider_type == "builtin":
provider = BuiltinToolProvider(
name=fake.company(),
description=fake.text(max_nb_chars=100),
icon="🔧",
icon_dark="🔧",
tenant_id="test_tenant_id",
provider="test_provider",
credential_type="api_key",
credentials={"api_key": "test_key"},
)
elif provider_type == "workflow":
provider = WorkflowToolProvider(
name=fake.company(),
description=fake.text(max_nb_chars=100),
icon='{"background": "#FF6B6B", "content": "🔧"}',
icon_dark='{"background": "#252525", "content": "🔧"}',
tenant_id="test_tenant_id",
user_id="test_user_id",
workflow_id="test_workflow_id",
)
elif provider_type == "mcp":
provider = MCPToolProvider(
name=fake.company(),
description=fake.text(max_nb_chars=100),
provider_icon='{"background": "#FF6B6B", "content": "🔧"}',
tenant_id="test_tenant_id",
user_id="test_user_id",
server_url="https://mcp.example.com",
server_identifier="test_server",
tools='[{"name": "test_tool", "description": "Test tool"}]',
authed=True,
)
else:
raise ValueError(f"Unknown provider type: {provider_type}")
from extensions.ext_database import db
db.session.add(provider)
db.session.commit()
return provider
def test_get_plugin_icon_url_success(self, db_session_with_containers, mock_external_service_dependencies):
"""
Test successful plugin icon URL generation.
This test verifies:
- Proper URL construction for plugin icons
- Correct tenant_id and filename handling
- URL format compliance
"""
# Arrange: Setup test data
fake = Faker()
tenant_id = fake.uuid4()
filename = "test_icon.png"
# Act: Execute the method under test
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 str(tenant_id) in result
assert filename in result
assert result.startswith("https://console.example.com")
# Verify URL structure
expected_url = f"https://console.example.com/console/api/workspaces/current/plugin/icon?tenant_id={tenant_id}&filename={filename}"
assert result == expected_url
def test_get_plugin_icon_url_with_empty_console_url(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test plugin icon URL generation when CONSOLE_API_URL is empty.
This test verifies:
- Fallback to relative URL when CONSOLE_API_URL is None
- Proper URL construction with relative path
"""
# Arrange: Setup mock with empty console URL
mock_external_service_dependencies["dify_config"].CONSOLE_API_URL = None
fake = Faker()
tenant_id = fake.uuid4()
filename = "test_icon.png"
# Act: Execute the method under test
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 str(tenant_id) in result
assert filename in result
# Verify URL structure
expected_url = f"/console/api/workspaces/current/plugin/icon?tenant_id={tenant_id}&filename={filename}"
assert result == expected_url
def test_get_tool_provider_icon_url_builtin_success(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test successful tool provider icon URL generation for builtin providers.
This test verifies:
- Proper URL construction for builtin tool providers
- Correct provider type handling
- URL format compliance
"""
# Arrange: Setup test data
fake = Faker()
provider_type = ToolProviderType.BUILT_IN
provider_name = fake.company()
icon = "🔧"
# Act: Execute the method under test
result = ToolTransformService.get_tool_provider_icon_url(provider_type, provider_name, icon)
# Assert: Verify the expected outcomes
assert result is not None
assert isinstance(result, str)
assert "console/api/workspaces/current/tool-provider/builtin" in result
# Note: provider_name may contain spaces that get URL encoded
assert provider_name.replace(" ", "%20") in result or provider_name in result
assert result.endswith("/icon")
assert result.startswith("https://console.example.com")
# Verify URL structure (accounting for URL encoding)
# The actual result will have URL-encoded spaces (%20), so we need to compare accordingly
expected_url = (
f"https://console.example.com/console/api/workspaces/current/tool-provider/builtin/{provider_name}/icon"
)
# Convert expected URL to match the actual URL encoding
expected_encoded = expected_url.replace(" ", "%20")
assert result == expected_encoded
def test_get_tool_provider_icon_url_api_success(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test successful tool provider icon URL generation for API providers.
This test verifies:
- Proper icon handling for API tool providers
- JSON string parsing for icon data
- Fallback icon when parsing fails
"""
# Arrange: Setup test data
fake = Faker()
provider_type = ToolProviderType.API
provider_name = fake.company()
icon = '{"background": "#FF6B6B", "content": "🔧"}'
# Act: Execute the method under test
result = ToolTransformService.get_tool_provider_icon_url(provider_type, provider_name, icon)
# Assert: Verify the expected outcomes
assert result is not None
assert isinstance(result, dict)
assert result["background"] == "#FF6B6B"
assert result["content"] == "🔧"
def test_get_tool_provider_icon_url_api_invalid_json(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test tool provider icon URL generation for API providers with invalid JSON.
This test verifies:
- Proper fallback when JSON parsing fails
- Default icon structure when exception occurs
"""
# Arrange: Setup test data with invalid JSON
fake = Faker()
provider_type = ToolProviderType.API
provider_name = fake.company()
icon = '{"invalid": json}'
# Act: Execute the method under test
result = ToolTransformService.get_tool_provider_icon_url(provider_type, provider_name, icon)
# Assert: Verify the expected outcomes
assert result is not None
assert isinstance(result, dict)
assert result["background"] == "#252525"
# Note: emoji characters may be represented as Unicode escape sequences
assert result["content"] == "😁" or result["content"] == "\ud83d\ude01"
def test_get_tool_provider_icon_url_workflow_success(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test successful tool provider icon URL generation for workflow providers.
This test verifies:
- Proper icon handling for workflow tool providers
- Direct icon return for workflow type
"""
# Arrange: Setup test data
fake = Faker()
provider_type = ToolProviderType.WORKFLOW
provider_name = fake.company()
icon = {"background": "#FF6B6B", "content": "🔧"}
# Act: Execute the method under test
result = ToolTransformService.get_tool_provider_icon_url(provider_type, provider_name, icon)
# Assert: Verify the expected outcomes
assert result is not None
assert isinstance(result, dict)
assert result["background"] == "#FF6B6B"
assert result["content"] == "🔧"
def test_get_tool_provider_icon_url_mcp_success(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test successful tool provider icon URL generation for MCP providers.
This test verifies:
- Direct icon return for MCP type
- No URL transformation for MCP providers
"""
# Arrange: Setup test data
fake = Faker()
provider_type = ToolProviderType.MCP
provider_name = fake.company()
icon = {"background": "#FF6B6B", "content": "🔧"}
# Act: Execute the method under test
result = ToolTransformService.get_tool_provider_icon_url(provider_type, provider_name, icon)
# Assert: Verify the expected outcomes
assert result is not None
assert isinstance(result, dict)
assert result["background"] == "#FF6B6B"
assert result["content"] == "🔧"
def test_get_tool_provider_icon_url_unknown_type(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test tool provider icon URL generation for unknown provider types.
This test verifies:
- Empty string return for unknown provider types
- Proper handling of unsupported types
"""
# Arrange: Setup test data with unknown type
fake = Faker()
provider_type = "unknown_type"
provider_name = fake.company()
icon = "🔧"
# Act: Execute the method under test
result = ToolTransformService.get_tool_provider_icon_url(provider_type, provider_name, icon)
# Assert: Verify the expected outcomes
assert result == ""
def test_repack_provider_dict_success(self, db_session_with_containers, mock_external_service_dependencies):
"""
Test successful provider repacking with dictionary input.
This test verifies:
- Proper icon URL generation for dictionary providers
- Correct provider type handling
- Icon transformation for different provider types
"""
# Arrange: Setup test data
fake = Faker()
tenant_id = fake.uuid4()
provider = {"type": ToolProviderType.BUILT_IN, "name": fake.company(), "icon": "🔧"}
# Act: Execute the method under test
ToolTransformService.repack_provider(str(tenant_id), provider)
# Assert: Verify the expected outcomes
assert "icon" in provider
assert isinstance(provider["icon"], str)
assert "console/api/workspaces/current/tool-provider/builtin" in provider["icon"]
# Note: provider name may contain spaces that get URL encoded
assert provider["name"].replace(" ", "%20") in provider["icon"] or provider["name"] in provider["icon"]
def test_repack_provider_entity_success(self, db_session_with_containers, mock_external_service_dependencies):
"""
Test successful provider repacking with ToolProviderApiEntity input.
This test verifies:
- Proper icon URL generation for entity providers
- Plugin icon handling when plugin_id is present
- Regular icon handling when plugin_id is not present
"""
# Arrange: Setup test data
fake = Faker()
tenant_id = fake.uuid4()
# Create provider entity with plugin_id
provider = ToolProviderApiEntity(
id=str(fake.uuid4()),
author=fake.name(),
name=fake.company(),
description=I18nObject(en_US=fake.text(max_nb_chars=100)),
icon="test_icon.png",
icon_dark="test_icon_dark.png",
label=I18nObject(en_US=fake.company()),
type=ToolProviderType.API,
masked_credentials={},
is_team_authorization=True,
plugin_id="test_plugin_id",
tools=[],
labels=[],
)
# Act: Execute the method under test
ToolTransformService.repack_provider(tenant_id, provider)
# Assert: Verify the expected outcomes
assert provider.icon is not None
assert isinstance(provider.icon, str)
assert "console/api/workspaces/current/plugin/icon" 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 str(tenant_id) in provider.icon_dark
assert "test_icon_dark.png" in provider.icon_dark
def test_repack_provider_entity_no_plugin_success(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test successful provider repacking with ToolProviderApiEntity input without plugin_id.
This test verifies:
- Proper icon URL generation for non-plugin providers
- Regular tool provider icon handling
- Dark icon handling when present
"""
# Arrange: Setup test data
fake = Faker()
tenant_id = fake.uuid4()
# Create provider entity without plugin_id
provider = ToolProviderApiEntity(
id=fake.uuid4(),
author=fake.name(),
name=fake.company(),
description=I18nObject(en_US=fake.text(max_nb_chars=100)),
icon='{"background": "#FF6B6B", "content": "🔧"}',
icon_dark='{"background": "#252525", "content": "🔧"}',
label=I18nObject(en_US=fake.company()),
type=ToolProviderType.API,
masked_credentials={},
is_team_authorization=True,
plugin_id=None,
tools=[],
labels=[],
)
# Act: Execute the method under test
ToolTransformService.repack_provider(str(tenant_id), provider)
# Assert: Verify the expected outcomes
assert provider.icon is not None
assert isinstance(provider.icon, dict)
assert provider.icon["background"] == "#FF6B6B"
assert provider.icon["content"] == "🔧"
# Verify dark icon handling
assert provider.icon_dark is not None
assert isinstance(provider.icon_dark, dict)
assert provider.icon_dark["background"] == "#252525"
assert provider.icon_dark["content"] == "🔧"
def test_repack_provider_entity_no_dark_icon(self, db_session_with_containers, mock_external_service_dependencies):
"""
Test provider repacking with ToolProviderApiEntity input without dark icon.
This test verifies:
- Proper handling when icon_dark is None or empty
- No errors when dark icon is not present
"""
# Arrange: Setup test data
fake = Faker()
tenant_id = fake.uuid4()
# Create provider entity without dark icon
provider = ToolProviderApiEntity(
id=fake.uuid4(),
author=fake.name(),
name=fake.company(),
description=I18nObject(en_US=fake.text(max_nb_chars=100)),
icon='{"background": "#FF6B6B", "content": "🔧"}',
icon_dark="",
label=I18nObject(en_US=fake.company()),
type=ToolProviderType.API,
masked_credentials={},
is_team_authorization=True,
plugin_id=None,
tools=[],
labels=[],
)
# Act: Execute the method under test
ToolTransformService.repack_provider(tenant_id, provider)
# Assert: Verify the expected outcomes
assert provider.icon is not None
assert isinstance(provider.icon, dict)
assert provider.icon["background"] == "#FF6B6B"
assert provider.icon["content"] == "🔧"
# Verify dark icon remains empty string
assert provider.icon_dark == ""
def test_builtin_provider_to_user_provider_success(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test successful conversion of builtin provider to user provider.
This test verifies:
- Proper entity creation with all required fields
- Credentials schema handling
- Team authorization setup
- Plugin ID handling
"""
# Arrange: Setup test data
fake = Faker()
# Create mock provider controller
mock_controller = Mock()
mock_controller.entity.identity.name = fake.company()
mock_controller.entity.identity.author = fake.name()
mock_controller.entity.identity.description = I18nObject(en_US=fake.text(max_nb_chars=100))
mock_controller.entity.identity.icon = "🔧"
mock_controller.entity.identity.icon_dark = "🔧"
mock_controller.entity.identity.label = I18nObject(en_US=fake.company())
mock_controller.plugin_id = None
mock_controller.plugin_unique_identifier = None
mock_controller.tool_labels = ["label1", "label2"]
mock_controller.need_credentials = True
# Mock credentials schema
mock_credential = Mock()
mock_credential.to_basic_provider_config.return_value.name = "api_key"
mock_controller.get_credentials_schema_by_type.return_value = [mock_credential]
# Create mock database provider
mock_db_provider = Mock()
mock_db_provider.credential_type = "api-key"
mock_db_provider.tenant_id = fake.uuid4()
mock_db_provider.credentials = {"api_key": "encrypted_key"}
# Mock encryption
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_plugin_credentials.return_value = {"api_key": ""}
mock_encrypter.return_value = (mock_encrypter_instance, None)
# Act: Execute the method under test
result = ToolTransformService.builtin_provider_to_user_provider(
mock_controller, mock_db_provider, decrypt_credentials=True
)
# Assert: Verify the expected outcomes
assert result is not None
assert result.id == mock_controller.entity.identity.name
assert result.author == mock_controller.entity.identity.author
assert result.name == mock_controller.entity.identity.name
assert result.description == mock_controller.entity.identity.description
assert result.icon == mock_controller.entity.identity.icon
assert result.icon_dark == mock_controller.entity.identity.icon_dark
assert result.label == mock_controller.entity.identity.label
assert result.type == ToolProviderType.BUILT_IN
assert result.is_team_authorization is True
assert result.plugin_id is None
assert result.tools == []
assert result.labels == ["label1", "label2"]
assert result.masked_credentials == {"api_key": ""}
assert result.original_credentials == {"api_key": "decrypted_key"}
def test_builtin_provider_to_user_provider_plugin_success(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test successful conversion of builtin provider to user provider with plugin.
This test verifies:
- Plugin ID and unique identifier handling
- Proper entity creation for plugin providers
"""
# Arrange: Setup test data
fake = Faker()
# Create mock provider controller with plugin
mock_controller = Mock()
mock_controller.entity.identity.name = fake.company()
mock_controller.entity.identity.author = fake.name()
mock_controller.entity.identity.description = I18nObject(en_US=fake.text(max_nb_chars=100))
mock_controller.entity.identity.icon = "🔧"
mock_controller.entity.identity.icon_dark = "🔧"
mock_controller.entity.identity.label = I18nObject(en_US=fake.company())
mock_controller.plugin_id = "test_plugin_id"
mock_controller.plugin_unique_identifier = "test_unique_id"
mock_controller.tool_labels = ["label1"]
mock_controller.need_credentials = False
# Mock credentials schema
mock_credential = Mock()
mock_credential.to_basic_provider_config.return_value.name = "api_key"
mock_controller.get_credentials_schema_by_type.return_value = [mock_credential]
# Act: Execute the method under test
result = ToolTransformService.builtin_provider_to_user_provider(
mock_controller, None, decrypt_credentials=False
)
# Assert: Verify the expected outcomes
assert result is not None
# Note: The method checks isinstance(provider_controller, PluginToolProviderController)
# Since we're using a Mock, this check will fail, so plugin_id will remain None
# In a real test with actual PluginToolProviderController, this would work
assert result.is_team_authorization is True
assert result.allow_delete is False
def test_builtin_provider_to_user_provider_no_credentials(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test conversion of builtin provider to user provider without credentials.
This test verifies:
- Proper handling when no credentials are needed
- Team authorization setup for no-credentials providers
"""
# Arrange: Setup test data
fake = Faker()
# Create mock provider controller
mock_controller = Mock()
mock_controller.entity.identity.name = fake.company()
mock_controller.entity.identity.author = fake.name()
mock_controller.entity.identity.description = I18nObject(en_US=fake.text(max_nb_chars=100))
mock_controller.entity.identity.icon = "🔧"
mock_controller.entity.identity.icon_dark = "🔧"
mock_controller.entity.identity.label = I18nObject(en_US=fake.company())
mock_controller.plugin_id = None
mock_controller.plugin_unique_identifier = None
mock_controller.tool_labels = []
mock_controller.need_credentials = False
# Mock credentials schema
mock_credential = Mock()
mock_credential.to_basic_provider_config.return_value.name = "api_key"
mock_controller.get_credentials_schema_by_type.return_value = [mock_credential]
# Act: Execute the method under test
result = ToolTransformService.builtin_provider_to_user_provider(
mock_controller, None, decrypt_credentials=False
)
# Assert: Verify the expected outcomes
assert result is not None
assert result.is_team_authorization is True
assert result.allow_delete is False
assert result.masked_credentials == {"api_key": ""}
def test_api_provider_to_controller_success(self, db_session_with_containers, mock_external_service_dependencies):
"""
Test successful conversion of API provider to controller.
This test verifies:
- Proper controller creation from database provider
- Auth type handling for different credential types
- Backward compatibility for auth types
"""
# Arrange: Setup test data
fake = Faker()
# Create API tool provider with api_key_header auth
provider = ApiToolProvider(
name=fake.company(),
description=fake.text(max_nb_chars=100),
icon='{"background": "#FF6B6B", "content": "🔧"}',
tenant_id=fake.uuid4(),
user_id=fake.uuid4(),
credentials_str='{"auth_type": "api_key_header", "api_key": "test_key"}',
schema="{}",
schema_type_str="openapi",
tools_str="[]",
)
from extensions.ext_database import db
db.session.add(provider)
db.session.commit()
# Act: Execute the method under test
result = ToolTransformService.api_provider_to_controller(provider)
# Assert: Verify the expected outcomes
assert result is not None
assert hasattr(result, "from_db")
# Additional assertions would depend on the actual controller implementation
def test_api_provider_to_controller_api_key_query(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test conversion of API provider to controller with api_key_query auth type.
This test verifies:
- Proper auth type handling for query parameter authentication
"""
# Arrange: Setup test data
fake = Faker()
# Create API tool provider with api_key_query auth
provider = ApiToolProvider(
name=fake.company(),
description=fake.text(max_nb_chars=100),
icon='{"background": "#FF6B6B", "content": "🔧"}',
tenant_id=fake.uuid4(),
user_id=fake.uuid4(),
credentials_str='{"auth_type": "api_key_query", "api_key": "test_key"}',
schema="{}",
schema_type_str="openapi",
tools_str="[]",
)
from extensions.ext_database import db
db.session.add(provider)
db.session.commit()
# Act: Execute the method under test
result = ToolTransformService.api_provider_to_controller(provider)
# Assert: Verify the expected outcomes
assert result is not None
assert hasattr(result, "from_db")
def test_api_provider_to_controller_backward_compatibility(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test conversion of API provider to controller with backward compatibility auth types.
This test verifies:
- Proper handling of legacy auth type values
- Backward compatibility for api_key and api_key_header
"""
# Arrange: Setup test data
fake = Faker()
# Create API tool provider with legacy auth type
provider = ApiToolProvider(
name=fake.company(),
description=fake.text(max_nb_chars=100),
icon='{"background": "#FF6B6B", "content": "🔧"}',
tenant_id=fake.uuid4(),
user_id=fake.uuid4(),
credentials_str='{"auth_type": "api_key", "api_key": "test_key"}',
schema="{}",
schema_type_str="openapi",
tools_str="[]",
)
from extensions.ext_database import db
db.session.add(provider)
db.session.commit()
# Act: Execute the method under test
result = ToolTransformService.api_provider_to_controller(provider)
# Assert: Verify the expected outcomes
assert result is not None
assert hasattr(result, "from_db")
def test_workflow_provider_to_controller_success(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test successful conversion of workflow provider to controller.
This test verifies:
- Proper controller creation from workflow provider
- Workflow-specific controller handling
"""
# Arrange: Setup test data
fake = Faker()
# Create workflow tool provider
provider = WorkflowToolProvider(
name=fake.company(),
description=fake.text(max_nb_chars=100),
icon='{"background": "#FF6B6B", "content": "🔧"}',
tenant_id=fake.uuid4(),
user_id=fake.uuid4(),
app_id=fake.uuid4(),
label="Test Workflow",
version="1.0.0",
parameter_configuration="[]",
)
from extensions.ext_database import db
db.session.add(provider)
db.session.commit()
# Mock the WorkflowToolProviderController.from_db method to avoid app dependency
with patch("services.tools.tools_transform_service.WorkflowToolProviderController.from_db") as mock_from_db:
mock_controller = Mock()
mock_from_db.return_value = mock_controller
# Act: Execute the method under test
result = ToolTransformService.workflow_provider_to_controller(provider)
# Assert: Verify the expected outcomes
assert result is not None
assert result == mock_controller
mock_from_db.assert_called_once_with(provider)

View File

@@ -0,0 +1,716 @@
import json
from unittest.mock import patch
import pytest
from faker import Faker
from models.tools import WorkflowToolProvider
from models.workflow import Workflow as WorkflowModel
from services.account_service import AccountService, TenantService
from services.app_service import AppService
from services.tools.workflow_tools_manage_service import WorkflowToolManageService
class TestWorkflowToolManageService:
"""Integration tests for WorkflowToolManageService using testcontainers."""
@pytest.fixture
def mock_external_service_dependencies(self):
"""Mock setup for external service dependencies."""
with (
patch("services.app_service.FeatureService") as mock_feature_service,
patch("services.app_service.EnterpriseService") as mock_enterprise_service,
patch("services.app_service.ModelManager") as mock_model_manager,
patch("services.account_service.FeatureService") as mock_account_feature_service,
patch(
"services.tools.workflow_tools_manage_service.WorkflowToolProviderController"
) as mock_workflow_tool_provider_controller,
patch("services.tools.workflow_tools_manage_service.ToolLabelManager") as mock_tool_label_manager,
patch("services.tools.workflow_tools_manage_service.ToolTransformService") as mock_tool_transform_service,
):
# Setup default mock returns for app service
mock_feature_service.get_system_features.return_value.webapp_auth.enabled = False
mock_enterprise_service.WebAppAuth.update_app_access_mode.return_value = None
mock_enterprise_service.WebAppAuth.cleanup_webapp.return_value = None
# Setup default mock returns for account service
mock_account_feature_service.get_system_features.return_value.is_allow_register = True
# Mock ModelManager for model configuration
mock_model_instance = mock_model_manager.return_value
mock_model_instance.get_default_model_instance.return_value = None
mock_model_instance.get_default_provider_model_name.return_value = ("openai", "gpt-3.5-turbo")
# Mock WorkflowToolProviderController
mock_workflow_tool_provider_controller.from_db.return_value = None
# Mock ToolLabelManager
mock_tool_label_manager.update_tool_labels.return_value = None
# Mock ToolTransformService
mock_tool_transform_service.workflow_provider_to_controller.return_value = None
yield {
"feature_service": mock_feature_service,
"enterprise_service": mock_enterprise_service,
"model_manager": mock_model_manager,
"account_feature_service": mock_account_feature_service,
"workflow_tool_provider_controller": mock_workflow_tool_provider_controller,
"tool_label_manager": mock_tool_label_manager,
"tool_transform_service": mock_tool_transform_service,
}
def _create_test_app_and_account(self, db_session_with_containers, mock_external_service_dependencies):
"""
Helper method to create a test app and account for testing.
Args:
db_session_with_containers: Database session from testcontainers infrastructure
mock_external_service_dependencies: Mock dependencies
Returns:
tuple: (app, account, workflow) - Created app, account and workflow instances
"""
fake = Faker()
# Setup mocks for account creation
mock_external_service_dependencies[
"account_feature_service"
].get_system_features.return_value.is_allow_register = True
# 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 with realistic data
app_args = {
"name": fake.company(),
"description": fake.text(max_nb_chars=100),
"mode": "workflow",
"icon_type": "emoji",
"icon": "🤖",
"icon_background": "#FF6B6B",
"api_rph": 100,
"api_rpm": 10,
}
app_service = AppService()
app = app_service.create_app(tenant.id, app_args, account)
# Create workflow for the app
workflow = WorkflowModel(
tenant_id=tenant.id,
app_id=app.id,
type="workflow",
version="1.0.0",
graph=json.dumps({}),
features=json.dumps({}),
created_by=account.id,
environment_variables=[],
conversation_variables=[],
)
from extensions.ext_database import db
db.session.add(workflow)
db.session.commit()
# Update app to reference the workflow
app.workflow_id = workflow.id
db.session.commit()
return app, account, workflow
def _create_test_workflow_tool_parameters(self):
"""Helper method to create valid workflow tool parameters."""
return [
{
"name": "input_text",
"description": "Input text for processing",
"form": "form",
"type": "string",
"required": True,
},
{
"name": "output_format",
"description": "Output format specification",
"form": "form",
"type": "select",
"required": False,
},
]
def test_create_workflow_tool_success(self, db_session_with_containers, mock_external_service_dependencies):
"""
Test successful workflow tool creation with valid parameters.
This test verifies:
- Proper workflow tool creation with all required fields
- Correct database state after creation
- Proper relationship establishment
- External service integration
- Return value correctness
"""
fake = Faker()
# Create test data
app, account, workflow = self._create_test_app_and_account(
db_session_with_containers, mock_external_service_dependencies
)
# Setup workflow tool creation parameters
tool_name = fake.word()
tool_label = fake.word()
tool_icon = {"type": "emoji", "emoji": "🔧"}
tool_description = fake.text(max_nb_chars=200)
tool_parameters = self._create_test_workflow_tool_parameters()
tool_privacy_policy = fake.text(max_nb_chars=100)
tool_labels = ["automation", "workflow"]
# Execute the method under test
result = WorkflowToolManageService.create_workflow_tool(
user_id=account.id,
tenant_id=account.current_tenant.id,
workflow_app_id=app.id,
name=tool_name,
label=tool_label,
icon=tool_icon,
description=tool_description,
parameters=tool_parameters,
privacy_policy=tool_privacy_policy,
labels=tool_labels,
)
# Verify the result
assert result == {"result": "success"}
# Verify database state
from extensions.ext_database import db
# Check if workflow tool provider was created
created_tool_provider = (
db.session.query(WorkflowToolProvider)
.where(
WorkflowToolProvider.tenant_id == account.current_tenant.id,
WorkflowToolProvider.app_id == app.id,
)
.first()
)
assert created_tool_provider is not None
assert created_tool_provider.name == tool_name
assert created_tool_provider.label == tool_label
assert created_tool_provider.icon == json.dumps(tool_icon)
assert created_tool_provider.description == tool_description
assert created_tool_provider.parameter_configuration == json.dumps(tool_parameters)
assert created_tool_provider.privacy_policy == tool_privacy_policy
assert created_tool_provider.version == workflow.version
assert created_tool_provider.user_id == account.id
assert created_tool_provider.tenant_id == account.current_tenant.id
assert created_tool_provider.app_id == app.id
# Verify external service calls
mock_external_service_dependencies["workflow_tool_provider_controller"].from_db.assert_called_once()
mock_external_service_dependencies["tool_label_manager"].update_tool_labels.assert_called_once()
mock_external_service_dependencies[
"tool_transform_service"
].workflow_provider_to_controller.assert_called_once()
def test_create_workflow_tool_duplicate_name_error(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test workflow tool creation fails when name already exists.
This test verifies:
- Proper error handling for duplicate tool names
- Database constraint enforcement
- Correct error message
"""
fake = Faker()
# Create test data
app, account, workflow = self._create_test_app_and_account(
db_session_with_containers, mock_external_service_dependencies
)
# Create first workflow tool
first_tool_name = fake.word()
first_tool_parameters = self._create_test_workflow_tool_parameters()
WorkflowToolManageService.create_workflow_tool(
user_id=account.id,
tenant_id=account.current_tenant.id,
workflow_app_id=app.id,
name=first_tool_name,
label=fake.word(),
icon={"type": "emoji", "emoji": "🔧"},
description=fake.text(max_nb_chars=200),
parameters=first_tool_parameters,
)
# Attempt to create second workflow tool with same name
second_tool_parameters = self._create_test_workflow_tool_parameters()
with pytest.raises(ValueError) as exc_info:
WorkflowToolManageService.create_workflow_tool(
user_id=account.id,
tenant_id=account.current_tenant.id,
workflow_app_id=app.id,
name=first_tool_name, # Same name
label=fake.word(),
icon={"type": "emoji", "emoji": "⚙️"},
description=fake.text(max_nb_chars=200),
parameters=second_tool_parameters,
)
# Verify error message
assert f"Tool with name {first_tool_name} or app_id {app.id} already exists" in str(exc_info.value)
# Verify only one tool was created
from extensions.ext_database import db
tool_count = (
db.session.query(WorkflowToolProvider)
.where(
WorkflowToolProvider.tenant_id == account.current_tenant.id,
)
.count()
)
assert tool_count == 1
def test_create_workflow_tool_invalid_app_error(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test workflow tool creation fails when app does not exist.
This test verifies:
- Proper error handling for non-existent apps
- Correct error message
- No database changes when app is invalid
"""
fake = Faker()
# Create test data
app, account, workflow = self._create_test_app_and_account(
db_session_with_containers, mock_external_service_dependencies
)
# Generate non-existent app ID
non_existent_app_id = fake.uuid4()
# Attempt to create workflow tool with non-existent app
tool_parameters = self._create_test_workflow_tool_parameters()
with pytest.raises(ValueError) as exc_info:
WorkflowToolManageService.create_workflow_tool(
user_id=account.id,
tenant_id=account.current_tenant.id,
workflow_app_id=non_existent_app_id, # Non-existent app ID
name=fake.word(),
label=fake.word(),
icon={"type": "emoji", "emoji": "🔧"},
description=fake.text(max_nb_chars=200),
parameters=tool_parameters,
)
# Verify error message
assert f"App {non_existent_app_id} not found" in str(exc_info.value)
# Verify no workflow tool was created
from extensions.ext_database import db
tool_count = (
db.session.query(WorkflowToolProvider)
.where(
WorkflowToolProvider.tenant_id == account.current_tenant.id,
)
.count()
)
assert tool_count == 0
def test_create_workflow_tool_invalid_parameters_error(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test workflow tool creation fails when parameters are invalid.
This test verifies:
- Proper error handling for invalid parameter configurations
- Parameter validation enforcement
- Correct error message
"""
fake = Faker()
# Create test data
app, account, workflow = self._create_test_app_and_account(
db_session_with_containers, mock_external_service_dependencies
)
# Setup invalid workflow tool parameters (missing required fields)
invalid_parameters = [
{
"name": "input_text",
# Missing description and form fields
"type": "string",
"required": True,
}
]
# Attempt to create workflow tool with invalid parameters
with pytest.raises(ValueError) as exc_info:
WorkflowToolManageService.create_workflow_tool(
user_id=account.id,
tenant_id=account.current_tenant.id,
workflow_app_id=app.id,
name=fake.word(),
label=fake.word(),
icon={"type": "emoji", "emoji": "🔧"},
description=fake.text(max_nb_chars=200),
parameters=invalid_parameters,
)
# Verify error message contains validation error
assert "validation error" in str(exc_info.value).lower()
# Verify no workflow tool was created
from extensions.ext_database import db
tool_count = (
db.session.query(WorkflowToolProvider)
.where(
WorkflowToolProvider.tenant_id == account.current_tenant.id,
)
.count()
)
assert tool_count == 0
def test_create_workflow_tool_duplicate_app_id_error(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test workflow tool creation fails when app_id already exists.
This test verifies:
- Proper error handling for duplicate app_id
- Database constraint enforcement for app_id uniqueness
- Correct error message
"""
fake = Faker()
# Create test data
app, account, workflow = self._create_test_app_and_account(
db_session_with_containers, mock_external_service_dependencies
)
# Create first workflow tool
first_tool_name = fake.word()
first_tool_parameters = self._create_test_workflow_tool_parameters()
WorkflowToolManageService.create_workflow_tool(
user_id=account.id,
tenant_id=account.current_tenant.id,
workflow_app_id=app.id,
name=first_tool_name,
label=fake.word(),
icon={"type": "emoji", "emoji": "🔧"},
description=fake.text(max_nb_chars=200),
parameters=first_tool_parameters,
)
# Attempt to create second workflow tool with same app_id but different name
second_tool_name = fake.word()
second_tool_parameters = self._create_test_workflow_tool_parameters()
with pytest.raises(ValueError) as exc_info:
WorkflowToolManageService.create_workflow_tool(
user_id=account.id,
tenant_id=account.current_tenant.id,
workflow_app_id=app.id, # Same app_id
name=second_tool_name, # Different name
label=fake.word(),
icon={"type": "emoji", "emoji": "⚙️"},
description=fake.text(max_nb_chars=200),
parameters=second_tool_parameters,
)
# Verify error message
assert f"Tool with name {second_tool_name} or app_id {app.id} already exists" in str(exc_info.value)
# Verify only one tool was created
from extensions.ext_database import db
tool_count = (
db.session.query(WorkflowToolProvider)
.where(
WorkflowToolProvider.tenant_id == account.current_tenant.id,
)
.count()
)
assert tool_count == 1
def test_create_workflow_tool_workflow_not_found_error(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test workflow tool creation fails when app has no workflow.
This test verifies:
- Proper error handling for apps without workflows
- Correct error message
- No database changes when workflow is missing
"""
fake = Faker()
# Create test data but without workflow
app, account, _ = self._create_test_app_and_account(
db_session_with_containers, mock_external_service_dependencies
)
# Remove workflow reference from app
from extensions.ext_database import db
app.workflow_id = None
db.session.commit()
# Attempt to create workflow tool for app without workflow
tool_parameters = self._create_test_workflow_tool_parameters()
with pytest.raises(ValueError) as exc_info:
WorkflowToolManageService.create_workflow_tool(
user_id=account.id,
tenant_id=account.current_tenant.id,
workflow_app_id=app.id,
name=fake.word(),
label=fake.word(),
icon={"type": "emoji", "emoji": "🔧"},
description=fake.text(max_nb_chars=200),
parameters=tool_parameters,
)
# Verify error message
assert f"Workflow not found for app {app.id}" in str(exc_info.value)
# Verify no workflow tool was created
tool_count = (
db.session.query(WorkflowToolProvider)
.where(
WorkflowToolProvider.tenant_id == account.current_tenant.id,
)
.count()
)
assert tool_count == 0
def test_update_workflow_tool_success(self, db_session_with_containers, mock_external_service_dependencies):
"""
Test successful workflow tool update with valid parameters.
This test verifies:
- Proper workflow tool update with all required fields
- Correct database state after update
- Proper relationship maintenance
- External service integration
- Return value correctness
"""
fake = Faker()
# Create test data
app, account, workflow = self._create_test_app_and_account(
db_session_with_containers, mock_external_service_dependencies
)
# Create initial workflow tool
initial_tool_name = fake.word()
initial_tool_parameters = self._create_test_workflow_tool_parameters()
WorkflowToolManageService.create_workflow_tool(
user_id=account.id,
tenant_id=account.current_tenant.id,
workflow_app_id=app.id,
name=initial_tool_name,
label=fake.word(),
icon={"type": "emoji", "emoji": "🔧"},
description=fake.text(max_nb_chars=200),
parameters=initial_tool_parameters,
)
# Get the created tool
from extensions.ext_database import db
created_tool = (
db.session.query(WorkflowToolProvider)
.where(
WorkflowToolProvider.tenant_id == account.current_tenant.id,
WorkflowToolProvider.app_id == app.id,
)
.first()
)
# Setup update parameters
updated_tool_name = fake.word()
updated_tool_label = fake.word()
updated_tool_icon = {"type": "emoji", "emoji": "⚙️"}
updated_tool_description = fake.text(max_nb_chars=200)
updated_tool_parameters = self._create_test_workflow_tool_parameters()
updated_tool_privacy_policy = fake.text(max_nb_chars=100)
updated_tool_labels = ["automation", "updated"]
# Execute the update method
result = WorkflowToolManageService.update_workflow_tool(
user_id=account.id,
tenant_id=account.current_tenant.id,
workflow_tool_id=created_tool.id,
name=updated_tool_name,
label=updated_tool_label,
icon=updated_tool_icon,
description=updated_tool_description,
parameters=updated_tool_parameters,
privacy_policy=updated_tool_privacy_policy,
labels=updated_tool_labels,
)
# Verify the result
assert result == {"result": "success"}
# Verify database state was updated
db.session.refresh(created_tool)
assert created_tool.name == updated_tool_name
assert created_tool.label == updated_tool_label
assert created_tool.icon == json.dumps(updated_tool_icon)
assert created_tool.description == updated_tool_description
assert created_tool.parameter_configuration == json.dumps(updated_tool_parameters)
assert created_tool.privacy_policy == updated_tool_privacy_policy
assert created_tool.version == workflow.version
assert created_tool.updated_at is not None
# Verify external service calls
mock_external_service_dependencies["workflow_tool_provider_controller"].from_db.assert_called()
mock_external_service_dependencies["tool_label_manager"].update_tool_labels.assert_called()
mock_external_service_dependencies["tool_transform_service"].workflow_provider_to_controller.assert_called()
def test_update_workflow_tool_not_found_error(self, db_session_with_containers, mock_external_service_dependencies):
"""
Test workflow tool update fails when tool does not exist.
This test verifies:
- Proper error handling for non-existent tools
- Correct error message
- No database changes when tool is invalid
"""
fake = Faker()
# Create test data
app, account, workflow = self._create_test_app_and_account(
db_session_with_containers, mock_external_service_dependencies
)
# Generate non-existent tool ID
non_existent_tool_id = fake.uuid4()
# Attempt to update non-existent workflow tool
tool_parameters = self._create_test_workflow_tool_parameters()
with pytest.raises(ValueError) as exc_info:
WorkflowToolManageService.update_workflow_tool(
user_id=account.id,
tenant_id=account.current_tenant.id,
workflow_tool_id=non_existent_tool_id, # Non-existent tool ID
name=fake.word(),
label=fake.word(),
icon={"type": "emoji", "emoji": "🔧"},
description=fake.text(max_nb_chars=200),
parameters=tool_parameters,
)
# Verify error message
assert f"Tool {non_existent_tool_id} not found" in str(exc_info.value)
# Verify no workflow tool was created
from extensions.ext_database import db
tool_count = (
db.session.query(WorkflowToolProvider)
.where(
WorkflowToolProvider.tenant_id == account.current_tenant.id,
)
.count()
)
assert tool_count == 0
def test_update_workflow_tool_same_name_success(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test workflow tool update succeeds when keeping the same name.
This test verifies:
- Proper handling when updating tool with same name
- Database state maintenance
- Update timestamp is set
"""
fake = Faker()
# Create test data
app, account, workflow = self._create_test_app_and_account(
db_session_with_containers, mock_external_service_dependencies
)
# Create first workflow tool
first_tool_name = fake.word()
first_tool_parameters = self._create_test_workflow_tool_parameters()
WorkflowToolManageService.create_workflow_tool(
user_id=account.id,
tenant_id=account.current_tenant.id,
workflow_app_id=app.id,
name=first_tool_name,
label=fake.word(),
icon={"type": "emoji", "emoji": "🔧"},
description=fake.text(max_nb_chars=200),
parameters=first_tool_parameters,
)
# Get the created tool
from extensions.ext_database import db
created_tool = (
db.session.query(WorkflowToolProvider)
.where(
WorkflowToolProvider.tenant_id == account.current_tenant.id,
WorkflowToolProvider.app_id == app.id,
)
.first()
)
# Attempt to update tool with same name (should not fail)
result = WorkflowToolManageService.update_workflow_tool(
user_id=account.id,
tenant_id=account.current_tenant.id,
workflow_tool_id=created_tool.id,
name=first_tool_name, # Same name
label=fake.word(),
icon={"type": "emoji", "emoji": "⚙️"},
description=fake.text(max_nb_chars=200),
parameters=first_tool_parameters,
)
# Verify update was successful
assert result == {"result": "success"}
# Verify tool still exists with the same name
db.session.refresh(created_tool)
assert created_tool.name == first_tool_name
assert created_tool.updated_at is not None