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

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