141 lines
5.2 KiB
Python
141 lines
5.2 KiB
Python
"""
|
||
连接方案可切换驱动层 · 离线回归(不依赖真机/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 # 离线设备,不得假成功
|