""" Тесты для helper_bot.handlers.admin.admin_handlers: хендлеры админ-панели с моками. """ from unittest.mock import AsyncMock, MagicMock, patch import pytest from aiogram import types from aiogram.fsm.context import FSMContext from helper_bot.handlers.admin.admin_handlers import ( admin_panel, cancel_ban_process, confirm_ban, get_banned_users, get_last_users, get_ml_stats, process_ban_duration, process_ban_reason, process_ban_target, start_ban_process, ) from helper_bot.handlers.admin.services import User as AdminUser @pytest.mark.unit @pytest.mark.asyncio class TestAdminHandlers: """Тесты хендлеров admin_handlers с моками.""" @pytest.fixture def mock_message(self): """Мок сообщения.""" msg = MagicMock(spec=types.Message) msg.from_user = MagicMock() msg.from_user.id = 123 msg.from_user.full_name = "Admin" msg.text = "Бан по нику" msg.answer = AsyncMock() msg.reply = AsyncMock() return msg @pytest.fixture def mock_state(self): """Мок FSMContext.""" state = MagicMock(spec=FSMContext) state.set_state = AsyncMock() state.get_state = AsyncMock(return_value="ADMIN") state.get_data = AsyncMock(return_value={}) state.update_data = AsyncMock() return state @pytest.fixture def mock_bot_db(self): """Мок БД.""" db = MagicMock() db.get_last_users = AsyncMock(return_value=[("User One", 1), ("User Two", 2)]) db.get_banned_users_from_db = AsyncMock(return_value=[]) db.get_username = AsyncMock(return_value="user") db.get_full_name_by_id = AsyncMock(return_value="Full Name") return db @patch("helper_bot.handlers.admin.admin_handlers.get_reply_keyboard_admin") async def test_admin_panel_sets_state_and_answers(self, mock_keyboard, mock_message, mock_state): """admin_panel устанавливает состояние ADMIN и отправляет приветствие.""" mock_keyboard.return_value = MagicMock() await admin_panel(mock_message, mock_state) mock_state.set_state.assert_awaited_once_with("ADMIN") mock_message.answer.assert_awaited_once() assert "админк" in mock_message.answer.call_args[0][0].lower() or "добро" in mock_message.answer.call_args[0][0].lower() @patch("helper_bot.handlers.admin.admin_handlers.return_to_admin_menu", new_callable=AsyncMock) async def test_cancel_ban_process_returns_to_menu(self, mock_return, mock_message, mock_state): """cancel_ban_process вызывает return_to_admin_menu.""" mock_state.get_state = AsyncMock(return_value="AWAIT_BAN_TARGET") await cancel_ban_process(mock_message, mock_state) mock_return.assert_awaited_once_with(mock_message, mock_state) @patch("helper_bot.handlers.admin.admin_handlers.create_keyboard_with_pagination") @patch("helper_bot.handlers.admin.admin_handlers.AdminService") async def test_get_last_users_answers_with_keyboard( self, mock_service_cls, mock_keyboard, mock_message, mock_state, mock_bot_db ): """get_last_users получает пользователей и отправляет клавиатуру.""" mock_service = MagicMock() mock_service.get_last_users = AsyncMock( return_value=[ AdminUser(1, "u1", "User One"), AdminUser(2, "u2", "User Two"), ] ) mock_service_cls.return_value = mock_service mock_keyboard.return_value = MagicMock() await get_last_users(mock_message, mock_state, bot_db=mock_bot_db) mock_service.get_last_users.assert_awaited_once() mock_keyboard.assert_called_once() mock_message.answer.assert_awaited_once() assert "Список пользователей" in mock_message.answer.call_args[1]["text"] or "пользователей" in mock_message.answer.call_args[1]["text"] @patch("helper_bot.handlers.admin.admin_handlers.create_keyboard_with_pagination") @patch("helper_bot.handlers.admin.admin_handlers.AdminService") async def test_get_banned_users_empty_answers_no_list( self, mock_service_cls, mock_keyboard, mock_message, mock_state, mock_bot_db ): """get_banned_users при пустом списке отправляет сообщение 'никого нет'.""" mock_service = MagicMock() mock_service.get_banned_users_for_display = AsyncMock(return_value=("Текст", [])) mock_service_cls.return_value = mock_service await get_banned_users(mock_message, mock_state, bot_db=mock_bot_db) mock_message.answer.assert_awaited_once() assert "никого нет" in mock_message.answer.call_args[1]["text"] or "заблокированных" in mock_message.answer.call_args[1]["text"] @patch("helper_bot.handlers.admin.admin_handlers.get_global_instance") async def test_get_ml_stats_disabled_answers_message(self, mock_get_global, mock_message, mock_state): """get_ml_stats при отключённом scoring_manager отправляет сообщение об отключении.""" mock_bdf = MagicMock() mock_bdf.get_scoring_manager.return_value = None mock_get_global.return_value = mock_bdf await get_ml_stats(mock_message, mock_state) mock_message.answer.assert_awaited_once() assert "ML" in mock_message.answer.call_args[0][0] or "RAG" in mock_message.answer.call_args[0][0] or "отключен" in mock_message.answer.call_args[0][0].lower() @patch("helper_bot.handlers.admin.admin_handlers.get_global_instance") async def test_get_ml_stats_with_rag_and_deepseek(self, mock_get_global, mock_message, mock_state): """get_ml_stats при включённом scoring возвращает статистику.""" mock_scoring = MagicMock() mock_scoring.get_stats = AsyncMock(return_value={ "rag": {"model_loaded": True, "vector_store": {"positive_count": 1, "negative_count": 0, "total_count": 1}}, "deepseek": {"enabled": True, "model": "test", "timeout": 30}, }) mock_bdf = MagicMock() mock_bdf.get_scoring_manager.return_value = mock_scoring mock_get_global.return_value = mock_bdf await get_ml_stats(mock_message, mock_state) mock_message.answer.assert_awaited_once() text = mock_message.answer.call_args[0][0] assert "ML" in text or "RAG" in text or "DeepSeek" in text async def test_start_ban_process_by_nick_sets_state_await_target(self, mock_message, mock_state): """start_ban_process при 'Бан по нику' устанавливает ban_type username и AWAIT_BAN_TARGET.""" mock_message.text = "Бан по нику" await start_ban_process(mock_message, mock_state) mock_state.update_data.assert_awaited_once() call_kw = mock_state.update_data.call_args[1] assert call_kw.get("ban_type") == "username" mock_state.set_state.assert_awaited_once_with("AWAIT_BAN_TARGET") mock_message.answer.assert_awaited_once() assert "username" in mock_message.answer.call_args[0][0].lower() or "ник" in mock_message.answer.call_args[0][0].lower() async def test_start_ban_process_by_id_sets_ban_type_id(self, mock_message, mock_state): """start_ban_process при 'Бан по ID' устанавливает ban_type id.""" mock_message.text = "Бан по ID" await start_ban_process(mock_message, mock_state) call_kw = mock_state.update_data.call_args[1] assert call_kw.get("ban_type") == "id" @patch("helper_bot.handlers.admin.admin_handlers.create_keyboard_for_ban_reason") @patch("helper_bot.handlers.admin.admin_handlers.format_user_info") @patch("helper_bot.handlers.admin.admin_handlers.return_to_admin_menu", new_callable=AsyncMock) @patch("helper_bot.handlers.admin.admin_handlers.AdminService") async def test_process_ban_target_user_not_found_returns_to_menu( self, mock_service_cls, mock_return, mock_format, mock_keyboard, mock_message, mock_state, mock_bot_db ): """process_ban_target при ненайденном пользователе по username возвращает в меню.""" mock_service = MagicMock() mock_service.get_user_by_username = AsyncMock(return_value=None) mock_service_cls.return_value = mock_service mock_state.get_data = AsyncMock(return_value={"ban_type": "username"}) mock_message.text = "unknown_user" mock_format.return_value = "User info" mock_keyboard.return_value = MagicMock() await process_ban_target(mock_message, mock_state, bot_db=mock_bot_db) mock_message.answer.assert_called() mock_return.assert_awaited_once() @patch("helper_bot.handlers.admin.admin_handlers.create_keyboard_for_ban_reason") @patch("helper_bot.handlers.admin.admin_handlers.format_user_info") @patch("helper_bot.handlers.admin.admin_handlers.AdminService") async def test_process_ban_reason_sets_state_await_duration( self, mock_service_cls, mock_format, mock_keyboard, mock_message, mock_state ): """process_ban_reason сохраняет причину и переводит в AWAIT_BAN_DURATION.""" mock_state.get_state = AsyncMock(return_value="AWAIT_BAN_DETAILS") mock_state.get_data = AsyncMock(return_value={}) mock_message.text = "Спам" mock_format.return_value = "Спам" mock_keyboard.return_value = MagicMock() await process_ban_reason(mock_message, mock_state) mock_state.update_data.assert_awaited_once_with(ban_reason="Спам") mock_state.set_state.assert_awaited_once_with("AWAIT_BAN_DURATION") mock_message.answer.assert_awaited_once() @patch("helper_bot.handlers.admin.admin_handlers.create_keyboard_for_approve_ban") @patch("helper_bot.handlers.admin.admin_handlers.format_ban_confirmation") async def test_process_ban_duration_forever_sets_ban_days_none( self, mock_format, mock_keyboard, mock_message, mock_state ): """process_ban_duration при 'Навсегда' устанавливает ban_days=None.""" mock_state.get_data = AsyncMock(return_value={ "target_user_id": 1, "target_username": "u", "target_full_name": "U", "ban_reason": "Спам", }) mock_message.text = "Навсегда" mock_format.return_value = "Подтверждение" mock_keyboard.return_value = MagicMock() await process_ban_duration(mock_message, mock_state) mock_state.update_data.assert_awaited_once() assert mock_state.update_data.call_args[1].get("ban_days") is None mock_state.set_state.assert_awaited_once_with("BAN_CONFIRMATION")