Files

499 lines
20 KiB
Python

"""
Интеграционные тесты для полного потока работы бота.
Проверяет взаимодействие между всеми компонентами системы.
"""
import pytest
import tempfile
import os
from unittest.mock import Mock, AsyncMock, patch
from datetime import datetime
# Добавляем корневую директорию в путь
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from core.application import Application
from core.config import Config
from database.models import User, Score, Error
from services.user_service import UserService
from handlers.user_handlers import UserHandlers
from utils.validators import InputValidator
class TestFullFlowIntegration:
"""Интеграционные тесты полного потока"""
@pytest.fixture
def temp_config(self):
"""Временный файл конфигурации для интеграционных тестов"""
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
f.write("""
BOT_TOKEN = "123456789:integration_test_token"
ADMIN_IDS = [123456789, 987654321]
OPENWEATHER_API_KEY = "test_key"
NEWS_API_KEY = "test_key"
OPENAI_API_KEY = "test_key"
""")
config_path = f.name
yield config_path
# Очистка
if os.path.exists(config_path):
os.unlink(config_path)
@pytest.fixture
def temp_db(self):
"""Временная база данных для тестов"""
with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
db_path = f.name
yield db_path
# Очистка
if os.path.exists(db_path):
os.unlink(db_path)
def test_config_initialization(self, temp_config):
"""Тест инициализации конфигурации"""
config = Config(temp_config)
assert config.bot_config.token == "123456789:integration_test_token"
assert 123456789 in config.bot_config.admin_ids
assert 987654321 in config.bot_config.admin_ids
assert config.api_keys.openweather == "test_key"
@pytest.mark.asyncio
async def test_user_service_integration(self, temp_db):
"""Тест интеграции сервиса пользователей с базой данных"""
from database.repository import UserRepository, ScoreRepository
# Создаем репозитории
user_repo = UserRepository(temp_db)
score_repo = ScoreRepository(temp_db)
# Создаем сервис
user_service = UserService(user_repo, score_repo)
# Тестируем создание пользователя
profile = await user_service.get_or_create_user(
123456789, "test_user", "Test", "User"
)
assert profile.user_id == 123456789
assert profile.username == "test_user"
assert profile.first_name == "Test"
assert profile.rank == "Новичок"
# Тестируем получение существующего пользователя
profile2 = await user_service.get_or_create_user(
123456789, "test_user", "Test", "User"
)
assert profile2.user_id == 123456789
assert profile2.username == "test_user"
# Закрываем соединения
user_repo.close()
score_repo.close()
@pytest.mark.asyncio
async def test_donation_full_flow_integration(self, temp_db):
"""Интеграционный тест полного цикла донатов - создание пользователя, донат, очки, достижения"""
from database.repository import UserRepository, ScoreRepository
from services.user_service import UserService
# Создаем репозитории
user_repo = UserRepository(temp_db)
score_repo = ScoreRepository(temp_db)
# Инициализируем достижения
user_repo.initialize_achievements()
# Создаем сервис
user_service = UserService(user_repo, score_repo)
# Шаг 1: Создание пользователя
user_id = 987654321
# Создаем пользователя напрямую через репозиторий
user_data = {
'telegram_id': user_id,
'username': "donator_user",
'first_name': "Donator",
'last_name': "Test",
'joined_date': datetime.now(),
'last_activity': datetime.now()
}
created_user = user_repo.create_user(user_data)
profile = user_service._map_to_profile(created_user)
assert profile.user_id == 1 # Внутренний ID в БД
assert profile.username == "donator_user"
assert profile.first_name == "Donator"
assert profile.rank == "Новичок"
assert profile.reputation == 0
# Проверяем начальные очки
initial_score = score_repo.get_total_score(user_id)
assert initial_score == 0
# Шаг 2: Добавление доната (500 рублей = 5 очков)
donation_amount = 500.0
success = await user_service.add_donation(user_id, donation_amount)
assert success is True
# Шаг 3: Проверка начисления очков
updated_score = score_repo.get_total_score(user_id)
expected_points = int(donation_amount // 100) # 5 очков
assert updated_score == expected_points
# Шаг 4: Проверка обновления профиля пользователя
# Получаем напрямую из репозитория
user_data_direct = user_repo.get_by_id(user_id)
updated_profile = user_service._map_to_profile(user_data_direct)
assert updated_profile.reputation == expected_points
# Проверяем ранг (должен остаться Новичок при 5 очках)
assert updated_profile.rank == "Новичок"
# Шаг 5: Проверка достижений
# Получаем достижения через репозиторий
achievement_badges = user_repo.get_user_achievements(user_id)
achievement_names = []
for badge, unlocked_at in achievement_badges:
# Находим имя достижения по badge
achievements = user_repo.get_all_achievements()
for achievement in achievements:
if achievement['badge'] == badge:
achievement_names.append(achievement['name'])
break
assert "Первый донат" in achievement_names
# Шаг 6: Второй донат для проверки накопления
second_donation = 300.0
success2 = await user_service.add_donation(user_id, second_donation)
assert success2 is True
# Проверяем итоговые очки (5 + 3 = 8)
final_score = score_repo.get_total_score(user_id)
assert final_score == 8
# Проверяем общую сумму донатов
total_donations = user_repo.get_total_donations(user_id, 2025)
assert total_donations == 800.0 # 500 + 300
# Шаг 7: Добавление большого доната для достижения "Меценат"
big_donation = 1000.0
success3 = await user_service.add_donation(user_id, big_donation)
assert success3 is True
# Проверяем итоговые очки (8 + 10 = 18)
final_score_after_big = score_repo.get_total_score(user_id)
assert final_score_after_big == 18
# Проверяем достижения - должно появиться "Меценат"
final_achievement_badges = user_repo.get_user_achievements(user_id)
final_achievement_names = []
for badge, unlocked_at in final_achievement_badges:
achievements = user_repo.get_all_achievements()
for achievement in achievements:
if achievement['badge'] == badge:
final_achievement_names.append(achievement['name'])
break
assert "Первый донат" in final_achievement_names
assert "Меценат" in final_achievement_names
# Закрываем соединения
user_repo.close()
score_repo.close()
def test_validator_integration(self):
"""Тест интеграции валидаторов с другими компонентами"""
# Тест цепочки валидации
assert InputValidator.validate_user_id("123456789") is True
assert InputValidator.validate_username("test_user") is True
assert InputValidator.validate_email("test@example.com") is True
# Тест комбинированной валидации
user_data = {
'id': "123456789",
'username': "test_user",
'email': "test@example.com"
}
# Валидация всех полей
assert InputValidator.validate_user_id(user_data['id']) is True
assert InputValidator.validate_username(user_data['username']) is True
assert InputValidator.validate_email(user_data['email']) is True
@pytest.mark.asyncio
async def test_handler_service_integration(self, temp_config, temp_db, mock_update, mock_context):
"""Тест интеграции обработчиков с сервисами"""
from database.repository import UserRepository, ScoreRepository
# Создаем компоненты
config = Config(temp_config)
user_repo = UserRepository(temp_db)
score_repo = ScoreRepository(temp_db)
user_service = UserService(user_repo, score_repo)
# Создаем обработчик
handler = UserHandlers(config, config, user_service)
# Мокируем успешное создание пользователя
profile = UserProfile(
user_id=123456789,
username="test_user",
first_name="Test",
reputation=100,
rank="Активист"
)
user_service.get_or_create_user = AsyncMock(return_value=profile)
# Мокируем ответ бота
mock_update.message.reply_text = AsyncMock()
# Тестируем обработку команды /start
await handler.handle_start(mock_update, mock_context)
# Проверяем, что пользователь был получен/создан
user_service.get_or_create_user.assert_called_once()
# Проверяем, что ответ был отправлен
mock_update.message.reply_text.assert_called_once()
# Закрываем соединения
user_repo.close()
score_repo.close()
def test_exception_handling_integration(self):
"""Тест интеграции системы исключений"""
from core.exceptions import ValidationError, DatabaseError, PermissionError
# Тест цепочки обработки ошибок
try:
raise ValidationError("Ошибка валидации", "email")
except Exception as e:
assert isinstance(e, ValidationError)
assert e.error_code == "VALIDATION_ERROR"
assert e.field == "email"
# Тест наследования
assert issubclass(ValidationError, Exception)
assert issubclass(DatabaseError, Exception)
assert issubclass(PermissionError, Exception)
class TestComponentInteraction:
"""Тесты взаимодействия компонентов"""
def test_config_and_validators_integration(self, temp_config):
"""Тест интеграции конфигурации с валидаторами"""
config = Config(temp_config)
# Используем данные из конфигурации для валидации
admin_ids = config.bot_config.admin_ids
for admin_id in admin_ids:
assert InputValidator.validate_user_id(str(admin_id)) is True
def test_formatters_and_models_integration(self):
"""Тест интеграции форматтеров с моделями данных"""
from utils.formatters import MessageFormatter
# Создаем тестовые данные модели
user_data = {
'id': 123456789,
'name': 'Test User',
'username': 'test_user',
'reputation': 150,
'rank': 'Активист',
'message_count': 45,
'joined_date': datetime.now().strftime('%Y-%m-%d')
}
# Тестируем форматирование
formatted = MessageFormatter.format_user_info(user_data)
assert "👤 <b>Информация о пользователе:</b>" in formatted
assert "🆔 ID: 123456789" in formatted
assert "🏆 Репутация: 150" in formatted
def test_helpers_and_validators_integration(self):
"""Тест интеграции хелперов с валидаторами"""
from utils.helpers import generate_user_mention, clean_string
# Тестируем комбинированное использование
user_id = 123456789
name = "Test User" # С лишними пробелами
# Очищаем имя и генерируем упоминание
clean_name = clean_string(name)
mention = generate_user_mention(user_id, clean_name)
assert mention == "[Test User](tg://user?id=123456789)"
# Проверяем, что ID валиден
assert InputValidator.validate_user_id(str(user_id)) is True
class TestErrorHandlingFlow:
"""Тесты потока обработки ошибок"""
@pytest.mark.asyncio
async def test_error_handling_in_handlers(self, temp_config, temp_db, mock_update, mock_context):
"""Тест обработки ошибок в обработчиках"""
from database.repository import UserRepository, ScoreRepository
from core.exceptions import ValidationError
# Создаем компоненты
config = Config(temp_config)
user_repo = UserRepository(temp_db)
score_repo = ScoreRepository(temp_db)
user_service = UserService(user_repo, score_repo)
handler = UserHandlers(config, config, user_service)
# Мокируем ошибку в сервисе
async def failing_service(*args, **kwargs):
raise ValidationError("Ошибка валидации данных пользователя")
user_service.get_or_create_user = failing_service
mock_update.message.reply_text = AsyncMock()
# Тестируем обработку команды с ошибкой
await handler.handle_start(mock_update, mock_context)
# Проверяем, что сообщение об ошибке было отправлено
mock_update.message.reply_text.assert_called_once()
error_message = mock_update.message.reply_text.call_args[0][0]
assert "⚠️ Ошибка валидации данных пользователя" in error_message
# Закрываем соединения
user_repo.close()
score_repo.close()
def test_error_propagation_through_layers(self):
"""Тест распространения ошибок через слои архитектуры"""
from core.exceptions import DatabaseError, ValidationError
# Тест, что ошибки правильно распространяются
try:
raise ValidationError("Ошибка валидации")
except ValidationError as e:
# Ошибка должна сохранять свою информацию
assert e.error_code == "VALIDATION_ERROR"
assert "Ошибка валидации" in str(e)
# Тест преобразования ошибок
original_error = ValueError("Базовая ошибка")
db_error = DatabaseError("Ошибка базы данных", original_error)
assert db_error.original_error == original_error
assert db_error.error_code == "DATABASE_ERROR"
class TestPerformanceIntegration:
"""Тесты производительности интеграции"""
def test_config_loading_performance(self, temp_config):
"""Тест производительности загрузки конфигурации"""
import time
start_time = time.time()
# Загружаем конфигурацию несколько раз
for _ in range(100):
config = Config(temp_config)
end_time = time.time()
total_time = end_time - start_time
# Должно быть быстро (менее 1 секунды для 100 загрузок)
assert total_time < 1.0
def test_validator_performance(self):
"""Тест производительности валидаторов"""
import time
test_data = [
("123456789", "user123", "test@example.com"),
("987654321", "test_user", "admin@test.com"),
("555666777", "another_user", "user@domain.org")
] * 50 # Увеличиваем объем для тестирования
start_time = time.time()
for user_id, username, email in test_data:
InputValidator.validate_user_id(user_id)
InputValidator.validate_username(username)
InputValidator.validate_email(email)
end_time = time.time()
total_time = end_time - start_time
# Должно быть быстро даже для большого количества данных
assert total_time < 0.5
class TestDataConsistencyIntegration:
"""Тесты согласованности данных"""
@pytest.mark.asyncio
async def test_user_data_consistency(self, temp_db):
"""Тест согласованности данных пользователя"""
from database.repository import UserRepository, ScoreRepository
user_repo = UserRepository(temp_db)
score_repo = ScoreRepository(temp_db)
user_service = UserService(user_repo, score_repo)
# Создаем пользователя
profile1 = await user_service.get_or_create_user(
123456789, "test_user", "Test", "User"
)
# Получаем пользователя снова
profile2 = await user_service.get_or_create_user(
123456789, "test_user", "Test", "User"
)
# Данные должны быть согласованными
assert profile1.user_id == profile2.user_id
assert profile1.username == profile2.username
assert profile1.reputation == profile2.reputation
user_repo.close()
score_repo.close()
def test_formatter_data_consistency(self):
"""Тест согласованности данных в форматтерах"""
from utils.formatters import MessageFormatter
# Тестовые данные пользователя
user_data = {
'id': 123456789,
'name': 'Test User',
'username': 'test_user',
'reputation': 150,
'rank': 'Активист',
'message_count': 45,
'joined_date': '2024-01-15'
}
# Форматируем данные
formatted = MessageFormatter.format_user_info(user_data)
# Проверяем, что все поля присутствуют в форматированном выводе
assert str(user_data['id']) in formatted
assert user_data['name'] in formatted
assert user_data['username'] in formatted
assert str(user_data['reputation']) in formatted
assert user_data['rank'] in formatted
assert str(user_data['message_count']) in formatted