fix(plugin): preserve credentials during local replacement (#39764)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
zyssyz123
2026-07-29 12:39:06 +00:00
committed by GitHub
co-authored by autofix-ci[bot]
parent 81fd1da3ff
commit 72c20daa61
15 changed files with 297 additions and 61 deletions
@@ -597,19 +597,28 @@ class TestPluginInstallFromPkgApi:
class TestPluginUninstallApi:
def test_uninstall(self, app: Flask):
@pytest.mark.parametrize("preserve_credentials", [False, True])
def test_uninstall(self, app: Flask, preserve_credentials: bool):
api = PluginUninstallApi()
method = unwrap(api.post)
payload = {"plugin_installation_id": "x"}
payload = {
"plugin_installation_id": "x",
"preserve_credentials": preserve_credentials,
}
with (
app.test_request_context("/", json=payload),
patch("controllers.console.workspace.plugin.PluginService.uninstall", return_value=True),
patch("controllers.console.workspace.plugin.PluginService.uninstall", return_value=True) as uninstall_mock,
):
result = method(api, "t1")
assert result["success"] is True
uninstall_mock.assert_called_once_with(
"t1",
"x",
preserve_credentials=preserve_credentials,
)
class TestPluginChangePermissionApi:
@@ -12,6 +12,7 @@ from sqlalchemy.orm import Session
from core.helper.model_provider_cache import ProviderCredentialsCacheType
from core.plugin.entities.plugin import PluginCategory, PluginInstallationSource
from core.plugin.entities.plugin_daemon import PluginInstallTask, PluginInstallTaskStatus, PluginModelProviderEntity
from core.provider_manager import ProviderConfigurationCacheSource
from graphon.model_runtime.entities.common_entities import I18nObject
from graphon.model_runtime.entities.provider_entities import ConfigurateMethod, ProviderEntity
from models.provider import Provider, ProviderCredential, ProviderType, TenantPreferredModelProvider
@@ -1224,6 +1225,7 @@ class TestPluginModelProviderCacheInvalidation:
patch(f"{MODULE}.PluginInstaller") as installer_cls,
patch(f"{MODULE}.ProviderCredentialsCache") as credentials_cache,
patch(f"{MODULE}.PluginService.invalidate_plugin_model_providers_cache") as invalidate_cache,
patch("core.provider_manager.ProviderManager.invalidate_configurations_cache") as invalidate_configurations,
):
mock_config.ENTERPRISE_ENABLED = False
installer = installer_cls.return_value
@@ -1237,6 +1239,13 @@ class TestPluginModelProviderCacheInvalidation:
assert result is True
installer.uninstall.assert_called_once_with(TENANT_ID, "installation-1")
invalidate_cache.assert_called_once_with(TENANT_ID)
invalidate_configurations.assert_called_once_with(
TENANT_ID,
sources=(
ProviderConfigurationCacheSource.PREFERRED_MODEL_PROVIDERS,
ProviderConfigurationCacheSource.PROVIDER_CREDENTIALS,
),
)
credentials_cache.assert_called_once_with(
tenant_id=TENANT_ID,
identity_id=provider_id,
@@ -452,3 +452,78 @@ class TestUninstall:
)
).all()
assert len(remaining_prefs) == 0
@patch("core.plugin.plugin_service.PluginInstaller")
def test_preserves_credentials_when_replacing_plugin(
self, mock_installer_cls: MagicMock, plugin_db: Session
) -> None:
tenant_id = str(uuid4())
plugin_id = "org/myplugin"
provider_name = f"{plugin_id}/model-provider"
credential = ProviderCredential(
tenant_id=tenant_id,
provider_name=provider_name,
credential_name="default",
encrypted_config="{}",
)
plugin_db.add(credential)
plugin_db.flush()
provider = Provider(
tenant_id=tenant_id,
provider_name=provider_name,
credential_id=credential.id,
)
plugin_db.add(provider)
preferred_provider = TenantPreferredModelProvider(
tenant_id=tenant_id,
provider_name=provider_name,
preferred_provider_type=ProviderType.CUSTOM,
)
plugin_db.add(preferred_provider)
plugin_db.commit()
plugin = MagicMock(installation_id="install-1", plugin_id=plugin_id)
installer = mock_installer_cls.return_value
installer.list_plugins.return_value = [plugin]
installer.uninstall.return_value = True
with patch("core.plugin.plugin_service.dify_config") as mock_config:
mock_config.ENTERPRISE_ENABLED = False
result = PluginService.uninstall(tenant_id, "install-1", preserve_credentials=True)
assert result is True
plugin_db.expire_all()
assert plugin_db.get(ProviderCredential, credential.id) is not None
assert plugin_db.get(Provider, provider.id).credential_id == credential.id
assert plugin_db.get(TenantPreferredModelProvider, preferred_provider.id) is not None
@patch("core.plugin.plugin_service.PluginInstaller")
def test_preserves_credentials_when_daemon_uninstall_fails(
self, mock_installer_cls: MagicMock, plugin_db: Session
) -> None:
tenant_id = str(uuid4())
plugin_id = "org/myplugin"
credential = ProviderCredential(
tenant_id=tenant_id,
provider_name=f"{plugin_id}/model-provider",
credential_name="default",
encrypted_config="{}",
)
plugin_db.add(credential)
plugin_db.commit()
plugin = MagicMock(installation_id="install-1", plugin_id=plugin_id)
installer = mock_installer_cls.return_value
installer.list_plugins.return_value = [plugin]
installer.uninstall.return_value = False
with patch("core.plugin.plugin_service.dify_config") as mock_config:
mock_config.ENTERPRISE_ENABLED = False
result = PluginService.uninstall(tenant_id, "install-1")
assert result is False
plugin_db.expire_all()
assert plugin_db.get(ProviderCredential, credential.id) is not None