mirror of
https://github.com/langgenius/dify.git
synced 2026-03-13 11:07:40 +08:00
Merge remote-tracking branch 'origin/main' into feat/trigger
This commit is contained in:
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -108,6 +108,7 @@ class TestWorkflowDraftVariableService:
|
||||
created_by=app.created_by,
|
||||
environment_variables=[],
|
||||
conversation_variables=[],
|
||||
rag_pipeline_variables=[],
|
||||
)
|
||||
from extensions.ext_database import db
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
@ -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
|
||||
@ -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
|
||||
Reference in New Issue
Block a user