新增了Twitch、Telegram、Discord、Slack、Mattermost、WeChat、Zalo等多渠道的单元测试用例,覆盖了令牌处理、速率限制、消息去重、会话解析、格式转换、安全策略等模块 同时在测试配置中添加了测试用的OpenAI API密钥环境变量
205 lines
6.6 KiB
Python
205 lines
6.6 KiB
Python
from __future__ import annotations
|
|
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from yuxi.channels.base import BaseChannelAdapter
|
|
from yuxi.channels.models import (
|
|
ChannelIdentity,
|
|
ChannelMessage,
|
|
ChannelResponse,
|
|
ChannelType,
|
|
DeliveryResult,
|
|
EventType,
|
|
HealthStatus,
|
|
)
|
|
|
|
|
|
class ConcreteAdapter(BaseChannelAdapter):
|
|
channel_id = "test"
|
|
channel_type = ChannelType.WEBCHAT
|
|
|
|
async def connect(self) -> None:
|
|
pass
|
|
|
|
async def disconnect(self) -> None:
|
|
pass
|
|
|
|
async def send(self, response: ChannelResponse) -> DeliveryResult:
|
|
return DeliveryResult(success=True)
|
|
|
|
def normalize_inbound(self, raw) -> ChannelMessage:
|
|
raise NotImplementedError
|
|
|
|
def format_outbound(self, response: ChannelResponse):
|
|
return {"content": response.content}
|
|
|
|
async def health_check(self) -> HealthStatus:
|
|
return HealthStatus(status="healthy")
|
|
|
|
|
|
class TestAbstractMethods:
|
|
def test_cannot_instantiate_without_abstract_methods(self):
|
|
with pytest.raises(TypeError):
|
|
|
|
class IncompleteAdapter(BaseChannelAdapter):
|
|
channel_id = "incomplete"
|
|
channel_type = ChannelType.WEBCHAT
|
|
|
|
IncompleteAdapter()
|
|
|
|
def test_can_instantiate_concrete_adapter(self):
|
|
adapter = ConcreteAdapter()
|
|
assert adapter.channel_id == "test"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_abstract_connect(self):
|
|
adapter = ConcreteAdapter()
|
|
await adapter.connect()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_abstract_send_returns_delivery_result(self):
|
|
adapter = ConcreteAdapter()
|
|
identity = ChannelIdentity(
|
|
channel_id="test", channel_type=ChannelType.WEBCHAT, channel_user_id="u1", channel_chat_id="c1"
|
|
)
|
|
response = ChannelResponse(identity=identity, content="hello")
|
|
result = await adapter.send(response)
|
|
assert isinstance(result, DeliveryResult)
|
|
assert result.success is True
|
|
|
|
|
|
class TestDefaultImplementations:
|
|
def setup_method(self):
|
|
self.adapter = ConcreteAdapter()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_media_raises_not_implemented(self):
|
|
with pytest.raises(NotImplementedError):
|
|
await self.adapter.send_media("c1", "image", b"data")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_edit_message_raises_not_implemented(self):
|
|
with pytest.raises(NotImplementedError):
|
|
await self.adapter.edit_message("c1", "msg1", "new content")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_message_raises_not_implemented(self):
|
|
with pytest.raises(NotImplementedError):
|
|
await self.adapter.delete_message("c1", "msg1")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_reaction_raises_not_implemented(self):
|
|
with pytest.raises(NotImplementedError):
|
|
await self.adapter.send_reaction("c1", "msg1", "👍")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_user_info_raises_not_implemented(self):
|
|
with pytest.raises(NotImplementedError):
|
|
await self.adapter.get_user_info("u1")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_download_media_raises_not_implemented(self):
|
|
with pytest.raises(NotImplementedError):
|
|
await self.adapter.download_media("file1")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_verify_webhook_returns_true(self):
|
|
result = await self.adapter.verify_webhook_signature({}, b"body")
|
|
assert result is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_refresh_token_returns_true(self):
|
|
result = await self.adapter._refresh_token_if_needed()
|
|
assert result is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pre_connect_returns_empty_dict(self):
|
|
result = await self.adapter.pre_connect()
|
|
assert result == {}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_stream_chunk(self):
|
|
identity = ChannelIdentity(
|
|
channel_id="test", channel_type=ChannelType.WEBCHAT, channel_user_id="", channel_chat_id="c1",
|
|
channel_message_id="msg1",
|
|
)
|
|
response = ChannelResponse(identity=identity, content="chunk")
|
|
|
|
adapter = ConcreteAdapter()
|
|
adapter.send = AsyncMock(return_value=DeliveryResult(success=True))
|
|
|
|
result = await adapter.send_stream_chunk("c1", "msg1", "chunk", False)
|
|
assert result.success is True
|
|
adapter.send.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_receive_is_empty_generator(self):
|
|
adapter = ConcreteAdapter()
|
|
items = []
|
|
async for msg in adapter.receive():
|
|
items.append(msg)
|
|
assert items == []
|
|
|
|
|
|
class TestClassVars:
|
|
def test_default_text_chunk_limit(self):
|
|
assert ConcreteAdapter.text_chunk_limit == 4096
|
|
|
|
def test_default_supports_markdown(self):
|
|
assert ConcreteAdapter.supports_markdown is False
|
|
|
|
def test_default_streaming_modes(self):
|
|
assert ConcreteAdapter.streaming_modes == ["off"]
|
|
|
|
def test_default_max_media_size_mb(self):
|
|
assert ConcreteAdapter.max_media_size_mb == 100
|
|
|
|
def test_default_webhook_path(self):
|
|
assert ConcreteAdapter.webhook_path is None
|
|
|
|
def test_supports_streaming_default(self):
|
|
assert ConcreteAdapter.supports_streaming is False
|
|
|
|
|
|
class TestCallback:
|
|
def setup_method(self):
|
|
self.adapter = ConcreteAdapter()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_on_message_registers_handler(self):
|
|
handler = AsyncMock()
|
|
self.adapter.on_message(handler)
|
|
assert self.adapter._message_handler is handler
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_message_calls_handler(self):
|
|
handler = AsyncMock()
|
|
self.adapter.on_message(handler)
|
|
|
|
identity = ChannelIdentity(
|
|
channel_id="test", channel_type=ChannelType.WEBCHAT, channel_user_id="u1", channel_chat_id="c1"
|
|
)
|
|
message = ChannelMessage(identity=identity, content="hello")
|
|
await self.adapter._handle_message(message)
|
|
handler.assert_called_once_with(message)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_message_noop_when_no_handler(self):
|
|
identity = ChannelIdentity(
|
|
channel_id="test", channel_type=ChannelType.WEBCHAT, channel_user_id="u1", channel_chat_id="c1"
|
|
)
|
|
message = ChannelMessage(identity=identity, content="hello")
|
|
await self.adapter._handle_message(message)
|
|
|
|
|
|
class TestInit:
|
|
def test_init_with_config(self):
|
|
adapter = ConcreteAdapter(config={"enabled": True, "display_name": "Test"})
|
|
assert adapter.config["enabled"] is True
|
|
|
|
def test_init_without_config(self):
|
|
adapter = ConcreteAdapter()
|
|
assert adapter.config == {}
|
|
assert adapter._message_handler is None |