From ed1e31d37944f9bd01b6da1f705a9ce47a0d9af5 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sat, 13 Jun 2026 22:54:35 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=85=BC=E5=AE=B9=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=E4=BB=AA=E8=A1=A8=E7=9B=98=E7=A9=BA=E8=BF=94=E5=9B=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/endpoints/plugin.py | 2 +- app/core/plugin.py | 8 ++++- tests/test_plugin_dashboard.py | 65 ++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 tests/test_plugin_dashboard.py diff --git a/app/api/endpoints/plugin.py b/app/api/endpoints/plugin.py index 8d202b1f..c4222e71 100644 --- a/app/api/endpoints/plugin.py +++ b/app/api/endpoints/plugin.py @@ -513,7 +513,7 @@ def plugin_dashboard( plugin_id: str, user_agent: Annotated[str | None, Header()] = None, _: User = Depends(get_current_active_superuser), -) -> schemas.PluginDashboard: +) -> Optional[schemas.PluginDashboard]: """ 根据插件ID获取插件仪表板 """ diff --git a/app/core/plugin.py b/app/core/plugin.py index 9de72588..4edc0597 100644 --- a/app/core/plugin.py +++ b/app/core/plugin.py @@ -1080,7 +1080,7 @@ class PluginManager(ConfigReloadMixin, metaclass=Singleton): logger.error(f"获取插件[{plugin_id}]仪表盘元数据出错:{str(e)}") return dashboard_meta - def get_plugin_dashboard(self, pid: str, key: str, user_agent: str = None) -> schemas.PluginDashboard: + def get_plugin_dashboard(self, pid: str, key: str, user_agent: str = None) -> Optional[schemas.PluginDashboard]: """ 获取插件仪表盘 """ @@ -1113,6 +1113,12 @@ class PluginManager(ConfigReloadMixin, metaclass=Singleton): logger.error(f"插件 {pid} 调用方法 get_dashboard 出错: {str(e)}") raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"插件 {pid} 调用方法 get_dashboard 出错: {str(e)}") + if dashboard is None: + return None + if not isinstance(dashboard, (tuple, list)) or len(dashboard) != 3: + logger.error(f"插件 {pid} 返回的仪表盘数据格式错误") + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"插件 {pid} 返回的仪表盘数据格式错误") cols, attrs, elements = dashboard return schemas.PluginDashboard( id=pid, diff --git a/tests/test_plugin_dashboard.py b/tests/test_plugin_dashboard.py new file mode 100644 index 00000000..98ce0f59 --- /dev/null +++ b/tests/test_plugin_dashboard.py @@ -0,0 +1,65 @@ +from types import SimpleNamespace +from typing import Any, Iterator + +import pytest +from fastapi import HTTPException + +from app.core.plugin import PluginManager +from app.utils.singleton import Singleton + + +@pytest.fixture +def plugin_manager() -> Iterator[PluginManager]: + """构造隔离的插件管理器实例,避免单例状态污染其它用例。""" + Singleton._instances.pop((PluginManager, (), frozenset()), None) + manager = PluginManager() + yield manager + Singleton._instances.pop((PluginManager, (), frozenset()), None) + + +def _plugin_with_dashboard(dashboard: Any) -> SimpleNamespace: + """构造仅包含仪表板接口的插件实例。""" + return SimpleNamespace( + plugin_name="演示插件", + get_render_mode=lambda: ("vue", "dist/assets"), + get_dashboard=lambda key=None, user_agent=None: dashboard, + ) + + +def test_plugin_dashboard_keeps_vue_elements_none(plugin_manager: PluginManager) -> None: + """Vue 仪表板的 elements=None 应原样返回给前端渲染远程组件。""" + plugin_manager.running_plugins["DemoPlugin"] = _plugin_with_dashboard( + ( + {"cols": 12}, + {"title": "演示插件", "border": True}, + None, + ) + ) + + dashboard = plugin_manager.get_plugin_dashboard("DemoPlugin", "usage") + + assert dashboard.id == "DemoPlugin" + assert dashboard.render_mode == "vue" + assert dashboard.cols == {"cols": 12} + assert dashboard.attrs == {"title": "演示插件", "border": True} + assert dashboard.elements is None + + +def test_plugin_dashboard_returns_none_when_plugin_has_no_dashboard(plugin_manager: PluginManager) -> None: + """插件声明当前无仪表板时应返回 None,而不是触发解包异常。""" + plugin_manager.running_plugins["DemoPlugin"] = _plugin_with_dashboard(None) + + assert plugin_manager.get_plugin_dashboard("DemoPlugin", "missing") is None + + +def test_plugin_dashboard_rejects_invalid_dashboard_shape(plugin_manager: PluginManager) -> None: + """非空但不符合三元组契约的仪表板数据应返回服务端错误。""" + plugin_manager.running_plugins["DemoPlugin"] = _plugin_with_dashboard( + {"cols": {}, "attrs": {}, "elements": []} + ) + + with pytest.raises(HTTPException) as exc_info: + plugin_manager.get_plugin_dashboard("DemoPlugin", "broken") + + assert exc_info.value.status_code == 500 + assert "仪表盘数据格式错误" in exc_info.value.detail