mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 02:18:08 +08:00
Merge branch 'main' into sandboxed-agent-rebase
Made-with: Cursor # Conflicts: # api/tests/unit_tests/controllers/console/app/test_message.py # api/tests/unit_tests/controllers/console/app/test_statistic.py # api/tests/unit_tests/controllers/console/app/test_workflow_draft_variable.py # api/tests/unit_tests/controllers/console/auth/test_data_source_bearer_auth.py # api/tests/unit_tests/controllers/console/auth/test_data_source_oauth.py # api/tests/unit_tests/controllers/console/auth/test_oauth_server.py # web/app/components/header/account-setting/data-source-page/data-source-notion/operate/index.tsx # web/app/components/header/account-setting/data-source-page/data-source-website/config-firecrawl-modal.tsx # web/app/components/header/account-setting/data-source-page/data-source-website/config-jina-reader-modal.tsx # web/app/components/header/account-setting/data-source-page/data-source-website/config-watercrawl-modal.tsx # web/app/components/header/account-setting/data-source-page/panel/config-item.tsx # web/app/components/header/account-setting/data-source-page/panel/index.tsx # web/app/components/workflow/nodes/knowledge-retrieval/node.tsx # web/package.json # web/pnpm-lock.yaml
This commit is contained in:
@ -1,643 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from core.tools.entities.tool_entities import ApiProviderSchemaType
|
||||
from services.tools.api_tools_manage_service import ApiToolManageService
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_db(mocker: MockerFixture) -> MagicMock:
|
||||
# Arrange
|
||||
mocked_db = mocker.patch("services.tools.api_tools_manage_service.db")
|
||||
mocked_db.session = MagicMock()
|
||||
return mocked_db
|
||||
|
||||
|
||||
def _tool_bundle(operation_id: str = "tool-1") -> SimpleNamespace:
|
||||
return SimpleNamespace(operation_id=operation_id)
|
||||
|
||||
|
||||
def test_parser_api_schema_should_return_schema_payload_when_schema_is_valid(mocker: MockerFixture) -> None:
|
||||
# Arrange
|
||||
mocker.patch(
|
||||
"services.tools.api_tools_manage_service.ApiBasedToolSchemaParser.auto_parse_to_tool_bundle",
|
||||
return_value=([_tool_bundle()], ApiProviderSchemaType.OPENAPI.value),
|
||||
)
|
||||
|
||||
# Act
|
||||
result = ApiToolManageService.parser_api_schema("valid-schema")
|
||||
|
||||
# Assert
|
||||
assert result["schema_type"] == ApiProviderSchemaType.OPENAPI.value
|
||||
assert len(result["credentials_schema"]) == 3
|
||||
assert "warning" in result
|
||||
|
||||
|
||||
def test_parser_api_schema_should_raise_value_error_when_parser_raises(mocker: MockerFixture) -> None:
|
||||
# Arrange
|
||||
mocker.patch(
|
||||
"services.tools.api_tools_manage_service.ApiBasedToolSchemaParser.auto_parse_to_tool_bundle",
|
||||
side_effect=RuntimeError("bad schema"),
|
||||
)
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(ValueError, match="invalid schema: invalid schema: bad schema"):
|
||||
ApiToolManageService.parser_api_schema("invalid")
|
||||
|
||||
|
||||
def test_convert_schema_to_tool_bundles_should_return_tool_bundles_when_valid(mocker: MockerFixture) -> None:
|
||||
# Arrange
|
||||
expected = ([_tool_bundle("a"), _tool_bundle("b")], ApiProviderSchemaType.SWAGGER)
|
||||
mocker.patch(
|
||||
"services.tools.api_tools_manage_service.ApiBasedToolSchemaParser.auto_parse_to_tool_bundle",
|
||||
return_value=expected,
|
||||
)
|
||||
extra_info: dict[str, str] = {}
|
||||
|
||||
# Act
|
||||
result = ApiToolManageService.convert_schema_to_tool_bundles("schema", extra_info=extra_info)
|
||||
|
||||
# Assert
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_convert_schema_to_tool_bundles_should_raise_value_error_when_parser_fails(mocker: MockerFixture) -> None:
|
||||
# Arrange
|
||||
mocker.patch(
|
||||
"services.tools.api_tools_manage_service.ApiBasedToolSchemaParser.auto_parse_to_tool_bundle",
|
||||
side_effect=ValueError("parse failed"),
|
||||
)
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(ValueError, match="invalid schema: parse failed"):
|
||||
ApiToolManageService.convert_schema_to_tool_bundles("schema")
|
||||
|
||||
|
||||
def test_create_api_tool_provider_should_raise_error_when_provider_already_exists(
|
||||
mock_db: MagicMock,
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
# Arrange
|
||||
mock_db.session.query.return_value.where.return_value.first.return_value = object()
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(ValueError, match="provider provider-a already exists"):
|
||||
ApiToolManageService.create_api_tool_provider(
|
||||
user_id="user-1",
|
||||
tenant_id="tenant-1",
|
||||
provider_name=" provider-a ",
|
||||
icon={"emoji": "X"},
|
||||
credentials={"auth_type": "none"},
|
||||
schema_type=ApiProviderSchemaType.OPENAPI,
|
||||
schema="schema",
|
||||
privacy_policy="privacy",
|
||||
custom_disclaimer="custom",
|
||||
labels=[],
|
||||
)
|
||||
|
||||
|
||||
def test_create_api_tool_provider_should_raise_error_when_tool_count_exceeds_limit(
|
||||
mock_db: MagicMock,
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
# Arrange
|
||||
mock_db.session.query.return_value.where.return_value.first.return_value = None
|
||||
many_tools = [_tool_bundle(str(i)) for i in range(101)]
|
||||
mocker.patch.object(
|
||||
ApiToolManageService,
|
||||
"convert_schema_to_tool_bundles",
|
||||
return_value=(many_tools, ApiProviderSchemaType.OPENAPI),
|
||||
)
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(ValueError, match="the number of apis should be less than 100"):
|
||||
ApiToolManageService.create_api_tool_provider(
|
||||
user_id="user-1",
|
||||
tenant_id="tenant-1",
|
||||
provider_name="provider-a",
|
||||
icon={"emoji": "X"},
|
||||
credentials={"auth_type": "none"},
|
||||
schema_type=ApiProviderSchemaType.OPENAPI,
|
||||
schema="schema",
|
||||
privacy_policy="privacy",
|
||||
custom_disclaimer="custom",
|
||||
labels=[],
|
||||
)
|
||||
|
||||
|
||||
def test_create_api_tool_provider_should_raise_error_when_auth_type_is_missing(
|
||||
mock_db: MagicMock,
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
# Arrange
|
||||
mock_db.session.query.return_value.where.return_value.first.return_value = None
|
||||
mocker.patch.object(
|
||||
ApiToolManageService,
|
||||
"convert_schema_to_tool_bundles",
|
||||
return_value=([_tool_bundle()], ApiProviderSchemaType.OPENAPI),
|
||||
)
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(ValueError, match="auth_type is required"):
|
||||
ApiToolManageService.create_api_tool_provider(
|
||||
user_id="user-1",
|
||||
tenant_id="tenant-1",
|
||||
provider_name="provider-a",
|
||||
icon={"emoji": "X"},
|
||||
credentials={},
|
||||
schema_type=ApiProviderSchemaType.OPENAPI,
|
||||
schema="schema",
|
||||
privacy_policy="privacy",
|
||||
custom_disclaimer="custom",
|
||||
labels=[],
|
||||
)
|
||||
|
||||
|
||||
def test_create_api_tool_provider_should_create_provider_when_input_is_valid(
|
||||
mock_db: MagicMock,
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
# Arrange
|
||||
mock_db.session.query.return_value.where.return_value.first.return_value = None
|
||||
mocker.patch.object(
|
||||
ApiToolManageService,
|
||||
"convert_schema_to_tool_bundles",
|
||||
return_value=([_tool_bundle()], ApiProviderSchemaType.OPENAPI),
|
||||
)
|
||||
mock_controller = MagicMock()
|
||||
mocker.patch(
|
||||
"services.tools.api_tools_manage_service.ApiToolProviderController.from_db",
|
||||
return_value=mock_controller,
|
||||
)
|
||||
mock_encrypter = MagicMock()
|
||||
mock_encrypter.encrypt.return_value = {"auth_type": "none"}
|
||||
mocker.patch(
|
||||
"services.tools.api_tools_manage_service.create_tool_provider_encrypter",
|
||||
return_value=(mock_encrypter, MagicMock()),
|
||||
)
|
||||
mocker.patch("services.tools.api_tools_manage_service.ToolLabelManager.update_tool_labels")
|
||||
|
||||
# Act
|
||||
result = ApiToolManageService.create_api_tool_provider(
|
||||
user_id="user-1",
|
||||
tenant_id="tenant-1",
|
||||
provider_name="provider-a",
|
||||
icon={"emoji": "X"},
|
||||
credentials={"auth_type": "none"},
|
||||
schema_type=ApiProviderSchemaType.OPENAPI,
|
||||
schema="schema",
|
||||
privacy_policy="privacy",
|
||||
custom_disclaimer="custom",
|
||||
labels=["news"],
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result == {"result": "success"}
|
||||
mock_controller.load_bundled_tools.assert_called_once()
|
||||
mock_db.session.add.assert_called_once()
|
||||
mock_db.session.commit.assert_called_once()
|
||||
|
||||
|
||||
def test_get_api_tool_provider_remote_schema_should_return_schema_when_response_is_valid(
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
# Arrange
|
||||
mocker.patch(
|
||||
"services.tools.api_tools_manage_service.get",
|
||||
return_value=SimpleNamespace(status_code=200, text="schema-content"),
|
||||
)
|
||||
mocker.patch.object(ApiToolManageService, "parser_api_schema", return_value={"ok": True})
|
||||
|
||||
# Act
|
||||
result = ApiToolManageService.get_api_tool_provider_remote_schema("user-1", "tenant-1", "https://schema")
|
||||
|
||||
# Assert
|
||||
assert result == {"schema": "schema-content"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status_code", [400, 404, 500])
|
||||
def test_get_api_tool_provider_remote_schema_should_raise_error_when_remote_fetch_is_invalid(
|
||||
status_code: int,
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
# Arrange
|
||||
mocker.patch(
|
||||
"services.tools.api_tools_manage_service.get",
|
||||
return_value=SimpleNamespace(status_code=status_code, text="schema-content"),
|
||||
)
|
||||
mock_logger = mocker.patch("services.tools.api_tools_manage_service.logger")
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(ValueError, match="invalid schema, please check the url you provided"):
|
||||
ApiToolManageService.get_api_tool_provider_remote_schema("user-1", "tenant-1", "https://schema")
|
||||
mock_logger.exception.assert_called_once()
|
||||
|
||||
|
||||
def test_list_api_tool_provider_tools_should_raise_error_when_provider_not_found(
|
||||
mock_db: MagicMock,
|
||||
) -> None:
|
||||
# Arrange
|
||||
mock_db.session.query.return_value.where.return_value.first.return_value = None
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(ValueError, match="you have not added provider provider-a"):
|
||||
ApiToolManageService.list_api_tool_provider_tools("user-1", "tenant-1", "provider-a")
|
||||
|
||||
|
||||
def test_list_api_tool_provider_tools_should_return_converted_tools_when_provider_exists(
|
||||
mock_db: MagicMock,
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
# Arrange
|
||||
provider = SimpleNamespace(tools=[_tool_bundle("tool-a"), _tool_bundle("tool-b")])
|
||||
mock_db.session.query.return_value.where.return_value.first.return_value = provider
|
||||
controller = MagicMock()
|
||||
mocker.patch(
|
||||
"services.tools.api_tools_manage_service.ToolTransformService.api_provider_to_controller",
|
||||
return_value=controller,
|
||||
)
|
||||
mocker.patch("services.tools.api_tools_manage_service.ToolLabelManager.get_tool_labels", return_value=["search"])
|
||||
mock_convert = mocker.patch(
|
||||
"services.tools.api_tools_manage_service.ToolTransformService.convert_tool_entity_to_api_entity",
|
||||
side_effect=[{"name": "tool-a"}, {"name": "tool-b"}],
|
||||
)
|
||||
|
||||
# Act
|
||||
result = ApiToolManageService.list_api_tool_provider_tools("user-1", "tenant-1", "provider-a")
|
||||
|
||||
# Assert
|
||||
assert result == [{"name": "tool-a"}, {"name": "tool-b"}]
|
||||
assert mock_convert.call_count == 2
|
||||
|
||||
|
||||
def test_update_api_tool_provider_should_raise_error_when_original_provider_not_found(
|
||||
mock_db: MagicMock,
|
||||
) -> None:
|
||||
# Arrange
|
||||
mock_db.session.query.return_value.where.return_value.first.return_value = None
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(ValueError, match="api provider provider-a does not exists"):
|
||||
ApiToolManageService.update_api_tool_provider(
|
||||
user_id="user-1",
|
||||
tenant_id="tenant-1",
|
||||
provider_name="provider-a",
|
||||
original_provider="provider-a",
|
||||
icon={},
|
||||
credentials={"auth_type": "none"},
|
||||
_schema_type=ApiProviderSchemaType.OPENAPI,
|
||||
schema="schema",
|
||||
privacy_policy=None,
|
||||
custom_disclaimer="custom",
|
||||
labels=[],
|
||||
)
|
||||
|
||||
|
||||
def test_update_api_tool_provider_should_raise_error_when_auth_type_missing(
|
||||
mock_db: MagicMock,
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
# Arrange
|
||||
provider = SimpleNamespace(credentials={}, name="old")
|
||||
mock_db.session.query.return_value.where.return_value.first.return_value = provider
|
||||
mocker.patch.object(
|
||||
ApiToolManageService,
|
||||
"convert_schema_to_tool_bundles",
|
||||
return_value=([_tool_bundle()], ApiProviderSchemaType.OPENAPI),
|
||||
)
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(ValueError, match="auth_type is required"):
|
||||
ApiToolManageService.update_api_tool_provider(
|
||||
user_id="user-1",
|
||||
tenant_id="tenant-1",
|
||||
provider_name="provider-a",
|
||||
original_provider="provider-a",
|
||||
icon={},
|
||||
credentials={},
|
||||
_schema_type=ApiProviderSchemaType.OPENAPI,
|
||||
schema="schema",
|
||||
privacy_policy=None,
|
||||
custom_disclaimer="custom",
|
||||
labels=[],
|
||||
)
|
||||
|
||||
|
||||
def test_update_api_tool_provider_should_update_provider_and_preserve_masked_credentials(
|
||||
mock_db: MagicMock,
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
# Arrange
|
||||
provider = SimpleNamespace(
|
||||
credentials={"auth_type": "none", "api_key_value": "encrypted-old"},
|
||||
name="old",
|
||||
icon="",
|
||||
schema="",
|
||||
description="",
|
||||
schema_type_str="",
|
||||
tools_str="",
|
||||
privacy_policy="",
|
||||
custom_disclaimer="",
|
||||
credentials_str="",
|
||||
)
|
||||
mock_db.session.query.return_value.where.return_value.first.return_value = provider
|
||||
mocker.patch.object(
|
||||
ApiToolManageService,
|
||||
"convert_schema_to_tool_bundles",
|
||||
return_value=([_tool_bundle()], ApiProviderSchemaType.OPENAPI),
|
||||
)
|
||||
controller = MagicMock()
|
||||
mocker.patch(
|
||||
"services.tools.api_tools_manage_service.ApiToolProviderController.from_db",
|
||||
return_value=controller,
|
||||
)
|
||||
cache = MagicMock()
|
||||
encrypter = MagicMock()
|
||||
encrypter.decrypt.return_value = {"auth_type": "none", "api_key_value": "plain-old"}
|
||||
encrypter.mask_plugin_credentials.return_value = {"api_key_value": "***"}
|
||||
encrypter.encrypt.return_value = {"auth_type": "none", "api_key_value": "encrypted-new"}
|
||||
mocker.patch(
|
||||
"services.tools.api_tools_manage_service.create_tool_provider_encrypter",
|
||||
return_value=(encrypter, cache),
|
||||
)
|
||||
mocker.patch("services.tools.api_tools_manage_service.ToolLabelManager.update_tool_labels")
|
||||
|
||||
# Act
|
||||
result = ApiToolManageService.update_api_tool_provider(
|
||||
user_id="user-1",
|
||||
tenant_id="tenant-1",
|
||||
provider_name="provider-new",
|
||||
original_provider="provider-old",
|
||||
icon={"emoji": "E"},
|
||||
credentials={"auth_type": "none", "api_key_value": "***"},
|
||||
_schema_type=ApiProviderSchemaType.OPENAPI,
|
||||
schema="schema",
|
||||
privacy_policy="privacy",
|
||||
custom_disclaimer="custom",
|
||||
labels=["news"],
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result == {"result": "success"}
|
||||
assert provider.name == "provider-new"
|
||||
assert provider.privacy_policy == "privacy"
|
||||
assert provider.credentials_str != ""
|
||||
cache.delete.assert_called_once()
|
||||
mock_db.session.commit.assert_called_once()
|
||||
|
||||
|
||||
def test_delete_api_tool_provider_should_raise_error_when_provider_missing(mock_db: MagicMock) -> None:
|
||||
# Arrange
|
||||
mock_db.session.query.return_value.where.return_value.first.return_value = None
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(ValueError, match="you have not added provider provider-a"):
|
||||
ApiToolManageService.delete_api_tool_provider("user-1", "tenant-1", "provider-a")
|
||||
|
||||
|
||||
def test_delete_api_tool_provider_should_delete_provider_when_exists(mock_db: MagicMock) -> None:
|
||||
# Arrange
|
||||
provider = object()
|
||||
mock_db.session.query.return_value.where.return_value.first.return_value = provider
|
||||
|
||||
# Act
|
||||
result = ApiToolManageService.delete_api_tool_provider("user-1", "tenant-1", "provider-a")
|
||||
|
||||
# Assert
|
||||
assert result == {"result": "success"}
|
||||
mock_db.session.delete.assert_called_once_with(provider)
|
||||
mock_db.session.commit.assert_called_once()
|
||||
|
||||
|
||||
def test_get_api_tool_provider_should_delegate_to_tool_manager(mocker: MockerFixture) -> None:
|
||||
# Arrange
|
||||
expected = {"provider": "value"}
|
||||
mock_get = mocker.patch(
|
||||
"services.tools.api_tools_manage_service.ToolManager.user_get_api_provider",
|
||||
return_value=expected,
|
||||
)
|
||||
|
||||
# Act
|
||||
result = ApiToolManageService.get_api_tool_provider("user-1", "tenant-1", "provider-a")
|
||||
|
||||
# Assert
|
||||
assert result == expected
|
||||
mock_get.assert_called_once_with(provider="provider-a", tenant_id="tenant-1")
|
||||
|
||||
|
||||
def test_test_api_tool_preview_should_raise_error_for_invalid_schema_type() -> None:
|
||||
# Arrange
|
||||
schema_type = "bad-schema-type"
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(ValueError, match="invalid schema type"):
|
||||
ApiToolManageService.test_api_tool_preview(
|
||||
tenant_id="tenant-1",
|
||||
provider_name="provider-a",
|
||||
tool_name="tool-a",
|
||||
credentials={"auth_type": "none"},
|
||||
parameters={},
|
||||
schema_type=schema_type, # type: ignore[arg-type]
|
||||
schema="schema",
|
||||
)
|
||||
|
||||
|
||||
def test_test_api_tool_preview_should_raise_error_when_schema_parser_fails(mocker: MockerFixture) -> None:
|
||||
# Arrange
|
||||
mocker.patch(
|
||||
"services.tools.api_tools_manage_service.ApiBasedToolSchemaParser.auto_parse_to_tool_bundle",
|
||||
side_effect=RuntimeError("invalid"),
|
||||
)
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(ValueError, match="invalid schema"):
|
||||
ApiToolManageService.test_api_tool_preview(
|
||||
tenant_id="tenant-1",
|
||||
provider_name="provider-a",
|
||||
tool_name="tool-a",
|
||||
credentials={"auth_type": "none"},
|
||||
parameters={},
|
||||
schema_type=ApiProviderSchemaType.OPENAPI,
|
||||
schema="schema",
|
||||
)
|
||||
|
||||
|
||||
def test_test_api_tool_preview_should_raise_error_when_tool_name_is_invalid(
|
||||
mock_db: MagicMock,
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
# Arrange
|
||||
mocker.patch(
|
||||
"services.tools.api_tools_manage_service.ApiBasedToolSchemaParser.auto_parse_to_tool_bundle",
|
||||
return_value=([_tool_bundle("tool-a")], ApiProviderSchemaType.OPENAPI),
|
||||
)
|
||||
mock_db.session.query.return_value.where.return_value.first.return_value = SimpleNamespace(id="provider-id")
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(ValueError, match="invalid tool name tool-b"):
|
||||
ApiToolManageService.test_api_tool_preview(
|
||||
tenant_id="tenant-1",
|
||||
provider_name="provider-a",
|
||||
tool_name="tool-b",
|
||||
credentials={"auth_type": "none"},
|
||||
parameters={},
|
||||
schema_type=ApiProviderSchemaType.OPENAPI,
|
||||
schema="schema",
|
||||
)
|
||||
|
||||
|
||||
def test_test_api_tool_preview_should_raise_error_when_auth_type_missing(
|
||||
mock_db: MagicMock,
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
# Arrange
|
||||
mocker.patch(
|
||||
"services.tools.api_tools_manage_service.ApiBasedToolSchemaParser.auto_parse_to_tool_bundle",
|
||||
return_value=([_tool_bundle("tool-a")], ApiProviderSchemaType.OPENAPI),
|
||||
)
|
||||
mock_db.session.query.return_value.where.return_value.first.return_value = SimpleNamespace(id="provider-id")
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(ValueError, match="auth_type is required"):
|
||||
ApiToolManageService.test_api_tool_preview(
|
||||
tenant_id="tenant-1",
|
||||
provider_name="provider-a",
|
||||
tool_name="tool-a",
|
||||
credentials={},
|
||||
parameters={},
|
||||
schema_type=ApiProviderSchemaType.OPENAPI,
|
||||
schema="schema",
|
||||
)
|
||||
|
||||
|
||||
def test_test_api_tool_preview_should_return_error_payload_when_tool_validation_raises(
|
||||
mock_db: MagicMock,
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
# Arrange
|
||||
db_provider = SimpleNamespace(id="provider-id", credentials={"auth_type": "none"})
|
||||
mock_db.session.query.return_value.where.return_value.first.return_value = db_provider
|
||||
mocker.patch(
|
||||
"services.tools.api_tools_manage_service.ApiBasedToolSchemaParser.auto_parse_to_tool_bundle",
|
||||
return_value=([_tool_bundle("tool-a")], ApiProviderSchemaType.OPENAPI),
|
||||
)
|
||||
provider_controller = MagicMock()
|
||||
tool_obj = MagicMock()
|
||||
tool_obj.fork_tool_runtime.return_value = tool_obj
|
||||
tool_obj.validate_credentials.side_effect = ValueError("validation failed")
|
||||
provider_controller.get_tool.return_value = tool_obj
|
||||
mocker.patch(
|
||||
"services.tools.api_tools_manage_service.ApiToolProviderController.from_db",
|
||||
return_value=provider_controller,
|
||||
)
|
||||
mock_encrypter = MagicMock()
|
||||
mock_encrypter.decrypt.return_value = {"auth_type": "none"}
|
||||
mock_encrypter.mask_plugin_credentials.return_value = {}
|
||||
mocker.patch(
|
||||
"services.tools.api_tools_manage_service.create_tool_provider_encrypter",
|
||||
return_value=(mock_encrypter, MagicMock()),
|
||||
)
|
||||
|
||||
# Act
|
||||
result = ApiToolManageService.test_api_tool_preview(
|
||||
tenant_id="tenant-1",
|
||||
provider_name="provider-a",
|
||||
tool_name="tool-a",
|
||||
credentials={"auth_type": "none"},
|
||||
parameters={},
|
||||
schema_type=ApiProviderSchemaType.OPENAPI,
|
||||
schema="schema",
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result == {"error": "validation failed"}
|
||||
|
||||
|
||||
def test_test_api_tool_preview_should_return_result_payload_when_validation_succeeds(
|
||||
mock_db: MagicMock,
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
# Arrange
|
||||
db_provider = SimpleNamespace(id="provider-id", credentials={"auth_type": "none"})
|
||||
mock_db.session.query.return_value.where.return_value.first.return_value = db_provider
|
||||
mocker.patch(
|
||||
"services.tools.api_tools_manage_service.ApiBasedToolSchemaParser.auto_parse_to_tool_bundle",
|
||||
return_value=([_tool_bundle("tool-a")], ApiProviderSchemaType.OPENAPI),
|
||||
)
|
||||
provider_controller = MagicMock()
|
||||
tool_obj = MagicMock()
|
||||
tool_obj.fork_tool_runtime.return_value = tool_obj
|
||||
tool_obj.validate_credentials.return_value = {"ok": True}
|
||||
provider_controller.get_tool.return_value = tool_obj
|
||||
mocker.patch(
|
||||
"services.tools.api_tools_manage_service.ApiToolProviderController.from_db",
|
||||
return_value=provider_controller,
|
||||
)
|
||||
mock_encrypter = MagicMock()
|
||||
mock_encrypter.decrypt.return_value = {"auth_type": "none"}
|
||||
mock_encrypter.mask_plugin_credentials.return_value = {}
|
||||
mocker.patch(
|
||||
"services.tools.api_tools_manage_service.create_tool_provider_encrypter",
|
||||
return_value=(mock_encrypter, MagicMock()),
|
||||
)
|
||||
|
||||
# Act
|
||||
result = ApiToolManageService.test_api_tool_preview(
|
||||
tenant_id="tenant-1",
|
||||
provider_name="provider-a",
|
||||
tool_name="tool-a",
|
||||
credentials={"auth_type": "none"},
|
||||
parameters={"x": "1"},
|
||||
schema_type=ApiProviderSchemaType.OPENAPI,
|
||||
schema="schema",
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result == {"result": {"ok": True}}
|
||||
|
||||
|
||||
def test_list_api_tools_should_return_all_user_providers_with_converted_tools(
|
||||
mock_db: MagicMock,
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
# Arrange
|
||||
provider_one = SimpleNamespace(name="p1")
|
||||
provider_two = SimpleNamespace(name="p2")
|
||||
mock_db.session.scalars.return_value.all.return_value = [provider_one, provider_two]
|
||||
|
||||
controller_one = MagicMock()
|
||||
controller_one.get_tools.return_value = ["tool-a"]
|
||||
controller_two = MagicMock()
|
||||
controller_two.get_tools.return_value = ["tool-b", "tool-c"]
|
||||
|
||||
user_provider_one = SimpleNamespace(labels=[], tools=[])
|
||||
user_provider_two = SimpleNamespace(labels=[], tools=[])
|
||||
|
||||
mocker.patch(
|
||||
"services.tools.api_tools_manage_service.ToolTransformService.api_provider_to_controller",
|
||||
side_effect=[controller_one, controller_two],
|
||||
)
|
||||
mocker.patch("services.tools.api_tools_manage_service.ToolLabelManager.get_tool_labels", return_value=["news"])
|
||||
mocker.patch(
|
||||
"services.tools.api_tools_manage_service.ToolTransformService.api_provider_to_user_provider",
|
||||
side_effect=[user_provider_one, user_provider_two],
|
||||
)
|
||||
mocker.patch("services.tools.api_tools_manage_service.ToolTransformService.repack_provider")
|
||||
mock_convert = mocker.patch(
|
||||
"services.tools.api_tools_manage_service.ToolTransformService.convert_tool_entity_to_api_entity",
|
||||
side_effect=[{"name": "tool-a"}, {"name": "tool-b"}, {"name": "tool-c"}],
|
||||
)
|
||||
|
||||
# Act
|
||||
result = ApiToolManageService.list_api_tools("tenant-1")
|
||||
|
||||
# Assert
|
||||
assert len(result) == 2
|
||||
assert user_provider_one.tools == [{"name": "tool-a"}]
|
||||
assert user_provider_two.tools == [{"name": "tool-b"}, {"name": "tool-c"}]
|
||||
assert mock_convert.call_count == 3
|
||||
@ -1,955 +0,0 @@
|
||||
"""
|
||||
Unit tests for services.tools.workflow_tools_manage_service
|
||||
|
||||
Covers WorkflowToolManageService: create, update, list, delete, get, list_single.
|
||||
"""
|
||||
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from core.tools.entities.tool_entities import ToolParameter, WorkflowToolParameterConfiguration
|
||||
from core.tools.errors import WorkflowToolHumanInputNotSupportedError
|
||||
from models.model import App
|
||||
from models.tools import WorkflowToolProvider
|
||||
from services.tools import workflow_tools_manage_service
|
||||
from services.tools.workflow_tools_manage_service import WorkflowToolManageService
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shared helpers / fake infrastructure
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class DummyWorkflow:
|
||||
"""Minimal in-memory Workflow substitute."""
|
||||
|
||||
def __init__(self, graph_dict: dict, version: str = "1.0.0") -> None:
|
||||
self._graph_dict = graph_dict
|
||||
self.version = version
|
||||
|
||||
@property
|
||||
def graph_dict(self) -> dict:
|
||||
return self._graph_dict
|
||||
|
||||
|
||||
class FakeQuery:
|
||||
"""Chainable query object that always returns a fixed result."""
|
||||
|
||||
def __init__(self, result: object) -> None:
|
||||
self._result = result
|
||||
|
||||
def where(self, *args: object, **kwargs: object) -> "FakeQuery":
|
||||
return self
|
||||
|
||||
def first(self) -> object:
|
||||
return self._result
|
||||
|
||||
def delete(self) -> int:
|
||||
return 1
|
||||
|
||||
|
||||
class DummySession:
|
||||
"""Minimal SQLAlchemy session substitute."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.added: list[WorkflowToolProvider] = []
|
||||
self.committed: bool = False
|
||||
|
||||
def __enter__(self) -> "DummySession":
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type: object, exc: object, tb: object) -> bool:
|
||||
return False
|
||||
|
||||
def add(self, obj: WorkflowToolProvider) -> None:
|
||||
self.added.append(obj)
|
||||
|
||||
def begin(self) -> "DummySession":
|
||||
return self
|
||||
|
||||
def commit(self) -> None:
|
||||
self.committed = True
|
||||
|
||||
|
||||
def _build_parameters() -> list[WorkflowToolParameterConfiguration]:
|
||||
return [
|
||||
WorkflowToolParameterConfiguration(name="input", description="input", form=ToolParameter.ToolParameterForm.LLM),
|
||||
]
|
||||
|
||||
|
||||
def _build_fake_db(
|
||||
*,
|
||||
existing_tool: WorkflowToolProvider | None = None,
|
||||
app: object | None = None,
|
||||
tool_by_id: WorkflowToolProvider | None = None,
|
||||
) -> tuple[MagicMock, DummySession]:
|
||||
"""
|
||||
Build a fake db object plus a DummySession for Session context-manager.
|
||||
|
||||
query(WorkflowToolProvider) returns existing_tool on first call,
|
||||
then tool_by_id on subsequent calls (or None if not provided).
|
||||
query(App) returns app.
|
||||
"""
|
||||
call_counts: dict[str, int] = {"wftp": 0}
|
||||
|
||||
def query(model: type) -> FakeQuery:
|
||||
if model is WorkflowToolProvider:
|
||||
call_counts["wftp"] += 1
|
||||
if call_counts["wftp"] == 1:
|
||||
return FakeQuery(existing_tool)
|
||||
return FakeQuery(tool_by_id)
|
||||
if model is App:
|
||||
return FakeQuery(app)
|
||||
return FakeQuery(None)
|
||||
|
||||
fake_db = MagicMock()
|
||||
fake_db.session = SimpleNamespace(query=query, commit=MagicMock())
|
||||
dummy_session = DummySession()
|
||||
return fake_db, dummy_session
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestCreateWorkflowTool
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestCreateWorkflowTool:
|
||||
"""Tests for WorkflowToolManageService.create_workflow_tool."""
|
||||
|
||||
def test_should_raise_when_human_input_nodes_present(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Human-input nodes must be rejected before any provider is created."""
|
||||
# Arrange
|
||||
workflow = DummyWorkflow(graph_dict={"nodes": [{"id": "n1", "data": {"type": "human-input"}}]})
|
||||
app = SimpleNamespace(workflow=workflow)
|
||||
fake_session = SimpleNamespace(query=lambda m: FakeQuery(None) if m is WorkflowToolProvider else FakeQuery(app))
|
||||
monkeypatch.setattr(workflow_tools_manage_service.db, "session", fake_session)
|
||||
mock_from_db = MagicMock()
|
||||
monkeypatch.setattr(workflow_tools_manage_service.WorkflowToolProviderController, "from_db", mock_from_db)
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(WorkflowToolHumanInputNotSupportedError) as exc_info:
|
||||
WorkflowToolManageService.create_workflow_tool(
|
||||
user_id="user-id",
|
||||
tenant_id="tenant-id",
|
||||
workflow_app_id="app-id",
|
||||
name="tool_name",
|
||||
label="Tool",
|
||||
icon={"type": "emoji", "emoji": "🔧"},
|
||||
description="desc",
|
||||
parameters=_build_parameters(),
|
||||
)
|
||||
|
||||
assert exc_info.value.error_code == "workflow_tool_human_input_not_supported"
|
||||
mock_from_db.assert_not_called()
|
||||
|
||||
def test_should_raise_when_duplicate_name_or_app_id(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Existing provider with same name or app_id raises ValueError."""
|
||||
# Arrange
|
||||
existing = MagicMock(spec=WorkflowToolProvider)
|
||||
monkeypatch.setattr(
|
||||
workflow_tools_manage_service.db,
|
||||
"session",
|
||||
SimpleNamespace(query=lambda m: FakeQuery(existing)),
|
||||
)
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(ValueError, match="already exists"):
|
||||
WorkflowToolManageService.create_workflow_tool(
|
||||
user_id="u",
|
||||
tenant_id="t",
|
||||
workflow_app_id="app-1",
|
||||
name="dup",
|
||||
label="Dup",
|
||||
icon={},
|
||||
description="",
|
||||
parameters=[],
|
||||
)
|
||||
|
||||
def test_should_raise_when_app_not_found(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""ValueError when the referenced App does not exist."""
|
||||
# Arrange
|
||||
call_count = {"n": 0}
|
||||
|
||||
def query(m: type) -> FakeQuery:
|
||||
call_count["n"] += 1
|
||||
if m is WorkflowToolProvider:
|
||||
return FakeQuery(None)
|
||||
return FakeQuery(None) # App returns None
|
||||
|
||||
monkeypatch.setattr(workflow_tools_manage_service.db, "session", SimpleNamespace(query=query))
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(ValueError, match="not found"):
|
||||
WorkflowToolManageService.create_workflow_tool(
|
||||
user_id="u",
|
||||
tenant_id="t",
|
||||
workflow_app_id="missing-app",
|
||||
name="n",
|
||||
label="L",
|
||||
icon={},
|
||||
description="",
|
||||
parameters=[],
|
||||
)
|
||||
|
||||
def test_should_raise_when_workflow_not_found(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""ValueError when the App has no attached Workflow."""
|
||||
# Arrange
|
||||
app_no_workflow = SimpleNamespace(workflow=None)
|
||||
|
||||
def query(m: type) -> FakeQuery:
|
||||
if m is WorkflowToolProvider:
|
||||
return FakeQuery(None)
|
||||
return FakeQuery(app_no_workflow)
|
||||
|
||||
monkeypatch.setattr(workflow_tools_manage_service.db, "session", SimpleNamespace(query=query))
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(ValueError, match="Workflow not found"):
|
||||
WorkflowToolManageService.create_workflow_tool(
|
||||
user_id="u",
|
||||
tenant_id="t",
|
||||
workflow_app_id="app-id",
|
||||
name="n",
|
||||
label="L",
|
||||
icon={},
|
||||
description="",
|
||||
parameters=[],
|
||||
)
|
||||
|
||||
def test_should_raise_when_from_db_fails(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Exceptions from WorkflowToolProviderController.from_db are wrapped as ValueError."""
|
||||
# Arrange
|
||||
workflow = DummyWorkflow(graph_dict={"nodes": []})
|
||||
app = SimpleNamespace(workflow=workflow)
|
||||
|
||||
def query(m: type) -> FakeQuery:
|
||||
if m is WorkflowToolProvider:
|
||||
return FakeQuery(None)
|
||||
return FakeQuery(app)
|
||||
|
||||
fake_db = MagicMock()
|
||||
fake_db.session = SimpleNamespace(query=query)
|
||||
monkeypatch.setattr(workflow_tools_manage_service, "db", fake_db)
|
||||
dummy_session = DummySession()
|
||||
monkeypatch.setattr(workflow_tools_manage_service, "Session", lambda *_, **__: dummy_session)
|
||||
monkeypatch.setattr(
|
||||
workflow_tools_manage_service.WorkflowToolProviderController,
|
||||
"from_db",
|
||||
MagicMock(side_effect=RuntimeError("bad config")),
|
||||
)
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(ValueError, match="bad config"):
|
||||
WorkflowToolManageService.create_workflow_tool(
|
||||
user_id="u",
|
||||
tenant_id="t",
|
||||
workflow_app_id="app-id",
|
||||
name="n",
|
||||
label="L",
|
||||
icon={},
|
||||
description="",
|
||||
parameters=[],
|
||||
)
|
||||
|
||||
def test_should_succeed_and_persist_provider(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Happy path: provider is added to session and success dict is returned."""
|
||||
# Arrange
|
||||
workflow = DummyWorkflow(graph_dict={"nodes": []}, version="2.0.0")
|
||||
app = SimpleNamespace(workflow=workflow)
|
||||
|
||||
def query(m: type) -> FakeQuery:
|
||||
if m is WorkflowToolProvider:
|
||||
return FakeQuery(None)
|
||||
return FakeQuery(app)
|
||||
|
||||
fake_db = MagicMock()
|
||||
fake_db.session = SimpleNamespace(query=query)
|
||||
monkeypatch.setattr(workflow_tools_manage_service, "db", fake_db)
|
||||
dummy_session = DummySession()
|
||||
monkeypatch.setattr(workflow_tools_manage_service, "Session", lambda *_, **__: dummy_session)
|
||||
monkeypatch.setattr(workflow_tools_manage_service.WorkflowToolProviderController, "from_db", MagicMock())
|
||||
|
||||
icon = {"type": "emoji", "emoji": "🔧"}
|
||||
|
||||
# Act
|
||||
result = WorkflowToolManageService.create_workflow_tool(
|
||||
user_id="user-id",
|
||||
tenant_id="tenant-id",
|
||||
workflow_app_id="app-id",
|
||||
name="tool_name",
|
||||
label="Tool",
|
||||
icon=icon,
|
||||
description="desc",
|
||||
parameters=_build_parameters(),
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result == {"result": "success"}
|
||||
assert len(dummy_session.added) == 1
|
||||
created: WorkflowToolProvider = dummy_session.added[0]
|
||||
assert created.name == "tool_name"
|
||||
assert created.label == "Tool"
|
||||
assert created.icon == json.dumps(icon)
|
||||
assert created.version == "2.0.0"
|
||||
|
||||
def test_should_call_label_manager_when_labels_provided(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Labels are forwarded to ToolLabelManager when provided."""
|
||||
# Arrange
|
||||
workflow = DummyWorkflow(graph_dict={"nodes": []})
|
||||
app = SimpleNamespace(workflow=workflow)
|
||||
|
||||
def query(m: type) -> FakeQuery:
|
||||
if m is WorkflowToolProvider:
|
||||
return FakeQuery(None)
|
||||
return FakeQuery(app)
|
||||
|
||||
fake_db = MagicMock()
|
||||
fake_db.session = SimpleNamespace(query=query)
|
||||
monkeypatch.setattr(workflow_tools_manage_service, "db", fake_db)
|
||||
dummy_session = DummySession()
|
||||
monkeypatch.setattr(workflow_tools_manage_service, "Session", lambda *_, **__: dummy_session)
|
||||
monkeypatch.setattr(workflow_tools_manage_service.WorkflowToolProviderController, "from_db", MagicMock())
|
||||
mock_label_mgr = MagicMock()
|
||||
monkeypatch.setattr(workflow_tools_manage_service.ToolLabelManager, "update_tool_labels", mock_label_mgr)
|
||||
mock_to_ctrl = MagicMock()
|
||||
monkeypatch.setattr(
|
||||
workflow_tools_manage_service.ToolTransformService, "workflow_provider_to_controller", mock_to_ctrl
|
||||
)
|
||||
|
||||
# Act
|
||||
WorkflowToolManageService.create_workflow_tool(
|
||||
user_id="u",
|
||||
tenant_id="t",
|
||||
workflow_app_id="app-id",
|
||||
name="n",
|
||||
label="L",
|
||||
icon={},
|
||||
description="",
|
||||
parameters=[],
|
||||
labels=["tag1", "tag2"],
|
||||
)
|
||||
|
||||
# Assert
|
||||
mock_label_mgr.assert_called_once()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestUpdateWorkflowTool
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestUpdateWorkflowTool:
|
||||
"""Tests for WorkflowToolManageService.update_workflow_tool."""
|
||||
|
||||
def _make_provider(self) -> WorkflowToolProvider:
|
||||
p = MagicMock(spec=WorkflowToolProvider)
|
||||
p.app_id = "app-id"
|
||||
p.tenant_id = "tenant-id"
|
||||
return p
|
||||
|
||||
def test_should_raise_when_name_duplicated(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""If another tool with the given name already exists, raise ValueError."""
|
||||
# Arrange
|
||||
existing = MagicMock(spec=WorkflowToolProvider)
|
||||
|
||||
def query(m: type) -> FakeQuery:
|
||||
return FakeQuery(existing)
|
||||
|
||||
monkeypatch.setattr(workflow_tools_manage_service.db, "session", SimpleNamespace(query=query))
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(ValueError, match="already exists"):
|
||||
WorkflowToolManageService.update_workflow_tool(
|
||||
user_id="u",
|
||||
tenant_id="t",
|
||||
workflow_tool_id="tool-1",
|
||||
name="dup",
|
||||
label="L",
|
||||
icon={},
|
||||
description="",
|
||||
parameters=[],
|
||||
)
|
||||
|
||||
def test_should_raise_when_tool_not_found(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""ValueError when the workflow tool to update does not exist."""
|
||||
# Arrange
|
||||
call_count = {"n": 0}
|
||||
|
||||
def query(m: type) -> FakeQuery:
|
||||
call_count["n"] += 1
|
||||
# 1st call: name uniqueness check → None (no duplicate)
|
||||
# 2nd call: fetch tool by id → None (not found)
|
||||
return FakeQuery(None)
|
||||
|
||||
monkeypatch.setattr(workflow_tools_manage_service.db, "session", SimpleNamespace(query=query))
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(ValueError, match="not found"):
|
||||
WorkflowToolManageService.update_workflow_tool(
|
||||
user_id="u",
|
||||
tenant_id="t",
|
||||
workflow_tool_id="missing",
|
||||
name="n",
|
||||
label="L",
|
||||
icon={},
|
||||
description="",
|
||||
parameters=[],
|
||||
)
|
||||
|
||||
def test_should_raise_when_app_not_found(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""ValueError when the tool's referenced App has been removed."""
|
||||
# Arrange
|
||||
provider = self._make_provider()
|
||||
call_count = {"n": 0}
|
||||
|
||||
def query(m: type) -> FakeQuery:
|
||||
call_count["n"] += 1
|
||||
if m is WorkflowToolProvider:
|
||||
# 1st: duplicate name check (None), 2nd: fetch provider
|
||||
return FakeQuery(None) if call_count["n"] == 1 else FakeQuery(provider)
|
||||
return FakeQuery(None) # App not found
|
||||
|
||||
monkeypatch.setattr(workflow_tools_manage_service.db, "session", SimpleNamespace(query=query))
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(ValueError, match="not found"):
|
||||
WorkflowToolManageService.update_workflow_tool(
|
||||
user_id="u",
|
||||
tenant_id="t",
|
||||
workflow_tool_id="tool-1",
|
||||
name="n",
|
||||
label="L",
|
||||
icon={},
|
||||
description="",
|
||||
parameters=[],
|
||||
)
|
||||
|
||||
def test_should_raise_when_workflow_not_found(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""ValueError when the App exists but has no Workflow."""
|
||||
# Arrange
|
||||
provider = self._make_provider()
|
||||
app_no_wf = SimpleNamespace(workflow=None)
|
||||
call_count = {"n": 0}
|
||||
|
||||
def query(m: type) -> FakeQuery:
|
||||
call_count["n"] += 1
|
||||
if m is WorkflowToolProvider:
|
||||
return FakeQuery(None) if call_count["n"] == 1 else FakeQuery(provider)
|
||||
return FakeQuery(app_no_wf)
|
||||
|
||||
monkeypatch.setattr(workflow_tools_manage_service.db, "session", SimpleNamespace(query=query))
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(ValueError, match="Workflow not found"):
|
||||
WorkflowToolManageService.update_workflow_tool(
|
||||
user_id="u",
|
||||
tenant_id="t",
|
||||
workflow_tool_id="tool-1",
|
||||
name="n",
|
||||
label="L",
|
||||
icon={},
|
||||
description="",
|
||||
parameters=[],
|
||||
)
|
||||
|
||||
def test_should_raise_when_from_db_fails(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Exceptions from from_db are re-raised as ValueError."""
|
||||
# Arrange
|
||||
provider = self._make_provider()
|
||||
workflow = DummyWorkflow(graph_dict={"nodes": []})
|
||||
app = SimpleNamespace(workflow=workflow)
|
||||
call_count = {"n": 0}
|
||||
|
||||
def query(m: type) -> FakeQuery:
|
||||
call_count["n"] += 1
|
||||
if m is WorkflowToolProvider:
|
||||
return FakeQuery(None) if call_count["n"] == 1 else FakeQuery(provider)
|
||||
return FakeQuery(app)
|
||||
|
||||
monkeypatch.setattr(
|
||||
workflow_tools_manage_service.db,
|
||||
"session",
|
||||
SimpleNamespace(query=query, commit=MagicMock()),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
workflow_tools_manage_service.WorkflowToolProviderController,
|
||||
"from_db",
|
||||
MagicMock(side_effect=RuntimeError("from_db error")),
|
||||
)
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(ValueError, match="from_db error"):
|
||||
WorkflowToolManageService.update_workflow_tool(
|
||||
user_id="u",
|
||||
tenant_id="t",
|
||||
workflow_tool_id="tool-1",
|
||||
name="n",
|
||||
label="L",
|
||||
icon={},
|
||||
description="",
|
||||
parameters=[],
|
||||
)
|
||||
|
||||
def test_should_succeed_and_call_commit(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Happy path: provider fields are updated and session committed."""
|
||||
# Arrange
|
||||
provider = self._make_provider()
|
||||
workflow = DummyWorkflow(graph_dict={"nodes": []}, version="3.0.0")
|
||||
app = SimpleNamespace(workflow=workflow)
|
||||
call_count = {"n": 0}
|
||||
|
||||
def query(m: type) -> FakeQuery:
|
||||
call_count["n"] += 1
|
||||
if m is WorkflowToolProvider:
|
||||
return FakeQuery(None) if call_count["n"] == 1 else FakeQuery(provider)
|
||||
return FakeQuery(app)
|
||||
|
||||
mock_commit = MagicMock()
|
||||
monkeypatch.setattr(
|
||||
workflow_tools_manage_service.db,
|
||||
"session",
|
||||
SimpleNamespace(query=query, commit=mock_commit),
|
||||
)
|
||||
monkeypatch.setattr(workflow_tools_manage_service.WorkflowToolProviderController, "from_db", MagicMock())
|
||||
|
||||
icon = {"type": "emoji", "emoji": "🛠"}
|
||||
|
||||
# Act
|
||||
result = WorkflowToolManageService.update_workflow_tool(
|
||||
user_id="u",
|
||||
tenant_id="t",
|
||||
workflow_tool_id="tool-1",
|
||||
name="new_name",
|
||||
label="New Label",
|
||||
icon=icon,
|
||||
description="new desc",
|
||||
parameters=_build_parameters(),
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result == {"result": "success"}
|
||||
mock_commit.assert_called_once()
|
||||
assert provider.name == "new_name"
|
||||
assert provider.label == "New Label"
|
||||
assert provider.icon == json.dumps(icon)
|
||||
assert provider.version == "3.0.0"
|
||||
|
||||
def test_should_call_label_manager_when_labels_provided(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Labels are forwarded to ToolLabelManager during update."""
|
||||
# Arrange
|
||||
provider = self._make_provider()
|
||||
workflow = DummyWorkflow(graph_dict={"nodes": []})
|
||||
app = SimpleNamespace(workflow=workflow)
|
||||
call_count = {"n": 0}
|
||||
|
||||
def query(m: type) -> FakeQuery:
|
||||
call_count["n"] += 1
|
||||
if m is WorkflowToolProvider:
|
||||
return FakeQuery(None) if call_count["n"] == 1 else FakeQuery(provider)
|
||||
return FakeQuery(app)
|
||||
|
||||
monkeypatch.setattr(
|
||||
workflow_tools_manage_service.db,
|
||||
"session",
|
||||
SimpleNamespace(query=query, commit=MagicMock()),
|
||||
)
|
||||
monkeypatch.setattr(workflow_tools_manage_service.WorkflowToolProviderController, "from_db", MagicMock())
|
||||
mock_label_mgr = MagicMock()
|
||||
monkeypatch.setattr(workflow_tools_manage_service.ToolLabelManager, "update_tool_labels", mock_label_mgr)
|
||||
monkeypatch.setattr(
|
||||
workflow_tools_manage_service.ToolTransformService, "workflow_provider_to_controller", MagicMock()
|
||||
)
|
||||
|
||||
# Act
|
||||
WorkflowToolManageService.update_workflow_tool(
|
||||
user_id="u",
|
||||
tenant_id="t",
|
||||
workflow_tool_id="tool-1",
|
||||
name="n",
|
||||
label="L",
|
||||
icon={},
|
||||
description="",
|
||||
parameters=[],
|
||||
labels=["a"],
|
||||
)
|
||||
|
||||
# Assert
|
||||
mock_label_mgr.assert_called_once()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestListTenantWorkflowTools
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestListTenantWorkflowTools:
|
||||
"""Tests for WorkflowToolManageService.list_tenant_workflow_tools."""
|
||||
|
||||
def test_should_return_empty_list_when_no_tools(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""An empty database yields an empty result list."""
|
||||
# Arrange
|
||||
fake_scalars = MagicMock()
|
||||
fake_scalars.all.return_value = []
|
||||
fake_db = MagicMock()
|
||||
fake_db.session.scalars.return_value = fake_scalars
|
||||
monkeypatch.setattr(workflow_tools_manage_service, "db", fake_db)
|
||||
|
||||
# Act
|
||||
result = WorkflowToolManageService.list_tenant_workflow_tools("u", "t")
|
||||
|
||||
# Assert
|
||||
assert result == []
|
||||
|
||||
def test_should_skip_broken_providers_and_log(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Providers that fail to load are logged and skipped."""
|
||||
# Arrange
|
||||
good_provider = MagicMock(spec=WorkflowToolProvider)
|
||||
good_provider.id = "good-id"
|
||||
good_provider.app_id = "app-good"
|
||||
bad_provider = MagicMock(spec=WorkflowToolProvider)
|
||||
bad_provider.id = "bad-id"
|
||||
bad_provider.app_id = "app-bad"
|
||||
|
||||
fake_scalars = MagicMock()
|
||||
fake_scalars.all.return_value = [good_provider, bad_provider]
|
||||
fake_db = MagicMock()
|
||||
fake_db.session.scalars.return_value = fake_scalars
|
||||
monkeypatch.setattr(workflow_tools_manage_service, "db", fake_db)
|
||||
|
||||
good_ctrl = MagicMock()
|
||||
good_ctrl.provider_id = "good-id"
|
||||
|
||||
def to_controller(provider: WorkflowToolProvider) -> MagicMock:
|
||||
if provider is bad_provider:
|
||||
raise RuntimeError("broken provider")
|
||||
return good_ctrl
|
||||
|
||||
monkeypatch.setattr(
|
||||
workflow_tools_manage_service.ToolTransformService, "workflow_provider_to_controller", to_controller
|
||||
)
|
||||
mock_get_labels = MagicMock(return_value={})
|
||||
monkeypatch.setattr(workflow_tools_manage_service.ToolLabelManager, "get_tools_labels", mock_get_labels)
|
||||
mock_to_user = MagicMock()
|
||||
mock_to_user.return_value.tools = []
|
||||
monkeypatch.setattr(
|
||||
workflow_tools_manage_service.ToolTransformService, "workflow_provider_to_user_provider", mock_to_user
|
||||
)
|
||||
monkeypatch.setattr(workflow_tools_manage_service.ToolTransformService, "repack_provider", MagicMock())
|
||||
mock_get_tools = MagicMock(return_value=[MagicMock()])
|
||||
good_ctrl.get_tools = mock_get_tools
|
||||
monkeypatch.setattr(
|
||||
workflow_tools_manage_service.ToolTransformService, "convert_tool_entity_to_api_entity", MagicMock()
|
||||
)
|
||||
|
||||
# Act
|
||||
result = WorkflowToolManageService.list_tenant_workflow_tools("u", "t")
|
||||
|
||||
# Assert - only good provider contributed
|
||||
assert len(result) == 1
|
||||
|
||||
def test_should_return_tools_for_all_providers(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""All successfully loaded providers appear in the result."""
|
||||
# Arrange
|
||||
provider = MagicMock(spec=WorkflowToolProvider)
|
||||
provider.id = "p-1"
|
||||
provider.app_id = "app-1"
|
||||
|
||||
fake_scalars = MagicMock()
|
||||
fake_scalars.all.return_value = [provider]
|
||||
fake_db = MagicMock()
|
||||
fake_db.session.scalars.return_value = fake_scalars
|
||||
monkeypatch.setattr(workflow_tools_manage_service, "db", fake_db)
|
||||
|
||||
ctrl = MagicMock()
|
||||
ctrl.provider_id = "p-1"
|
||||
ctrl.get_tools.return_value = [MagicMock()]
|
||||
monkeypatch.setattr(
|
||||
workflow_tools_manage_service.ToolTransformService,
|
||||
"workflow_provider_to_controller",
|
||||
MagicMock(return_value=ctrl),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
workflow_tools_manage_service.ToolLabelManager, "get_tools_labels", MagicMock(return_value={"p-1": []})
|
||||
)
|
||||
user_provider = MagicMock()
|
||||
user_provider.tools = []
|
||||
monkeypatch.setattr(
|
||||
workflow_tools_manage_service.ToolTransformService,
|
||||
"workflow_provider_to_user_provider",
|
||||
MagicMock(return_value=user_provider),
|
||||
)
|
||||
monkeypatch.setattr(workflow_tools_manage_service.ToolTransformService, "repack_provider", MagicMock())
|
||||
monkeypatch.setattr(
|
||||
workflow_tools_manage_service.ToolTransformService, "convert_tool_entity_to_api_entity", MagicMock()
|
||||
)
|
||||
|
||||
# Act
|
||||
result = WorkflowToolManageService.list_tenant_workflow_tools("u", "t")
|
||||
|
||||
# Assert
|
||||
assert len(result) == 1
|
||||
assert result[0] is user_provider
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestDeleteWorkflowTool
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestDeleteWorkflowTool:
|
||||
"""Tests for WorkflowToolManageService.delete_workflow_tool."""
|
||||
|
||||
def test_should_delete_and_commit(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""delete_workflow_tool queries, deletes, commits, and returns success."""
|
||||
# Arrange
|
||||
mock_query = MagicMock()
|
||||
mock_query.where.return_value.delete.return_value = 1
|
||||
mock_commit = MagicMock()
|
||||
fake_session = SimpleNamespace(query=lambda m: mock_query, commit=mock_commit)
|
||||
monkeypatch.setattr(workflow_tools_manage_service.db, "session", fake_session)
|
||||
|
||||
# Act
|
||||
result = WorkflowToolManageService.delete_workflow_tool("u", "t", "tool-1")
|
||||
|
||||
# Assert
|
||||
assert result == {"result": "success"}
|
||||
mock_commit.assert_called_once()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestGetWorkflowToolByToolId / ByAppId
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGetWorkflowToolByToolIdAndAppId:
|
||||
"""Tests for get_workflow_tool_by_tool_id and get_workflow_tool_by_app_id."""
|
||||
|
||||
def test_get_by_tool_id_should_raise_when_db_tool_is_none(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Raises ValueError when no WorkflowToolProvider found by tool id."""
|
||||
# Arrange
|
||||
monkeypatch.setattr(
|
||||
workflow_tools_manage_service.db,
|
||||
"session",
|
||||
SimpleNamespace(query=lambda m: FakeQuery(None)),
|
||||
)
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(ValueError, match="Tool not found"):
|
||||
WorkflowToolManageService.get_workflow_tool_by_tool_id("u", "t", "missing")
|
||||
|
||||
def test_get_by_app_id_should_raise_when_db_tool_is_none(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Raises ValueError when no WorkflowToolProvider found by app id."""
|
||||
# Arrange
|
||||
monkeypatch.setattr(
|
||||
workflow_tools_manage_service.db,
|
||||
"session",
|
||||
SimpleNamespace(query=lambda m: FakeQuery(None)),
|
||||
)
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(ValueError, match="Tool not found"):
|
||||
WorkflowToolManageService.get_workflow_tool_by_app_id("u", "t", "missing-app")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestGetWorkflowTool (private _get_workflow_tool)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGetWorkflowTool:
|
||||
"""Tests for the internal _get_workflow_tool helper."""
|
||||
|
||||
def test_should_raise_when_db_tool_none(self) -> None:
|
||||
"""_get_workflow_tool raises ValueError when db_tool is None."""
|
||||
with pytest.raises(ValueError, match="Tool not found"):
|
||||
WorkflowToolManageService._get_workflow_tool("t", None)
|
||||
|
||||
def test_should_raise_when_app_not_found(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""ValueError when the corresponding App row is missing."""
|
||||
# Arrange
|
||||
db_tool = MagicMock(spec=WorkflowToolProvider)
|
||||
db_tool.app_id = "app-1"
|
||||
db_tool.tenant_id = "t"
|
||||
monkeypatch.setattr(
|
||||
workflow_tools_manage_service.db,
|
||||
"session",
|
||||
SimpleNamespace(query=lambda m: FakeQuery(None)),
|
||||
)
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(ValueError, match="not found"):
|
||||
WorkflowToolManageService._get_workflow_tool("t", db_tool)
|
||||
|
||||
def test_should_raise_when_workflow_not_found(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""ValueError when App has no attached Workflow."""
|
||||
# Arrange
|
||||
db_tool = MagicMock(spec=WorkflowToolProvider)
|
||||
db_tool.app_id = "app-1"
|
||||
db_tool.tenant_id = "t"
|
||||
app = SimpleNamespace(workflow=None)
|
||||
monkeypatch.setattr(
|
||||
workflow_tools_manage_service.db,
|
||||
"session",
|
||||
SimpleNamespace(query=lambda m: FakeQuery(app)),
|
||||
)
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(ValueError, match="Workflow not found"):
|
||||
WorkflowToolManageService._get_workflow_tool("t", db_tool)
|
||||
|
||||
def test_should_raise_when_no_workflow_tools(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""ValueError when the controller returns no WorkflowTool instances."""
|
||||
# Arrange
|
||||
db_tool = MagicMock(spec=WorkflowToolProvider)
|
||||
db_tool.app_id = "app-1"
|
||||
db_tool.tenant_id = "t"
|
||||
db_tool.id = "tool-1"
|
||||
workflow = DummyWorkflow(graph_dict={"nodes": []})
|
||||
app = SimpleNamespace(workflow=workflow)
|
||||
monkeypatch.setattr(
|
||||
workflow_tools_manage_service.db,
|
||||
"session",
|
||||
SimpleNamespace(query=lambda m: FakeQuery(app)),
|
||||
)
|
||||
ctrl = MagicMock()
|
||||
ctrl.get_tools.return_value = []
|
||||
monkeypatch.setattr(
|
||||
workflow_tools_manage_service.ToolTransformService,
|
||||
"workflow_provider_to_controller",
|
||||
MagicMock(return_value=ctrl),
|
||||
)
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(ValueError, match="not found"):
|
||||
WorkflowToolManageService._get_workflow_tool("t", db_tool)
|
||||
|
||||
def test_should_return_dict_on_success(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Happy path: returns a dict with name, label, icon, synced, etc."""
|
||||
# Arrange
|
||||
db_tool = MagicMock(spec=WorkflowToolProvider)
|
||||
db_tool.app_id = "app-1"
|
||||
db_tool.tenant_id = "t"
|
||||
db_tool.id = "tool-1"
|
||||
db_tool.name = "my_tool"
|
||||
db_tool.label = "My Tool"
|
||||
db_tool.icon = json.dumps({"emoji": "🔧"})
|
||||
db_tool.description = "some desc"
|
||||
db_tool.privacy_policy = ""
|
||||
db_tool.version = "1.0"
|
||||
db_tool.parameter_configurations = []
|
||||
workflow = DummyWorkflow(graph_dict={"nodes": []}, version="1.0")
|
||||
app = SimpleNamespace(workflow=workflow)
|
||||
monkeypatch.setattr(
|
||||
workflow_tools_manage_service.db,
|
||||
"session",
|
||||
SimpleNamespace(query=lambda m: FakeQuery(app)),
|
||||
)
|
||||
|
||||
workflow_tool = MagicMock()
|
||||
workflow_tool.entity.output_schema = {"type": "object"}
|
||||
ctrl = MagicMock()
|
||||
ctrl.get_tools.return_value = [workflow_tool]
|
||||
monkeypatch.setattr(
|
||||
workflow_tools_manage_service.ToolTransformService,
|
||||
"workflow_provider_to_controller",
|
||||
MagicMock(return_value=ctrl),
|
||||
)
|
||||
mock_convert = MagicMock(return_value={"tool": "api_entity"})
|
||||
monkeypatch.setattr(
|
||||
workflow_tools_manage_service.ToolTransformService, "convert_tool_entity_to_api_entity", mock_convert
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
workflow_tools_manage_service.ToolLabelManager, "get_tool_labels", MagicMock(return_value=[])
|
||||
)
|
||||
|
||||
# Act
|
||||
result = WorkflowToolManageService._get_workflow_tool("t", db_tool)
|
||||
|
||||
# Assert
|
||||
assert result["name"] == "my_tool"
|
||||
assert result["label"] == "My Tool"
|
||||
assert result["synced"] is True
|
||||
assert "icon" in result
|
||||
assert "output_schema" in result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestListSingleWorkflowTools
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestListSingleWorkflowTools:
|
||||
"""Tests for WorkflowToolManageService.list_single_workflow_tools."""
|
||||
|
||||
def test_should_raise_when_tool_not_found(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""ValueError when the specified tool does not exist in DB."""
|
||||
# Arrange
|
||||
monkeypatch.setattr(
|
||||
workflow_tools_manage_service.db,
|
||||
"session",
|
||||
SimpleNamespace(query=lambda m: FakeQuery(None)),
|
||||
)
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(ValueError, match="not found"):
|
||||
WorkflowToolManageService.list_single_workflow_tools("u", "t", "tool-1")
|
||||
|
||||
def test_should_raise_when_no_workflow_tools(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""ValueError when the controller yields no tools for the provider."""
|
||||
# Arrange
|
||||
db_tool = MagicMock(spec=WorkflowToolProvider)
|
||||
db_tool.id = "tool-1"
|
||||
db_tool.tenant_id = "t"
|
||||
monkeypatch.setattr(
|
||||
workflow_tools_manage_service.db,
|
||||
"session",
|
||||
SimpleNamespace(query=lambda m: FakeQuery(db_tool)),
|
||||
)
|
||||
ctrl = MagicMock()
|
||||
ctrl.get_tools.return_value = []
|
||||
monkeypatch.setattr(
|
||||
workflow_tools_manage_service.ToolTransformService,
|
||||
"workflow_provider_to_controller",
|
||||
MagicMock(return_value=ctrl),
|
||||
)
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(ValueError, match="not found"):
|
||||
WorkflowToolManageService.list_single_workflow_tools("u", "t", "tool-1")
|
||||
|
||||
def test_should_return_api_entity_list(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Happy path: returns list with one ToolApiEntity."""
|
||||
# Arrange
|
||||
db_tool = MagicMock(spec=WorkflowToolProvider)
|
||||
db_tool.id = "tool-1"
|
||||
db_tool.tenant_id = "t"
|
||||
monkeypatch.setattr(
|
||||
workflow_tools_manage_service.db,
|
||||
"session",
|
||||
SimpleNamespace(query=lambda m: FakeQuery(db_tool)),
|
||||
)
|
||||
workflow_tool = MagicMock()
|
||||
ctrl = MagicMock()
|
||||
ctrl.get_tools.return_value = [workflow_tool]
|
||||
monkeypatch.setattr(
|
||||
workflow_tools_manage_service.ToolTransformService,
|
||||
"workflow_provider_to_controller",
|
||||
MagicMock(return_value=ctrl),
|
||||
)
|
||||
api_entity = MagicMock()
|
||||
monkeypatch.setattr(
|
||||
workflow_tools_manage_service.ToolTransformService,
|
||||
"convert_tool_entity_to_api_entity",
|
||||
MagicMock(return_value=api_entity),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
workflow_tools_manage_service.ToolLabelManager, "get_tool_labels", MagicMock(return_value=[])
|
||||
)
|
||||
|
||||
# Act
|
||||
result = WorkflowToolManageService.list_single_workflow_tools("u", "t", "tool-1")
|
||||
|
||||
# Assert
|
||||
assert result == [api_entity]
|
||||
Reference in New Issue
Block a user