blob: b53793fd1090aaee02e605ffc0b1a47bbc12718c [file] [log] [blame] [raw]
"""
Telegram Bot黑盒测试
端到端功能验证,不依赖内部实现细节
"""
import pytest
import tempfile
import os
import sys
from pathlib import Path
from unittest.mock import Mock, patch, AsyncMock
import asyncio
import json
# 添加src目录到Python路径
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
from claude_agent.telegram.bot import TelegramBot
class TestTelegramBotBlackBox:
"""Telegram Bot黑盒测试"""
@pytest.fixture
def temp_config_file(self):
"""创建临时配置文件"""
config_content = """
[telegram]
bot_token = "123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789"
allowed_users = [12345, 67890]
allowed_groups = [-100123, -200456]
[telegram.message]
stream_update_interval = 0.1
max_message_length = 4096
enable_markdown = true
context_history_limit = 20
[telegram.files]
supported_image_formats = ["jpg", "jpeg", "png", "gif"]
supported_document_formats = ["pdf", "txt", "md"]
max_file_size_mb = 10
temp_dir = "temp/blackbox_test"
[agent]
default_mode = "interactive"
max_conversation_history = 50
[logging]
level = "INFO"
colored = true
"""
with tempfile.NamedTemporaryFile(mode='w', suffix='.toml', delete=False) as f:
f.write(config_content)
return f.name
def test_bot_creation_with_valid_config(self, temp_config_file):
"""测试使用有效配置创建Bot"""
with patch('claude_agent.telegram.bot.get_config_manager') as mock_get_config:
# Mock配置管理器
mock_config_manager = Mock()
mock_config_manager.get_telegram_config.return_value = {
'bot_token': '123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789',
'allowed_users': [12345, 67890],
'allowed_groups': [-100123, -200456],
'message': {
'stream_update_interval': 0.1,
'context_history_limit': 20
},
'files': {
'temp_dir': '/tmp/blackbox_test',
'max_file_size_mb': 10
}
}
mock_get_config.return_value = mock_config_manager
# 创建Bot应该成功
bot = TelegramBot("test")
# 验证基本属性
assert bot.config_name == "test"
assert not bot.is_running
# 验证统计信息
stats = bot.get_stats()
assert stats['config_name'] == "test"
assert stats['allowed_users_count'] == 2
assert stats['allowed_groups_count'] == 2
assert stats['is_running'] is False
def test_bot_creation_with_invalid_token(self):
"""测试使用无效Token创建Bot"""
with patch('claude_agent.telegram.bot.get_config_manager') as mock_get_config:
mock_config_manager = Mock()
# 使用空字符串,这会触发验证错误
mock_config_manager.get_telegram_config.return_value = {
'bot_token': '', # 空字符串会失败
'allowed_users': [12345],
'allowed_groups': []
}
mock_get_config.return_value = mock_config_manager
# 应该抛出配置错误
with pytest.raises(ValueError, match="请配置有效的Telegram Bot Token"):
TelegramBot("test")
def test_bot_creation_missing_required_config(self):
"""测试缺少必需配置项"""
with patch('claude_agent.telegram.bot.get_config_manager') as mock_get_config:
mock_config_manager = Mock()
mock_config_manager.get_telegram_config.return_value = {
'bot_token': '123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789',
# 缺少 allowed_users 和 allowed_groups
}
mock_get_config.return_value = mock_config_manager
# 应该抛出配置错误
with pytest.raises(ValueError, match="缺少必需的配置项"):
TelegramBot("test")
@pytest.mark.asyncio
async def test_bot_initialization_components(self):
"""测试Bot组件初始化"""
with patch('claude_agent.telegram.bot.get_config_manager') as mock_get_config:
mock_config_manager = Mock()
mock_config_manager.get_telegram_config.return_value = {
'bot_token': '123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789',
'allowed_users': [12345],
'allowed_groups': [-67890],
'message': {'context_history_limit': 10},
'files': {'temp_dir': '/tmp/test', 'max_file_size_mb': 5}
}
mock_get_config.return_value = mock_config_manager
# Mock Application
with patch('claude_agent.telegram.bot.Application') as mock_app_class:
mock_builder = Mock()
mock_app = Mock()
mock_app.bot = Mock()
mock_app.initialize = AsyncMock()
mock_app.add_handler = Mock()
mock_app.add_error_handler = Mock()
mock_builder.token.return_value = mock_builder
mock_builder.build.return_value = mock_app
mock_app_class.builder.return_value = mock_builder
# 创建并初始化Bot
bot = TelegramBot("test")
await bot.initialize()
# 验证所有组件都被创建
assert bot.context_manager is not None
assert bot.file_handler is not None
assert bot.claude_agent is not None
assert bot.stream_sender is not None
assert bot.message_handler is not None
assert bot.telegram_client is not None
# 验证Application配置
mock_builder.token.assert_called_with('123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789')
def test_bot_stats_reporting(self):
"""测试Bot统计信息报告"""
with patch('claude_agent.telegram.bot.get_config_manager') as mock_get_config:
mock_config_manager = Mock()
mock_config_manager.get_telegram_config.return_value = {
'bot_token': '123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789',
'allowed_users': [111, 222, 333],
'allowed_groups': [-444, -555],
'message': {'context_history_limit': 10},
'files': {'temp_dir': '/tmp', 'max_file_size_mb': 5}
}
mock_get_config.return_value = mock_config_manager
bot = TelegramBot("production")
# Mock上下文管理器
mock_context_manager = Mock()
mock_context_manager.get_chat_count.return_value = 15
bot.set_context_manager(mock_context_manager)
stats = bot.get_stats()
# 验证统计信息
expected_stats = {
'is_running': False,
'config_name': 'production',
'allowed_users_count': 3,
'allowed_groups_count': 2,
'active_chats': 15
}
for key, value in expected_stats.items():
assert stats[key] == value
@pytest.mark.asyncio
async def test_admin_message_broadcasting(self):
"""测试管理员消息广播"""
with patch('claude_agent.telegram.bot.get_config_manager') as mock_get_config:
mock_config_manager = Mock()
mock_config_manager.get_telegram_config.return_value = {
'bot_token': '123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789',
'allowed_users': [111, 222, 333],
'allowed_groups': [-444],
'message': {'context_history_limit': 10},
'files': {'temp_dir': '/tmp', 'max_file_size_mb': 5}
}
mock_get_config.return_value = mock_config_manager
bot = TelegramBot("test")
# Mock Telegram客户端
mock_client = Mock()
mock_client.send_message = AsyncMock()
bot.telegram_client = mock_client
# 发送管理员消息
test_message = "System maintenance at 2AM"
await bot.send_admin_message(test_message)
# 验证向所有允许用户发送消息
assert mock_client.send_message.call_count == 3
calls = mock_client.send_message.call_args_list
# 验证发送给正确的用户
sent_to_users = {call[1]['chat_id'] for call in calls}
assert sent_to_users == {111, 222, 333}
# 验证消息格式
for call in calls:
message_text = call[1]['text']
assert "📢 管理员消息:" in message_text
assert test_message in message_text
def test_dependency_injection_functionality(self):
"""测试依赖注入功能"""
with patch('claude_agent.telegram.bot.get_config_manager') as mock_get_config:
mock_config_manager = Mock()
mock_config_manager.get_telegram_config.return_value = {
'bot_token': '123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789',
'allowed_users': [12345],
'allowed_groups': [-67890],
'message': {'context_history_limit': 10},
'files': {'temp_dir': '/tmp', 'max_file_size_mb': 5}
}
mock_get_config.return_value = mock_config_manager
bot = TelegramBot("test")
# 创建Mock组件
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
def test_configuration_validation_edge_cases(self):
"""测试配置验证边缘情况"""
with patch('claude_agent.telegram.bot.get_config_manager') as mock_get_config:
mock_config_manager = Mock()
# 测试空Token
mock_config_manager.get_telegram_config.return_value = {
'bot_token': '',
'allowed_users': [123],
'allowed_groups': []
}
mock_get_config.return_value = mock_config_manager
with pytest.raises(ValueError):
TelegramBot("test")
# 测试默认Token
mock_config_manager.get_telegram_config.return_value = {
'bot_token': 'YOUR_BOT_TOKEN_HERE',
'allowed_users': [123],
'allowed_groups': []
}
with pytest.raises(ValueError):
TelegramBot("test")
# 测试空的用户和群组列表(应该允许)
mock_config_manager.get_telegram_config.return_value = {
'bot_token': '123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789',
'allowed_users': [],
'allowed_groups': []
}
# 应该成功创建,但没有授权用户
bot = TelegramBot("test")
assert bot.telegram_config['allowed_users'] == []
assert bot.telegram_config['allowed_groups'] == []
@pytest.mark.asyncio
async def test_bot_error_handling(self):
"""测试Bot错误处理"""
with patch('claude_agent.telegram.bot.get_config_manager') as mock_get_config:
mock_config_manager = Mock()
mock_config_manager.get_telegram_config.return_value = {
'bot_token': '123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789',
'allowed_users': [12345],
'allowed_groups': [],
'message': {'context_history_limit': 10},
'files': {'temp_dir': '/tmp', 'max_file_size_mb': 5}
}
mock_get_config.return_value = mock_config_manager
bot = TelegramBot("test")
# 测试发送管理员消息时的错误处理
mock_client = Mock()
mock_client.send_message = AsyncMock(side_effect=Exception("Network error"))
bot.telegram_client = mock_client
# 应该不抛出异常,只记录日志
await bot.send_admin_message("Test message")
def test_config_driven_behavior(self):
"""测试配置驱动的行为"""
with patch('claude_agent.telegram.bot.get_config_manager') as mock_get_config:
mock_config_manager = Mock()
# 测试不同的配置值
configs = [
{
'bot_token': '111:AAA',
'allowed_users': [1, 2, 3, 4, 5],
'allowed_groups': [-1, -2],
'message': {'context_history_limit': 100},
'files': {'temp_dir': '/tmp/large', 'max_file_size_mb': 50}
},
{
'bot_token': '222:BBB',
'allowed_users': [100],
'allowed_groups': [-100, -200, -300],
'message': {'context_history_limit': 10},
'files': {'temp_dir': '/tmp/small', 'max_file_size_mb': 1}
}
]
for i, config in enumerate(configs):
mock_config_manager.get_telegram_config.return_value = config
mock_get_config.return_value = mock_config_manager
bot = TelegramBot(f"test_{i}")
stats = bot.get_stats()
# 验证配置正确反映在统计中
assert stats['allowed_users_count'] == len(config['allowed_users'])
assert stats['allowed_groups_count'] == len(config['allowed_groups'])
assert bot.telegram_config['bot_token'] == config['bot_token']
def test_bot_lifecycle_state_management(self):
"""测试Bot生命周期状态管理"""
with patch('claude_agent.telegram.bot.get_config_manager') as mock_get_config:
mock_config_manager = Mock()
mock_config_manager.get_telegram_config.return_value = {
'bot_token': '123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789',
'allowed_users': [12345],
'allowed_groups': [],
'message': {'context_history_limit': 10},
'files': {'temp_dir': '/tmp', 'max_file_size_mb': 5}
}
mock_get_config.return_value = mock_config_manager
bot = TelegramBot("test")
# 初始状态
assert not bot.is_running
stats = bot.get_stats()
assert stats['is_running'] is False
# 模拟状态变化(在实际启动中会改变)
bot.is_running = True
stats = bot.get_stats()
assert stats['is_running'] is True
def test_multiple_bot_instances_independence(self):
"""测试多个Bot实例的独立性"""
with patch('claude_agent.telegram.bot.get_config_manager') as mock_get_config:
mock_config_manager = Mock()
# 创建两个不同配置的Bot
config1 = {
'bot_token': '111:AAA',
'allowed_users': [111],
'allowed_groups': [-111],
'message': {'context_history_limit': 10},
'files': {'temp_dir': '/tmp/bot1', 'max_file_size_mb': 5}
}
config2 = {
'bot_token': '222:BBB',
'allowed_users': [222, 333],
'allowed_groups': [-222],
'message': {'context_history_limit': 20},
'files': {'temp_dir': '/tmp/bot2', 'max_file_size_mb': 10}
}
# 创建第一个Bot
mock_config_manager.get_telegram_config.return_value = config1
mock_get_config.return_value = mock_config_manager
bot1 = TelegramBot("bot1")
# 创建第二个Bot
mock_config_manager.get_telegram_config.return_value = config2
bot2 = TelegramBot("bot2")
# 验证两个Bot配置独立
stats1 = bot1.get_stats()
stats2 = bot2.get_stats()
assert stats1['config_name'] == "bot1"
assert stats2['config_name'] == "bot2"
assert stats1['allowed_users_count'] == 1
assert stats2['allowed_users_count'] == 2
assert bot1.telegram_config['bot_token'] == '111:AAA'
assert bot2.telegram_config['bot_token'] == '222:BBB'
def test_file_path_resolution(self):
"""测试文件路径处理"""
with patch('claude_agent.telegram.bot.get_config_manager') as mock_get_config:
mock_config_manager = Mock()
mock_config_manager.get_telegram_config.return_value = {
'bot_token': '123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789',
'allowed_users': [12345],
'allowed_groups': [],
'message': {'context_history_limit': 10},
'files': {
'temp_dir': 'relative/path/telegram', # 相对路径
'max_file_size_mb': 5
}
}
mock_get_config.return_value = mock_config_manager
bot = TelegramBot("test")
# 验证配置被保存(当前实现没有路径解析)
temp_dir = bot.telegram_config['files']['temp_dir']
assert temp_dir == 'relative/path/telegram' # 保持原始值
class TestTelegramBotIntegrationBlackBox:
"""Telegram Bot集成黑盒测试"""
def test_end_to_end_configuration_loading(self):
"""端到端配置加载测试"""
# 创建临时配置文件
config_content = """
[telegram]
bot_token = "987654321:ZYXWVUTSRQPONMLKJIHGFEDCBAxyz987654"
allowed_users = [111, 222]
allowed_groups = [-333]
[telegram.message]
stream_update_interval = 2.0
max_message_length = 2048
context_history_limit = 25
[telegram.files]
max_file_size_mb = 15
temp_dir = "test_temp"
[agent]
default_mode = "yolo"
[logging]
level = "DEBUG"
"""
with tempfile.NamedTemporaryFile(mode='w', suffix='.toml', delete=False) as f:
f.write(config_content)
temp_file = f.name
try:
# Mock配置管理器直接加载文件
if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib
with open(temp_file, 'rb') as f:
loaded_config = tomllib.load(f)
with patch('claude_agent.telegram.bot.get_config_manager') as mock_get_config:
mock_config_manager = Mock()
mock_config_manager.get_telegram_config.return_value = loaded_config['telegram']
mock_get_config.return_value = mock_config_manager
# 创建Bot并验证配置加载
bot = TelegramBot("integration_test")
# 验证配置正确加载
assert bot.telegram_config['bot_token'] == "987654321:ZYXWVUTSRQPONMLKJIHGFEDCBAxyz987654"
assert bot.telegram_config['allowed_users'] == [111, 222]
assert bot.telegram_config['allowed_groups'] == [-333]
assert bot.telegram_config['message']['stream_update_interval'] == 2.0
assert bot.telegram_config['message']['max_message_length'] == 2048
assert bot.telegram_config['files']['max_file_size_mb'] == 15
finally:
os.unlink(temp_file)
def test_complete_component_interaction(self):
"""完整组件交互测试"""
with patch('claude_agent.telegram.bot.get_config_manager') as mock_get_config:
mock_config_manager = Mock()
mock_config_manager.get_telegram_config.return_value = {
'bot_token': '123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789',
'allowed_users': [12345],
'allowed_groups': [-67890],
'message': {'context_history_limit': 10},
'files': {'temp_dir': '/tmp/integration', 'max_file_size_mb': 5}
}
mock_get_config.return_value = mock_config_manager
# 创建Bot
bot = TelegramBot("integration")
# 创建Mock组件并注入
mock_context_manager = Mock()
mock_file_handler = Mock()
mock_claude_agent = Mock()
mock_stream_sender = Mock()
# 配置Mock行为
mock_context_manager.get_chat_count.return_value = 5
mock_context_manager.get_context.return_value = []
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)
# 验证组件协同工作
stats = bot.get_stats()
assert stats['active_chats'] == 5
# 验证依赖关系正确建立
assert bot.context_manager == mock_context_manager
assert bot.file_handler == mock_file_handler
def test_production_readiness_checklist(self):
"""生产就绪性检查清单"""
with patch('claude_agent.telegram.bot.get_config_manager') as mock_get_config:
mock_config_manager = Mock()
mock_config_manager.get_telegram_config.return_value = {
'bot_token': '123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789',
'allowed_users': [12345],
'allowed_groups': [-67890],
'message': {
'stream_update_interval': 1.0,
'max_message_length': 4096,
'context_history_limit': 50
},
'files': {
'temp_dir': '/var/tmp/telegram_bot',
'max_file_size_mb': 20,
'supported_image_formats': ['jpg', 'png', 'gif'],
'supported_document_formats': ['pdf', 'txt', 'md']
}
}
mock_get_config.return_value = mock_config_manager
bot = TelegramBot("production")
# 生产就绪性检查
checks = {
'valid_token_format': bot.telegram_config['bot_token'].count(':') == 1,
'has_authorized_users': len(bot.telegram_config['allowed_users']) > 0,
'reasonable_file_size_limit': bot.telegram_config['files']['max_file_size_mb'] <= 50,
'reasonable_context_limit': bot.telegram_config['message']['context_history_limit'] <= 100,
'valid_update_interval': 0.1 <= bot.telegram_config['message']['stream_update_interval'] <= 5.0
}
# 所有检查都应该通过
for check_name, result in checks.items():
assert result, f"Production readiness check failed: {check_name}"
# 验证统计信息完整
stats = bot.get_stats()
required_stats = ['is_running', 'config_name', 'allowed_users_count', 'allowed_groups_count']
for stat in required_stats:
assert stat in stats, f"Missing required stat: {stat}"