Merge remote-tracking branch 'origin/main' into feat/trigger

This commit is contained in:
yessenia
2025-09-25 17:14:24 +08:00
3013 changed files with 148826 additions and 44294 deletions

View File

@ -13,7 +13,6 @@ from services.account_service import AccountService, RegisterService, TenantServ
from services.errors.account import (
AccountAlreadyInTenantError,
AccountLoginError,
AccountNotFoundError,
AccountPasswordError,
AccountRegisterError,
CurrentPasswordIncorrectError,
@ -91,6 +90,28 @@ class TestAccountService:
assert account.password is None
assert account.password_salt is None
def test_create_account_password_invalid_new_password(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test account create with invalid new password format.
"""
fake = Faker()
email = fake.email()
name = fake.name()
# Setup mocks
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
# Test with too short password (assuming minimum length validation)
with pytest.raises(ValueError): # Password validation error
AccountService.create_account(
email=email,
name=name,
interface_language="en-US",
password="invalid_new_password",
)
def test_create_account_registration_disabled(self, db_session_with_containers, mock_external_service_dependencies):
"""
Test account creation when registration is disabled.
@ -139,7 +160,7 @@ class TestAccountService:
fake = Faker()
email = fake.email()
password = fake.password(length=12)
with pytest.raises(AccountNotFoundError):
with pytest.raises(AccountPasswordError):
AccountService.authenticate(email, password)
def test_authenticate_banned_account(self, db_session_with_containers, mock_external_service_dependencies):
@ -940,7 +961,8 @@ class TestAccountService:
Test getting user through non-existent email.
"""
fake = Faker()
non_existent_email = fake.email()
domain = f"test-{fake.random_letters(10)}.com"
non_existent_email = fake.email(domain=domain)
found_user = AccountService.get_user_through_email(non_existent_email)
assert found_user is None
@ -3278,7 +3300,7 @@ class TestRegisterService:
redis_client.setex(cache_key, 24 * 60 * 60, account_id)
# Execute invitation retrieval
result = RegisterService._get_invitation_by_token(
result = RegisterService.get_invitation_by_token(
token=token,
workspace_id=workspace_id,
email=email,
@ -3316,7 +3338,7 @@ class TestRegisterService:
redis_client.setex(token_key, 24 * 60 * 60, json.dumps(invitation_data))
# Execute invitation retrieval
result = RegisterService._get_invitation_by_token(token=token)
result = RegisterService.get_invitation_by_token(token=token)
# Verify result contains expected data
assert result is not None

View File

@ -42,7 +42,7 @@ class TestAdvancedPromptTemplateService:
# Test data for Baichuan model
args = {
"app_mode": AppMode.CHAT.value,
"app_mode": AppMode.CHAT,
"model_mode": "completion",
"model_name": "baichuan-13b-chat",
"has_context": "true",
@ -77,7 +77,7 @@ class TestAdvancedPromptTemplateService:
# Test data for common model
args = {
"app_mode": AppMode.CHAT.value,
"app_mode": AppMode.CHAT,
"model_mode": "completion",
"model_name": "gpt-3.5-turbo",
"has_context": "true",
@ -116,7 +116,7 @@ class TestAdvancedPromptTemplateService:
for model_name in test_cases:
args = {
"app_mode": AppMode.CHAT.value,
"app_mode": AppMode.CHAT,
"model_mode": "completion",
"model_name": model_name,
"has_context": "true",
@ -144,7 +144,7 @@ class TestAdvancedPromptTemplateService:
fake = Faker()
# Act: Execute the method under test
result = AdvancedPromptTemplateService.get_common_prompt(AppMode.CHAT.value, "completion", "true")
result = AdvancedPromptTemplateService.get_common_prompt(AppMode.CHAT, "completion", "true")
# Assert: Verify the expected outcomes
assert result is not None
@ -173,7 +173,7 @@ class TestAdvancedPromptTemplateService:
fake = Faker()
# Act: Execute the method under test
result = AdvancedPromptTemplateService.get_common_prompt(AppMode.CHAT.value, "chat", "true")
result = AdvancedPromptTemplateService.get_common_prompt(AppMode.CHAT, "chat", "true")
# Assert: Verify the expected outcomes
assert result is not None
@ -202,7 +202,7 @@ class TestAdvancedPromptTemplateService:
fake = Faker()
# Act: Execute the method under test
result = AdvancedPromptTemplateService.get_common_prompt(AppMode.COMPLETION.value, "completion", "true")
result = AdvancedPromptTemplateService.get_common_prompt(AppMode.COMPLETION, "completion", "true")
# Assert: Verify the expected outcomes
assert result is not None
@ -230,7 +230,7 @@ class TestAdvancedPromptTemplateService:
fake = Faker()
# Act: Execute the method under test
result = AdvancedPromptTemplateService.get_common_prompt(AppMode.COMPLETION.value, "chat", "true")
result = AdvancedPromptTemplateService.get_common_prompt(AppMode.COMPLETION, "chat", "true")
# Assert: Verify the expected outcomes
assert result is not None
@ -257,7 +257,7 @@ class TestAdvancedPromptTemplateService:
fake = Faker()
# Act: Execute the method under test
result = AdvancedPromptTemplateService.get_common_prompt(AppMode.CHAT.value, "completion", "false")
result = AdvancedPromptTemplateService.get_common_prompt(AppMode.CHAT, "completion", "false")
# Assert: Verify the expected outcomes
assert result is not None
@ -303,7 +303,7 @@ class TestAdvancedPromptTemplateService:
fake = Faker()
# Act: Execute the method under test
result = AdvancedPromptTemplateService.get_common_prompt(AppMode.CHAT.value, "unsupported_mode", "true")
result = AdvancedPromptTemplateService.get_common_prompt(AppMode.CHAT, "unsupported_mode", "true")
# Assert: Verify empty dict is returned
assert result == {}
@ -442,7 +442,7 @@ class TestAdvancedPromptTemplateService:
fake = Faker()
# Act: Execute the method under test
result = AdvancedPromptTemplateService.get_baichuan_prompt(AppMode.CHAT.value, "completion", "true")
result = AdvancedPromptTemplateService.get_baichuan_prompt(AppMode.CHAT, "completion", "true")
# Assert: Verify the expected outcomes
assert result is not None
@ -473,7 +473,7 @@ class TestAdvancedPromptTemplateService:
fake = Faker()
# Act: Execute the method under test
result = AdvancedPromptTemplateService.get_baichuan_prompt(AppMode.CHAT.value, "chat", "true")
result = AdvancedPromptTemplateService.get_baichuan_prompt(AppMode.CHAT, "chat", "true")
# Assert: Verify the expected outcomes
assert result is not None
@ -502,7 +502,7 @@ class TestAdvancedPromptTemplateService:
fake = Faker()
# Act: Execute the method under test
result = AdvancedPromptTemplateService.get_baichuan_prompt(AppMode.COMPLETION.value, "completion", "true")
result = AdvancedPromptTemplateService.get_baichuan_prompt(AppMode.COMPLETION, "completion", "true")
# Assert: Verify the expected outcomes
assert result is not None
@ -530,7 +530,7 @@ class TestAdvancedPromptTemplateService:
fake = Faker()
# Act: Execute the method under test
result = AdvancedPromptTemplateService.get_baichuan_prompt(AppMode.COMPLETION.value, "chat", "true")
result = AdvancedPromptTemplateService.get_baichuan_prompt(AppMode.COMPLETION, "chat", "true")
# Assert: Verify the expected outcomes
assert result is not None
@ -557,7 +557,7 @@ class TestAdvancedPromptTemplateService:
fake = Faker()
# Act: Execute the method under test
result = AdvancedPromptTemplateService.get_baichuan_prompt(AppMode.CHAT.value, "completion", "false")
result = AdvancedPromptTemplateService.get_baichuan_prompt(AppMode.CHAT, "completion", "false")
# Assert: Verify the expected outcomes
assert result is not None
@ -603,7 +603,7 @@ class TestAdvancedPromptTemplateService:
fake = Faker()
# Act: Execute the method under test
result = AdvancedPromptTemplateService.get_baichuan_prompt(AppMode.CHAT.value, "unsupported_mode", "true")
result = AdvancedPromptTemplateService.get_baichuan_prompt(AppMode.CHAT, "unsupported_mode", "true")
# Assert: Verify empty dict is returned
assert result == {}
@ -621,7 +621,7 @@ class TestAdvancedPromptTemplateService:
fake = Faker()
# Test all app modes
app_modes = [AppMode.CHAT.value, AppMode.COMPLETION.value]
app_modes = [AppMode.CHAT, AppMode.COMPLETION]
model_modes = ["completion", "chat"]
for app_mode in app_modes:
@ -653,7 +653,7 @@ class TestAdvancedPromptTemplateService:
fake = Faker()
# Test all app modes
app_modes = [AppMode.CHAT.value, AppMode.COMPLETION.value]
app_modes = [AppMode.CHAT, AppMode.COMPLETION]
model_modes = ["completion", "chat"]
for app_mode in app_modes:
@ -686,10 +686,10 @@ class TestAdvancedPromptTemplateService:
# Test edge cases
edge_cases = [
{"app_mode": "", "model_mode": "completion", "model_name": "gpt-3.5-turbo", "has_context": "true"},
{"app_mode": AppMode.CHAT.value, "model_mode": "", "model_name": "gpt-3.5-turbo", "has_context": "true"},
{"app_mode": AppMode.CHAT.value, "model_mode": "completion", "model_name": "", "has_context": "true"},
{"app_mode": AppMode.CHAT, "model_mode": "", "model_name": "gpt-3.5-turbo", "has_context": "true"},
{"app_mode": AppMode.CHAT, "model_mode": "completion", "model_name": "", "has_context": "true"},
{
"app_mode": AppMode.CHAT.value,
"app_mode": AppMode.CHAT,
"model_mode": "completion",
"model_name": "gpt-3.5-turbo",
"has_context": "",
@ -723,7 +723,7 @@ class TestAdvancedPromptTemplateService:
# Test with context
args = {
"app_mode": AppMode.CHAT.value,
"app_mode": AppMode.CHAT,
"model_mode": "completion",
"model_name": "gpt-3.5-turbo",
"has_context": "true",
@ -757,7 +757,7 @@ class TestAdvancedPromptTemplateService:
# Test with context
args = {
"app_mode": AppMode.CHAT.value,
"app_mode": AppMode.CHAT,
"model_mode": "completion",
"model_name": "baichuan-13b-chat",
"has_context": "true",
@ -786,25 +786,25 @@ class TestAdvancedPromptTemplateService:
# Test different scenarios
test_scenarios = [
{
"app_mode": AppMode.CHAT.value,
"app_mode": AppMode.CHAT,
"model_mode": "completion",
"model_name": "gpt-3.5-turbo",
"has_context": "true",
},
{
"app_mode": AppMode.CHAT.value,
"app_mode": AppMode.CHAT,
"model_mode": "chat",
"model_name": "gpt-3.5-turbo",
"has_context": "true",
},
{
"app_mode": AppMode.COMPLETION.value,
"app_mode": AppMode.COMPLETION,
"model_mode": "completion",
"model_name": "gpt-3.5-turbo",
"has_context": "true",
},
{
"app_mode": AppMode.COMPLETION.value,
"app_mode": AppMode.COMPLETION,
"model_mode": "chat",
"model_name": "gpt-3.5-turbo",
"has_context": "true",
@ -843,25 +843,25 @@ class TestAdvancedPromptTemplateService:
# Test different scenarios
test_scenarios = [
{
"app_mode": AppMode.CHAT.value,
"app_mode": AppMode.CHAT,
"model_mode": "completion",
"model_name": "baichuan-13b-chat",
"has_context": "true",
},
{
"app_mode": AppMode.CHAT.value,
"app_mode": AppMode.CHAT,
"model_mode": "chat",
"model_name": "baichuan-13b-chat",
"has_context": "true",
},
{
"app_mode": AppMode.COMPLETION.value,
"app_mode": AppMode.COMPLETION,
"model_mode": "completion",
"model_name": "baichuan-13b-chat",
"has_context": "true",
},
{
"app_mode": AppMode.COMPLETION.value,
"app_mode": AppMode.COMPLETION,
"model_mode": "chat",
"model_name": "baichuan-13b-chat",
"has_context": "true",

View File

@ -1,10 +1,11 @@
import json
from unittest.mock import MagicMock, patch
from unittest.mock import MagicMock, create_autospec, patch
import pytest
from faker import Faker
from core.plugin.impl.exc import PluginDaemonClientSideError
from models.account import Account
from models.model import AppModelConfig, Conversation, EndUser, Message, MessageAgentThought
from services.account_service import AccountService, TenantService
from services.agent_service import AgentService
@ -21,7 +22,7 @@ class TestAgentService:
patch("services.agent_service.PluginAgentClient") as mock_plugin_agent_client,
patch("services.agent_service.ToolManager") as mock_tool_manager,
patch("services.agent_service.AgentConfigManager") as mock_agent_config_manager,
patch("services.agent_service.current_user") as mock_current_user,
patch("services.agent_service.current_user", create_autospec(Account, instance=True)) as mock_current_user,
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,

View File

@ -1,9 +1,10 @@
from unittest.mock import patch
from unittest.mock import create_autospec, patch
import pytest
from faker import Faker
from werkzeug.exceptions import NotFound
from models.account import Account
from models.model import MessageAnnotation
from services.annotation_service import AppAnnotationService
from services.app_service import AppService
@ -24,7 +25,9 @@ class TestAnnotationService:
patch("services.annotation_service.enable_annotation_reply_task") as mock_enable_task,
patch("services.annotation_service.disable_annotation_reply_task") as mock_disable_task,
patch("services.annotation_service.batch_import_annotations_task") as mock_batch_import_task,
patch("services.annotation_service.current_user") as mock_current_user,
patch(
"services.annotation_service.current_user", create_autospec(Account, instance=True)
) as mock_current_user,
):
# Setup default mock returns
mock_account_feature_service.get_features.return_value.billing.enabled = False

View File

@ -322,7 +322,87 @@ class TestAppDslService:
# Verify workflow service was called
mock_external_service_dependencies["workflow_service"].return_value.get_draft_workflow.assert_called_once_with(
app
app, None
)
def test_export_dsl_with_workflow_id_success(self, db_session_with_containers, mock_external_service_dependencies):
"""
Test successful DSL export with specific workflow ID.
"""
fake = Faker()
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
# Update app to workflow mode
app.mode = "workflow"
db_session_with_containers.commit()
# Mock workflow service to return a workflow when specific workflow_id is provided
mock_workflow = MagicMock()
mock_workflow.to_dict.return_value = {
"graph": {"nodes": [{"id": "start", "type": "start", "data": {"type": "start"}}], "edges": []},
"features": {},
"environment_variables": [],
"conversation_variables": [],
}
# Mock the get_draft_workflow method to return different workflows based on workflow_id
def mock_get_draft_workflow(app_model, workflow_id=None):
if workflow_id == "specific-workflow-id":
return mock_workflow
return None
mock_external_service_dependencies[
"workflow_service"
].return_value.get_draft_workflow.side_effect = mock_get_draft_workflow
# Export DSL with specific workflow ID
exported_dsl = AppDslService.export_dsl(app, include_secret=False, workflow_id="specific-workflow-id")
# Parse exported YAML
exported_data = yaml.safe_load(exported_dsl)
# Verify exported data structure
assert exported_data["kind"] == "app"
assert exported_data["app"]["name"] == app.name
assert exported_data["app"]["mode"] == "workflow"
# Verify workflow was exported
assert "workflow" in exported_data
assert "graph" in exported_data["workflow"]
assert "nodes" in exported_data["workflow"]["graph"]
# Verify dependencies were exported
assert "dependencies" in exported_data
assert isinstance(exported_data["dependencies"], list)
# Verify workflow service was called with specific workflow ID
mock_external_service_dependencies["workflow_service"].return_value.get_draft_workflow.assert_called_once_with(
app, "specific-workflow-id"
)
def test_export_dsl_with_invalid_workflow_id_raises_error(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test that export_dsl raises error when invalid workflow ID is provided.
"""
fake = Faker()
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
# Update app to workflow mode
app.mode = "workflow"
db_session_with_containers.commit()
# Mock workflow service to return None when invalid workflow ID is provided
mock_external_service_dependencies["workflow_service"].return_value.get_draft_workflow.return_value = None
# Export DSL with invalid workflow ID should raise ValueError
with pytest.raises(ValueError, match="Missing draft workflow configuration, please check."):
AppDslService.export_dsl(app, include_secret=False, workflow_id="invalid-workflow-id")
# Verify workflow service was called with the invalid workflow ID
mock_external_service_dependencies["workflow_service"].return_value.get_draft_workflow.assert_called_once_with(
app, "invalid-workflow-id"
)
def test_check_dependencies_success(self, db_session_with_containers, mock_external_service_dependencies):

View File

@ -1,9 +1,10 @@
from unittest.mock import patch
from unittest.mock import create_autospec, patch
import pytest
from faker import Faker
from constants.model_template import default_app_templates
from models.account import Account
from models.model import App, Site
from services.account_service import AccountService, TenantService
from services.app_service import AppService
@ -161,8 +162,13 @@ class TestAppService:
app_service = AppService()
created_app = app_service.create_app(tenant.id, app_args, account)
# Get app using the service
retrieved_app = app_service.get_app(created_app)
# Get app using the service - needs current_user mock
mock_current_user = create_autospec(Account, instance=True)
mock_current_user.id = account.id
mock_current_user.current_tenant_id = account.current_tenant_id
with patch("services.app_service.current_user", mock_current_user):
retrieved_app = app_service.get_app(created_app)
# Verify retrieved app matches created app
assert retrieved_app.id == created_app.id
@ -406,7 +412,11 @@ class TestAppService:
"use_icon_as_answer_icon": True,
}
with patch("flask_login.utils._get_user", return_value=account):
mock_current_user = create_autospec(Account, instance=True)
mock_current_user.id = account.id
mock_current_user.current_tenant_id = account.current_tenant_id
with patch("services.app_service.current_user", mock_current_user):
updated_app = app_service.update_app(app, update_args)
# Verify updated fields
@ -456,7 +466,11 @@ class TestAppService:
# Update app name
new_name = "New App Name"
with patch("flask_login.utils._get_user", return_value=account):
mock_current_user = create_autospec(Account, instance=True)
mock_current_user.id = account.id
mock_current_user.current_tenant_id = account.current_tenant_id
with patch("services.app_service.current_user", mock_current_user):
updated_app = app_service.update_app_name(app, new_name)
assert updated_app.name == new_name
@ -504,7 +518,11 @@ class TestAppService:
# Update app icon
new_icon = "🌟"
new_icon_background = "#FFD93D"
with patch("flask_login.utils._get_user", return_value=account):
mock_current_user = create_autospec(Account, instance=True)
mock_current_user.id = account.id
mock_current_user.current_tenant_id = account.current_tenant_id
with patch("services.app_service.current_user", mock_current_user):
updated_app = app_service.update_app_icon(app, new_icon, new_icon_background)
assert updated_app.icon == new_icon
@ -551,13 +569,17 @@ class TestAppService:
original_site_status = app.enable_site
# Update site status to disabled
with patch("flask_login.utils._get_user", return_value=account):
mock_current_user = create_autospec(Account, instance=True)
mock_current_user.id = account.id
mock_current_user.current_tenant_id = account.current_tenant_id
with patch("services.app_service.current_user", mock_current_user):
updated_app = app_service.update_app_site_status(app, False)
assert updated_app.enable_site is False
assert updated_app.updated_by == account.id
# Update site status back to enabled
with patch("flask_login.utils._get_user", return_value=account):
with patch("services.app_service.current_user", mock_current_user):
updated_app = app_service.update_app_site_status(updated_app, True)
assert updated_app.enable_site is True
assert updated_app.updated_by == account.id
@ -602,13 +624,17 @@ class TestAppService:
original_api_status = app.enable_api
# Update API status to disabled
with patch("flask_login.utils._get_user", return_value=account):
mock_current_user = create_autospec(Account, instance=True)
mock_current_user.id = account.id
mock_current_user.current_tenant_id = account.current_tenant_id
with patch("services.app_service.current_user", mock_current_user):
updated_app = app_service.update_app_api_status(app, False)
assert updated_app.enable_api is False
assert updated_app.updated_by == account.id
# Update API status back to enabled
with patch("flask_login.utils._get_user", return_value=account):
with patch("services.app_service.current_user", mock_current_user):
updated_app = app_service.update_app_api_status(updated_app, True)
assert updated_app.enable_api is True
assert updated_app.updated_by == account.id

View File

@ -1,9 +1,10 @@
import hashlib
from io import BytesIO
from unittest.mock import patch
from unittest.mock import create_autospec, patch
import pytest
from faker import Faker
from sqlalchemy import Engine
from werkzeug.exceptions import NotFound
from configs import dify_config
@ -17,6 +18,12 @@ from services.file_service import FileService
class TestFileService:
"""Integration tests for FileService using testcontainers."""
@pytest.fixture
def engine(self, db_session_with_containers):
bind = db_session_with_containers.get_bind()
assert isinstance(bind, Engine)
return bind
@pytest.fixture
def mock_external_service_dependencies(self):
"""Mock setup for external service dependencies."""
@ -156,7 +163,7 @@ class TestFileService:
return upload_file
# Test upload_file method
def test_upload_file_success(self, db_session_with_containers, mock_external_service_dependencies):
def test_upload_file_success(self, db_session_with_containers, engine, mock_external_service_dependencies):
"""
Test successful file upload with valid parameters.
"""
@ -167,7 +174,7 @@ class TestFileService:
content = b"test file content"
mimetype = "application/pdf"
upload_file = FileService.upload_file(
upload_file = FileService(engine).upload_file(
filename=filename,
content=content,
mimetype=mimetype,
@ -187,13 +194,9 @@ class TestFileService:
# Verify storage was called
mock_external_service_dependencies["storage"].save.assert_called_once()
# Verify database state
from extensions.ext_database import db
db.session.refresh(upload_file)
assert upload_file.id is not None
def test_upload_file_with_end_user(self, db_session_with_containers, mock_external_service_dependencies):
def test_upload_file_with_end_user(self, db_session_with_containers, engine, mock_external_service_dependencies):
"""
Test file upload with end user instead of account.
"""
@ -204,7 +207,7 @@ class TestFileService:
content = b"test image content"
mimetype = "image/jpeg"
upload_file = FileService.upload_file(
upload_file = FileService(engine).upload_file(
filename=filename,
content=content,
mimetype=mimetype,
@ -215,7 +218,9 @@ class TestFileService:
assert upload_file.created_by == end_user.id
assert upload_file.created_by_role == CreatorUserRole.END_USER.value
def test_upload_file_with_datasets_source(self, db_session_with_containers, mock_external_service_dependencies):
def test_upload_file_with_datasets_source(
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test file upload with datasets source parameter.
"""
@ -226,7 +231,7 @@ class TestFileService:
content = b"test file content"
mimetype = "application/pdf"
upload_file = FileService.upload_file(
upload_file = FileService(engine).upload_file(
filename=filename,
content=content,
mimetype=mimetype,
@ -239,7 +244,7 @@ class TestFileService:
assert upload_file.source_url == "https://example.com/source"
def test_upload_file_invalid_filename_characters(
self, db_session_with_containers, mock_external_service_dependencies
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test file upload with invalid filename characters.
@ -252,14 +257,16 @@ class TestFileService:
mimetype = "text/plain"
with pytest.raises(ValueError, match="Filename contains invalid characters"):
FileService.upload_file(
FileService(engine).upload_file(
filename=filename,
content=content,
mimetype=mimetype,
user=account,
)
def test_upload_file_filename_too_long(self, db_session_with_containers, mock_external_service_dependencies):
def test_upload_file_filename_too_long(
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test file upload with filename that exceeds length limit.
"""
@ -272,7 +279,7 @@ class TestFileService:
content = b"test content"
mimetype = "text/plain"
upload_file = FileService.upload_file(
upload_file = FileService(engine).upload_file(
filename=filename,
content=content,
mimetype=mimetype,
@ -288,7 +295,7 @@ class TestFileService:
assert len(base_name) <= 200
def test_upload_file_datasets_unsupported_type(
self, db_session_with_containers, mock_external_service_dependencies
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test file upload for datasets with unsupported file type.
@ -301,7 +308,7 @@ class TestFileService:
mimetype = "image/jpeg"
with pytest.raises(UnsupportedFileTypeError):
FileService.upload_file(
FileService(engine).upload_file(
filename=filename,
content=content,
mimetype=mimetype,
@ -309,7 +316,7 @@ class TestFileService:
source="datasets",
)
def test_upload_file_too_large(self, db_session_with_containers, mock_external_service_dependencies):
def test_upload_file_too_large(self, db_session_with_containers, engine, mock_external_service_dependencies):
"""
Test file upload with file size exceeding limit.
"""
@ -322,7 +329,7 @@ class TestFileService:
mimetype = "image/jpeg"
with pytest.raises(FileTooLargeError):
FileService.upload_file(
FileService(engine).upload_file(
filename=filename,
content=content,
mimetype=mimetype,
@ -331,7 +338,7 @@ class TestFileService:
# Test is_file_size_within_limit method
def test_is_file_size_within_limit_image_success(
self, db_session_with_containers, mock_external_service_dependencies
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test file size check for image files within limit.
@ -339,12 +346,12 @@ class TestFileService:
extension = "jpg"
file_size = dify_config.UPLOAD_IMAGE_FILE_SIZE_LIMIT * 1024 * 1024 # Exactly at limit
result = FileService.is_file_size_within_limit(extension=extension, file_size=file_size)
result = FileService(engine).is_file_size_within_limit(extension=extension, file_size=file_size)
assert result is True
def test_is_file_size_within_limit_video_success(
self, db_session_with_containers, mock_external_service_dependencies
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test file size check for video files within limit.
@ -352,12 +359,12 @@ class TestFileService:
extension = "mp4"
file_size = dify_config.UPLOAD_VIDEO_FILE_SIZE_LIMIT * 1024 * 1024 # Exactly at limit
result = FileService.is_file_size_within_limit(extension=extension, file_size=file_size)
result = FileService(engine).is_file_size_within_limit(extension=extension, file_size=file_size)
assert result is True
def test_is_file_size_within_limit_audio_success(
self, db_session_with_containers, mock_external_service_dependencies
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test file size check for audio files within limit.
@ -365,12 +372,12 @@ class TestFileService:
extension = "mp3"
file_size = dify_config.UPLOAD_AUDIO_FILE_SIZE_LIMIT * 1024 * 1024 # Exactly at limit
result = FileService.is_file_size_within_limit(extension=extension, file_size=file_size)
result = FileService(engine).is_file_size_within_limit(extension=extension, file_size=file_size)
assert result is True
def test_is_file_size_within_limit_document_success(
self, db_session_with_containers, mock_external_service_dependencies
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test file size check for document files within limit.
@ -378,12 +385,12 @@ class TestFileService:
extension = "pdf"
file_size = dify_config.UPLOAD_FILE_SIZE_LIMIT * 1024 * 1024 # Exactly at limit
result = FileService.is_file_size_within_limit(extension=extension, file_size=file_size)
result = FileService(engine).is_file_size_within_limit(extension=extension, file_size=file_size)
assert result is True
def test_is_file_size_within_limit_image_exceeded(
self, db_session_with_containers, mock_external_service_dependencies
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test file size check for image files exceeding limit.
@ -391,12 +398,12 @@ class TestFileService:
extension = "jpg"
file_size = dify_config.UPLOAD_IMAGE_FILE_SIZE_LIMIT * 1024 * 1024 + 1 # Exceeds limit
result = FileService.is_file_size_within_limit(extension=extension, file_size=file_size)
result = FileService(engine).is_file_size_within_limit(extension=extension, file_size=file_size)
assert result is False
def test_is_file_size_within_limit_unknown_extension(
self, db_session_with_containers, mock_external_service_dependencies
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test file size check for unknown file extension.
@ -404,12 +411,12 @@ class TestFileService:
extension = "xyz"
file_size = dify_config.UPLOAD_FILE_SIZE_LIMIT * 1024 * 1024 # Uses default limit
result = FileService.is_file_size_within_limit(extension=extension, file_size=file_size)
result = FileService(engine).is_file_size_within_limit(extension=extension, file_size=file_size)
assert result is True
# Test upload_text method
def test_upload_text_success(self, db_session_with_containers, mock_external_service_dependencies):
def test_upload_text_success(self, db_session_with_containers, engine, mock_external_service_dependencies):
"""
Test successful text upload.
"""
@ -417,25 +424,30 @@ class TestFileService:
text = "This is a test text content"
text_name = "test_text.txt"
# Mock current_user
with patch("services.file_service.current_user") as mock_current_user:
mock_current_user.current_tenant_id = str(fake.uuid4())
mock_current_user.id = str(fake.uuid4())
# Mock current_user using create_autospec
mock_current_user = create_autospec(Account, instance=True)
mock_current_user.current_tenant_id = str(fake.uuid4())
mock_current_user.id = str(fake.uuid4())
upload_file = FileService.upload_text(text=text, text_name=text_name)
upload_file = FileService(engine).upload_text(
text=text,
text_name=text_name,
user_id=mock_current_user.id,
tenant_id=mock_current_user.current_tenant_id,
)
assert upload_file is not None
assert upload_file.name == text_name
assert upload_file.size == len(text)
assert upload_file.extension == "txt"
assert upload_file.mime_type == "text/plain"
assert upload_file.used is True
assert upload_file.used_by == mock_current_user.id
assert upload_file is not None
assert upload_file.name == text_name
assert upload_file.size == len(text)
assert upload_file.extension == "txt"
assert upload_file.mime_type == "text/plain"
assert upload_file.used is True
assert upload_file.used_by == mock_current_user.id
# Verify storage was called
mock_external_service_dependencies["storage"].save.assert_called_once()
# Verify storage was called
mock_external_service_dependencies["storage"].save.assert_called_once()
def test_upload_text_name_too_long(self, db_session_with_containers, mock_external_service_dependencies):
def test_upload_text_name_too_long(self, db_session_with_containers, engine, mock_external_service_dependencies):
"""
Test text upload with name that exceeds length limit.
"""
@ -443,19 +455,24 @@ class TestFileService:
text = "test content"
long_name = "a" * 250 # Longer than 200 characters
# Mock current_user
with patch("services.file_service.current_user") as mock_current_user:
mock_current_user.current_tenant_id = str(fake.uuid4())
mock_current_user.id = str(fake.uuid4())
# Mock current_user using create_autospec
mock_current_user = create_autospec(Account, instance=True)
mock_current_user.current_tenant_id = str(fake.uuid4())
mock_current_user.id = str(fake.uuid4())
upload_file = FileService.upload_text(text=text, text_name=long_name)
upload_file = FileService(engine).upload_text(
text=text,
text_name=long_name,
user_id=mock_current_user.id,
tenant_id=mock_current_user.current_tenant_id,
)
# Verify name was truncated
assert len(upload_file.name) <= 200
assert upload_file.name == "a" * 200
# Verify name was truncated
assert len(upload_file.name) <= 200
assert upload_file.name == "a" * 200
# Test get_file_preview method
def test_get_file_preview_success(self, db_session_with_containers, mock_external_service_dependencies):
def test_get_file_preview_success(self, db_session_with_containers, engine, mock_external_service_dependencies):
"""
Test successful file preview generation.
"""
@ -471,12 +488,14 @@ class TestFileService:
db.session.commit()
result = FileService.get_file_preview(file_id=upload_file.id)
result = FileService(engine).get_file_preview(file_id=upload_file.id)
assert result == "extracted text content"
mock_external_service_dependencies["extract_processor"].load_from_upload_file.assert_called_once()
def test_get_file_preview_file_not_found(self, db_session_with_containers, mock_external_service_dependencies):
def test_get_file_preview_file_not_found(
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test file preview with non-existent file.
"""
@ -484,10 +503,10 @@ class TestFileService:
non_existent_id = str(fake.uuid4())
with pytest.raises(NotFound, match="File not found"):
FileService.get_file_preview(file_id=non_existent_id)
FileService(engine).get_file_preview(file_id=non_existent_id)
def test_get_file_preview_unsupported_file_type(
self, db_session_with_containers, mock_external_service_dependencies
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test file preview with unsupported file type.
@ -505,9 +524,11 @@ class TestFileService:
db.session.commit()
with pytest.raises(UnsupportedFileTypeError):
FileService.get_file_preview(file_id=upload_file.id)
FileService(engine).get_file_preview(file_id=upload_file.id)
def test_get_file_preview_text_truncation(self, db_session_with_containers, mock_external_service_dependencies):
def test_get_file_preview_text_truncation(
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test file preview with text that exceeds preview limit.
"""
@ -527,13 +548,13 @@ class TestFileService:
long_text = "x" * 5000 # Longer than PREVIEW_WORDS_LIMIT
mock_external_service_dependencies["extract_processor"].load_from_upload_file.return_value = long_text
result = FileService.get_file_preview(file_id=upload_file.id)
result = FileService(engine).get_file_preview(file_id=upload_file.id)
assert len(result) == 3000 # PREVIEW_WORDS_LIMIT
assert result == "x" * 3000
# Test get_image_preview method
def test_get_image_preview_success(self, db_session_with_containers, mock_external_service_dependencies):
def test_get_image_preview_success(self, db_session_with_containers, engine, mock_external_service_dependencies):
"""
Test successful image preview generation.
"""
@ -553,7 +574,7 @@ class TestFileService:
nonce = "test_nonce"
sign = "test_signature"
generator, mime_type = FileService.get_image_preview(
generator, mime_type = FileService(engine).get_image_preview(
file_id=upload_file.id,
timestamp=timestamp,
nonce=nonce,
@ -564,7 +585,9 @@ class TestFileService:
assert mime_type == upload_file.mime_type
mock_external_service_dependencies["file_helpers"].verify_image_signature.assert_called_once()
def test_get_image_preview_invalid_signature(self, db_session_with_containers, mock_external_service_dependencies):
def test_get_image_preview_invalid_signature(
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test image preview with invalid signature.
"""
@ -582,14 +605,16 @@ class TestFileService:
sign = "invalid_signature"
with pytest.raises(NotFound, match="File not found or signature is invalid"):
FileService.get_image_preview(
FileService(engine).get_image_preview(
file_id=upload_file.id,
timestamp=timestamp,
nonce=nonce,
sign=sign,
)
def test_get_image_preview_file_not_found(self, db_session_with_containers, mock_external_service_dependencies):
def test_get_image_preview_file_not_found(
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test image preview with non-existent file.
"""
@ -601,7 +626,7 @@ class TestFileService:
sign = "test_signature"
with pytest.raises(NotFound, match="File not found or signature is invalid"):
FileService.get_image_preview(
FileService(engine).get_image_preview(
file_id=non_existent_id,
timestamp=timestamp,
nonce=nonce,
@ -609,7 +634,7 @@ class TestFileService:
)
def test_get_image_preview_unsupported_file_type(
self, db_session_with_containers, mock_external_service_dependencies
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test image preview with non-image file type.
@ -631,7 +656,7 @@ class TestFileService:
sign = "test_signature"
with pytest.raises(UnsupportedFileTypeError):
FileService.get_image_preview(
FileService(engine).get_image_preview(
file_id=upload_file.id,
timestamp=timestamp,
nonce=nonce,
@ -640,7 +665,7 @@ class TestFileService:
# Test get_file_generator_by_file_id method
def test_get_file_generator_by_file_id_success(
self, db_session_with_containers, mock_external_service_dependencies
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test successful file generator retrieval.
@ -655,7 +680,7 @@ class TestFileService:
nonce = "test_nonce"
sign = "test_signature"
generator, file_obj = FileService.get_file_generator_by_file_id(
generator, file_obj = FileService(engine).get_file_generator_by_file_id(
file_id=upload_file.id,
timestamp=timestamp,
nonce=nonce,
@ -663,11 +688,11 @@ class TestFileService:
)
assert generator is not None
assert file_obj == upload_file
assert file_obj.id == upload_file.id
mock_external_service_dependencies["file_helpers"].verify_file_signature.assert_called_once()
def test_get_file_generator_by_file_id_invalid_signature(
self, db_session_with_containers, mock_external_service_dependencies
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test file generator retrieval with invalid signature.
@ -686,7 +711,7 @@ class TestFileService:
sign = "invalid_signature"
with pytest.raises(NotFound, match="File not found or signature is invalid"):
FileService.get_file_generator_by_file_id(
FileService(engine).get_file_generator_by_file_id(
file_id=upload_file.id,
timestamp=timestamp,
nonce=nonce,
@ -694,7 +719,7 @@ class TestFileService:
)
def test_get_file_generator_by_file_id_file_not_found(
self, db_session_with_containers, mock_external_service_dependencies
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test file generator retrieval with non-existent file.
@ -707,7 +732,7 @@ class TestFileService:
sign = "test_signature"
with pytest.raises(NotFound, match="File not found or signature is invalid"):
FileService.get_file_generator_by_file_id(
FileService(engine).get_file_generator_by_file_id(
file_id=non_existent_id,
timestamp=timestamp,
nonce=nonce,
@ -715,7 +740,9 @@ class TestFileService:
)
# Test get_public_image_preview method
def test_get_public_image_preview_success(self, db_session_with_containers, mock_external_service_dependencies):
def test_get_public_image_preview_success(
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test successful public image preview generation.
"""
@ -731,14 +758,14 @@ class TestFileService:
db.session.commit()
generator, mime_type = FileService.get_public_image_preview(file_id=upload_file.id)
generator, mime_type = FileService(engine).get_public_image_preview(file_id=upload_file.id)
assert generator is not None
assert mime_type == upload_file.mime_type
mock_external_service_dependencies["storage"].load.assert_called_once()
def test_get_public_image_preview_file_not_found(
self, db_session_with_containers, mock_external_service_dependencies
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test public image preview with non-existent file.
@ -747,10 +774,10 @@ class TestFileService:
non_existent_id = str(fake.uuid4())
with pytest.raises(NotFound, match="File not found or signature is invalid"):
FileService.get_public_image_preview(file_id=non_existent_id)
FileService(engine).get_public_image_preview(file_id=non_existent_id)
def test_get_public_image_preview_unsupported_file_type(
self, db_session_with_containers, mock_external_service_dependencies
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test public image preview with non-image file type.
@ -768,10 +795,10 @@ class TestFileService:
db.session.commit()
with pytest.raises(UnsupportedFileTypeError):
FileService.get_public_image_preview(file_id=upload_file.id)
FileService(engine).get_public_image_preview(file_id=upload_file.id)
# Test edge cases and boundary conditions
def test_upload_file_empty_content(self, db_session_with_containers, mock_external_service_dependencies):
def test_upload_file_empty_content(self, db_session_with_containers, engine, mock_external_service_dependencies):
"""
Test file upload with empty content.
"""
@ -782,7 +809,7 @@ class TestFileService:
content = b""
mimetype = "text/plain"
upload_file = FileService.upload_file(
upload_file = FileService(engine).upload_file(
filename=filename,
content=content,
mimetype=mimetype,
@ -793,7 +820,7 @@ class TestFileService:
assert upload_file.size == 0
def test_upload_file_special_characters_in_name(
self, db_session_with_containers, mock_external_service_dependencies
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test file upload with special characters in filename (but valid ones).
@ -805,7 +832,7 @@ class TestFileService:
content = b"test content"
mimetype = "text/plain"
upload_file = FileService.upload_file(
upload_file = FileService(engine).upload_file(
filename=filename,
content=content,
mimetype=mimetype,
@ -816,7 +843,7 @@ class TestFileService:
assert upload_file.name == filename
def test_upload_file_different_case_extensions(
self, db_session_with_containers, mock_external_service_dependencies
self, db_session_with_containers, engine, mock_external_service_dependencies
):
"""
Test file upload with different case extensions.
@ -828,7 +855,7 @@ class TestFileService:
content = b"test content"
mimetype = "application/pdf"
upload_file = FileService.upload_file(
upload_file = FileService(engine).upload_file(
filename=filename,
content=content,
mimetype=mimetype,
@ -838,7 +865,7 @@ class TestFileService:
assert upload_file is not None
assert upload_file.extension == "pdf" # Should be converted to lowercase
def test_upload_text_empty_text(self, db_session_with_containers, mock_external_service_dependencies):
def test_upload_text_empty_text(self, db_session_with_containers, engine, mock_external_service_dependencies):
"""
Test text upload with empty text.
"""
@ -846,17 +873,22 @@ class TestFileService:
text = ""
text_name = "empty.txt"
# Mock current_user
with patch("services.file_service.current_user") as mock_current_user:
mock_current_user.current_tenant_id = str(fake.uuid4())
mock_current_user.id = str(fake.uuid4())
# Mock current_user using create_autospec
mock_current_user = create_autospec(Account, instance=True)
mock_current_user.current_tenant_id = str(fake.uuid4())
mock_current_user.id = str(fake.uuid4())
upload_file = FileService.upload_text(text=text, text_name=text_name)
upload_file = FileService(engine).upload_text(
text=text,
text_name=text_name,
user_id=mock_current_user.id,
tenant_id=mock_current_user.current_tenant_id,
)
assert upload_file is not None
assert upload_file.size == 0
assert upload_file is not None
assert upload_file.size == 0
def test_file_size_limits_edge_cases(self, db_session_with_containers, mock_external_service_dependencies):
def test_file_size_limits_edge_cases(self, db_session_with_containers, engine, mock_external_service_dependencies):
"""
Test file size limits with edge case values.
"""
@ -868,15 +900,15 @@ class TestFileService:
("pdf", dify_config.UPLOAD_FILE_SIZE_LIMIT),
]:
file_size = limit_config * 1024 * 1024
result = FileService.is_file_size_within_limit(extension=extension, file_size=file_size)
result = FileService(engine).is_file_size_within_limit(extension=extension, file_size=file_size)
assert result is True
# Test one byte over limit
file_size = limit_config * 1024 * 1024 + 1
result = FileService.is_file_size_within_limit(extension=extension, file_size=file_size)
result = FileService(engine).is_file_size_within_limit(extension=extension, file_size=file_size)
assert result is False
def test_upload_file_with_source_url(self, db_session_with_containers, mock_external_service_dependencies):
def test_upload_file_with_source_url(self, db_session_with_containers, engine, mock_external_service_dependencies):
"""
Test file upload with source URL that gets overridden by signed URL.
"""
@ -888,7 +920,7 @@ class TestFileService:
mimetype = "application/pdf"
source_url = "https://original-source.com/file.pdf"
upload_file = FileService.upload_file(
upload_file = FileService(engine).upload_file(
filename=filename,
content=content,
mimetype=mimetype,
@ -901,7 +933,7 @@ class TestFileService:
# The signed URL should only be set when source_url is empty
# Let's test that scenario
upload_file2 = FileService.upload_file(
upload_file2 = FileService(engine).upload_file(
filename="test2.pdf",
content=b"test content 2",
mimetype="application/pdf",

View File

@ -1,4 +1,4 @@
from unittest.mock import patch
from unittest.mock import create_autospec, patch
import pytest
from faker import Faker
@ -17,7 +17,9 @@ class TestMetadataService:
def mock_external_service_dependencies(self):
"""Mock setup for external service dependencies."""
with (
patch("services.metadata_service.current_user") as mock_current_user,
patch(
"services.metadata_service.current_user", create_autospec(Account, instance=True)
) as mock_current_user,
patch("services.metadata_service.redis_client") as mock_redis_client,
patch("services.dataset_service.DocumentService") as mock_document_service,
):
@ -253,7 +255,7 @@ class TestMetadataService:
mock_external_service_dependencies["current_user"].id = account.id
# Try to create metadata with built-in field name
built_in_field_name = BuiltInField.document_name.value
built_in_field_name = BuiltInField.document_name
metadata_args = MetadataArgs(type="string", name=built_in_field_name)
# Act & Assert: Verify proper error handling
@ -373,7 +375,7 @@ class TestMetadataService:
metadata = MetadataService.create_metadata(dataset.id, metadata_args)
# Try to update with built-in field name
built_in_field_name = BuiltInField.document_name.value
built_in_field_name = BuiltInField.document_name
with pytest.raises(ValueError, match="Metadata name already exists in Built-in fields."):
MetadataService.update_metadata_name(dataset.id, metadata.id, built_in_field_name)
@ -538,11 +540,11 @@ class TestMetadataService:
field_names = [field["name"] for field in result]
field_types = [field["type"] for field in result]
assert BuiltInField.document_name.value in field_names
assert BuiltInField.uploader.value in field_names
assert BuiltInField.upload_date.value in field_names
assert BuiltInField.last_update_date.value in field_names
assert BuiltInField.source.value in field_names
assert BuiltInField.document_name in field_names
assert BuiltInField.uploader in field_names
assert BuiltInField.upload_date in field_names
assert BuiltInField.last_update_date in field_names
assert BuiltInField.source in field_names
# Verify field types
assert "string" in field_types
@ -680,11 +682,11 @@ class TestMetadataService:
# Set document metadata with built-in fields
document.doc_metadata = {
BuiltInField.document_name.value: document.name,
BuiltInField.uploader.value: "test_uploader",
BuiltInField.upload_date.value: 1234567890.0,
BuiltInField.last_update_date.value: 1234567890.0,
BuiltInField.source.value: "test_source",
BuiltInField.document_name: document.name,
BuiltInField.uploader: "test_uploader",
BuiltInField.upload_date: 1234567890.0,
BuiltInField.last_update_date: 1234567890.0,
BuiltInField.source: "test_source",
}
db.session.add(document)
db.session.commit()

View File

@ -2,6 +2,7 @@ from unittest.mock import MagicMock, patch
import pytest
from faker import Faker
from sqlalchemy import select
from models.account import TenantAccountJoin, TenantAccountRole
from models.model import Account, Tenant
@ -468,7 +469,7 @@ class TestModelLoadBalancingService:
assert load_balancing_config.id is not None
# Verify inherit config was created in database
inherit_configs = (
db.session.query(LoadBalancingModelConfig).where(LoadBalancingModelConfig.name == "__inherit__").all()
)
inherit_configs = db.session.scalars(
select(LoadBalancingModelConfig).where(LoadBalancingModelConfig.name == "__inherit__")
).all()
assert len(inherit_configs) == 1

View File

@ -1,7 +1,8 @@
from unittest.mock import patch
from unittest.mock import create_autospec, patch
import pytest
from faker import Faker
from sqlalchemy import select
from werkzeug.exceptions import NotFound
from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole
@ -17,7 +18,7 @@ class TestTagService:
def mock_external_service_dependencies(self):
"""Mock setup for external service dependencies."""
with (
patch("services.tag_service.current_user") as mock_current_user,
patch("services.tag_service.current_user", create_autospec(Account, instance=True)) as mock_current_user,
):
# Setup default mock returns
mock_current_user.current_tenant_id = "test-tenant-id"
@ -954,7 +955,9 @@ class TestTagService:
from extensions.ext_database import db
# Verify only one binding exists
bindings = db.session.query(TagBinding).where(TagBinding.tag_id == tag.id, TagBinding.target_id == app.id).all()
bindings = db.session.scalars(
select(TagBinding).where(TagBinding.tag_id == tag.id, TagBinding.target_id == app.id)
).all()
assert len(bindings) == 1
def test_save_tag_binding_invalid_target_type(self, db_session_with_containers, mock_external_service_dependencies):
@ -1064,7 +1067,9 @@ class TestTagService:
# No error should be raised, and database state should remain unchanged
from extensions.ext_database import db
bindings = db.session.query(TagBinding).where(TagBinding.tag_id == tag.id, TagBinding.target_id == app.id).all()
bindings = db.session.scalars(
select(TagBinding).where(TagBinding.tag_id == tag.id, TagBinding.target_id == app.id)
).all()
assert len(bindings) == 0
def test_check_target_exists_knowledge_success(

View File

@ -2,6 +2,7 @@ from unittest.mock import patch
import pytest
from faker import Faker
from sqlalchemy import select
from core.app.entities.app_invoke_entities import InvokeFrom
from models.account import Account
@ -354,16 +355,14 @@ class TestWebConversationService:
# Verify only one pinned conversation record exists
from extensions.ext_database import db
pinned_conversations = (
db.session.query(PinnedConversation)
.where(
pinned_conversations = db.session.scalars(
select(PinnedConversation).where(
PinnedConversation.app_id == app.id,
PinnedConversation.conversation_id == conversation.id,
PinnedConversation.created_by_role == "account",
PinnedConversation.created_by == account.id,
)
.all()
)
).all()
assert len(pinned_conversations) == 1

View File

@ -1,3 +1,5 @@
import time
import uuid
from unittest.mock import patch
import pytest
@ -57,10 +59,12 @@ class TestWebAppAuthService:
tuple: (account, tenant) - Created account and tenant instances
"""
fake = Faker()
import uuid
# Create account
# Create account with unique email to avoid collisions
unique_email = f"test_{uuid.uuid4().hex[:8]}@example.com"
account = Account(
email=fake.email(),
email=unique_email,
name=fake.name(),
interface_language="en-US",
status="active",
@ -109,8 +113,11 @@ class TestWebAppAuthService:
password = fake.password(length=12)
# Create account with password
import uuid
unique_email = f"test_{uuid.uuid4().hex[:8]}@example.com"
account = Account(
email=fake.email(),
email=unique_email,
name=fake.name(),
interface_language="en-US",
status="active",
@ -243,9 +250,15 @@ class TestWebAppAuthService:
- Proper error handling for non-existent accounts
- Correct exception type and message
"""
# Arrange: Use non-existent email
fake = Faker()
non_existent_email = fake.email()
# Arrange: Generate a guaranteed non-existent email
# Use UUID and timestamp to ensure uniqueness
unique_id = str(uuid.uuid4()).replace("-", "")
timestamp = str(int(time.time() * 1000000)) # microseconds
non_existent_email = f"nonexistent_{unique_id}_{timestamp}@test-domain-that-never-exists.invalid"
# Double-check this email doesn't exist in the database
existing_account = db_session_with_containers.query(Account).filter_by(email=non_existent_email).first()
assert existing_account is None, f"Test email {non_existent_email} already exists in database"
# Act & Assert: Verify proper error handling
with pytest.raises(AccountNotFoundError):
@ -322,9 +335,12 @@ class TestWebAppAuthService:
"""
# Arrange: Create account without password
fake = Faker()
import uuid
unique_email = f"test_{uuid.uuid4().hex[:8]}@example.com"
account = Account(
email=fake.email(),
email=unique_email,
name=fake.name(),
interface_language="en-US",
status="active",
@ -431,9 +447,12 @@ class TestWebAppAuthService:
"""
# Arrange: Create banned account
fake = Faker()
import uuid
unique_email = f"test_{uuid.uuid4().hex[:8]}@example.com"
account = Account(
email=fake.email(),
email=unique_email,
name=fake.name(),
interface_language="en-US",
status=AccountStatus.BANNED.value,

View File

@ -108,6 +108,7 @@ class TestWorkflowDraftVariableService:
created_by=app.created_by,
environment_variables=[],
conversation_variables=[],
rag_pipeline_variables=[],
)
from extensions.ext_database import db

View File

@ -96,7 +96,7 @@ class TestWorkflowService:
app.tenant_id = fake.uuid4()
app.name = fake.company()
app.description = fake.text()
app.mode = AppMode.WORKFLOW.value
app.mode = AppMode.WORKFLOW
app.icon_type = "emoji"
app.icon = "🤖"
app.icon_background = "#FFEAD5"
@ -883,7 +883,7 @@ class TestWorkflowService:
# Create chat mode app
app = self._create_test_app(db_session_with_containers, fake)
app.mode = AppMode.CHAT.value
app.mode = AppMode.CHAT
# Create app model config (required for conversion)
from models.model import AppModelConfig
@ -926,7 +926,7 @@ class TestWorkflowService:
# Assert
assert result is not None
assert result.mode == AppMode.ADVANCED_CHAT.value # CHAT mode converts to ADVANCED_CHAT, not WORKFLOW
assert result.mode == AppMode.ADVANCED_CHAT # CHAT mode converts to ADVANCED_CHAT, not WORKFLOW
assert result.name == conversion_args["name"]
assert result.icon == conversion_args["icon"]
assert result.icon_type == conversion_args["icon_type"]
@ -945,7 +945,7 @@ class TestWorkflowService:
# Create completion mode app
app = self._create_test_app(db_session_with_containers, fake)
app.mode = AppMode.COMPLETION.value
app.mode = AppMode.COMPLETION
# Create app model config (required for conversion)
from models.model import AppModelConfig
@ -988,7 +988,7 @@ class TestWorkflowService:
# Assert
assert result is not None
assert result.mode == AppMode.WORKFLOW.value
assert result.mode == AppMode.WORKFLOW
assert result.name == conversion_args["name"]
assert result.icon == conversion_args["icon"]
assert result.icon_type == conversion_args["icon_type"]
@ -1007,7 +1007,7 @@ class TestWorkflowService:
# Create workflow mode app (already in workflow mode)
app = self._create_test_app(db_session_with_containers, fake)
app.mode = AppMode.WORKFLOW.value
app.mode = AppMode.WORKFLOW
from extensions.ext_database import db
@ -1030,7 +1030,7 @@ class TestWorkflowService:
# Arrange
fake = Faker()
app = self._create_test_app(db_session_with_containers, fake)
app.mode = AppMode.ADVANCED_CHAT.value
app.mode = AppMode.ADVANCED_CHAT
from extensions.ext_database import db
@ -1061,7 +1061,7 @@ class TestWorkflowService:
# Arrange
fake = Faker()
app = self._create_test_app(db_session_with_containers, fake)
app.mode = AppMode.WORKFLOW.value
app.mode = AppMode.WORKFLOW
from extensions.ext_database import db
@ -1421,16 +1421,19 @@ class TestWorkflowService:
# Mock successful node execution
def mock_successful_invoke():
from core.workflow.entities.node_entities import NodeRunResult
from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus
from core.workflow.nodes.base.node import BaseNode
from core.workflow.nodes.event import RunCompletedEvent
import uuid
from datetime import datetime
from core.workflow.enums import NodeType, WorkflowNodeExecutionStatus
from core.workflow.graph_events import NodeRunSucceededEvent
from core.workflow.node_events import NodeRunResult
from core.workflow.nodes.base.node import Node
# Create mock node
mock_node = MagicMock(spec=BaseNode)
mock_node.type_ = "start" # Use valid NodeType
mock_node = MagicMock(spec=Node)
mock_node.node_type = NodeType.START
mock_node.title = "Test Node"
mock_node.continue_on_error = False
mock_node.error_strategy = None
# Create mock result with valid metadata
mock_result = NodeRunResult(
@ -1441,25 +1444,37 @@ class TestWorkflowService:
metadata={"total_tokens": 100}, # Use valid metadata field
)
# Create mock event
mock_event = RunCompletedEvent(run_result=mock_result)
# Create mock event with all required fields
mock_event = NodeRunSucceededEvent(
id=str(uuid.uuid4()),
node_id=node_id,
node_type=NodeType.START,
node_run_result=mock_result,
start_at=datetime.now(),
)
return mock_node, [mock_event]
# Return node and generator
def event_generator():
yield mock_event
return mock_node, event_generator()
workflow_service = WorkflowService()
# Act
result = workflow_service._handle_node_run_result(
result = workflow_service._handle_single_step_result(
invoke_node_fn=mock_successful_invoke, start_at=start_at, node_id=node_id
)
# Assert
assert result is not None
assert result.node_id == node_id
assert result.node_type == "start" # Should match the mock node type
from core.workflow.enums import NodeType
assert result.node_type == NodeType.START # Should match the mock node type
assert result.title == "Test Node"
# Import the enum for comparison
from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus
from core.workflow.enums import WorkflowNodeExecutionStatus
assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED
assert result.inputs is not None
@ -1481,34 +1496,47 @@ class TestWorkflowService:
# Mock failed node execution
def mock_failed_invoke():
from core.workflow.entities.node_entities import NodeRunResult
from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus
from core.workflow.nodes.base.node import BaseNode
from core.workflow.nodes.event import RunCompletedEvent
import uuid
from datetime import datetime
from core.workflow.enums import NodeType, WorkflowNodeExecutionStatus
from core.workflow.graph_events import NodeRunFailedEvent
from core.workflow.node_events import NodeRunResult
from core.workflow.nodes.base.node import Node
# Create mock node
mock_node = MagicMock(spec=BaseNode)
mock_node.type_ = "llm" # Use valid NodeType
mock_node = MagicMock(spec=Node)
mock_node.node_type = NodeType.LLM
mock_node.title = "Test Node"
mock_node.continue_on_error = False
mock_node.error_strategy = None
# Create mock failed result
mock_result = NodeRunResult(
status=WorkflowNodeExecutionStatus.FAILED,
inputs={"input1": "value1"},
error="Test error message",
error_type="TestError",
)
# Create mock event
mock_event = RunCompletedEvent(run_result=mock_result)
# Create mock event with all required fields
mock_event = NodeRunFailedEvent(
id=str(uuid.uuid4()),
node_id=node_id,
node_type=NodeType.LLM,
node_run_result=mock_result,
error="Test error message",
start_at=datetime.now(),
)
return mock_node, [mock_event]
# Return node and generator
def event_generator():
yield mock_event
return mock_node, event_generator()
workflow_service = WorkflowService()
# Act
result = workflow_service._handle_node_run_result(
result = workflow_service._handle_single_step_result(
invoke_node_fn=mock_failed_invoke, start_at=start_at, node_id=node_id
)
@ -1516,7 +1544,7 @@ class TestWorkflowService:
assert result is not None
assert result.node_id == node_id
# Import the enum for comparison
from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus
from core.workflow.enums import WorkflowNodeExecutionStatus
assert result.status == WorkflowNodeExecutionStatus.FAILED
assert result.error is not None
@ -1537,17 +1565,18 @@ class TestWorkflowService:
# Mock node execution with continue_on_error
def mock_continue_on_error_invoke():
from core.workflow.entities.node_entities import NodeRunResult
from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus
from core.workflow.nodes.base.node import BaseNode
from core.workflow.nodes.enums import ErrorStrategy
from core.workflow.nodes.event import RunCompletedEvent
import uuid
from datetime import datetime
from core.workflow.enums import ErrorStrategy, NodeType, WorkflowNodeExecutionStatus
from core.workflow.graph_events import NodeRunFailedEvent
from core.workflow.node_events import NodeRunResult
from core.workflow.nodes.base.node import Node
# Create mock node with continue_on_error
mock_node = MagicMock(spec=BaseNode)
mock_node.type_ = "tool" # Use valid NodeType
mock_node = MagicMock(spec=Node)
mock_node.node_type = NodeType.TOOL
mock_node.title = "Test Node"
mock_node.continue_on_error = True
mock_node.error_strategy = ErrorStrategy.DEFAULT_VALUE
mock_node.default_value_dict = {"default_output": "default_value"}
@ -1556,18 +1585,28 @@ class TestWorkflowService:
status=WorkflowNodeExecutionStatus.FAILED,
inputs={"input1": "value1"},
error="Test error message",
error_type="TestError",
)
# Create mock event
mock_event = RunCompletedEvent(run_result=mock_result)
# Create mock event with all required fields
mock_event = NodeRunFailedEvent(
id=str(uuid.uuid4()),
node_id=node_id,
node_type=NodeType.TOOL,
node_run_result=mock_result,
error="Test error message",
start_at=datetime.now(),
)
return mock_node, [mock_event]
# Return node and generator
def event_generator():
yield mock_event
return mock_node, event_generator()
workflow_service = WorkflowService()
# Act
result = workflow_service._handle_node_run_result(
result = workflow_service._handle_single_step_result(
invoke_node_fn=mock_continue_on_error_invoke, start_at=start_at, node_id=node_id
)
@ -1575,7 +1614,7 @@ class TestWorkflowService:
assert result is not None
assert result.node_id == node_id
# Import the enum for comparison
from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus
from core.workflow.enums import WorkflowNodeExecutionStatus
assert result.status == WorkflowNodeExecutionStatus.EXCEPTION # Should be EXCEPTION, not FAILED
assert result.outputs is not None

View File

@ -706,7 +706,14 @@ class TestMCPToolManageService:
# Verify mock interactions
mock_mcp_client.assert_called_once_with(
"https://example.com/mcp", mcp_provider.id, tenant.id, authed=False, for_list=True
"https://example.com/mcp",
mcp_provider.id,
tenant.id,
authed=False,
for_list=True,
headers={},
timeout=30.0,
sse_read_timeout=300.0,
)
def test_list_mcp_tool_from_remote_server_auth_error(
@ -1181,6 +1188,11 @@ class TestMCPToolManageService:
db_session_with_containers, mock_external_service_dependencies
)
# Create MCP provider first
mcp_provider = self._create_test_mcp_provider(
db_session_with_containers, mock_external_service_dependencies, tenant.id, account.id
)
# Mock MCPClient and its context manager
mock_tools = [
type("MockTool", (), {"model_dump": lambda self: {"name": "test_tool_1", "description": "Test tool 1"}})(),
@ -1194,7 +1206,7 @@ class TestMCPToolManageService:
# Act: Execute the method under test
result = MCPToolManageService._re_connect_mcp_provider(
"https://example.com/mcp", "test_provider_id", tenant.id
"https://example.com/mcp", mcp_provider.id, tenant.id
)
# Assert: Verify the expected outcomes
@ -1213,7 +1225,14 @@ class TestMCPToolManageService:
# Verify mock interactions
mock_mcp_client.assert_called_once_with(
"https://example.com/mcp", "test_provider_id", tenant.id, authed=False, for_list=True
"https://example.com/mcp",
mcp_provider.id,
tenant.id,
authed=False,
for_list=True,
headers={},
timeout=30.0,
sse_read_timeout=300.0,
)
def test_re_connect_mcp_provider_auth_error(self, db_session_with_containers, mock_external_service_dependencies):
@ -1231,6 +1250,11 @@ class TestMCPToolManageService:
db_session_with_containers, mock_external_service_dependencies
)
# Create MCP provider first
mcp_provider = self._create_test_mcp_provider(
db_session_with_containers, mock_external_service_dependencies, tenant.id, account.id
)
# Mock MCPClient to raise authentication error
with patch("services.tools.mcp_tools_manage_service.MCPClient") as mock_mcp_client:
from core.mcp.error import MCPAuthError
@ -1240,7 +1264,7 @@ class TestMCPToolManageService:
# Act: Execute the method under test
result = MCPToolManageService._re_connect_mcp_provider(
"https://example.com/mcp", "test_provider_id", tenant.id
"https://example.com/mcp", mcp_provider.id, tenant.id
)
# Assert: Verify the expected outcomes
@ -1265,6 +1289,11 @@ class TestMCPToolManageService:
db_session_with_containers, mock_external_service_dependencies
)
# Create MCP provider first
mcp_provider = self._create_test_mcp_provider(
db_session_with_containers, mock_external_service_dependencies, tenant.id, account.id
)
# Mock MCPClient to raise connection error
with patch("services.tools.mcp_tools_manage_service.MCPClient") as mock_mcp_client:
from core.mcp.error import MCPError
@ -1274,4 +1303,4 @@ class TestMCPToolManageService:
# Act & Assert: Verify proper error handling
with pytest.raises(ValueError, match="Failed to re-connect MCP server: Connection failed"):
MCPToolManageService._re_connect_mcp_provider("https://example.com/mcp", "test_provider_id", tenant.id)
MCPToolManageService._re_connect_mcp_provider("https://example.com/mcp", mcp_provider.id, tenant.id)

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.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,
):
# 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 = ToolTransformService.get_plugin_icon_url(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 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 = ToolTransformService.get_plugin_icon_url(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 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.value
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.value
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.value
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.value
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.value
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.value, "name": fake.company(), "icon": "🔧"}
# Act: Execute the method under test
ToolTransformService.repack_provider(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=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 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 "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(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_tool_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

View File

@ -0,0 +1,554 @@
import json
from unittest.mock import patch
import pytest
from faker import Faker
from core.app.app_config.entities import (
DatasetEntity,
DatasetRetrieveConfigEntity,
ExternalDataVariableEntity,
ModelConfigEntity,
PromptTemplateEntity,
VariableEntity,
VariableEntityType,
)
from core.model_runtime.entities.llm_entities import LLMMode
from core.prompt.utils.prompt_template_parser import PromptTemplateParser
from models.account import Account, Tenant
from models.api_based_extension import APIBasedExtension
from models.model import App, AppMode, AppModelConfig
from models.workflow import Workflow
from services.workflow.workflow_converter import WorkflowConverter
class TestWorkflowConverter:
"""Integration tests for WorkflowConverter using testcontainers."""
@pytest.fixture
def mock_external_service_dependencies(self):
"""Mock setup for external service dependencies."""
with (
patch("services.workflow.workflow_converter.encrypter") as mock_encrypter,
patch("services.workflow.workflow_converter.SimplePromptTransform") as mock_prompt_transform,
patch("services.workflow.workflow_converter.AgentChatAppConfigManager") as mock_agent_chat_config_manager,
patch("services.workflow.workflow_converter.ChatAppConfigManager") as mock_chat_config_manager,
patch("services.workflow.workflow_converter.CompletionAppConfigManager") as mock_completion_config_manager,
):
# Setup default mock returns
mock_encrypter.decrypt_token.return_value = "decrypted_api_key"
mock_prompt_transform.return_value.get_prompt_template.return_value = {
"prompt_template": PromptTemplateParser(template="You are a helpful assistant {{text_input}}"),
"prompt_rules": {"human_prefix": "Human", "assistant_prefix": "Assistant"},
}
mock_agent_chat_config_manager.get_app_config.return_value = self._create_mock_app_config()
mock_chat_config_manager.get_app_config.return_value = self._create_mock_app_config()
mock_completion_config_manager.get_app_config.return_value = self._create_mock_app_config()
yield {
"encrypter": mock_encrypter,
"prompt_transform": mock_prompt_transform,
"agent_chat_config_manager": mock_agent_chat_config_manager,
"chat_config_manager": mock_chat_config_manager,
"completion_config_manager": mock_completion_config_manager,
}
def _create_mock_app_config(self):
"""Helper method to create a mock app config."""
mock_config = type("obj", (object,), {})()
mock_config.variables = [
VariableEntity(
variable="text_input",
label="Text Input",
type=VariableEntityType.TEXT_INPUT,
)
]
mock_config.model = ModelConfigEntity(
provider="openai",
model="gpt-4",
mode=LLMMode.CHAT.value,
parameters={},
stop=[],
)
mock_config.prompt_template = PromptTemplateEntity(
prompt_type=PromptTemplateEntity.PromptType.SIMPLE,
simple_prompt_template="You are a helpful assistant {{text_input}}",
)
mock_config.dataset = None
mock_config.external_data_variables = []
mock_config.additional_features = type("obj", (object,), {"file_upload": None})()
mock_config.app_model_config_dict = {}
return mock_config
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.value,
current=True,
)
db.session.add(join)
db.session.commit()
# Set current tenant for account
account.current_tenant = tenant
return account, tenant
def _create_test_app(self, db_session_with_containers, mock_external_service_dependencies, tenant, account):
"""
Helper method to create a test app for testing.
Args:
db_session_with_containers: Database session from testcontainers infrastructure
mock_external_service_dependencies: Mock dependencies
tenant: Tenant instance
account: Account instance
Returns:
App: Created app instance
"""
fake = Faker()
# Create app
app = App(
tenant_id=tenant.id,
name=fake.company(),
mode=AppMode.CHAT.value,
icon_type="emoji",
icon="🤖",
icon_background="#FF6B6B",
enable_site=True,
enable_api=True,
api_rpm=100,
api_rph=10,
is_demo=False,
is_public=False,
created_by=account.id,
updated_by=account.id,
)
from extensions.ext_database import db
db.session.add(app)
db.session.commit()
# Create app model config
app_model_config = AppModelConfig(
app_id=app.id,
provider="openai",
model="gpt-4",
configs={},
created_by=account.id,
updated_by=account.id,
)
db.session.add(app_model_config)
db.session.commit()
# Link app model config to app
app.app_model_config_id = app_model_config.id
db.session.commit()
return app
def test_convert_to_workflow_success(self, db_session_with_containers, mock_external_service_dependencies):
"""
Test successful conversion of app to workflow.
This test verifies:
- Proper app to workflow conversion
- Correct database state after conversion
- Proper relationship establishment
- Workflow creation with correct configuration
"""
# Arrange: Create test data
fake = Faker()
account, tenant = self._create_test_account_and_tenant(
db_session_with_containers, mock_external_service_dependencies
)
app = self._create_test_app(db_session_with_containers, mock_external_service_dependencies, tenant, account)
# Act: Execute the conversion
workflow_converter = WorkflowConverter()
new_app = workflow_converter.convert_to_workflow(
app_model=app,
account=account,
name="Test Workflow App",
icon_type="emoji",
icon="🚀",
icon_background="#4CAF50",
)
# Assert: Verify the expected outcomes
assert new_app is not None
assert new_app.name == "Test Workflow App"
assert new_app.mode == AppMode.ADVANCED_CHAT.value
assert new_app.icon_type == "emoji"
assert new_app.icon == "🚀"
assert new_app.icon_background == "#4CAF50"
assert new_app.tenant_id == app.tenant_id
assert new_app.created_by == account.id
# Verify database state
from extensions.ext_database import db
db.session.refresh(new_app)
assert new_app.id is not None
# Verify workflow was created
workflow = db.session.query(Workflow).where(Workflow.app_id == new_app.id).first()
assert workflow is not None
assert workflow.tenant_id == app.tenant_id
assert workflow.type == "chat"
def test_convert_to_workflow_without_app_model_config_error(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test error handling when app model config is missing.
This test verifies:
- Proper error handling for missing app model config
- Correct exception type and message
- Database state remains unchanged
"""
# Arrange: Create test data without app model config
fake = Faker()
account, tenant = self._create_test_account_and_tenant(
db_session_with_containers, mock_external_service_dependencies
)
app = App(
tenant_id=tenant.id,
name=fake.company(),
mode=AppMode.CHAT.value,
icon_type="emoji",
icon="🤖",
icon_background="#FF6B6B",
enable_site=True,
enable_api=True,
api_rpm=100,
api_rph=10,
is_demo=False,
is_public=False,
created_by=account.id,
updated_by=account.id,
)
from extensions.ext_database import db
db.session.add(app)
db.session.commit()
# Act & Assert: Verify proper error handling
workflow_converter = WorkflowConverter()
# Check initial state
initial_workflow_count = db.session.query(Workflow).count()
with pytest.raises(ValueError, match="App model config is required"):
workflow_converter.convert_to_workflow(
app_model=app,
account=account,
name="Test Workflow App",
icon_type="emoji",
icon="🚀",
icon_background="#4CAF50",
)
# Verify database state remains unchanged
# The workflow creation happens in convert_app_model_config_to_workflow
# which is called before the app_model_config check, so we need to clean up
db.session.rollback()
final_workflow_count = db.session.query(Workflow).count()
assert final_workflow_count == initial_workflow_count
def test_convert_app_model_config_to_workflow_success(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test successful conversion of app model config to workflow.
This test verifies:
- Proper app model config to workflow conversion
- Correct workflow graph structure
- Proper node creation and configuration
- Database state management
"""
# Arrange: Create test data
fake = Faker()
account, tenant = self._create_test_account_and_tenant(
db_session_with_containers, mock_external_service_dependencies
)
app = self._create_test_app(db_session_with_containers, mock_external_service_dependencies, tenant, account)
# Act: Execute the conversion
workflow_converter = WorkflowConverter()
workflow = workflow_converter.convert_app_model_config_to_workflow(
app_model=app,
app_model_config=app.app_model_config,
account_id=account.id,
)
# Assert: Verify the expected outcomes
assert workflow is not None
assert workflow.tenant_id == app.tenant_id
assert workflow.app_id == app.id
assert workflow.type == "chat"
assert workflow.version == Workflow.VERSION_DRAFT
assert workflow.created_by == account.id
# Verify workflow graph structure
graph = json.loads(workflow.graph)
assert "nodes" in graph
assert "edges" in graph
assert len(graph["nodes"]) > 0
assert len(graph["edges"]) > 0
# Verify start node exists
start_node = next((node for node in graph["nodes"] if node["data"]["type"] == "start"), None)
assert start_node is not None
assert start_node["id"] == "start"
# Verify LLM node exists
llm_node = next((node for node in graph["nodes"] if node["data"]["type"] == "llm"), None)
assert llm_node is not None
assert llm_node["id"] == "llm"
# Verify answer node exists for chat mode
answer_node = next((node for node in graph["nodes"] if node["data"]["type"] == "answer"), None)
assert answer_node is not None
assert answer_node["id"] == "answer"
# Verify database state
from extensions.ext_database import db
db.session.refresh(workflow)
assert workflow.id is not None
# Verify features were set
features = json.loads(workflow._features) if workflow._features else {}
assert isinstance(features, dict)
def test_convert_to_start_node_success(self, db_session_with_containers, mock_external_service_dependencies):
"""
Test successful conversion to start node.
This test verifies:
- Proper start node creation with variables
- Correct node structure and data
- Variable encoding and formatting
"""
# Arrange: Create test variables
variables = [
VariableEntity(
variable="text_input",
label="Text Input",
type=VariableEntityType.TEXT_INPUT,
),
VariableEntity(
variable="number_input",
label="Number Input",
type=VariableEntityType.NUMBER,
),
]
# Act: Execute the conversion
workflow_converter = WorkflowConverter()
start_node = workflow_converter._convert_to_start_node(variables=variables)
# Assert: Verify the expected outcomes
assert start_node is not None
assert start_node["id"] == "start"
assert start_node["data"]["title"] == "START"
assert start_node["data"]["type"] == "start"
assert len(start_node["data"]["variables"]) == 2
# Verify variable encoding
first_variable = start_node["data"]["variables"][0]
assert first_variable["variable"] == "text_input"
assert first_variable["label"] == "Text Input"
assert first_variable["type"] == "text-input"
second_variable = start_node["data"]["variables"][1]
assert second_variable["variable"] == "number_input"
assert second_variable["label"] == "Number Input"
assert second_variable["type"] == "number"
def test_convert_to_http_request_node_success(self, db_session_with_containers, mock_external_service_dependencies):
"""
Test successful conversion to HTTP request node.
This test verifies:
- Proper HTTP request node creation
- Correct API configuration and authorization
- Code node creation for response parsing
- External data variable mapping
"""
# Arrange: Create test data
fake = Faker()
account, tenant = self._create_test_account_and_tenant(
db_session_with_containers, mock_external_service_dependencies
)
app = self._create_test_app(db_session_with_containers, mock_external_service_dependencies, tenant, account)
# Create API based extension
api_based_extension = APIBasedExtension(
tenant_id=tenant.id,
name="Test API Extension",
api_key="encrypted_api_key",
api_endpoint="https://api.example.com/test",
)
from extensions.ext_database import db
db.session.add(api_based_extension)
db.session.commit()
# Mock encrypter
mock_external_service_dependencies["encrypter"].decrypt_token.return_value = "decrypted_api_key"
variables = [
VariableEntity(
variable="user_input",
label="User Input",
type=VariableEntityType.TEXT_INPUT,
)
]
external_data_variables = [
ExternalDataVariableEntity(
variable="external_data", type="api", config={"api_based_extension_id": api_based_extension.id}
)
]
# Act: Execute the conversion
workflow_converter = WorkflowConverter()
nodes, external_data_variable_node_mapping = workflow_converter._convert_to_http_request_node(
app_model=app,
variables=variables,
external_data_variables=external_data_variables,
)
# Assert: Verify the expected outcomes
assert len(nodes) == 2 # HTTP request node + code node
assert len(external_data_variable_node_mapping) == 1
# Verify HTTP request node
http_request_node = nodes[0]
assert http_request_node["data"]["type"] == "http-request"
assert http_request_node["data"]["method"] == "post"
assert http_request_node["data"]["url"] == api_based_extension.api_endpoint
assert http_request_node["data"]["authorization"]["type"] == "api-key"
assert http_request_node["data"]["authorization"]["config"]["type"] == "bearer"
assert http_request_node["data"]["authorization"]["config"]["api_key"] == "decrypted_api_key"
# Verify code node
code_node = nodes[1]
assert code_node["data"]["type"] == "code"
assert code_node["data"]["code_language"] == "python3"
assert "response_json" in code_node["data"]["variables"][0]["variable"]
# Verify mapping
assert external_data_variable_node_mapping["external_data"] == code_node["id"]
def test_convert_to_knowledge_retrieval_node_success(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test successful conversion to knowledge retrieval node.
This test verifies:
- Proper knowledge retrieval node creation
- Correct dataset configuration
- Model configuration integration
- Query variable selector setup
"""
# Arrange: Create test data
fake = Faker()
account, tenant = self._create_test_account_and_tenant(
db_session_with_containers, mock_external_service_dependencies
)
# Create dataset config
dataset_config = DatasetEntity(
dataset_ids=["dataset_1", "dataset_2"],
retrieve_config=DatasetRetrieveConfigEntity(
retrieve_strategy=DatasetRetrieveConfigEntity.RetrieveStrategy.MULTIPLE,
top_k=10,
score_threshold=0.8,
reranking_model={"provider": "cohere", "model": "rerank-v2"},
reranking_enabled=True,
),
)
model_config = ModelConfigEntity(
provider="openai",
model="gpt-4",
mode=LLMMode.CHAT.value,
parameters={"temperature": 0.7},
stop=[],
)
# Act: Execute the conversion for advanced chat mode
workflow_converter = WorkflowConverter()
node = workflow_converter._convert_to_knowledge_retrieval_node(
new_app_mode=AppMode.ADVANCED_CHAT,
dataset_config=dataset_config,
model_config=model_config,
)
# Assert: Verify the expected outcomes
assert node is not None
assert node["data"]["type"] == "knowledge-retrieval"
assert node["data"]["title"] == "KNOWLEDGE RETRIEVAL"
assert node["data"]["dataset_ids"] == ["dataset_1", "dataset_2"]
assert node["data"]["retrieval_mode"] == "multiple"
assert node["data"]["query_variable_selector"] == ["sys", "query"]
# Verify multiple retrieval config
multiple_config = node["data"]["multiple_retrieval_config"]
assert multiple_config["top_k"] == 10
assert multiple_config["score_threshold"] == 0.8
assert multiple_config["reranking_model"]["provider"] == "cohere"
assert multiple_config["reranking_model"]["model"] == "rerank-v2"
# Verify single retrieval config is None for multiple strategy
assert node["data"]["single_retrieval_config"] is None