新增了Twitch、Telegram、Discord、Slack、Mattermost、WeChat、Zalo等多渠道的单元测试用例,覆盖了令牌处理、速率限制、消息去重、会话解析、格式转换、安全策略等模块 同时在测试配置中添加了测试用的OpenAI API密钥环境变量
179 lines
6.5 KiB
Python
179 lines
6.5 KiB
Python
from __future__ import annotations
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from yuxi.gateway.protocol.errors import ErrorCode, ErrorDetail
|
|
from yuxi.gateway.protocol.frames import EventFrame, RequestFrame, ResponseFrame
|
|
|
|
|
|
class TestRequestFrame:
|
|
def test_minimal_construction(self):
|
|
frame = RequestFrame(id="req-001", method="chat.send")
|
|
assert frame.type == "req"
|
|
assert frame.id == "req-001"
|
|
assert frame.method == "chat.send"
|
|
assert frame.params == {}
|
|
|
|
def test_with_params(self):
|
|
frame = RequestFrame(id="req-002", method="chat.send", params={"content": "hello"})
|
|
assert frame.params == {"content": "hello"}
|
|
|
|
def test_id_is_required(self):
|
|
with pytest.raises(ValidationError):
|
|
RequestFrame(method="chat.send")
|
|
|
|
def test_method_is_required(self):
|
|
with pytest.raises(ValidationError):
|
|
RequestFrame(id="req-001")
|
|
|
|
def test_json_serialization(self):
|
|
frame = RequestFrame(id="req-001", method="chat.send", params={"key": "val"})
|
|
data = frame.model_dump()
|
|
assert data == {"type": "req", "id": "req-001", "method": "chat.send", "params": {"key": "val"}}
|
|
|
|
def test_json_deserialization(self):
|
|
data = {"type": "req", "id": "req-001", "method": "chat.send", "params": {"key": "val"}}
|
|
frame = RequestFrame.model_validate(data)
|
|
assert frame.id == "req-001"
|
|
assert frame.method == "chat.send"
|
|
|
|
def test_params_default_empty_dict(self):
|
|
frame = RequestFrame(id="r1", method="ping")
|
|
assert frame.params == {}
|
|
assert isinstance(frame.params, dict)
|
|
|
|
def test_params_not_shared(self):
|
|
frame1 = RequestFrame(id="r1", method="ping")
|
|
frame2 = RequestFrame(id="r2", method="pong")
|
|
frame1.params["key"] = "val"
|
|
assert frame2.params == {}
|
|
|
|
def test_id_accepts_uuid_string(self):
|
|
frame = RequestFrame(id="550e8400-e29b-41d4-a716-446655440000", method="chat.send")
|
|
assert frame.id == "550e8400-e29b-41d4-a716-446655440000"
|
|
|
|
def test_params_accepts_nested_dicts(self):
|
|
frame = RequestFrame(id="r1", method="config.update", params={"nested": {"key": "value"}})
|
|
assert frame.params["nested"]["key"] == "value"
|
|
|
|
|
|
class TestResponseFrame:
|
|
def test_success_response(self):
|
|
frame = ResponseFrame(id="req-001", ok=True, payload={"result": "ok"})
|
|
assert frame.type == "res"
|
|
assert frame.id == "req-001"
|
|
assert frame.ok is True
|
|
assert frame.payload == {"result": "ok"}
|
|
assert frame.error is None
|
|
|
|
def test_error_response(self):
|
|
error = ErrorDetail(code=ErrorCode.INTERNAL_ERROR, message="something went wrong")
|
|
frame = ResponseFrame(id="req-001", ok=False, error=error)
|
|
assert frame.ok is False
|
|
assert frame.error.code == ErrorCode.INTERNAL_ERROR
|
|
assert frame.error.message == "something went wrong"
|
|
|
|
def test_id_is_required(self):
|
|
with pytest.raises(ValidationError):
|
|
ResponseFrame(ok=True)
|
|
|
|
def test_ok_is_required(self):
|
|
with pytest.raises(ValidationError):
|
|
ResponseFrame(id="req-001")
|
|
|
|
def test_payload_default_none(self):
|
|
frame = ResponseFrame(id="r1", ok=True)
|
|
assert frame.payload is None
|
|
|
|
def test_error_default_none(self):
|
|
frame = ResponseFrame(id="r1", ok=True)
|
|
assert frame.error is None
|
|
|
|
def test_json_serialization_success(self):
|
|
frame = ResponseFrame(id="req-001", ok=True, payload="hello")
|
|
data = frame.model_dump()
|
|
assert data == {"type": "res", "id": "req-001", "ok": True, "payload": "hello", "error": None}
|
|
|
|
def test_json_serialization_error(self):
|
|
error = ErrorDetail(code=ErrorCode.RATE_LIMITED, message="too fast", retryable=True, retry_after_ms=1000)
|
|
frame = ResponseFrame(id="req-001", ok=False, error=error)
|
|
data = frame.model_dump()
|
|
assert data["ok"] is False
|
|
assert data["error"]["code"] == -32000
|
|
assert data["error"]["retryable"] is True
|
|
assert data["error"]["retry_after_ms"] == 1000
|
|
|
|
def test_deserialization_with_error(self):
|
|
data = {
|
|
"type": "res",
|
|
"id": "r1",
|
|
"ok": False,
|
|
"error": {"code": -32601, "message": "not found"},
|
|
"payload": None,
|
|
}
|
|
frame = ResponseFrame.model_validate(data)
|
|
assert frame.error.code == ErrorCode.METHOD_NOT_FOUND
|
|
|
|
def test_payload_accepts_list(self):
|
|
frame = ResponseFrame(id="r1", ok=True, payload=[1, 2, 3])
|
|
assert frame.payload == [1, 2, 3]
|
|
|
|
def test_payload_accepts_scalar(self):
|
|
frame = ResponseFrame(id="r1", ok=True, payload=42)
|
|
assert frame.payload == 42
|
|
|
|
|
|
class TestEventFrame:
|
|
def test_minimal_construction(self):
|
|
frame = EventFrame(event="message.received")
|
|
assert frame.type == "event"
|
|
assert frame.event == "message.received"
|
|
assert frame.payload is None
|
|
assert frame.seq == 0
|
|
assert frame.state_version == 0
|
|
|
|
def test_with_payload_and_seq(self):
|
|
frame = EventFrame(event="message.received", payload={"text": "hi"}, seq=5, state_version=3)
|
|
assert frame.payload == {"text": "hi"}
|
|
assert frame.seq == 5
|
|
assert frame.state_version == 3
|
|
|
|
def test_event_is_required(self):
|
|
with pytest.raises(ValidationError):
|
|
EventFrame()
|
|
|
|
def test_default_seq_zero(self):
|
|
frame = EventFrame(event="ping")
|
|
assert frame.seq == 0
|
|
|
|
def test_default_state_version_zero(self):
|
|
frame = EventFrame(event="ping")
|
|
assert frame.state_version == 0
|
|
|
|
def test_json_serialization(self):
|
|
frame = EventFrame(event="message.received", payload={"text": "hi"}, seq=1)
|
|
data = frame.model_dump()
|
|
expected = {
|
|
"type": "event", "event": "message.received",
|
|
"payload": {"text": "hi"}, "seq": 1, "state_version": 0,
|
|
}
|
|
assert data == expected
|
|
|
|
def test_deserialization(self):
|
|
data = {"type": "event", "event": "typing", "payload": None, "seq": 0, "state_version": 0}
|
|
frame = EventFrame.model_validate(data)
|
|
assert frame.event == "typing"
|
|
|
|
def test_event_accepts_dotted_name(self):
|
|
frame = EventFrame(event="bot.added")
|
|
assert frame.event == "bot.added"
|
|
|
|
def test_seq_must_be_int(self):
|
|
with pytest.raises(ValidationError):
|
|
EventFrame(event="test", seq="invalid")
|
|
|
|
def test_state_version_must_be_int(self):
|
|
with pytest.raises(ValidationError):
|
|
EventFrame(event="test", state_version="invalid")
|