ForcePilot/backend/test/unit/gateway/protocol/test_handshake.py
Kris 3264900bc9 test: 新增多渠道单元测试用例并配置测试环境变量
新增了Twitch、Telegram、Discord、Slack、Mattermost、WeChat、Zalo等多渠道的单元测试用例,覆盖了令牌处理、速率限制、消息去重、会话解析、格式转换、安全策略等模块
同时在测试配置中添加了测试用的OpenAI API密钥环境变量
2026-05-12 00:56:47 +08:00

297 lines
11 KiB
Python

from __future__ import annotations
import pytest
from pydantic import ValidationError
from yuxi.gateway.protocol.handshake import ConnectParams, HelloFeatures, HelloOk, handle_handshake
class TestConnectParams:
def test_default_construction(self):
params = ConnectParams()
assert params.min_protocol == 1
assert params.max_protocol == 1
assert params.client_id == ""
assert params.client_version == ""
assert params.client_platform == ""
assert params.auth_token is None
assert params.caps == []
assert params.client_display_name == ""
assert params.client_device_family == ""
assert params.client_model_identifier == ""
assert params.client_mode == "default"
assert params.client_instance_id == ""
assert params.role == ""
assert params.locale == ""
assert params.user_agent == ""
def test_full_construction(self):
params = ConnectParams(
min_protocol=1,
max_protocol=1,
client_id="web-client-001",
client_version="2.3.0",
client_platform="web",
auth_token="jwt-token-here",
caps=["streaming", "commands"],
client_display_name="TestBot",
client_device_family="desktop",
client_model_identifier="win-x64",
client_mode="default",
client_instance_id="inst-001",
commands=["/start", "/help"],
permissions={"admin": True},
path_env="/api/v1",
role="bot",
scopes=["messages.read", "messages.write"],
locale="zh-CN",
user_agent="ForcePilot/2.3.0",
)
assert params.client_id == "web-client-001"
assert params.client_version == "2.3.0"
assert params.client_platform == "web"
assert params.auth_token == "jwt-token-here"
assert params.caps == ["streaming", "commands"]
assert params.client_display_name == "TestBot"
assert params.client_device_family == "desktop"
assert params.scopes == ["messages.read", "messages.write"]
assert params.locale == "zh-CN"
def test_client_platform_values(self):
for platform in ["web", "cli", "android", "ios"]:
params = ConnectParams(client_platform=platform)
assert params.client_platform == platform
def test_min_protocol_int(self):
with pytest.raises(ValidationError):
ConnectParams(min_protocol="invalid")
def test_max_protocol_int(self):
with pytest.raises(ValidationError):
ConnectParams(max_protocol="invalid")
def test_auth_token_optional(self):
params = ConnectParams()
assert params.auth_token is None
def test_caps_empty_default(self):
params = ConnectParams()
assert params.caps == []
def test_caps_from_list(self):
params = ConnectParams(caps=["streaming", "reactions"])
assert params.caps == ["streaming", "reactions"]
def test_json_serialization(self):
params = ConnectParams(client_id="cli-1", client_platform="cli", locale="en")
data = params.model_dump()
assert data["client_id"] == "cli-1"
assert data["client_platform"] == "cli"
assert data["locale"] == "en"
def test_json_deserialization(self):
data = {
"min_protocol": 1,
"max_protocol": 1,
"client_id": "web-1",
"client_version": "1.0",
"client_platform": "web",
"auth_token": None,
"caps": ["streaming"],
"client_display_name": "WebBot",
}
params = ConnectParams.model_validate(data)
assert params.client_id == "web-1"
assert params.client_platform == "web"
assert params.client_display_name == "WebBot"
def test_field_count_gte_17(self):
params = ConnectParams()
fields = list(ConnectParams.model_fields.keys())
assert len(fields) >= 17, f"ConnectParams has {len(fields)} fields, need >= 17: {fields}"
class TestHelloFeatures:
def test_default_construction(self):
features = HelloFeatures()
assert features.methods == []
assert features.events == []
def test_full_construction(self):
features = HelloFeatures(
methods=["ping", "chat.send"],
events=["message.received", "typing"],
)
assert features.methods == ["ping", "chat.send"]
assert features.events == ["message.received", "typing"]
def test_json_serialization(self):
features = HelloFeatures(methods=["ping"], events=["tick"])
data = features.model_dump()
assert data["methods"] == ["ping"]
assert data["events"] == ["tick"]
class TestHelloOk:
def test_minimal_construction(self):
hello = HelloOk(protocol=1, server_version="1.0.0", server_id="gw-1", session_id="sess-1")
assert hello.protocol == 1
assert hello.server_version == "1.0.0"
assert hello.server_id == "gw-1"
assert hello.session_id == "sess-1"
assert hello.features.methods == []
assert hello.features.events == []
assert hello.type == "hello-ok"
assert hello.snapshot is None
assert hello.dm_policy == "pairing"
assert hello.group_policy == "allowlist"
assert hello.conn_id == ""
assert hello.max_payload == 1048576
assert hello.max_buffered_bytes == 10485760
assert hello.tick_interval_ms == 5000
def test_full_construction(self):
features = HelloFeatures(methods=["ping"], events=["tick"])
hello = HelloOk(
protocol=1,
server_version="2.0.0",
server_id="forcepilot-gw",
session_id="abc-123",
features=features,
snapshot={"threads": 5},
dm_policy="pairing",
group_policy="allowlist",
conn_id="conn-abc12345",
max_payload=2097152,
max_buffered_bytes=20971520,
tick_interval_ms=3000,
)
assert hello.features.methods == ["ping"]
assert hello.snapshot == {"threads": 5}
assert hello.conn_id == "conn-abc12345"
assert hello.max_payload == 2097152
assert hello.tick_interval_ms == 3000
def test_protocol_is_required(self):
with pytest.raises(ValidationError):
HelloOk(server_version="1.0.0", server_id="gw", session_id="s1")
def test_server_version_is_required(self):
with pytest.raises(ValidationError):
HelloOk(protocol=1, server_id="gw", session_id="s1")
def test_server_id_is_required(self):
with pytest.raises(ValidationError):
HelloOk(protocol=1, server_version="1.0.0", session_id="s1")
def test_session_id_is_required(self):
with pytest.raises(ValidationError):
HelloOk(protocol=1, server_version="1.0.0", server_id="gw")
def test_default_dm_policy(self):
hello = HelloOk(protocol=1, server_version="1.0.0", server_id="gw", session_id="s1")
assert hello.dm_policy == "pairing"
def test_default_group_policy(self):
hello = HelloOk(protocol=1, server_version="1.0.0", server_id="gw", session_id="s1")
assert hello.group_policy == "allowlist"
def test_json_serialization(self):
hello = HelloOk(protocol=1, server_version="1.0.0", server_id="gw-1", session_id="sess-1")
data = hello.model_dump()
assert data["protocol"] == 1
assert data["type"] == "hello-ok"
assert data["dm_policy"] == "pairing"
assert data["max_payload"] == 1048576
def test_deserialization(self):
data = {
"protocol": 1,
"server_version": "1.0.0",
"server_id": "gw-1",
"session_id": "sess-1",
"features": {"methods": [], "events": []},
"snapshot": None,
"dm_policy": "pairing",
"group_policy": "allowlist",
"conn_id": "",
"max_payload": 1048576,
"max_buffered_bytes": 10485760,
"tick_interval_ms": 5000,
}
hello = HelloOk.model_validate(data)
assert hello.server_version == "1.0.0"
assert hello.max_payload == 1048576
def test_field_count_gte_11(self):
hello = HelloOk(protocol=1, server_version="1.0.0", server_id="gw", session_id="s1")
fields = list(HelloOk.model_fields.keys())
assert len(fields) >= 11, f"HelloOk has {len(fields)} fields, need >= 11: {fields}"
class TestHandleHandshake:
@pytest.mark.asyncio
async def test_successful_handshake(self):
params = ConnectParams(min_protocol=1, max_protocol=1, client_id="cli-1")
hello = await handle_handshake(params)
assert isinstance(hello, HelloOk)
assert hello.protocol == 1
assert hello.server_version == "1.0.0"
assert hello.server_id == "forcepilot-gateway"
assert len(hello.session_id) > 0
assert hello.conn_id.startswith("conn-")
@pytest.mark.asyncio
async def test_version_negotiation_fails(self):
params = ConnectParams(min_protocol=3, max_protocol=5)
with pytest.raises(ValueError, match="Protocol version negotiation failed"):
await handle_handshake(params)
@pytest.mark.asyncio
async def test_features_structured(self):
params = ConnectParams(min_protocol=1, max_protocol=1)
hello = await handle_handshake(params)
assert isinstance(hello.features, HelloFeatures)
assert len(hello.features.methods) > 0
assert len(hello.features.events) > 0
@pytest.mark.asyncio
async def test_session_id_unique(self):
params = ConnectParams(min_protocol=1, max_protocol=1)
hello1 = await handle_handshake(params)
hello2 = await handle_handshake(params)
assert hello1.session_id != hello2.session_id
@pytest.mark.asyncio
async def test_session_id_is_uuid_format(self):
params = ConnectParams(min_protocol=1, max_protocol=1)
hello = await handle_handshake(params)
parts = hello.session_id.split("-")
assert len(parts) == 5
@pytest.mark.asyncio
async def test_default_dm_policy(self):
params = ConnectParams(min_protocol=1, max_protocol=1)
hello = await handle_handshake(params)
assert hello.dm_policy == "pairing"
@pytest.mark.asyncio
async def test_default_group_policy(self):
params = ConnectParams(min_protocol=1, max_protocol=1)
hello = await handle_handshake(params)
assert hello.group_policy == "allowlist"
@pytest.mark.asyncio
async def test_hello_type_is_hello_ok(self):
params = ConnectParams(min_protocol=1, max_protocol=1)
hello = await handle_handshake(params)
assert hello.type == "hello-ok"
@pytest.mark.asyncio
async def test_policy_defaults(self):
params = ConnectParams(min_protocol=1, max_protocol=1)
hello = await handle_handshake(params)
assert hello.max_payload == 1048576
assert hello.max_buffered_bytes == 10485760
assert hello.tick_interval_ms == 5000