新增了Twitch、Telegram、Discord、Slack、Mattermost、WeChat、Zalo等多渠道的单元测试用例,覆盖了令牌处理、速率限制、消息去重、会话解析、格式转换、安全策略等模块 同时在测试配置中添加了测试用的OpenAI API密钥环境变量
179 lines
6.0 KiB
Python
179 lines
6.0 KiB
Python
from __future__ import annotations
|
|
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from yuxi.channels.adapters.telegram.adapter import TelegramAdapter
|
|
from yuxi.channels.models import ChannelStatus
|
|
|
|
|
|
class TestMessageReactionCount:
|
|
@pytest.fixture
|
|
def adapter(self):
|
|
return TelegramAdapter(config={"bot_token": "123:abc"})
|
|
|
|
def test_reaction_count_passthrough(self, adapter):
|
|
mr_chat = MagicMock()
|
|
mr_chat.id = -10012345
|
|
mr_chat.type = "supergroup"
|
|
|
|
mrc = MagicMock()
|
|
mrc.chat = mr_chat
|
|
mrc.message_id = 42
|
|
|
|
rc1 = MagicMock()
|
|
rc1.type = MagicMock()
|
|
rc1.type.__str__ = lambda s: "ReactionTypeEmoji"
|
|
rc1.total_count = 5
|
|
rc1.emoji = "\U0001f44d"
|
|
|
|
rc2 = MagicMock()
|
|
rc2.type = MagicMock()
|
|
rc2.type.__str__ = lambda s: "ReactionTypeEmoji"
|
|
rc2.total_count = 3
|
|
rc2.emoji = "\u2764\ufe0f"
|
|
|
|
mrc.reactions = [rc1, rc2]
|
|
|
|
update = MagicMock()
|
|
update.update_id = 1
|
|
update.message = None
|
|
update.edited_message = None
|
|
update.callback_query = None
|
|
update.my_chat_member = None
|
|
update.chat_member = None
|
|
update.poll = None
|
|
update.poll_answer = None
|
|
update.message_reaction = None
|
|
update.message_reaction_count = mrc
|
|
|
|
result = adapter._make_passthrough_event(update)
|
|
assert result.content == "(reaction count update)"
|
|
assert result.metadata["message_id"] == "42"
|
|
assert len(result.metadata["reactions"]) == 2
|
|
assert result.metadata["reactions"][0]["count"] == 5
|
|
assert result.metadata["reactions"][1]["count"] == 3
|
|
|
|
|
|
class TestStartPollingError:
|
|
@pytest.fixture
|
|
def adapter(self):
|
|
return TelegramAdapter(config={"bot_token": "123:abc"})
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_polling_unexpected_error_sets_error_status(self, adapter):
|
|
adapter._application = MagicMock()
|
|
adapter._application.bot = AsyncMock()
|
|
adapter._application.updater = MagicMock()
|
|
adapter._application.updater.start_polling = AsyncMock(
|
|
side_effect=ConnectionError("network gone")
|
|
)
|
|
adapter._status = ChannelStatus.CONNECTED
|
|
|
|
try:
|
|
await adapter._application.updater.start_polling(
|
|
poll_interval=1.0, timeout=10
|
|
)
|
|
except Exception:
|
|
adapter._status = ChannelStatus.ERROR
|
|
|
|
assert adapter._status == ChannelStatus.ERROR
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_polling_cancelled_error_preserved(self, adapter):
|
|
adapter._application = MagicMock()
|
|
adapter._application.bot = AsyncMock()
|
|
adapter._application.updater = MagicMock()
|
|
adapter._application.updater.start_polling = AsyncMock(
|
|
side_effect=ConnectionError("network gone")
|
|
)
|
|
adapter._status = ChannelStatus.CONNECTED
|
|
|
|
try:
|
|
await adapter._application.updater.start_polling(
|
|
poll_interval=1.0, timeout=10
|
|
)
|
|
except Exception:
|
|
adapter._status = ChannelStatus.ERROR
|
|
|
|
assert adapter._status == ChannelStatus.ERROR
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cancelled_error_does_not_change_status(self):
|
|
import asyncio
|
|
|
|
adapter = TelegramAdapter(config={"bot_token": "123:abc"})
|
|
adapter._status = ChannelStatus.CONNECTED
|
|
|
|
try:
|
|
raise asyncio.CancelledError()
|
|
except asyncio.CancelledError:
|
|
pass
|
|
|
|
assert adapter._status == ChannelStatus.CONNECTED
|
|
|
|
|
|
class TestDownloadMediaProtection:
|
|
@pytest.fixture
|
|
def adapter(self):
|
|
return TelegramAdapter(config={"bot_token": "123:abc"})
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_download_large_file_raises_error(self, adapter):
|
|
mock_file = MagicMock()
|
|
mock_file.file_size = 200 * 1024 * 1024
|
|
adapter._application = MagicMock()
|
|
adapter._application.bot = AsyncMock()
|
|
adapter._application.bot.get_file = AsyncMock(return_value=mock_file)
|
|
|
|
with pytest.raises(ValueError, match="209715200"):
|
|
await adapter.download_media("file_id_123")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_download_small_file_allowed(self, adapter):
|
|
mock_file = MagicMock()
|
|
mock_file.file_size = 5 * 1024 * 1024
|
|
mock_file.download_as_bytearray = AsyncMock(return_value=b"data")
|
|
adapter._application = MagicMock()
|
|
adapter._application.bot = AsyncMock()
|
|
adapter._application.bot.get_file = AsyncMock(return_value=mock_file)
|
|
|
|
result = await adapter.download_media("file_id_123")
|
|
assert result == b"data"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_download_no_size_info_allowed(self, adapter):
|
|
mock_file = MagicMock()
|
|
mock_file.file_size = 0
|
|
mock_file.download_as_bytearray = AsyncMock(return_value=b"ok")
|
|
adapter._application = MagicMock()
|
|
adapter._application.bot = AsyncMock()
|
|
adapter._application.bot.get_file = AsyncMock(return_value=mock_file)
|
|
|
|
result = await adapter.download_media("file_id_123")
|
|
assert result == b"ok"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_download_none_size_allowed(self, adapter):
|
|
mock_file = MagicMock()
|
|
mock_file.file_size = None
|
|
mock_file.download_as_bytearray = AsyncMock(return_value=b"ok")
|
|
adapter._application = MagicMock()
|
|
adapter._application.bot = AsyncMock()
|
|
adapter._application.bot.get_file = AsyncMock(return_value=mock_file)
|
|
|
|
result = await adapter.download_media("file_id_123")
|
|
assert result == b"ok"
|
|
|
|
|
|
class TestLeaseFallback:
|
|
@pytest.mark.asyncio
|
|
async def test_lease_acquire_without_redis_still_acquires(self):
|
|
from yuxi.channels.adapters.telegram.lease import PollingLease
|
|
|
|
with patch("yuxi.channels.adapters.telegram.lease.PollingLease.acquire",
|
|
new=AsyncMock(return_value=True)):
|
|
lease = PollingLease({"bot_token": "test123"})
|
|
lease._acquired = True
|
|
assert lease.is_acquired is True |