blob: b5e76fed65b144d8af4478e0c9441f126cadf718 [file] [log] [blame] [raw]
"""
Telegram Bot核心业务逻辑单元测试
专注测试配置验证、组件初始化、生命周期管理等核心功能
"""
import pytest
import asyncio
import signal
from unittest.mock import Mock, AsyncMock, patch, MagicMock
from telegram import Bot as TelegramBotAPI, User
from telegram.ext import Application
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))
from claude_agent.telegram.bot import TelegramBot
class TestTelegramBotCore:
"""Telegram Bot核心功能测试"""
@pytest.fixture
def valid_config_manager(self):
"""创建有效的配置管理器"""
config_manager = Mock()
telegram_config = {
'bot_token': '123456789:ABCDEFGHijklmnopqrstuvwxyz-123456789',
'allowed_users': [111111, 222222],
'allowed_groups': [-333333],
'message': {
'context_history_limit': 50,
'stream_update_interval': 1.0
},
'files': {
'temp_dir': 'temp/telegram',
'max_file_size_mb': 20
}
}
webhook_config = {
'enabled': False
}
config_manager.get_telegram_config.return_value = telegram_config
config_manager.get_webhook_config.return_value = webhook_config
return config_manager
@pytest.fixture
def invalid_config_manager(self):
"""创建无效的配置管理器"""
config_manager = Mock()
# 缺少必需的配置项
telegram_config = {
'bot_token': '123456789:ABCDEFGHijklmnopqrstuvwxyz-123456789'
# 缺少 allowed_users 和 allowed_groups
}
config_manager.get_telegram_config.return_value = telegram_config
return config_manager
@pytest.fixture
def empty_token_config_manager(self):
"""创建空token的配置管理器"""
config_manager = Mock()
telegram_config = {
'bot_token': 'YOUR_BOT_TOKEN_HERE', # 默认无效token
'allowed_users': [111111],
'allowed_groups': [-333333]
}
config_manager.get_telegram_config.return_value = telegram_config
return config_manager
def test_initialization_with_valid_config(self, valid_config_manager):
"""测试使用有效配置初始化"""
with patch('claude_agent.telegram.bot.get_config_manager', return_value=valid_config_manager):
bot = TelegramBot(config_name="test")
assert bot.config_name == "test"
assert bot.config_manager == valid_config_manager
assert bot.telegram_config == valid_config_manager.get_telegram_config()
assert not bot.is_running
assert bot.application is None
assert bot.webhook_integration is None
def test_initialization_with_invalid_config(self, invalid_config_manager):
"""测试使用无效配置初始化应该抛出异常"""
with patch('claude_agent.telegram.bot.get_config_manager', return_value=invalid_config_manager):
with pytest.raises(ValueError, match="缺少必需的配置项"):
TelegramBot(config_name="invalid")
def test_initialization_with_empty_token(self, empty_token_config_manager):
"""测试使用空token配置初始化应该抛出异常"""
with patch('claude_agent.telegram.bot.get_config_manager', return_value=empty_token_config_manager):
with pytest.raises(ValueError, match="请配置有效的Telegram Bot Token"):
TelegramBot(config_name="empty_token")
def test_config_validation_missing_required_keys(self):
"""测试配置验证:缺少必需的键"""
config_manager = Mock()
# 测试缺少 bot_token
telegram_config = {
'allowed_users': [111111],
'allowed_groups': [-333333]
}
config_manager.get_telegram_config.return_value = telegram_config
with patch('claude_agent.telegram.bot.get_config_manager', return_value=config_manager):
with pytest.raises(ValueError, match="缺少必需的配置项: telegram.bot_token"):
TelegramBot()
def test_config_validation_all_missing_keys(self):
"""测试配置验证:所有必需的键都缺少"""
config_manager = Mock()
telegram_config = {} # 空配置
config_manager.get_telegram_config.return_value = telegram_config
with patch('claude_agent.telegram.bot.get_config_manager', return_value=config_manager):
with pytest.raises(ValueError) as exc_info:
TelegramBot()
# 应该抛出第一个缺少的配置项错误
assert "缺少必需的配置项" in str(exc_info.value)
@pytest.mark.asyncio
async def test_initialize_components_success(self, valid_config_manager):
"""测试组件初始化成功"""
with patch('claude_agent.telegram.bot.get_config_manager', return_value=valid_config_manager):
bot = TelegramBot()
# Mock Application.builder()
mock_application = Mock(spec=Application)
mock_bot_api = Mock(spec=TelegramBotAPI)
mock_user = Mock(spec=User)
mock_user.id = 12345
mock_user.username = "test_bot"
mock_bot_api.get_me = AsyncMock(return_value=mock_user)
mock_application.bot = mock_bot_api
mock_builder = Mock()
mock_builder.token.return_value = mock_builder
mock_builder.build.return_value = mock_application
with patch('claude_agent.telegram.bot.Application.builder', return_value=mock_builder):
# Mock所有组件创建
with patch('claude_agent.telegram.bot.TelegramClientAdapter') as mock_client_adapter:
with patch('claude_agent.telegram.bot.ContextManager') as mock_context_manager:
with patch('claude_agent.telegram.bot.FileHandler') as mock_file_handler:
with patch('claude_agent.telegram.bot.ClaudeAgentAdapter') as mock_claude_adapter:
with patch('claude_agent.telegram.bot.StreamMessageSender') as mock_stream_sender:
with patch('claude_agent.telegram.bot.MessageHandler') as mock_message_handler:
# Mock webhook初始化
with patch.object(bot, '_initialize_webhook_integration', new_callable=AsyncMock):
with patch.object(bot, '_process_webhook_message_queue', new_callable=AsyncMock):
with patch.object(bot, '_register_handlers'):
await bot.initialize()
# 验证组件创建
assert bot.application == mock_application
mock_client_adapter.assert_called_once_with(mock_bot_api)
mock_context_manager.assert_called_once()
mock_file_handler.assert_called_once()
mock_claude_adapter.assert_called_once()
mock_stream_sender.assert_called_once()
mock_message_handler.assert_called_once()
@pytest.mark.asyncio
async def test_initialize_components_bot_api_failure(self, valid_config_manager):
"""测试Bot API获取失败的情况"""
with patch('claude_agent.telegram.bot.get_config_manager', return_value=valid_config_manager):
bot = TelegramBot()
mock_application = Mock(spec=Application)
mock_bot_api = Mock(spec=TelegramBotAPI)
mock_bot_api.get_me = AsyncMock(side_effect=Exception("API Error"))
mock_application.bot = mock_bot_api
mock_builder = Mock()
mock_builder.token.return_value = mock_builder
mock_builder.build.return_value = mock_application
with patch('claude_agent.telegram.bot.Application.builder', return_value=mock_builder):
with pytest.raises(Exception, match="API Error"):
await bot.initialize()
@pytest.mark.asyncio
async def test_start_already_running(self, valid_config_manager):
"""测试启动已经在运行的Bot"""
with patch('claude_agent.telegram.bot.get_config_manager', return_value=valid_config_manager):
bot = TelegramBot()
bot.is_running = True # 设置为已运行状态
# Mock telegram_client 避免stop方法中发送通知失败
mock_telegram_client = Mock()
mock_telegram_client.send_message = AsyncMock()
bot.telegram_client = mock_telegram_client
with patch('asyncio.get_running_loop'):
await bot.start()
# 由于start方法的finally块会调用stop,所以最终is_running会变为False
# 但是我们可以验证没有进行初始化
assert bot.application is None # 未进行初始化
@pytest.mark.asyncio
async def test_start_initialization_failure(self, valid_config_manager):
"""测试启动时初始化失败"""
with patch('claude_agent.telegram.bot.get_config_manager', return_value=valid_config_manager):
bot = TelegramBot()
with patch.object(bot, 'initialize', side_effect=Exception("Init failed")):
with patch.object(bot, 'stop', new_callable=AsyncMock) as mock_stop:
with pytest.raises(Exception, match="Init failed"):
await bot.start()
# 应该调用stop方法清理
mock_stop.assert_called_once()
@pytest.mark.asyncio
async def test_stop_not_running(self, valid_config_manager):
"""测试停止未运行的Bot"""
with patch('claude_agent.telegram.bot.get_config_manager', return_value=valid_config_manager):
bot = TelegramBot()
bot.is_running = False
# 应该能正常调用而不出错
await bot.stop()
assert not bot.is_running
@pytest.mark.asyncio
async def test_stop_with_cleanup(self, valid_config_manager):
"""测试停止Bot并清理资源"""
with patch('claude_agent.telegram.bot.get_config_manager', return_value=valid_config_manager):
bot = TelegramBot()
bot.is_running = True
# Mock应用程序
mock_application = Mock()
mock_updater = Mock()
mock_updater.stop = AsyncMock()
mock_application.updater = mock_updater
mock_application.stop = AsyncMock()
mock_application.shutdown = AsyncMock()
bot.application = mock_application
# Mock组件 - 使用AsyncMock
mock_file_handler = Mock()
mock_file_handler.cleanup_temp_files = AsyncMock() # 修复:使用AsyncMock
bot.file_handler = mock_file_handler
mock_context_manager = Mock()
mock_context_manager.cleanup_old_chats = Mock()
bot.context_manager = mock_context_manager
# Mock webhook集成
mock_webhook_integration = Mock()
mock_webhook_integration.stop = AsyncMock()
bot.webhook_integration = mock_webhook_integration
# Mock webhook队列任务 - 创建一个真正的已取消任务
async def dummy_task():
await asyncio.sleep(10) # 长时间睡眠的任务
webhook_task = asyncio.create_task(dummy_task())
webhook_task.cancel()
bot.webhook_queue_task = webhook_task
with patch.object(bot, '_send_shutdown_notification', new_callable=AsyncMock):
await bot.stop()
# 验证清理调用
mock_updater.stop.assert_called_once()
mock_application.stop.assert_called_once()
mock_application.shutdown.assert_called_once()
mock_webhook_integration.stop.assert_called_once()
mock_file_handler.cleanup_temp_files.assert_called_once()
mock_context_manager.cleanup_old_chats.assert_called_once()
# 任务已被取消,不需要验证cancel调用
assert not bot.is_running
def test_signal_handlers_setup(self, valid_config_manager):
"""测试信号处理器设置"""
with patch('claude_agent.telegram.bot.get_config_manager', return_value=valid_config_manager):
bot = TelegramBot()
with patch('signal.signal') as mock_signal:
bot._setup_signal_handlers()
# 验证SIGINT和SIGTERM信号都被设置
expected_calls = [
((signal.SIGINT, mock_signal.call_args_list[0][0][1]),),
((signal.SIGTERM, mock_signal.call_args_list[1][0][1]),)
]
assert mock_signal.call_count == 2
def test_signal_handler_functionality(self, valid_config_manager):
"""测试信号处理器功能"""
with patch('claude_agent.telegram.bot.get_config_manager', return_value=valid_config_manager):
bot = TelegramBot()
# 验证shutdown_event初始状态
assert not bot.shutdown_event.is_set()
with patch('signal.signal') as mock_signal:
bot._setup_signal_handlers()
# 获取信号处理函数
signal_handler = mock_signal.call_args_list[0][0][1]
# 模拟收到信号
signal_handler(signal.SIGINT, None)
# 验证shutdown_event被设置
assert bot.shutdown_event.is_set()
@pytest.mark.asyncio
async def test_send_startup_notification_success(self, valid_config_manager):
"""测试启动通知发送成功"""
with patch('claude_agent.telegram.bot.get_config_manager', return_value=valid_config_manager):
bot = TelegramBot()
mock_telegram_client = Mock()
mock_telegram_client.send_message = AsyncMock()
bot.telegram_client = mock_telegram_client
await bot._send_startup_notification()
# 验证发送给所有允许的用户
expected_calls = len(bot.telegram_config['allowed_users'])
assert mock_telegram_client.send_message.call_count == expected_calls
# 验证消息内容
call_args = mock_telegram_client.send_message.call_args_list[0]
assert "Claude Telegram Bot 已启动" in call_args[1]['text']
@pytest.mark.asyncio
async def test_send_startup_notification_partial_failure(self, valid_config_manager):
"""测试启动通知部分失败"""
with patch('claude_agent.telegram.bot.get_config_manager', return_value=valid_config_manager):
bot = TelegramBot()
mock_telegram_client = Mock()
# 第一个用户发送成功,第二个用户发送失败
mock_telegram_client.send_message = AsyncMock(side_effect=[None, Exception("Send failed")])
bot.telegram_client = mock_telegram_client
# 应该不抛出异常,只记录警告
await bot._send_startup_notification()
assert mock_telegram_client.send_message.call_count == 2
@pytest.mark.asyncio
async def test_send_shutdown_notification_success(self, valid_config_manager):
"""测试关闭通知发送成功"""
with patch('claude_agent.telegram.bot.get_config_manager', return_value=valid_config_manager):
bot = TelegramBot()
mock_telegram_client = Mock()
mock_telegram_client.send_message = AsyncMock()
bot.telegram_client = mock_telegram_client
await bot._send_shutdown_notification()
# 验证发送给所有允许的用户
expected_calls = len(bot.telegram_config['allowed_users'])
assert mock_telegram_client.send_message.call_count == expected_calls
# 验证消息内容
call_args = mock_telegram_client.send_message.call_args_list[0]
assert "Claude Telegram Bot 正在关闭" in call_args[1]['text']
def test_get_stats_basic(self, valid_config_manager):
"""测试获取基本统计信息"""
with patch('claude_agent.telegram.bot.get_config_manager', return_value=valid_config_manager):
bot = TelegramBot()
stats = bot.get_stats()
assert stats['is_running'] is False
assert stats['config_name'] == "local" # 默认值
assert stats['allowed_users_count'] == 2
assert stats['allowed_groups_count'] == 1
def test_get_stats_with_context_manager(self, valid_config_manager):
"""测试获取包含上下文管理器的统计信息"""
with patch('claude_agent.telegram.bot.get_config_manager', return_value=valid_config_manager):
bot = TelegramBot()
mock_context_manager = Mock()
mock_context_manager.get_chat_count.return_value = 5
bot.context_manager = mock_context_manager
stats = bot.get_stats()
assert stats['active_chats'] == 5
@pytest.mark.asyncio
async def test_send_admin_message_success(self, valid_config_manager):
"""测试发送管理员消息成功"""
with patch('claude_agent.telegram.bot.get_config_manager', return_value=valid_config_manager):
bot = TelegramBot()
mock_telegram_client = Mock()
mock_telegram_client.send_message = AsyncMock()
bot.telegram_client = mock_telegram_client
test_message = "System maintenance at 3AM"
await bot.send_admin_message(test_message)
# 验证发送给所有允许的用户
expected_calls = len(bot.telegram_config['allowed_users'])
assert mock_telegram_client.send_message.call_count == expected_calls
# 验证消息格式
call_args = mock_telegram_client.send_message.call_args_list[0]
assert "📢 管理员消息:" in call_args[1]['text']
assert test_message in call_args[1]['text']
@pytest.mark.asyncio
async def test_send_admin_message_failure(self, valid_config_manager):
"""测试发送管理员消息失败"""
with patch('claude_agent.telegram.bot.get_config_manager', return_value=valid_config_manager):
bot = TelegramBot()
mock_telegram_client = Mock()
mock_telegram_client.send_message = AsyncMock(side_effect=Exception("Send failed"))
bot.telegram_client = mock_telegram_client
# 应该不抛出异常,只记录错误
await bot.send_admin_message("Test message")
# 应该尝试发送给所有用户
expected_calls = len(bot.telegram_config['allowed_users'])
assert mock_telegram_client.send_message.call_count == expected_calls
def test_dependency_injection_methods(self, valid_config_manager):
"""测试依赖注入方法(用于测试)"""
with patch('claude_agent.telegram.bot.get_config_manager', return_value=valid_config_manager):
bot = TelegramBot()
mock_context_manager = Mock()
mock_file_handler = Mock()
mock_claude_agent = Mock()
mock_stream_sender = Mock()
# 测试所有依赖注入方法
bot.set_context_manager(mock_context_manager)
bot.set_file_handler(mock_file_handler)
bot.set_claude_agent(mock_claude_agent)
bot.set_stream_sender(mock_stream_sender)
assert bot.context_manager == mock_context_manager
assert bot.file_handler == mock_file_handler
assert bot.claude_agent == mock_claude_agent
assert bot.stream_sender == mock_stream_sender
class TestTelegramBotErrorHandling:
"""Telegram Bot错误处理测试"""
@pytest.fixture
def bot_with_valid_config(self):
"""创建有效配置的Bot实例"""
config_manager = Mock()
telegram_config = {
'bot_token': '123456789:ValidToken',
'allowed_users': [111],
'allowed_groups': [-222]
}
config_manager.get_telegram_config.return_value = telegram_config
config_manager.get_webhook_config.return_value = {'enabled': False}
with patch('claude_agent.telegram.bot.get_config_manager', return_value=config_manager):
return TelegramBot()
@pytest.mark.asyncio
async def test_error_handler_with_chat(self, bot_with_valid_config):
"""测试错误处理器(有聊天信息)"""
bot = bot_with_valid_config
mock_telegram_client = Mock()
mock_telegram_client.send_message = AsyncMock()
bot.telegram_client = mock_telegram_client
# 创建Mock Update
mock_update = Mock()
mock_chat = Mock()
mock_chat.id = 123456
mock_update.effective_chat = mock_chat
# 创建Mock Context
mock_context = Mock()
mock_context.error = Exception("Test error")
await bot._error_handler(mock_update, mock_context)
# 验证发送错误消息
mock_telegram_client.send_message.assert_called_once()
call_args = mock_telegram_client.send_message.call_args
assert call_args[1]['chat_id'] == 123456
assert "处理消息时出现内部错误" in call_args[1]['text']
@pytest.mark.asyncio
async def test_error_handler_without_chat(self, bot_with_valid_config):
"""测试错误处理器(无聊天信息)"""
bot = bot_with_valid_config
mock_telegram_client = Mock()
mock_telegram_client.send_message = AsyncMock()
bot.telegram_client = mock_telegram_client
# 无Update或无effective_chat
mock_context = Mock()
mock_context.error = Exception("Test error")
await bot._error_handler(None, mock_context)
# 不应该发送消息
mock_telegram_client.send_message.assert_not_called()
@pytest.mark.asyncio
async def test_error_handler_send_message_fails(self, bot_with_valid_config):
"""测试错误处理器发送消息失败"""
bot = bot_with_valid_config
mock_telegram_client = Mock()
mock_telegram_client.send_message = AsyncMock(side_effect=Exception("Send failed"))
bot.telegram_client = mock_telegram_client
# 创建Mock Update
mock_update = Mock()
mock_chat = Mock()
mock_chat.id = 123456
mock_update.effective_chat = mock_chat
mock_context = Mock()
mock_context.error = Exception("Original error")
# 应该不抛出异常,只记录错误
await bot._error_handler(mock_update, mock_context)
mock_telegram_client.send_message.assert_called_once()