""" 连接方案可切换驱动层 · 离线回归(不依赖真机/8899) 验证: - 默认 4 类方案存在(jiqing 默认 + aochuang/legacy 内置 + 自定义可注册) - 三级开关(global/project/device)解析优先级 - 禁用方案回退 jiqing - 自定义方案注册 / 删除 / 内置不可删 - http 驱动字段映射(body_map / query_map) 注意:本测试为离线 mock 回归,按真机铁律仅作回归,不替代真机 E2E。 """ import os import sys import tempfile import importlib import pytest sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "app")) @pytest.fixture() def mgr(tmp_path, monkeypatch): import services.connection_provider as cp importlib.reload(cp) # 重定向持久化到临时目录 monkeypatch.setattr(cp, "_DATA_DIR", tmp_path) monkeypatch.setattr(cp, "_CONFIG_PATH", tmp_path / "connection_providers.json") m = cp.ConnectionProviderManager() m.load(force=True) return m, cp def test_builtin_providers(mgr): m, cp = mgr ids = {p["id"] for p in m.list_providers()} assert {"jiqing", "aochuang", "legacy"} <= ids # 默认 active = jiqing assert m.resolve_active_id() == "jiqing" jq = m.get("jiqing") assert jq.kind == "native" and jq.enabled and jq.builtin def test_switch_requires_enabled(mgr): m, cp = mgr # aochuang 默认未启用 → 切换应拒绝 with pytest.raises(ValueError): m.switch("aochuang", scope=cp.SCOPE_GLOBAL) # 启用后可切 m.register({"id": "aochuang", "enabled": True, "base_url": "https://007.example/api"}) res = m.switch("aochuang", scope=cp.SCOPE_GLOBAL) assert res["switched_to"] == "aochuang" assert m.resolve_active_id() == "aochuang" def test_scope_priority(mgr): m, cp = mgr m.register({"id": "aochuang", "enabled": True, "base_url": "https://007.example/api"}) m.register({"id": "legacy", "enabled": True, "base_url": "https://legacy.example"}) m.switch("aochuang", scope=cp.SCOPE_GLOBAL) m.switch("legacy", scope=cp.SCOPE_PROJECT, project_id="cunkebao") m.switch("jiqing", scope=cp.SCOPE_DEVICE, device_id="dev1") # 设备级最高 assert m.resolve_active_id(device_id="dev1", project_id="cunkebao") == "jiqing" # 项目级次之 assert m.resolve_active_id(project_id="cunkebao") == "legacy" # 全局兜底 assert m.resolve_active_id() == "aochuang" def test_disabled_active_falls_back(mgr): m, cp = mgr m.register({"id": "aochuang", "enabled": True, "base_url": "https://007.example/api"}) m.switch("aochuang", scope=cp.SCOPE_GLOBAL) assert m.resolve_active_id() == "aochuang" # 再禁用 aochuang → 回退 jiqing m.register({"id": "aochuang", "enabled": False}) assert m.resolve_active_id() == "jiqing" def test_custom_register_and_remove(mgr): m, cp = mgr meta = m.register({ "id": "custom_x", "name": "我的自建方案", "kind": "http", "enabled": True, "base_url": "https://x.example", "endpoints": {"send_message": {"method": "POST", "path": "/s"}}, }) assert meta["id"] == "custom_x" and meta["builtin"] is False assert "send_message" in meta["capabilities"] m.remove("custom_x") assert m.get("custom_x") is None # 内置不可删 with pytest.raises(ValueError): m.remove("jiqing") def test_http_field_mapping(mgr): m, cp = mgr prov = cp.HttpConnectionProvider({ "id": "t", "kind": "http", "base_url": "https://x", "endpoints": { "send_message": {"method": "POST", "path": "/send", "body_map": {"device_id": "deviceId", "to_id": "wxid", "content": "content"}}, }, }) src = {"device_id": "d1", "platform": "wechat", "to_id": "u1", "content": "hi", "msg_type": "text"} body = prov._apply_map(src, {"device_id": "deviceId", "to_id": "wxid", "content": "content"}) assert body["deviceId"] == "d1" and body["wxid"] == "u1" and body["content"] == "hi" # 未映射字段透传保留 assert body.get("platform") == "wechat" def test_persistence_roundtrip(mgr): m, cp = mgr m.register({"id": "aochuang", "enabled": True, "base_url": "https://007.example/api"}) m.switch("aochuang", scope=cp.SCOPE_GLOBAL) # 新实例从磁盘加载 m2 = cp.ConnectionProviderManager() m2.load(force=True) assert m2.resolve_active_id() == "aochuang" def test_http_execute_unknown_action_501(mgr): import asyncio m, cp = mgr prov = cp.HttpConnectionProvider({"id": "t", "kind": "http", "base_url": "https://x", "endpoints": {}}) res = asyncio.run(prov.execute("d1", "wechat", "send_message", {"to_id": "u", "content": "c"})) assert res["code"] == 501 and res["success"] is False def test_jiqing_execute_offline_payload(mgr): """jiqing 原生方案在无 WS 设备时应返回离线 503(复用 device_transport,不 mock 成功)。""" import asyncio m, cp = mgr prov = m.get("jiqing") res = asyncio.run(prov.execute("nonexist_dev", "wechat", "send_message", {"to_id": "filehelper", "content": "hi"})) assert res.get("provider") == "jiqing" assert res.get("success") is False # 离线设备,不得假成功