diff --git a/tests/test_host_business_actions.py b/tests/test_host_business_actions.py new file mode 100644 index 000000000..1c02dcb19 --- /dev/null +++ b/tests/test_host_business_actions.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +import importlib.util +import sys +from pathlib import Path +from types import ModuleType, SimpleNamespace + + +ROOT = Path(__file__).resolve().parents[1] + + +def test_host_status_exposes_json_business_actions(monkeypatch) -> None: + fake_host = SimpleNamespace( + devices_names={}, + _online_devices=set(), + device_machine_names={}, + _subscribed_topics=set(), + _action_clients={}, + _action_value_mappings={ + "CONDUCTIVITY_STATION": { + "station_status": { + "type": "UniLabJsonCommand", + "schema": {"description": "查询工站状态"}, + }, + "auto-close": {"type": "UniLabJsonCommand", "schema": {}}, + "legacy_action": {"type": "EmptyIn", "schema": {}}, + } + }, + device_status={}, + device_status_timestamps={}, + ) + + config_module = ModuleType("unilabos.config.config") + config_module.BasicConfig = SimpleNamespace(is_host_mode=True) + host_node_module = ModuleType("unilabos.ros.nodes.presets.host_node") + host_node_module.HostNode = SimpleNamespace(get_instance=lambda _timeout: fake_host) + action_utils_module = ModuleType("unilabos.app.web.utils.action_utils") + action_utils_module.get_action_info = lambda client, full_name: { + "client": client, + "full_name": full_name, + } + + monkeypatch.setitem(sys.modules, "unilabos.config.config", config_module) + monkeypatch.setitem(sys.modules, "unilabos.ros.nodes.presets.host_node", host_node_module) + monkeypatch.setitem(sys.modules, "unilabos.app.web.utils.action_utils", action_utils_module) + + module_path = ROOT / "unilabos" / "app" / "web" / "utils" / "host_utils.py" + spec = importlib.util.spec_from_file_location("test_host_utils", module_path) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + info = module.get_host_node_info() + + assert info["business_actions"] == { + "CONDUCTIVITY_STATION/station_status": { + "device_id": "CONDUCTIVITY_STATION", + "action_name": "station_status", + "action_type": "UniLabJsonCommand", + "description": "查询工站状态", + } + } diff --git a/tests/test_ws_client_ssl.py b/tests/test_ws_client_ssl.py new file mode 100644 index 000000000..a506de192 --- /dev/null +++ b/tests/test_ws_client_ssl.py @@ -0,0 +1,19 @@ +from unittest.mock import patch + +from unilabos.app.ssl_utils import create_wss_ssl_context + + +def test_wss_ssl_context_uses_certifi_ca_bundle() -> None: + expected_context = object() + + with ( + patch("unilabos.app.ssl_utils.certifi.where", return_value="certifi-ca.pem"), + patch( + "unilabos.app.ssl_utils.ssl.create_default_context", + return_value=expected_context, + ) as create_default_context, + ): + actual_context = create_wss_ssl_context() + + assert actual_context is expected_context + create_default_context.assert_called_once_with(cafile="certifi-ca.pem") diff --git a/unilabos/app/ssl_utils.py b/unilabos/app/ssl_utils.py new file mode 100644 index 000000000..0a610ff93 --- /dev/null +++ b/unilabos/app/ssl_utils.py @@ -0,0 +1,12 @@ +"""WebSocket TLS 工具。""" + +from __future__ import annotations + +import ssl + +import certifi + + +def create_wss_ssl_context() -> ssl.SSLContext: + """使用 certifi CA 构建 WSS 上下文,避免 Windows 证书库异常。""" + return ssl.create_default_context(cafile=certifi.where()) diff --git a/unilabos/app/web/templates/status.html b/unilabos/app/web/templates/status.html index a0d3dfbc9..4414d53ac 100644 --- a/unilabos/app/web/templates/status.html +++ b/unilabos/app/web/templates/status.html @@ -201,6 +201,34 @@
| 设备 ID | +动作名 | +类型 | +说明 | +
|---|---|---|---|
| {{ action_info.device_id }} | +{{ action_info.action_name }} |
+ {{ action_info.action_type }} | +{{ action_info.description }} | +
| 没有发现已注册的业务动作 | +|||