From 86b690392066f452f2bb07bed7e0615a10a5df57 Mon Sep 17 00:00:00 2001 From: Andrey Date: Wed, 27 Aug 2025 20:56:22 +0300 Subject: [PATCH] Add auto unban functionality and update related tests and dependencies --- .gitignore | 3 + Makefile | 8 + database/db.py | 25 -- helper_bot/utils/auto_unban_scheduler.py | 175 ++++++++++++++ requirements.txt | 3 + run_helper.py | 9 + tests/test_auto_unban_integration.py | 251 ++++++++++++++++++++ tests/test_auto_unban_scheduler.py | 288 +++++++++++++++++++++++ tests/test_db.py | 23 -- 9 files changed, 737 insertions(+), 48 deletions(-) create mode 100644 helper_bot/utils/auto_unban_scheduler.py create mode 100644 tests/test_auto_unban_integration.py create mode 100644 tests/test_auto_unban_scheduler.py diff --git a/.gitignore b/.gitignore index 37bcbc7..1f3e790 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ /database/test.db /database/test.db-shm /database/test.db-wal +/database/test_auto_unban.db +/database/test_auto_unban.db-shm +/database/test_auto_unban.db-wal /settings.ini /myenv/ /venv/ diff --git a/Makefile b/Makefile index 1d96859..e27e12a 100644 --- a/Makefile +++ b/Makefile @@ -54,6 +54,14 @@ test-keyboards: test-monitor: 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 test-coverage: python3 -m pytest tests/ --cov=helper_bot --cov=database --cov-report=term diff --git a/database/db.py b/database/db.py index d0639fc..3d8715f 100644 --- a/database/db.py +++ b/database/db.py @@ -485,31 +485,6 @@ class BotDB: finally: 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): """ Возвращает список пользователей, у которых истекает срок блокировки сегодня. diff --git a/helper_bot/utils/auto_unban_scheduler.py b/helper_bot/utils/auto_unban_scheduler.py new file mode 100644 index 0000000..ee4c44c --- /dev/null +++ b/helper_bot/utils/auto_unban_scheduler.py @@ -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"🤖 Отчет об автоматическом разбане\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 += "✅ Разблокированные пользователи:\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 += "❌ Ошибки при разблокировке:\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"🚨 Ошибка автоматического разбана\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 diff --git a/requirements.txt b/requirements.txt index 20cf7ec..a4de8c2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,9 @@ loguru==0.7.2 # System monitoring psutil~=6.1.0 +# Scheduling +apscheduler~=3.10.4 + # Testing pytest==8.2.2 pytest-asyncio==1.1.0 diff --git a/run_helper.py b/run_helper.py index 9579ba9..1b5be34 100644 --- a/run_helper.py +++ b/run_helper.py @@ -11,6 +11,7 @@ if CURRENT_DIR not in sys.path: from helper_bot.main import start_bot from helper_bot.utils.base_dependency_factory import get_global_instance 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): @@ -40,6 +41,11 @@ async def main(): # Создаем экземпляр монитора 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() @@ -71,6 +77,9 @@ async def main(): except Exception as e: print(f"Ошибка при отправке сообщения об отключении: {e}") + print("Останавливаем планировщик автоматического разбана...") + auto_unban_scheduler.stop_scheduler() + print("Останавливаем задачи...") # Отменяем задачи bot_task.cancel() diff --git a/tests/test_auto_unban_integration.py b/tests/test_auto_unban_integration.py new file mode 100644 index 0000000..ff4b16e --- /dev/null +++ b/tests/test_auto_unban_integration.py @@ -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 diff --git a/tests/test_auto_unban_scheduler.py b/tests/test_auto_unban_scheduler.py new file mode 100644 index 0000000..5dd22d8 --- /dev/null +++ b/tests/test_auto_unban_scheduler.py @@ -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() diff --git a/tests/test_db.py b/tests/test_db.py index 845d4ce..1cf932a 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -512,30 +512,7 @@ def test_update_info_about_stickers_error(bot): 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):