Подготовка к релизу

This commit is contained in:
2025-10-30 18:28:26 +03:00
parent 69c0162c6b
commit 4981db90a0
191 changed files with 45557 additions and 3651 deletions
+499
View File
@@ -0,0 +1,499 @@
"""
Интеграционные тесты для полного потока работы бота.
Проверяет взаимодействие между всеми компонентами системы.
"""
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