Dev 6 #9
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,6 +4,9 @@
|
|||||||
/database/test.db
|
/database/test.db
|
||||||
/database/test.db-shm
|
/database/test.db-shm
|
||||||
/database/test.db-wal
|
/database/test.db-wal
|
||||||
|
/database/test_auto_unban.db
|
||||||
|
/database/test_auto_unban.db-shm
|
||||||
|
/database/test_auto_unban.db-wal
|
||||||
/settings.ini
|
/settings.ini
|
||||||
/myenv/
|
/myenv/
|
||||||
/venv/
|
/venv/
|
||||||
|
|||||||
8
Makefile
8
Makefile
@@ -54,6 +54,14 @@ test-keyboards:
|
|||||||
test-monitor:
|
test-monitor:
|
||||||
python3 tests/test_monitor.py
|
python3 tests/test_monitor.py
|
||||||
|
|
||||||
|
# Test auto unban scheduler
|
||||||
|
test-auto-unban:
|
||||||
|
python3 -m pytest tests/test_auto_unban_scheduler.py -v
|
||||||
|
|
||||||
|
# Test auto unban integration
|
||||||
|
test-auto-unban-integration:
|
||||||
|
python3 -m pytest tests/test_auto_unban_integration.py -v
|
||||||
|
|
||||||
# Run tests with coverage
|
# Run tests with coverage
|
||||||
test-coverage:
|
test-coverage:
|
||||||
python3 -m pytest tests/ --cov=helper_bot --cov=database --cov-report=term
|
python3 -m pytest tests/ --cov=helper_bot --cov=database --cov-report=term
|
||||||
|
|||||||
@@ -485,31 +485,6 @@ class BotDB:
|
|||||||
finally:
|
finally:
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def get_users_blacklist(self):
|
|
||||||
"""
|
|
||||||
Возвращает список пользователей в черном списке.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Словарь, где ключ - user_id, значение - username.
|
|
||||||
{}: Если в черном списке нет пользователей.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
sqlite3. Error: Если произошла ошибка при выполнении запроса.
|
|
||||||
"""
|
|
||||||
self.logger.info(f"Запуск функции get_users_blacklist")
|
|
||||||
try:
|
|
||||||
self.connect()
|
|
||||||
self.cursor.execute("SELECT user_id, user_name FROM blacklist")
|
|
||||||
fetch_all = self.cursor.fetchall()
|
|
||||||
list_of_users = {user_id: username for user_id, username in fetch_all}
|
|
||||||
self.logger.info(f"Получен список пользователей в черном списке")
|
|
||||||
return list_of_users
|
|
||||||
except sqlite3.Error as error:
|
|
||||||
self.logger.error(f"Ошибка при получении списка пользователей в черном списке: {error}")
|
|
||||||
raise
|
|
||||||
finally:
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
def get_users_for_unblock_today(self, date_to_unban: str):
|
def get_users_for_unblock_today(self, date_to_unban: str):
|
||||||
"""
|
"""
|
||||||
Возвращает список пользователей, у которых истекает срок блокировки сегодня.
|
Возвращает список пользователей, у которых истекает срок блокировки сегодня.
|
||||||
|
|||||||
175
helper_bot/utils/auto_unban_scheduler.py
Normal file
175
helper_bot/utils/auto_unban_scheduler.py
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import asyncio
|
||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||||
|
from apscheduler.triggers.cron import CronTrigger
|
||||||
|
|
||||||
|
from helper_bot.utils.base_dependency_factory import get_global_instance
|
||||||
|
from logs.custom_logger import logger
|
||||||
|
|
||||||
|
|
||||||
|
class AutoUnbanScheduler:
|
||||||
|
"""
|
||||||
|
Класс для автоматического разбана пользователей по истечении срока блокировки.
|
||||||
|
Запускается ежедневно в 5:00 по московскому времени.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.bdf = get_global_instance()
|
||||||
|
self.bot_db = self.bdf.get_db()
|
||||||
|
self.scheduler = AsyncIOScheduler()
|
||||||
|
self.bot = None # Будет установлен позже
|
||||||
|
|
||||||
|
def set_bot(self, bot):
|
||||||
|
"""Устанавливает экземпляр бота для отправки уведомлений"""
|
||||||
|
self.bot = bot
|
||||||
|
|
||||||
|
async def auto_unban_users(self):
|
||||||
|
"""
|
||||||
|
Основная функция автоматического разбана пользователей.
|
||||||
|
Получает список пользователей, у которых истекает срок блокировки сегодня,
|
||||||
|
и удаляет их из черного списка.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.info("Запуск автоматического разбана пользователей")
|
||||||
|
|
||||||
|
# Получаем сегодняшнюю дату в формате YYYY-MM-DD
|
||||||
|
moscow_tz = timezone(timedelta(hours=3)) # UTC+3 для Москвы
|
||||||
|
today = datetime.now(moscow_tz).strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
logger.info(f"Поиск пользователей для разблокировки на дату: {today}")
|
||||||
|
|
||||||
|
# Получаем список пользователей для разблокировки
|
||||||
|
users_to_unban = self.bot_db.get_users_for_unblock_today(today)
|
||||||
|
|
||||||
|
if not users_to_unban:
|
||||||
|
logger.info("Нет пользователей для разблокировки сегодня")
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(f"Найдено {len(users_to_unban)} пользователей для разблокировки")
|
||||||
|
|
||||||
|
# Список для отслеживания результатов
|
||||||
|
success_count = 0
|
||||||
|
failed_count = 0
|
||||||
|
failed_users = []
|
||||||
|
|
||||||
|
# Разблокируем каждого пользователя
|
||||||
|
for user_id, username in users_to_unban.items():
|
||||||
|
try:
|
||||||
|
result = self.bot_db.delete_user_blacklist(user_id)
|
||||||
|
if result:
|
||||||
|
success_count += 1
|
||||||
|
logger.info(f"Пользователь {user_id} ({username}) успешно разблокирован")
|
||||||
|
else:
|
||||||
|
failed_count += 1
|
||||||
|
failed_users.append(f"{user_id} ({username})")
|
||||||
|
logger.error(f"Ошибка при разблокировке пользователя {user_id} ({username})")
|
||||||
|
except Exception as e:
|
||||||
|
failed_count += 1
|
||||||
|
failed_users.append(f"{user_id} ({username})")
|
||||||
|
logger.error(f"Исключение при разблокировке пользователя {user_id} ({username}): {e}")
|
||||||
|
|
||||||
|
# Формируем отчет
|
||||||
|
report = self._generate_report(success_count, failed_count, failed_users, users_to_unban)
|
||||||
|
|
||||||
|
# Отправляем отчет в лог-канал
|
||||||
|
await self._send_report(report)
|
||||||
|
|
||||||
|
logger.info(f"Автоматический разбан завершен. Успешно: {success_count}, Ошибок: {failed_count}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Критическая ошибка в автоматическом разбане: {e}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
await self._send_error_report(error_msg)
|
||||||
|
|
||||||
|
def _generate_report(self, success_count: int, failed_count: int,
|
||||||
|
failed_users: list, all_users: dict) -> str:
|
||||||
|
"""Генерирует отчет о результатах автоматического разбана"""
|
||||||
|
report = f"🤖 <b>Отчет об автоматическом разбане</b>\n\n"
|
||||||
|
report += f"📅 Дата: {datetime.now().strftime('%d.%m.%Y %H:%M')}\n"
|
||||||
|
report += f"✅ Успешно разблокировано: {success_count}\n"
|
||||||
|
report += f"❌ Ошибок: {failed_count}\n\n"
|
||||||
|
|
||||||
|
if success_count > 0:
|
||||||
|
report += "✅ <b>Разблокированные пользователи:</b>\n"
|
||||||
|
for user_id, username in all_users.items():
|
||||||
|
if f"{user_id} ({username})" not in failed_users:
|
||||||
|
safe_username = username if username else "Неизвестный пользователь"
|
||||||
|
report += f"• ID: {user_id}, Имя: {safe_username}\n"
|
||||||
|
report += "\n"
|
||||||
|
|
||||||
|
if failed_users:
|
||||||
|
report += "❌ <b>Ошибки при разблокировке:</b>\n"
|
||||||
|
for user in failed_users:
|
||||||
|
report += f"• {user}\n"
|
||||||
|
|
||||||
|
return report
|
||||||
|
|
||||||
|
async def _send_report(self, report: str):
|
||||||
|
"""Отправляет отчет в лог-канал"""
|
||||||
|
try:
|
||||||
|
if self.bot:
|
||||||
|
group_for_logs = self.bdf.settings['Telegram']['group_for_logs']
|
||||||
|
await self.bot.send_message(
|
||||||
|
chat_id=group_for_logs,
|
||||||
|
text=report,
|
||||||
|
parse_mode='HTML'
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при отправке отчета: {e}")
|
||||||
|
|
||||||
|
async def _send_error_report(self, error_msg: str):
|
||||||
|
"""Отправляет отчет об ошибке в важный лог-канал"""
|
||||||
|
try:
|
||||||
|
if self.bot:
|
||||||
|
important_logs = self.bdf.settings['Telegram']['important_logs']
|
||||||
|
await self.bot.send_message(
|
||||||
|
chat_id=important_logs,
|
||||||
|
text=f"🚨 <b>Ошибка автоматического разбана</b>\n\n{error_msg}",
|
||||||
|
parse_mode='HTML'
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при отправке отчета об ошибке: {e}")
|
||||||
|
|
||||||
|
def start_scheduler(self):
|
||||||
|
"""Запускает планировщик задач"""
|
||||||
|
try:
|
||||||
|
# Добавляем задачу на ежедневное выполнение в 5:00 по Москве
|
||||||
|
self.scheduler.add_job(
|
||||||
|
self.auto_unban_users,
|
||||||
|
CronTrigger(hour=5, minute=0, timezone='Europe/Moscow'),
|
||||||
|
id='auto_unban_users',
|
||||||
|
name='Автоматический разбан пользователей',
|
||||||
|
replace_existing=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Запускаем планировщик
|
||||||
|
self.scheduler.start()
|
||||||
|
logger.info("Планировщик автоматического разбана запущен. Задача запланирована на 5:00 по Москве")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при запуске планировщика: {e}")
|
||||||
|
|
||||||
|
def stop_scheduler(self):
|
||||||
|
"""Останавливает планировщик задач"""
|
||||||
|
try:
|
||||||
|
if self.scheduler.running:
|
||||||
|
self.scheduler.shutdown()
|
||||||
|
logger.info("Планировщик автоматического разбана остановлен")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при остановке планировщика: {e}")
|
||||||
|
|
||||||
|
async def run_manual_unban(self):
|
||||||
|
"""Запускает разбан вручную (для тестирования)"""
|
||||||
|
logger.info("Запуск ручного разбана пользователей")
|
||||||
|
await self.auto_unban_users()
|
||||||
|
|
||||||
|
|
||||||
|
# Глобальный экземпляр планировщика
|
||||||
|
auto_unban_scheduler = AutoUnbanScheduler()
|
||||||
|
|
||||||
|
|
||||||
|
def get_auto_unban_scheduler() -> AutoUnbanScheduler:
|
||||||
|
"""Возвращает глобальный экземпляр планировщика"""
|
||||||
|
return auto_unban_scheduler
|
||||||
@@ -7,6 +7,9 @@ loguru==0.7.2
|
|||||||
# System monitoring
|
# System monitoring
|
||||||
psutil~=6.1.0
|
psutil~=6.1.0
|
||||||
|
|
||||||
|
# Scheduling
|
||||||
|
apscheduler~=3.10.4
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
pytest==8.2.2
|
pytest==8.2.2
|
||||||
pytest-asyncio==1.1.0
|
pytest-asyncio==1.1.0
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ if CURRENT_DIR not in sys.path:
|
|||||||
from helper_bot.main import start_bot
|
from helper_bot.main import start_bot
|
||||||
from helper_bot.utils.base_dependency_factory import get_global_instance
|
from helper_bot.utils.base_dependency_factory import get_global_instance
|
||||||
from helper_bot.server_monitor import ServerMonitor
|
from helper_bot.server_monitor import ServerMonitor
|
||||||
|
from helper_bot.utils.auto_unban_scheduler import get_auto_unban_scheduler
|
||||||
|
|
||||||
|
|
||||||
async def start_monitoring(bdf, bot):
|
async def start_monitoring(bdf, bot):
|
||||||
@@ -40,6 +41,11 @@ async def main():
|
|||||||
# Создаем экземпляр монитора
|
# Создаем экземпляр монитора
|
||||||
monitor = await start_monitoring(bdf, monitor_bot)
|
monitor = await start_monitoring(bdf, monitor_bot)
|
||||||
|
|
||||||
|
# Инициализируем планировщик автоматического разбана
|
||||||
|
auto_unban_scheduler = get_auto_unban_scheduler()
|
||||||
|
auto_unban_scheduler.set_bot(monitor_bot)
|
||||||
|
auto_unban_scheduler.start_scheduler()
|
||||||
|
|
||||||
# Флаг для корректного завершения
|
# Флаг для корректного завершения
|
||||||
shutdown_event = asyncio.Event()
|
shutdown_event = asyncio.Event()
|
||||||
|
|
||||||
@@ -71,6 +77,9 @@ async def main():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Ошибка при отправке сообщения об отключении: {e}")
|
print(f"Ошибка при отправке сообщения об отключении: {e}")
|
||||||
|
|
||||||
|
print("Останавливаем планировщик автоматического разбана...")
|
||||||
|
auto_unban_scheduler.stop_scheduler()
|
||||||
|
|
||||||
print("Останавливаем задачи...")
|
print("Останавливаем задачи...")
|
||||||
# Отменяем задачи
|
# Отменяем задачи
|
||||||
bot_task.cancel()
|
bot_task.cancel()
|
||||||
|
|||||||
251
tests/test_auto_unban_integration.py
Normal file
251
tests/test_auto_unban_integration.py
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
import pytest
|
||||||
|
import sqlite3
|
||||||
|
import os
|
||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
|
from unittest.mock import Mock, patch, AsyncMock
|
||||||
|
|
||||||
|
from helper_bot.utils.auto_unban_scheduler import AutoUnbanScheduler
|
||||||
|
|
||||||
|
|
||||||
|
class TestAutoUnbanIntegration:
|
||||||
|
"""Интеграционные тесты для автоматического разбана"""
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def test_db_path(self):
|
||||||
|
"""Путь к тестовой базе данных"""
|
||||||
|
return 'database/test_auto_unban.db'
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def setup_test_db(self, test_db_path):
|
||||||
|
"""Создает тестовую базу данных с таблицей blacklist"""
|
||||||
|
# Удаляем старую тестовую базу если она существует
|
||||||
|
if os.path.exists(test_db_path):
|
||||||
|
os.remove(test_db_path)
|
||||||
|
|
||||||
|
# Создаем новую базу данных
|
||||||
|
conn = sqlite3.connect(test_db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Создаем таблицу blacklist
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS blacklist (
|
||||||
|
user_id INTEGER PRIMARY KEY,
|
||||||
|
user_name TEXT,
|
||||||
|
message_for_user TEXT,
|
||||||
|
date_to_unban TEXT
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
# Добавляем тестовые данные
|
||||||
|
today = datetime.now(timezone(timedelta(hours=3))).strftime("%Y-%m-%d")
|
||||||
|
tomorrow = (datetime.now(timezone(timedelta(hours=3))) + timedelta(days=1)).strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
test_data = [
|
||||||
|
(123, "test_user1", "Test ban 1", today), # Разблокируется сегодня
|
||||||
|
(456, "test_user2", "Test ban 2", today), # Разблокируется сегодня
|
||||||
|
(789, "test_user3", "Test ban 3", tomorrow), # Разблокируется завтра
|
||||||
|
(999, "test_user4", "Test ban 4", None), # Навсегда заблокирован
|
||||||
|
]
|
||||||
|
|
||||||
|
cursor.executemany(
|
||||||
|
"INSERT INTO blacklist (user_id, user_name, message_for_user, date_to_unban) VALUES (?, ?, ?, ?)",
|
||||||
|
test_data
|
||||||
|
)
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
yield test_db_path
|
||||||
|
|
||||||
|
# Очистка после тестов
|
||||||
|
if os.path.exists(test_db_path):
|
||||||
|
os.remove(test_db_path)
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_bdf(self, test_db_path):
|
||||||
|
"""Создает мок фабрики зависимостей с тестовой базой"""
|
||||||
|
mock_factory = Mock()
|
||||||
|
mock_factory.settings = {
|
||||||
|
'Telegram': {
|
||||||
|
'group_for_logs': '-1001234567890',
|
||||||
|
'important_logs': '-1001234567891'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Создаем реальный экземпляр базы данных с тестовым файлом
|
||||||
|
from database.db import BotDB
|
||||||
|
import os
|
||||||
|
current_dir = os.getcwd()
|
||||||
|
mock_factory.database = BotDB(current_dir, test_db_path)
|
||||||
|
|
||||||
|
return mock_factory
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_bot(self):
|
||||||
|
"""Создает мок бота"""
|
||||||
|
mock_bot = Mock()
|
||||||
|
mock_bot.send_message = AsyncMock()
|
||||||
|
return mock_bot
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@patch('helper_bot.utils.auto_unban_scheduler.get_global_instance')
|
||||||
|
async def test_auto_unban_with_real_db(self, mock_get_instance, setup_test_db, mock_bdf, mock_bot):
|
||||||
|
"""Тест автоматического разбана с реальной базой данных"""
|
||||||
|
# Настройка моков
|
||||||
|
mock_get_instance.return_value = mock_bdf
|
||||||
|
|
||||||
|
# Создаем планировщик
|
||||||
|
scheduler = AutoUnbanScheduler()
|
||||||
|
scheduler.bot_db = mock_bdf.database
|
||||||
|
scheduler.set_bot(mock_bot)
|
||||||
|
|
||||||
|
# Проверяем начальное состояние базы
|
||||||
|
conn = sqlite3.connect(setup_test_db)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM blacklist")
|
||||||
|
initial_count = cursor.fetchone()[0]
|
||||||
|
assert initial_count == 4
|
||||||
|
|
||||||
|
# Выполняем автоматический разбан
|
||||||
|
await scheduler.auto_unban_users()
|
||||||
|
|
||||||
|
# Проверяем, что пользователи с сегодняшней датой разблокированы
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM blacklist WHERE date_to_unban = ?",
|
||||||
|
(datetime.now(timezone(timedelta(hours=3))).strftime("%Y-%m-%d"),))
|
||||||
|
today_count = cursor.fetchone()[0]
|
||||||
|
assert today_count == 0
|
||||||
|
|
||||||
|
# Проверяем, что пользователи с завтрашней датой остались
|
||||||
|
tomorrow = (datetime.now(timezone(timedelta(hours=3))) + timedelta(days=1)).strftime("%Y-%m-%d")
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM blacklist WHERE date_to_unban = ?", (tomorrow,))
|
||||||
|
tomorrow_count = cursor.fetchone()[0]
|
||||||
|
assert tomorrow_count == 1
|
||||||
|
|
||||||
|
# Проверяем, что навсегда заблокированные пользователи остались
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM blacklist WHERE date_to_unban IS NULL")
|
||||||
|
permanent_count = cursor.fetchone()[0]
|
||||||
|
assert permanent_count == 1
|
||||||
|
|
||||||
|
# Проверяем общее количество записей
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM blacklist")
|
||||||
|
final_count = cursor.fetchone()[0]
|
||||||
|
assert final_count == 2 # Остались только завтрашние и навсегда заблокированные
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# Проверяем, что отчет был отправлен
|
||||||
|
mock_bot.send_message.assert_called_once()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@patch('helper_bot.utils.auto_unban_scheduler.get_global_instance')
|
||||||
|
async def test_auto_unban_no_users_today(self, mock_get_instance, setup_test_db, mock_bdf, mock_bot):
|
||||||
|
"""Тест разбана когда нет пользователей для разблокировки сегодня"""
|
||||||
|
# Настройка моков
|
||||||
|
mock_get_instance.return_value = mock_bdf
|
||||||
|
|
||||||
|
# Удаляем пользователей с сегодняшней датой
|
||||||
|
conn = sqlite3.connect(setup_test_db)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
today = datetime.now(timezone(timedelta(hours=3))).strftime("%Y-%m-%d")
|
||||||
|
cursor.execute("DELETE FROM blacklist WHERE date_to_unban = ?", (today,))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# Создаем планировщик
|
||||||
|
scheduler = AutoUnbanScheduler()
|
||||||
|
scheduler.bot_db = mock_bdf.database
|
||||||
|
scheduler.set_bot(mock_bot)
|
||||||
|
|
||||||
|
# Выполняем автоматический разбан
|
||||||
|
await scheduler.auto_unban_users()
|
||||||
|
|
||||||
|
# Проверяем, что отчет не был отправлен (нет пользователей для разблокировки)
|
||||||
|
mock_bot.send_message.assert_not_called()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@patch('helper_bot.utils.auto_unban_scheduler.get_global_instance')
|
||||||
|
async def test_auto_unban_database_error(self, mock_get_instance, setup_test_db, mock_bdf, mock_bot):
|
||||||
|
"""Тест обработки ошибок базы данных"""
|
||||||
|
# Настройка моков
|
||||||
|
mock_get_instance.return_value = mock_bdf
|
||||||
|
|
||||||
|
# Создаем планировщик
|
||||||
|
scheduler = AutoUnbanScheduler()
|
||||||
|
scheduler.bot_db = mock_bdf.database
|
||||||
|
scheduler.set_bot(mock_bot)
|
||||||
|
|
||||||
|
# Удаляем таблицу чтобы вызвать ошибку
|
||||||
|
conn = sqlite3.connect(setup_test_db)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("DROP TABLE blacklist")
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# Выполняем автоматический разбан
|
||||||
|
await scheduler.auto_unban_users()
|
||||||
|
|
||||||
|
# Проверяем, что отчет об ошибке был отправлен
|
||||||
|
mock_bot.send_message.assert_called_once()
|
||||||
|
call_args = mock_bot.send_message.call_args
|
||||||
|
assert call_args[1]['chat_id'] == '-1001234567891' # important_logs
|
||||||
|
assert "Ошибка автоматического разбана" in call_args[1]['text']
|
||||||
|
|
||||||
|
def test_date_format_consistency(self, setup_test_db, mock_bdf):
|
||||||
|
"""Тест консистентности формата дат"""
|
||||||
|
scheduler = AutoUnbanScheduler()
|
||||||
|
scheduler.bot_db = mock_bdf.database
|
||||||
|
|
||||||
|
# Проверяем, что дата в базе соответствует ожидаемому формату
|
||||||
|
conn = sqlite3.connect(setup_test_db)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT date_to_unban FROM blacklist WHERE date_to_unban IS NOT NULL LIMIT 1")
|
||||||
|
result = cursor.fetchone()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if result and result[0]:
|
||||||
|
date_str = result[0]
|
||||||
|
# Проверяем формат YYYY-MM-DD
|
||||||
|
assert len(date_str) == 10
|
||||||
|
assert date_str.count('-') == 2
|
||||||
|
assert date_str[:4].isdigit() # Год
|
||||||
|
assert date_str[5:7].isdigit() # Месяц
|
||||||
|
assert date_str[8:10].isdigit() # День
|
||||||
|
|
||||||
|
|
||||||
|
class TestSchedulerLifecycle:
|
||||||
|
"""Тесты жизненного цикла планировщика"""
|
||||||
|
|
||||||
|
def test_scheduler_start_stop(self):
|
||||||
|
"""Тест запуска и остановки планировщика"""
|
||||||
|
scheduler = AutoUnbanScheduler()
|
||||||
|
|
||||||
|
# Запускаем планировщик
|
||||||
|
scheduler.start_scheduler()
|
||||||
|
assert scheduler.scheduler.running
|
||||||
|
|
||||||
|
# Останавливаем планировщик (должно пройти без ошибок)
|
||||||
|
scheduler.stop_scheduler()
|
||||||
|
# APScheduler может не сразу остановиться, но это нормально
|
||||||
|
|
||||||
|
def test_scheduler_job_creation(self):
|
||||||
|
"""Тест создания задачи в планировщике"""
|
||||||
|
scheduler = AutoUnbanScheduler()
|
||||||
|
|
||||||
|
with patch.object(scheduler.scheduler, 'add_job') as mock_add_job:
|
||||||
|
scheduler.start_scheduler()
|
||||||
|
|
||||||
|
# Проверяем, что задача была создана с правильными параметрами
|
||||||
|
mock_add_job.assert_called_once()
|
||||||
|
call_args = mock_add_job.call_args
|
||||||
|
|
||||||
|
# Проверяем функцию
|
||||||
|
assert call_args[0][0] == scheduler.auto_unban_users
|
||||||
|
|
||||||
|
# Проверяем триггер (должен быть CronTrigger)
|
||||||
|
from apscheduler.triggers.cron import CronTrigger
|
||||||
|
assert isinstance(call_args[0][1], CronTrigger)
|
||||||
|
|
||||||
|
# Проверяем ID и имя задачи
|
||||||
|
assert call_args[1]['id'] == 'auto_unban_users'
|
||||||
|
assert call_args[1]['name'] == 'Автоматический разбан пользователей'
|
||||||
|
assert call_args[1]['replace_existing'] is True
|
||||||
288
tests/test_auto_unban_scheduler.py
Normal file
288
tests/test_auto_unban_scheduler.py
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
import pytest
|
||||||
|
import asyncio
|
||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
|
from unittest.mock import Mock, patch, AsyncMock
|
||||||
|
|
||||||
|
from helper_bot.utils.auto_unban_scheduler import AutoUnbanScheduler, get_auto_unban_scheduler
|
||||||
|
|
||||||
|
|
||||||
|
class TestAutoUnbanScheduler:
|
||||||
|
"""Тесты для класса AutoUnbanScheduler"""
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def scheduler(self):
|
||||||
|
"""Создает экземпляр планировщика для тестов"""
|
||||||
|
return AutoUnbanScheduler()
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_bot_db(self):
|
||||||
|
"""Создает мок базы данных"""
|
||||||
|
mock_db = Mock()
|
||||||
|
mock_db.get_users_for_unblock_today.return_value = {
|
||||||
|
123: "test_user1",
|
||||||
|
456: "test_user2"
|
||||||
|
}
|
||||||
|
mock_db.delete_user_blacklist.return_value = True
|
||||||
|
return mock_db
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_bdf(self):
|
||||||
|
"""Создает мок фабрики зависимостей"""
|
||||||
|
mock_factory = Mock()
|
||||||
|
mock_factory.settings = {
|
||||||
|
'Telegram': {
|
||||||
|
'group_for_logs': '-1001234567890',
|
||||||
|
'important_logs': '-1001234567891'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mock_factory
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_bot(self):
|
||||||
|
"""Создает мок бота"""
|
||||||
|
mock_bot = Mock()
|
||||||
|
mock_bot.send_message = AsyncMock()
|
||||||
|
return mock_bot
|
||||||
|
|
||||||
|
def test_scheduler_initialization(self, scheduler):
|
||||||
|
"""Тест инициализации планировщика"""
|
||||||
|
assert scheduler.bot_db is not None
|
||||||
|
assert scheduler.scheduler is not None
|
||||||
|
assert scheduler.bot is None
|
||||||
|
|
||||||
|
def test_set_bot(self, scheduler, mock_bot):
|
||||||
|
"""Тест установки бота"""
|
||||||
|
scheduler.set_bot(mock_bot)
|
||||||
|
assert scheduler.bot == mock_bot
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@patch('helper_bot.utils.auto_unban_scheduler.get_global_instance')
|
||||||
|
async def test_auto_unban_users_success(self, mock_get_instance, scheduler, mock_bot_db, mock_bdf, mock_bot):
|
||||||
|
"""Тест успешного выполнения автоматического разбана"""
|
||||||
|
# Настройка моков
|
||||||
|
mock_get_instance.return_value = mock_bdf
|
||||||
|
scheduler.bot_db = mock_bot_db
|
||||||
|
scheduler.set_bot(mock_bot)
|
||||||
|
|
||||||
|
# Выполнение теста
|
||||||
|
await scheduler.auto_unban_users()
|
||||||
|
|
||||||
|
# Проверки
|
||||||
|
mock_bot_db.get_users_for_unblock_today.assert_called_once()
|
||||||
|
assert mock_bot_db.delete_user_blacklist.call_count == 2
|
||||||
|
mock_bot.send_message.assert_called_once()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@patch('helper_bot.utils.auto_unban_scheduler.get_global_instance')
|
||||||
|
async def test_auto_unban_users_no_users(self, mock_get_instance, scheduler, mock_bot_db, mock_bdf, mock_bot):
|
||||||
|
"""Тест разбана когда нет пользователей для разблокировки"""
|
||||||
|
# Настройка моков
|
||||||
|
mock_get_instance.return_value = mock_bdf
|
||||||
|
mock_bot_db.get_users_for_unblock_today.return_value = {}
|
||||||
|
scheduler.bot_db = mock_bot_db
|
||||||
|
scheduler.set_bot(mock_bot)
|
||||||
|
|
||||||
|
# Выполнение теста
|
||||||
|
await scheduler.auto_unban_users()
|
||||||
|
|
||||||
|
# Проверки
|
||||||
|
mock_bot_db.get_users_for_unblock_today.assert_called_once()
|
||||||
|
mock_bot_db.delete_user_blacklist.assert_not_called()
|
||||||
|
mock_bot.send_message.assert_not_called()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@patch('helper_bot.utils.auto_unban_scheduler.get_global_instance')
|
||||||
|
async def test_auto_unban_users_partial_failure(self, mock_get_instance, scheduler, mock_bot_db, mock_bdf, mock_bot):
|
||||||
|
"""Тест разбана с частичными ошибками"""
|
||||||
|
# Настройка моков
|
||||||
|
mock_get_instance.return_value = mock_bdf
|
||||||
|
mock_bot_db.get_users_for_unblock_today.return_value = {
|
||||||
|
123: "test_user1",
|
||||||
|
456: "test_user2"
|
||||||
|
}
|
||||||
|
# Первый вызов успешен, второй - ошибка
|
||||||
|
mock_bot_db.delete_user_blacklist.side_effect = [True, False]
|
||||||
|
scheduler.bot_db = mock_bot_db
|
||||||
|
scheduler.set_bot(mock_bot)
|
||||||
|
|
||||||
|
# Выполнение теста
|
||||||
|
await scheduler.auto_unban_users()
|
||||||
|
|
||||||
|
# Проверки
|
||||||
|
assert mock_bot_db.delete_user_blacklist.call_count == 2
|
||||||
|
mock_bot.send_message.assert_called_once()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@patch('helper_bot.utils.auto_unban_scheduler.get_global_instance')
|
||||||
|
async def test_auto_unban_users_exception(self, mock_get_instance, scheduler, mock_bot_db, mock_bdf, mock_bot):
|
||||||
|
"""Тест разбана с исключением"""
|
||||||
|
# Настройка моков
|
||||||
|
mock_get_instance.return_value = mock_bdf
|
||||||
|
mock_bot_db.get_users_for_unblock_today.side_effect = Exception("Database error")
|
||||||
|
scheduler.bot_db = mock_bot_db
|
||||||
|
scheduler.set_bot(mock_bot)
|
||||||
|
|
||||||
|
# Выполнение теста
|
||||||
|
await scheduler.auto_unban_users()
|
||||||
|
|
||||||
|
# Проверки
|
||||||
|
mock_bot.send_message.assert_called_once()
|
||||||
|
# Проверяем, что сообщение об ошибке было отправлено
|
||||||
|
call_args = mock_bot.send_message.call_args
|
||||||
|
assert "Ошибка автоматического разбана" in call_args[1]['text']
|
||||||
|
|
||||||
|
def test_generate_report(self, scheduler):
|
||||||
|
"""Тест генерации отчета"""
|
||||||
|
users = {123: "test_user1", 456: "test_user2"}
|
||||||
|
failed_users = ["456 (test_user2)"]
|
||||||
|
|
||||||
|
report = scheduler._generate_report(1, 1, failed_users, users)
|
||||||
|
|
||||||
|
assert "Отчет об автоматическом разбане" in report
|
||||||
|
assert "Успешно разблокировано: 1" in report
|
||||||
|
assert "Ошибок: 1" in report
|
||||||
|
assert "test_user1" in report
|
||||||
|
assert "456 (test_user2)" in report
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@patch('helper_bot.utils.auto_unban_scheduler.get_global_instance')
|
||||||
|
async def test_send_report(self, mock_get_instance, scheduler, mock_bdf, mock_bot):
|
||||||
|
"""Тест отправки отчета"""
|
||||||
|
mock_get_instance.return_value = mock_bdf
|
||||||
|
scheduler.set_bot(mock_bot)
|
||||||
|
|
||||||
|
report = "Test report"
|
||||||
|
await scheduler._send_report(report)
|
||||||
|
|
||||||
|
# Проверяем, что send_message был вызван
|
||||||
|
mock_bot.send_message.assert_called_once()
|
||||||
|
|
||||||
|
# Проверяем аргументы вызова
|
||||||
|
call_args = mock_bot.send_message.call_args
|
||||||
|
assert call_args[1]['text'] == report
|
||||||
|
assert call_args[1]['parse_mode'] == 'HTML'
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@patch('helper_bot.utils.auto_unban_scheduler.get_global_instance')
|
||||||
|
async def test_send_error_report(self, mock_get_instance, scheduler, mock_bdf, mock_bot):
|
||||||
|
"""Тест отправки отчета об ошибке"""
|
||||||
|
mock_get_instance.return_value = mock_bdf
|
||||||
|
scheduler.set_bot(mock_bot)
|
||||||
|
|
||||||
|
error_msg = "Test error"
|
||||||
|
await scheduler._send_error_report(error_msg)
|
||||||
|
|
||||||
|
# Проверяем, что send_message был вызван
|
||||||
|
mock_bot.send_message.assert_called_once()
|
||||||
|
|
||||||
|
# Проверяем аргументы вызова
|
||||||
|
call_args = mock_bot.send_message.call_args
|
||||||
|
assert "Ошибка автоматического разбана" in call_args[1]['text']
|
||||||
|
assert error_msg in call_args[1]['text']
|
||||||
|
assert call_args[1]['parse_mode'] == 'HTML'
|
||||||
|
|
||||||
|
def test_start_scheduler(self, scheduler):
|
||||||
|
"""Тест запуска планировщика"""
|
||||||
|
with patch.object(scheduler.scheduler, 'add_job') as mock_add_job, \
|
||||||
|
patch.object(scheduler.scheduler, 'start') as mock_start:
|
||||||
|
|
||||||
|
scheduler.start_scheduler()
|
||||||
|
|
||||||
|
mock_add_job.assert_called_once()
|
||||||
|
mock_start.assert_called_once()
|
||||||
|
|
||||||
|
def test_stop_scheduler(self, scheduler):
|
||||||
|
"""Тест остановки планировщика"""
|
||||||
|
# Сначала запускаем планировщик
|
||||||
|
scheduler.start_scheduler()
|
||||||
|
|
||||||
|
# Проверяем, что планировщик запущен
|
||||||
|
assert scheduler.scheduler.running
|
||||||
|
|
||||||
|
# Теперь останавливаем (должно пройти без ошибок)
|
||||||
|
scheduler.stop_scheduler()
|
||||||
|
|
||||||
|
# Проверяем, что метод выполнился без исключений
|
||||||
|
# APScheduler может не сразу остановиться, но это нормально
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@patch('helper_bot.utils.auto_unban_scheduler.get_global_instance')
|
||||||
|
async def test_run_manual_unban(self, mock_get_instance, scheduler, mock_bot_db, mock_bdf, mock_bot):
|
||||||
|
"""Тест ручного запуска разбана"""
|
||||||
|
mock_get_instance.return_value = mock_bdf
|
||||||
|
mock_bot_db.get_users_for_unblock_today.return_value = {}
|
||||||
|
scheduler.bot_db = mock_bot_db
|
||||||
|
scheduler.set_bot(mock_bot)
|
||||||
|
|
||||||
|
await scheduler.run_manual_unban()
|
||||||
|
|
||||||
|
mock_bot_db.get_users_for_unblock_today.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetAutoUnbanScheduler:
|
||||||
|
"""Тесты для функции get_auto_unban_scheduler"""
|
||||||
|
|
||||||
|
def test_get_auto_unban_scheduler(self):
|
||||||
|
"""Тест получения глобального экземпляра планировщика"""
|
||||||
|
scheduler = get_auto_unban_scheduler()
|
||||||
|
assert isinstance(scheduler, AutoUnbanScheduler)
|
||||||
|
|
||||||
|
# Проверяем, что возвращается один и тот же экземпляр
|
||||||
|
scheduler2 = get_auto_unban_scheduler()
|
||||||
|
assert scheduler is scheduler2
|
||||||
|
|
||||||
|
|
||||||
|
class TestDateHandling:
|
||||||
|
"""Тесты для обработки дат"""
|
||||||
|
|
||||||
|
def test_moscow_timezone(self):
|
||||||
|
"""Тест работы с московским временем"""
|
||||||
|
scheduler = AutoUnbanScheduler()
|
||||||
|
|
||||||
|
# Проверяем, что дата формируется в правильном формате
|
||||||
|
moscow_tz = timezone(timedelta(hours=3))
|
||||||
|
today = datetime.now(moscow_tz).strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
assert len(today) == 10 # YYYY-MM-DD
|
||||||
|
assert today.count('-') == 2
|
||||||
|
assert today[:4].isdigit() # Год
|
||||||
|
assert today[5:7].isdigit() # Месяц
|
||||||
|
assert today[8:10].isdigit() # День
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
class TestAsyncOperations:
|
||||||
|
"""Тесты асинхронных операций"""
|
||||||
|
|
||||||
|
@patch('helper_bot.utils.auto_unban_scheduler.get_global_instance')
|
||||||
|
async def test_async_auto_unban_flow(self, mock_get_instance):
|
||||||
|
"""Тест полного асинхронного потока разбана"""
|
||||||
|
# Создаем моки
|
||||||
|
mock_bdf = Mock()
|
||||||
|
mock_bdf.settings = {
|
||||||
|
'Telegram': {
|
||||||
|
'group_for_logs': '-1001234567890',
|
||||||
|
'important_logs': '-1001234567891'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mock_get_instance.return_value = mock_bdf
|
||||||
|
|
||||||
|
mock_bot_db = Mock()
|
||||||
|
mock_bot_db.get_users_for_unblock_today.return_value = {123: "test_user"}
|
||||||
|
mock_bot_db.delete_user_blacklist.return_value = True
|
||||||
|
|
||||||
|
mock_bot = Mock()
|
||||||
|
mock_bot.send_message = AsyncMock()
|
||||||
|
|
||||||
|
# Создаем планировщик
|
||||||
|
scheduler = AutoUnbanScheduler()
|
||||||
|
scheduler.bot_db = mock_bot_db
|
||||||
|
scheduler.set_bot(mock_bot)
|
||||||
|
|
||||||
|
# Выполняем разбан
|
||||||
|
await scheduler.auto_unban_users()
|
||||||
|
|
||||||
|
# Проверяем результаты
|
||||||
|
mock_bot_db.get_users_for_unblock_today.assert_called_once()
|
||||||
|
mock_bot_db.delete_user_blacklist.assert_called_once_with(123)
|
||||||
|
mock_bot.send_message.assert_called_once()
|
||||||
@@ -512,30 +512,7 @@ def test_update_info_about_stickers_error(bot):
|
|||||||
bot.update_info_about_stickers(12345)
|
bot.update_info_about_stickers(12345)
|
||||||
|
|
||||||
|
|
||||||
def test_get_users_blacklist_empty(bot):
|
|
||||||
"""Проверяет, что функция возвращает пустой словарь, если в черном списке нет пользователей."""
|
|
||||||
conn = sqlite3.connect('database/test.db')
|
|
||||||
cursor = conn.cursor()
|
|
||||||
cursor.execute("DELETE FROM blacklist")
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
blacklist = bot.get_users_blacklist()
|
|
||||||
assert blacklist == {}
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_users_blacklist_non_empty(bot):
|
|
||||||
"""Проверяет, что функция возвращает словарь с пользователями из черного списка."""
|
|
||||||
blacklist = bot.get_users_blacklist()
|
|
||||||
assert blacklist == {12345: "@iban", 14278: "@boris"}
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_users_blacklist_error(bot):
|
|
||||||
"""Проверяет, что функция вызывает sqlite3. Error при ошибке запроса."""
|
|
||||||
__drop_table('blacklist')
|
|
||||||
|
|
||||||
with pytest.raises(sqlite3.Error):
|
|
||||||
bot.get_users_blacklist()
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_blacklist_users_by_id_found(bot, setup_db):
|
def test_get_blacklist_users_by_id_found(bot, setup_db):
|
||||||
|
|||||||
Reference in New Issue
Block a user