mirror of
https://github.com/langgenius/dify.git
synced 2026-09-24 23:22:26 +08:00
refactor(api): clarify DSL import and plugin migration boundaries (#38483)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
autofix-ci[bot]
parent
b9c7199d34
commit
0a3426ea38
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -8,17 +9,17 @@ from services.plugin.plugin_migration import PluginMigration
|
||||
MIGRATION_MODULE = "services.plugin.plugin_migration"
|
||||
|
||||
|
||||
def test_fetch_plugin_unique_identifier_returns_none_when_disabled(mocker: MockerFixture) -> None:
|
||||
def test_fetch_latest_package_identifier_returns_none_when_disabled(mocker: MockerFixture) -> None:
|
||||
mocker.patch("services.plugin.plugin_migration.dify_config.MARKETPLACE_ENABLED", False)
|
||||
batch_fetch = mocker.patch("services.plugin.plugin_migration.marketplace.batch_fetch_plugin_manifests")
|
||||
|
||||
result = PluginMigration._fetch_plugin_unique_identifier("langgenius/openai")
|
||||
result = PluginMigration._fetch_latest_package_identifier("langgenius/openai")
|
||||
|
||||
assert result is None
|
||||
batch_fetch.assert_not_called()
|
||||
|
||||
|
||||
def test_fetch_plugin_unique_identifier_calls_marketplace_when_enabled(mocker: MockerFixture) -> None:
|
||||
def test_fetch_latest_package_identifier_calls_marketplace_when_enabled(mocker: MockerFixture) -> None:
|
||||
mocker.patch("services.plugin.plugin_migration.dify_config.MARKETPLACE_ENABLED", True)
|
||||
manifest = mocker.MagicMock()
|
||||
manifest.latest_package_identifier = "langgenius/openai:1.0.0@abc"
|
||||
@@ -27,7 +28,7 @@ def test_fetch_plugin_unique_identifier_calls_marketplace_when_enabled(mocker: M
|
||||
return_value=[manifest],
|
||||
)
|
||||
|
||||
result = PluginMigration._fetch_plugin_unique_identifier("langgenius/openai")
|
||||
result = PluginMigration._fetch_latest_package_identifier("langgenius/openai")
|
||||
|
||||
assert result == "langgenius/openai:1.0.0@abc"
|
||||
|
||||
@@ -75,7 +76,27 @@ class TestHandlePluginInstanceInstall:
|
||||
|
||||
mock_marketplace.download_plugin_pkg.assert_called_once()
|
||||
invalidate_cache.assert_called_once_with("tenant1")
|
||||
assert "success" in result or "failed" in result
|
||||
assert result["success"] == ["langgenius/openai"]
|
||||
assert result["failed"] == []
|
||||
|
||||
def test_reports_failed_plugin_ids_when_install_batch_raises(self) -> None:
|
||||
with (
|
||||
patch(f"{MIGRATION_MODULE}.dify_config") as mock_cfg,
|
||||
patch(f"{MIGRATION_MODULE}.marketplace") as mock_marketplace,
|
||||
patch(f"{MIGRATION_MODULE}.PluginInstaller") as mock_installer_cls,
|
||||
):
|
||||
mock_cfg.MARKETPLACE_ENABLED = True
|
||||
mock_marketplace.download_plugin_pkg.return_value = b"pkg_data"
|
||||
mock_installer = MagicMock()
|
||||
mock_installer_cls.return_value = mock_installer
|
||||
mock_installer.install_from_identifiers.side_effect = RuntimeError("install failed")
|
||||
|
||||
result = PluginMigration.handle_plugin_instance_install(
|
||||
"tenant1", {"langgenius/openai": "langgenius/openai:1.0.0@abc"}
|
||||
)
|
||||
|
||||
assert result["success"] == []
|
||||
assert result["failed"] == ["langgenius/openai"]
|
||||
|
||||
def test_install_plugins_invalidates_cache_after_direct_tenant_install(self, tmp_path) -> None:
|
||||
extracted_plugins = tmp_path / "plugins.jsonl"
|
||||
@@ -102,3 +123,30 @@ class TestHandlePluginInstanceInstall:
|
||||
|
||||
mock_installer.install_from_identifiers.assert_called_once()
|
||||
invalidate_cache.assert_called_once_with("tenant1")
|
||||
|
||||
def test_install_plugins_reports_missing_plugin_ids(self, tmp_path) -> None:
|
||||
extracted_plugins = tmp_path / "plugins.jsonl"
|
||||
output_file = tmp_path / "output.json"
|
||||
extracted_plugins.write_text('{"tenant_id":"tenant1","plugins":["langgenius/openai","langgenius/missing"]}\n')
|
||||
|
||||
with (
|
||||
patch(
|
||||
f"{MIGRATION_MODULE}.PluginMigration.extract_unique_plugins",
|
||||
return_value={
|
||||
"plugins": {"langgenius/openai": "langgenius/openai:1.0.0@abc"},
|
||||
"plugin_not_exist": ["langgenius/missing"],
|
||||
},
|
||||
),
|
||||
patch(f"{MIGRATION_MODULE}.PluginMigration.handle_plugin_instance_install", return_value={}),
|
||||
patch(f"{MIGRATION_MODULE}.PluginInstaller") as mock_installer_cls,
|
||||
patch(f"{MIGRATION_MODULE}.PluginService.invalidate_plugin_model_providers_cache"),
|
||||
):
|
||||
mock_installer = MagicMock()
|
||||
mock_installer.list_plugins.return_value = []
|
||||
mock_installer_cls.return_value = mock_installer
|
||||
|
||||
PluginMigration.install_plugins(str(extracted_plugins), str(output_file), workers=1)
|
||||
|
||||
assert json.loads(output_file.read_text())["not_installed"] == [
|
||||
{"tenant_id": "tenant1", "plugin_not_exist": ["langgenius/missing"]}
|
||||
]
|
||||
|
||||
@@ -643,6 +643,19 @@ def test_import_rag_pipeline_yaml_content_requires_mapping() -> None:
|
||||
assert "content must be a mapping" in result.error
|
||||
|
||||
|
||||
def test_import_rag_pipeline_rejects_oversized_yaml_content_by_bytes(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr("services.rag_pipeline.rag_pipeline_dsl_service.DSL_MAX_SIZE", 1)
|
||||
service = RagPipelineDslService(session=Mock())
|
||||
account = Mock(current_tenant_id="t1")
|
||||
|
||||
result = service.import_rag_pipeline(account=account, import_mode="yaml-content", yaml_content="é")
|
||||
|
||||
assert result.status == ImportStatus.FAILED
|
||||
assert "10MB" in result.error
|
||||
|
||||
|
||||
def test_confirm_import_returns_failed_when_pending_data_is_invalid_type(mocker: MockerFixture) -> None:
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline_dsl_service.redis_client.get", return_value=object())
|
||||
service = RagPipelineDslService(session=Mock())
|
||||
@@ -901,6 +914,46 @@ def test_import_rag_pipeline_url_size_exceeds_limit(mocker: MockerFixture) -> No
|
||||
assert "10MB" in result.error
|
||||
|
||||
|
||||
def test_import_rag_pipeline_rejects_oversized_yaml_url_bytes_before_decode(
|
||||
mocker: MockerFixture,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr("services.rag_pipeline.rag_pipeline_dsl_service.DSL_MAX_SIZE", 1)
|
||||
response = Mock()
|
||||
response.raise_for_status.return_value = None
|
||||
response.content = b"\xff\xff"
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline_dsl_service.remote_fetcher.make_request", return_value=response)
|
||||
service = RagPipelineDslService(session=Mock())
|
||||
account = Mock(current_tenant_id="t1")
|
||||
|
||||
result = service.import_rag_pipeline(
|
||||
account=account,
|
||||
import_mode="yaml-url",
|
||||
yaml_url="https://example.com/pipeline.yaml",
|
||||
)
|
||||
|
||||
assert result.status == ImportStatus.FAILED
|
||||
assert "10MB" in result.error
|
||||
|
||||
|
||||
def test_import_rag_pipeline_returns_decode_error_for_invalid_yaml_url_bytes(mocker: MockerFixture) -> None:
|
||||
response = Mock()
|
||||
response.raise_for_status.return_value = None
|
||||
response.content = b"\xff"
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline_dsl_service.remote_fetcher.make_request", return_value=response)
|
||||
service = RagPipelineDslService(session=Mock())
|
||||
account = Mock(current_tenant_id="t1")
|
||||
|
||||
result = service.import_rag_pipeline(
|
||||
account=account,
|
||||
import_mode="yaml-url",
|
||||
yaml_url="https://example.com/pipeline.yaml",
|
||||
)
|
||||
|
||||
assert result.status == ImportStatus.FAILED
|
||||
assert "utf-8" in result.error
|
||||
|
||||
|
||||
def test_import_rag_pipeline_fails_when_rag_pipeline_data_missing() -> None:
|
||||
service = RagPipelineDslService(session=Mock())
|
||||
account = Mock(current_tenant_id="t1")
|
||||
|
||||
@@ -89,7 +89,7 @@ def test_deal_dependencies_installs_missing_marketplace_plugins(mocker: MockerFi
|
||||
installer_cls.return_value.list_plugins.return_value = [SimpleNamespace(plugin_id="installed-plugin")]
|
||||
|
||||
migration_cls = mocker.patch("services.rag_pipeline.rag_pipeline_transform_service.PluginMigration")
|
||||
migration_cls.return_value._fetch_plugin_unique_identifier.return_value = "missing-plugin:1.0.0"
|
||||
migration_cls.return_value._fetch_latest_package_identifier.return_value = "missing-plugin:1.0.0"
|
||||
|
||||
install_mock = mocker.patch(
|
||||
"services.rag_pipeline.rag_pipeline_transform_service.PluginService.install_from_marketplace_pkg"
|
||||
@@ -518,7 +518,7 @@ def test_deal_dependencies_installs_when_enabled(mocker: MockerFixture) -> None:
|
||||
installer = mocker.patch("services.rag_pipeline.rag_pipeline_transform_service.PluginInstaller").return_value
|
||||
installer.list_plugins.return_value = []
|
||||
migration = mocker.patch("services.rag_pipeline.rag_pipeline_transform_service.PluginMigration").return_value
|
||||
migration._fetch_plugin_unique_identifier.return_value = "langgenius/openai:1.0.0@abc"
|
||||
migration._fetch_latest_package_identifier.return_value = "langgenius/openai:1.0.0@abc"
|
||||
install_call = mocker.patch(
|
||||
"services.rag_pipeline.rag_pipeline_transform_service.PluginService.install_from_marketplace_pkg"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
|
||||
from services.app_dsl_service import AppDslService, ImportStatus
|
||||
|
||||
|
||||
def test_import_app_rejects_oversized_yaml_content_by_bytes(monkeypatch) -> None:
|
||||
monkeypatch.setattr("services.app_dsl_service.DSL_MAX_SIZE", 1)
|
||||
service = AppDslService(session=SimpleNamespace())
|
||||
|
||||
result = service.import_app(
|
||||
account=SimpleNamespace(current_tenant_id="tenant-1"),
|
||||
import_mode="yaml-content",
|
||||
yaml_content="é",
|
||||
)
|
||||
|
||||
assert result.status == ImportStatus.FAILED
|
||||
assert "10MB" in result.error
|
||||
|
||||
|
||||
def test_import_app_rejects_oversized_yaml_url_bytes_before_decode(monkeypatch) -> None:
|
||||
monkeypatch.setattr("services.app_dsl_service.DSL_MAX_SIZE", 1)
|
||||
response = Mock()
|
||||
response.raise_for_status.return_value = None
|
||||
response.content = b"\xff\xff"
|
||||
monkeypatch.setattr("services.app_dsl_service.remote_fetcher.make_request", Mock(return_value=response))
|
||||
service = AppDslService(session=SimpleNamespace())
|
||||
|
||||
result = service.import_app(
|
||||
account=SimpleNamespace(current_tenant_id="tenant-1"),
|
||||
import_mode="yaml-url",
|
||||
yaml_url="https://example.com/app.yaml",
|
||||
)
|
||||
|
||||
assert result.status == ImportStatus.FAILED
|
||||
assert "10MB" in result.error
|
||||
|
||||
|
||||
def test_import_app_returns_decode_error_for_invalid_yaml_url_bytes(monkeypatch) -> None:
|
||||
response = Mock()
|
||||
response.raise_for_status.return_value = None
|
||||
response.content = b"\xff"
|
||||
monkeypatch.setattr("services.app_dsl_service.remote_fetcher.make_request", Mock(return_value=response))
|
||||
service = AppDslService(session=SimpleNamespace())
|
||||
|
||||
result = service.import_app(
|
||||
account=SimpleNamespace(current_tenant_id="tenant-1"),
|
||||
import_mode="yaml-url",
|
||||
yaml_url="https://example.com/app.yaml",
|
||||
)
|
||||
|
||||
assert result.status == ImportStatus.FAILED
|
||||
assert "utf-8" in result.error
|
||||
@@ -95,7 +95,7 @@ def test_import_snippet_rejects_oversized_yaml_url_content(monkeypatch: pytest.M
|
||||
monkeypatch.setattr("services.snippet_dsl_service.DSL_MAX_SIZE", 3)
|
||||
monkeypatch.setattr(
|
||||
"services.snippet_dsl_service.ssrf_proxy.get",
|
||||
Mock(return_value=SimpleNamespace(status_code=200, text="too large")),
|
||||
Mock(return_value=SimpleNamespace(status_code=200, content=b"too large")),
|
||||
)
|
||||
|
||||
result = service.import_snippet(
|
||||
@@ -108,6 +108,43 @@ def test_import_snippet_rejects_oversized_yaml_url_content(monkeypatch: pytest.M
|
||||
assert "YAML content size exceeds maximum limit" in result.error
|
||||
|
||||
|
||||
def test_import_snippet_rejects_oversized_yaml_url_bytes_before_decode(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
service = SnippetDslService(session=SimpleNamespace())
|
||||
monkeypatch.setattr("services.snippet_dsl_service.DSL_MAX_SIZE", 1)
|
||||
monkeypatch.setattr(
|
||||
"services.snippet_dsl_service.ssrf_proxy.get",
|
||||
Mock(return_value=SimpleNamespace(status_code=200, content=b"\xff\xff")),
|
||||
)
|
||||
|
||||
result = service.import_snippet(
|
||||
account=SimpleNamespace(current_tenant_id="tenant-1"),
|
||||
import_mode=ImportMode.YAML_URL.value,
|
||||
yaml_url="https://example.com/snippet.yaml",
|
||||
)
|
||||
|
||||
assert result.status == ImportStatus.FAILED
|
||||
assert "YAML content size exceeds maximum limit" in result.error
|
||||
|
||||
|
||||
def test_import_snippet_returns_decode_error_for_invalid_yaml_url_bytes(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
service = SnippetDslService(session=SimpleNamespace())
|
||||
monkeypatch.setattr(
|
||||
"services.snippet_dsl_service.ssrf_proxy.get",
|
||||
Mock(return_value=SimpleNamespace(status_code=200, content=b"\xff")),
|
||||
)
|
||||
|
||||
result = service.import_snippet(
|
||||
account=SimpleNamespace(current_tenant_id="tenant-1"),
|
||||
import_mode=ImportMode.YAML_URL.value,
|
||||
yaml_url="https://example.com/snippet.yaml",
|
||||
)
|
||||
|
||||
assert result.status == ImportStatus.FAILED
|
||||
assert "utf-8" in result.error
|
||||
|
||||
|
||||
def test_import_snippet_returns_failed_when_yaml_url_fetch_raises(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
service = SnippetDslService(session=SimpleNamespace())
|
||||
monkeypatch.setattr(
|
||||
@@ -127,12 +164,12 @@ def test_import_snippet_returns_failed_when_yaml_url_fetch_raises(monkeypatch: p
|
||||
|
||||
def test_import_snippet_rejects_oversized_yaml_content(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
service = SnippetDslService(session=SimpleNamespace())
|
||||
monkeypatch.setattr("services.snippet_dsl_service.DSL_MAX_SIZE", 3)
|
||||
monkeypatch.setattr("services.snippet_dsl_service.DSL_MAX_SIZE", 1)
|
||||
|
||||
result = service.import_snippet(
|
||||
account=SimpleNamespace(current_tenant_id="tenant-1"),
|
||||
import_mode=ImportMode.YAML_CONTENT.value,
|
||||
yaml_content="too large",
|
||||
yaml_content="é",
|
||||
)
|
||||
|
||||
assert result.status == ImportStatus.FAILED
|
||||
|
||||
Reference in New Issue
Block a user