Files
dify/api/tests/unit_tests/services/plugin/test_dependencies_analysis.py
2026-03-12 11:27:29 +08:00

173 lines
7.3 KiB
Python

"""Tests for services.plugin.dependencies_analysis.DependenciesAnalysisService.
Covers: provider ID resolution, leaked dependency detection with version
extraction, dependency generation from multiple sources, and latest
dependencies via marketplace.
"""
from __future__ import annotations
from unittest.mock import MagicMock, patch
import pytest
from core.plugin.entities.plugin import PluginDependency, PluginInstallationSource
from services.plugin.dependencies_analysis import DependenciesAnalysisService
class TestAnalyzeToolDependency:
def test_valid_three_part_id(self):
result = DependenciesAnalysisService.analyze_tool_dependency("langgenius/google/google")
assert result == "langgenius/google"
def test_single_part_expands_to_langgenius(self):
result = DependenciesAnalysisService.analyze_tool_dependency("websearch")
assert result == "langgenius/websearch"
def test_invalid_format_raises(self):
with pytest.raises(ValueError):
DependenciesAnalysisService.analyze_tool_dependency("bad/format")
class TestAnalyzeModelProviderDependency:
def test_valid_three_part_id(self):
result = DependenciesAnalysisService.analyze_model_provider_dependency("langgenius/openai/openai")
assert result == "langgenius/openai"
def test_google_maps_to_gemini(self):
result = DependenciesAnalysisService.analyze_model_provider_dependency("langgenius/google/google")
assert result == "langgenius/gemini"
def test_single_part_expands(self):
result = DependenciesAnalysisService.analyze_model_provider_dependency("anthropic")
assert result == "langgenius/anthropic"
class TestGetLeakedDependencies:
def _make_dependency(self, identifier: str, dep_type=PluginDependency.Type.Marketplace):
return PluginDependency(
type=dep_type,
value=PluginDependency.Marketplace(marketplace_plugin_unique_identifier=identifier),
)
@patch("services.plugin.dependencies_analysis.PluginInstaller")
def test_returns_empty_when_all_present(self, mock_installer_cls):
mock_installer_cls.return_value.fetch_missing_dependencies.return_value = []
deps = [self._make_dependency("org/plugin:1.0.0@hash")]
result = DependenciesAnalysisService.get_leaked_dependencies("t1", deps)
assert result == []
@patch("services.plugin.dependencies_analysis.PluginInstaller")
def test_returns_missing_with_version_extracted(self, mock_installer_cls):
missing = MagicMock()
missing.plugin_unique_identifier = "org/plugin:1.2.3@hash"
missing.current_identifier = "org/plugin:1.0.0@oldhash"
mock_installer_cls.return_value.fetch_missing_dependencies.return_value = [missing]
deps = [self._make_dependency("org/plugin:1.2.3@hash")]
result = DependenciesAnalysisService.get_leaked_dependencies("t1", deps)
assert len(result) == 1
assert result[0].value.version == "1.2.3"
@patch("services.plugin.dependencies_analysis.PluginInstaller")
def test_skips_present_dependencies(self, mock_installer_cls):
missing = MagicMock()
missing.plugin_unique_identifier = "org/missing:1.0.0@hash"
missing.current_identifier = None
mock_installer_cls.return_value.fetch_missing_dependencies.return_value = [missing]
deps = [
self._make_dependency("org/present:1.0.0@hash"),
self._make_dependency("org/missing:1.0.0@hash"),
]
result = DependenciesAnalysisService.get_leaked_dependencies("t1", deps)
assert len(result) == 1
class TestGenerateDependencies:
def _make_installation(self, source, identifier, meta=None):
install = MagicMock()
install.source = source
install.plugin_unique_identifier = identifier
install.meta = meta or {}
return install
@patch("services.plugin.dependencies_analysis.PluginInstaller")
def test_github_source(self, mock_installer_cls):
install = self._make_installation(
PluginInstallationSource.Github,
"org/plugin:1.0.0@hash",
{"repo": "org/repo", "version": "v1.0", "package": "plugin.difypkg"},
)
mock_installer_cls.return_value.fetch_plugin_installation_by_ids.return_value = [install]
result = DependenciesAnalysisService.generate_dependencies("t1", ["p1"])
assert len(result) == 1
assert result[0].type == PluginDependency.Type.Github
assert result[0].value.repo == "org/repo"
@patch("services.plugin.dependencies_analysis.PluginInstaller")
def test_marketplace_source(self, mock_installer_cls):
install = self._make_installation(PluginInstallationSource.Marketplace, "org/plugin:1.0.0@hash")
mock_installer_cls.return_value.fetch_plugin_installation_by_ids.return_value = [install]
result = DependenciesAnalysisService.generate_dependencies("t1", ["p1"])
assert result[0].type == PluginDependency.Type.Marketplace
@patch("services.plugin.dependencies_analysis.PluginInstaller")
def test_package_source(self, mock_installer_cls):
install = self._make_installation(PluginInstallationSource.Package, "org/plugin:1.0.0@hash")
mock_installer_cls.return_value.fetch_plugin_installation_by_ids.return_value = [install]
result = DependenciesAnalysisService.generate_dependencies("t1", ["p1"])
assert result[0].type == PluginDependency.Type.Package
@patch("services.plugin.dependencies_analysis.PluginInstaller")
def test_remote_source_raises(self, mock_installer_cls):
install = self._make_installation(PluginInstallationSource.Remote, "org/plugin:1.0.0@hash")
mock_installer_cls.return_value.fetch_plugin_installation_by_ids.return_value = [install]
with pytest.raises(ValueError, match="remote plugin"):
DependenciesAnalysisService.generate_dependencies("t1", ["p1"])
@patch("services.plugin.dependencies_analysis.PluginInstaller")
def test_deduplicates_input_ids(self, mock_installer_cls):
mock_installer_cls.return_value.fetch_plugin_installation_by_ids.return_value = []
DependenciesAnalysisService.generate_dependencies("t1", ["p1", "p1", "p2"])
call_args = mock_installer_cls.return_value.fetch_plugin_installation_by_ids.call_args[0]
assert len(call_args[1]) == 2 # deduplicated
class TestGenerateLatestDependencies:
@patch("services.plugin.dependencies_analysis.dify_config")
def test_returns_empty_when_marketplace_disabled(self, mock_config):
mock_config.MARKETPLACE_ENABLED = False
result = DependenciesAnalysisService.generate_latest_dependencies(["p1"])
assert result == []
@patch("services.plugin.dependencies_analysis.marketplace")
@patch("services.plugin.dependencies_analysis.dify_config")
def test_returns_marketplace_deps_when_enabled(self, mock_config, mock_marketplace):
mock_config.MARKETPLACE_ENABLED = True
manifest = MagicMock()
manifest.latest_package_identifier = "org/plugin:2.0.0@newhash"
mock_marketplace.batch_fetch_plugin_manifests.return_value = [manifest]
result = DependenciesAnalysisService.generate_latest_dependencies(["p1"])
assert len(result) == 1
assert result[0].type == PluginDependency.Type.Marketplace