import time from datetime import datetime from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest from helper_bot.handlers.callback.callback_handlers import ( change_page, delete_voice_message, process_ban_user, process_unlock_user, return_to_main_menu, save_voice_message, ) from helper_bot.handlers.voice.constants import CALLBACK_DELETE, CALLBACK_SAVE @pytest.fixture def mock_call(): """Мок для CallbackQuery""" call = Mock() call.message = Mock() call.message.message_id = 12345 call.message.voice = Mock() call.message.voice.file_id = "test_file_id_123" call.bot = Mock() call.bot.delete_message = AsyncMock() call.answer = AsyncMock() return call @pytest.fixture def mock_bot_db(): """Мок для базы данных""" mock_db = Mock() mock_db.get_user_id_by_message_id_for_voice_bot = AsyncMock(return_value=67890) mock_db.delete_audio_moderate_record = AsyncMock() return mock_db @pytest.fixture def mock_settings(): """Мок для настроек""" return {"Telegram": {"group_for_posts": "test_group_id"}} @pytest.fixture def mock_audio_service(): """Мок для AudioFileService""" mock_service = Mock() mock_service.generate_file_name = AsyncMock( return_value="message_from_67890_number_1" ) mock_service.save_audio_file = AsyncMock() mock_service.download_and_save_audio = AsyncMock() return mock_service class TestSaveVoiceMessage: """Тесты для функции save_voice_message""" @pytest.mark.asyncio async def test_save_voice_message_success( self, mock_call, mock_bot_db, mock_settings, mock_audio_service ): """Тест успешного сохранения голосового сообщения""" with patch( "helper_bot.handlers.callback.callback_handlers.AudioFileService" ) as mock_service_class: mock_service_class.return_value = mock_audio_service await save_voice_message( mock_call, bot_db=mock_bot_db, settings=mock_settings ) # Проверяем, что все методы вызваны mock_bot_db.get_user_id_by_message_id_for_voice_bot.assert_called_once_with( 12345 ) mock_audio_service.generate_file_name.assert_called_once_with(67890) mock_audio_service.save_audio_file.assert_called_once() mock_audio_service.download_and_save_audio.assert_called_once_with( mock_call.bot, mock_call.message, "message_from_67890_number_1" ) # Проверяем удаление сообщения из чата mock_call.bot.delete_message.assert_called_once_with( chat_id="test_group_id", message_id=12345 ) # Проверяем удаление записи из audio_moderate mock_bot_db.delete_audio_moderate_record.assert_called_once_with(12345) # Проверяем ответ пользователю mock_call.answer.assert_called_once_with(text="Сохранено!", cache_time=3) @pytest.mark.asyncio async def test_save_voice_message_with_correct_parameters( self, mock_call, mock_bot_db, mock_settings, mock_audio_service ): """Тест сохранения с правильными параметрами""" with patch( "helper_bot.handlers.callback.callback_handlers.AudioFileService" ) as mock_service_class: mock_service_class.return_value = mock_audio_service await save_voice_message( mock_call, bot_db=mock_bot_db, settings=mock_settings ) # Проверяем параметры save_audio_file save_call_args = mock_audio_service.save_audio_file.call_args assert save_call_args[0][0] == "message_from_67890_number_1" # file_name assert save_call_args[0][1] == 67890 # user_id assert isinstance(save_call_args[0][2], datetime) # date_added assert save_call_args[0][3] == "test_file_id_123" # file_id @pytest.mark.asyncio async def test_save_voice_message_exception_handling( self, mock_call, mock_bot_db, mock_settings ): """Тест обработки исключений при сохранении""" mock_bot_db.get_user_id_by_message_id_for_voice_bot.side_effect = Exception( "Database error" ) await save_voice_message(mock_call, bot_db=mock_bot_db, settings=mock_settings) # Проверяем, что при ошибке отправляется соответствующий ответ mock_call.answer.assert_called_once_with( text="Ошибка при сохранении!", cache_time=3 ) @pytest.mark.asyncio async def test_save_voice_message_audio_service_exception( self, mock_call, mock_bot_db, mock_settings, mock_audio_service ): """Тест обработки исключений в AudioFileService""" mock_audio_service.save_audio_file.side_effect = Exception("Save error") with patch( "helper_bot.handlers.callback.callback_handlers.AudioFileService" ) as mock_service_class: mock_service_class.return_value = mock_audio_service await save_voice_message( mock_call, bot_db=mock_bot_db, settings=mock_settings ) # Проверяем, что при ошибке отправляется соответствующий ответ mock_call.answer.assert_called_once_with( text="Ошибка при сохранении!", cache_time=3 ) @pytest.mark.asyncio async def test_save_voice_message_download_exception( self, mock_call, mock_bot_db, mock_settings, mock_audio_service ): """Тест обработки исключений при скачивании файла""" mock_audio_service.download_and_save_audio.side_effect = Exception( "Download error" ) with patch( "helper_bot.handlers.callback.callback_handlers.AudioFileService" ) as mock_service_class: mock_service_class.return_value = mock_audio_service await save_voice_message( mock_call, bot_db=mock_bot_db, settings=mock_settings ) # Проверяем, что при ошибке отправляется соответствующий ответ mock_call.answer.assert_called_once_with( text="Ошибка при сохранении!", cache_time=3 ) class TestDeleteVoiceMessage: """Тесты для функции delete_voice_message""" @pytest.mark.asyncio async def test_delete_voice_message_success( self, mock_call, mock_bot_db, mock_settings ): """Тест успешного удаления голосового сообщения""" await delete_voice_message( mock_call, bot_db=mock_bot_db, settings=mock_settings ) # Проверяем удаление сообщения из чата mock_call.bot.delete_message.assert_called_once_with( chat_id="test_group_id", message_id=12345 ) # Проверяем удаление записи из audio_moderate mock_bot_db.delete_audio_moderate_record.assert_called_once_with(12345) # Проверяем ответ пользователю mock_call.answer.assert_called_once_with(text="Удалено!", cache_time=3) @pytest.mark.asyncio async def test_delete_voice_message_exception_handling( self, mock_call, mock_bot_db, mock_settings ): """Тест обработки исключений при удалении""" mock_call.bot.delete_message.side_effect = Exception("Delete error") await delete_voice_message( mock_call, bot_db=mock_bot_db, settings=mock_settings ) # Проверяем, что при ошибке отправляется соответствующий ответ mock_call.answer.assert_called_once_with( text="Ошибка при удалении!", cache_time=3 ) @pytest.mark.asyncio async def test_delete_voice_message_database_exception( self, mock_call, mock_bot_db, mock_settings ): """Тест обработки исключений в базе данных при удалении""" mock_bot_db.delete_audio_moderate_record.side_effect = Exception( "Database error" ) await delete_voice_message( mock_call, bot_db=mock_bot_db, settings=mock_settings ) # Проверяем, что при ошибке отправляется соответствующий ответ mock_call.answer.assert_called_once_with( text="Ошибка при удалении!", cache_time=3 ) class TestCallbackHandlersIntegration: """Интеграционные тесты для callback handlers""" @pytest.mark.asyncio async def test_save_voice_message_full_workflow( self, mock_call, mock_bot_db, mock_settings ): """Тест полного рабочего процесса сохранения""" with patch( "helper_bot.handlers.callback.callback_handlers.AudioFileService" ) as mock_service_class: mock_service = Mock() mock_service.generate_file_name = AsyncMock( return_value="message_from_67890_number_1" ) mock_service.save_audio_file = AsyncMock() mock_service.download_and_save_audio = AsyncMock() mock_service_class.return_value = mock_service await save_voice_message( mock_call, bot_db=mock_bot_db, settings=mock_settings ) # Проверяем последовательность вызовов assert mock_bot_db.get_user_id_by_message_id_for_voice_bot.called assert mock_service.generate_file_name.called assert mock_service.save_audio_file.called assert mock_service.download_and_save_audio.called assert mock_call.bot.delete_message.called assert mock_bot_db.delete_audio_moderate_record.called assert mock_call.answer.called @pytest.mark.asyncio async def test_delete_voice_message_full_workflow( self, mock_call, mock_bot_db, mock_settings ): """Тест полного рабочего процесса удаления""" await delete_voice_message( mock_call, bot_db=mock_bot_db, settings=mock_settings ) # Проверяем последовательность вызовов assert mock_call.bot.delete_message.called assert mock_bot_db.delete_audio_moderate_record.called assert mock_call.answer.called @pytest.mark.asyncio async def test_audio_moderate_cleanup_consistency( self, mock_call, mock_bot_db, mock_settings ): """Тест консистентности очистки audio_moderate""" # Тестируем, что в обоих случаях (сохранение и удаление) # вызывается delete_audio_moderate_record # Создаем отдельные моки для каждого теста mock_bot_db_save = Mock() mock_bot_db_save.get_user_id_by_message_id_for_voice_bot = AsyncMock( return_value=67890 ) mock_bot_db_save.delete_audio_moderate_record = AsyncMock() mock_bot_db_delete = Mock() mock_bot_db_delete.delete_audio_moderate_record = AsyncMock() # Тест для сохранения with patch( "helper_bot.handlers.callback.callback_handlers.AudioFileService" ) as mock_service_class: mock_service = Mock() mock_service.generate_file_name = AsyncMock( return_value="message_from_67890_number_1" ) mock_service.save_audio_file = AsyncMock() mock_service.download_and_save_audio = AsyncMock() mock_service_class.return_value = mock_service await save_voice_message( mock_call, bot_db=mock_bot_db_save, settings=mock_settings ) save_calls = mock_bot_db_save.delete_audio_moderate_record.call_count # Тест для удаления await delete_voice_message( mock_call, bot_db=mock_bot_db_delete, settings=mock_settings ) delete_calls = mock_bot_db_delete.delete_audio_moderate_record.call_count # Проверяем, что в обоих случаях вызывается очистка assert save_calls == 1 assert delete_calls == 1 class TestCallbackHandlersEdgeCases: """Тесты граничных случаев для callback handlers""" @pytest.mark.asyncio async def test_save_voice_message_no_voice_attribute( self, mock_bot_db, mock_settings ): """Тест сохранения когда у сообщения нет voice атрибута""" call = Mock() call.message = Mock() call.message.message_id = 12345 call.message.voice = None # Нет голосового сообщения call.bot = Mock() call.bot.delete_message = AsyncMock() call.answer = AsyncMock() with patch("helper_bot.handlers.callback.callback_handlers.AudioFileService"): await save_voice_message(call, bot_db=mock_bot_db, settings=mock_settings) # Должна быть ошибка call.answer.assert_called_once_with( text="Ошибка при сохранении!", cache_time=3 ) @pytest.mark.asyncio async def test_save_voice_message_user_not_found( self, mock_call, mock_bot_db, mock_settings ): """Тест сохранения когда пользователь не найден""" mock_bot_db.get_user_id_by_message_id_for_voice_bot.return_value = None with patch("helper_bot.handlers.callback.callback_handlers.AudioFileService"): await save_voice_message( mock_call, bot_db=mock_bot_db, settings=mock_settings ) # Должна быть ошибка mock_call.answer.assert_called_once_with( text="Ошибка при сохранении!", cache_time=3 ) @pytest.mark.asyncio async def test_delete_voice_message_with_different_message_id( self, mock_bot_db, mock_settings ): """Тест удаления с другим message_id""" call = Mock() call.message = Mock() call.message.message_id = 99999 # Другой ID call.bot = Mock() call.bot.delete_message = AsyncMock() call.answer = AsyncMock() await delete_voice_message(call, bot_db=mock_bot_db, settings=mock_settings) # Проверяем, что используется правильный message_id call.bot.delete_message.assert_called_once_with( chat_id="test_group_id", message_id=99999 ) mock_bot_db.delete_audio_moderate_record.assert_called_once_with(99999) @pytest.mark.unit @pytest.mark.asyncio class TestReturnToMainMenu: """Тесты для return_to_main_menu.""" @pytest.fixture def mock_call(self): call = Mock() call.message = Mock() call.message.message_id = 1 call.message.from_user = Mock() call.message.from_user.id = 123 call.message.delete = AsyncMock() call.message.answer = AsyncMock() return call @patch("helper_bot.handlers.callback.callback_handlers.get_reply_keyboard_admin") async def test_return_to_main_menu_deletes_and_answers( self, mock_keyboard, mock_call ): """return_to_main_menu удаляет сообщение и отправляет приветствие.""" mock_keyboard.return_value = MagicMock() await return_to_main_menu(mock_call) mock_call.message.delete.assert_called_once() mock_call.message.answer.assert_called_once() assert ( "админк" in mock_call.message.answer.call_args[0][0].lower() or "добро" in mock_call.message.answer.call_args[0][0].lower() ) @pytest.mark.unit @pytest.mark.asyncio class TestChangePage: """Тесты для change_page.""" @pytest.fixture def mock_bot_db_for_page(self): """Мок БД для change_page.""" db = Mock() db.get_last_users = AsyncMock(return_value=[("U1", 1), ("U2", 2)]) return db @pytest.fixture def mock_call_list_users(self): call = Mock() call.data = "page_2" call.message = Mock() call.message.text = "Список пользователей которые последними обращались к боту" call.message.chat = Mock() call.message.chat.id = 1 call.message.message_id = 10 call.bot = Mock() call.bot.edit_message_reply_markup = AsyncMock() call.answer = AsyncMock() return call @pytest.fixture def mock_bot_db(self): db = Mock() db.get_last_users = AsyncMock(return_value=[("U1", 1), ("U2", 2)]) return db @patch( "helper_bot.handlers.callback.callback_handlers.create_keyboard_with_pagination" ) async def test_change_page_list_users_edits_markup( self, mock_keyboard, mock_call_list_users, mock_bot_db_for_page ): """change_page для списка пользователей редактирует reply_markup.""" mock_keyboard.return_value = MagicMock() await change_page(mock_call_list_users, bot_db=mock_bot_db_for_page) mock_bot_db_for_page.get_last_users.assert_awaited_once_with(30) mock_call_list_users.bot.edit_message_reply_markup.assert_awaited_once() @patch( "helper_bot.handlers.callback.callback_handlers.get_banned_users_buttons", new_callable=AsyncMock, ) @patch( "helper_bot.handlers.callback.callback_handlers.get_banned_users_list", new_callable=AsyncMock, ) @patch( "helper_bot.handlers.callback.callback_handlers.create_keyboard_with_pagination" ) async def test_change_page_banned_list_edits_text_and_markup( self, mock_keyboard, mock_get_list, mock_get_buttons, mock_bot_db_for_page ): """change_page для списка забаненных редактирует текст и клавиатуру.""" mock_get_list.return_value = "Текст страницы" mock_get_buttons.return_value = [] call = Mock() call.data = "page_1" call.message = Mock() call.message.text = "Заблокированные пользователи" call.message.chat = Mock() call.message.chat.id = 1 call.message.message_id = 10 call.bot = Mock() call.bot.edit_message_text = AsyncMock() call.bot.edit_message_reply_markup = AsyncMock() call.answer = AsyncMock() mock_keyboard.return_value = MagicMock() await change_page(call, bot_db=mock_bot_db_for_page) mock_get_list.assert_awaited_once() mock_get_buttons.assert_awaited_once() call.bot.edit_message_text.assert_awaited_once() call.bot.edit_message_reply_markup.assert_awaited_once() async def test_change_page_invalid_page_number_answers_error( self, mock_bot_db_for_page ): """change_page при некорректном номере страницы отвечает ошибкой.""" call = Mock() call.data = "page_abc" call.answer = AsyncMock() await change_page(call, bot_db=mock_bot_db_for_page) call.answer.assert_awaited_once_with( text="Ошибка: некорректный номер страницы", show_alert=True, cache_time=3 ) @pytest.mark.unit @pytest.mark.asyncio class TestProcessBanUser: """Тесты для process_ban_user.""" @pytest.fixture def mock_bot_db_ban(self): """Мок БД для process_ban_user.""" db = Mock() db.get_full_name_by_id = AsyncMock(return_value="Full Name") return db @pytest.fixture def mock_call(self): call = Mock() call.data = "ban_123456" call.from_user = Mock() call.message = Mock() call.message.answer = AsyncMock() call.answer = AsyncMock() return call @pytest.fixture def mock_state(self): state = Mock() state.update_data = AsyncMock() state.set_state = AsyncMock() return state @patch( "helper_bot.handlers.callback.callback_handlers.create_keyboard_for_ban_reason" ) @patch("helper_bot.handlers.callback.callback_handlers.format_user_info") @patch("helper_bot.handlers.callback.callback_handlers.get_ban_service") async def test_process_ban_user_success_sets_state_await_details( self, mock_get_ban, mock_format, mock_keyboard, mock_call, mock_state, mock_bot_db_ban, ): """process_ban_user при успехе переводит в AWAIT_BAN_DETAILS.""" mock_ban = Mock() mock_ban.ban_user = AsyncMock(return_value="username") mock_get_ban.return_value = mock_ban mock_format.return_value = "User info" mock_keyboard.return_value = MagicMock() await process_ban_user(mock_call, mock_state, bot_db=mock_bot_db_ban) mock_state.update_data.assert_awaited_once() mock_state.set_state.assert_awaited_once_with("AWAIT_BAN_DETAILS") mock_call.message.answer.assert_awaited_once() @patch("helper_bot.handlers.callback.callback_handlers.get_reply_keyboard_admin") @patch("helper_bot.handlers.callback.callback_handlers.get_ban_service") async def test_process_ban_user_not_found_returns_to_admin( self, mock_get_ban, mock_keyboard, mock_call, mock_state, mock_bot_db_ban ): """process_ban_user при UserNotFoundError возвращает в админ-меню.""" from helper_bot.handlers.callback.exceptions import UserNotFoundError mock_ban = Mock() mock_ban.ban_user = AsyncMock(side_effect=UserNotFoundError("not found")) mock_get_ban.return_value = mock_ban mock_keyboard.return_value = MagicMock() await process_ban_user(mock_call, mock_state, bot_db=mock_bot_db_ban) mock_call.message.answer.assert_awaited_once() mock_state.set_state.assert_awaited_once_with("ADMIN") async def test_process_ban_user_invalid_user_id_answers_error( self, mock_call, mock_state, mock_bot_db_ban ): """process_ban_user при некорректном user_id отвечает ошибкой.""" mock_call.data = "ban_abc" await process_ban_user(mock_call, mock_state, bot_db=mock_bot_db_ban) mock_call.answer.assert_awaited_once_with( text="Ошибка: некорректный ID пользователя", show_alert=True, cache_time=3 ) @pytest.mark.unit @pytest.mark.asyncio class TestProcessUnlockUser: """Тесты для process_unlock_user.""" @patch("helper_bot.handlers.callback.callback_handlers.get_ban_service") async def test_process_unlock_user_success_answers_unlocked(self, mock_get_ban): """process_unlock_user при успехе отвечает сообщением о разблокировке.""" call = Mock() call.data = "unlock_123" call.answer = AsyncMock() mock_ban = Mock() mock_ban.unlock_user = AsyncMock(return_value="username") mock_get_ban.return_value = mock_ban await process_unlock_user(call) mock_ban.unlock_user.assert_awaited_once_with("123") call.answer.assert_awaited_once() assert ( "username" in call.answer.call_args[0][0] or "разблокирован" in call.answer.call_args[0][0].lower() ) @patch("helper_bot.handlers.callback.callback_handlers.get_ban_service") async def test_process_unlock_user_not_found_answers_error(self, mock_get_ban): """process_unlock_user при UserNotFoundError отвечает что пользователь не найден.""" from helper_bot.handlers.callback.exceptions import UserNotFoundError call = Mock() call.data = "unlock_999" call.answer = AsyncMock() mock_ban = Mock() mock_ban.unlock_user = AsyncMock(side_effect=UserNotFoundError("not found")) mock_get_ban.return_value = mock_ban await process_unlock_user(call) call.answer.assert_awaited_once_with( text="Пользователь не найден в базе", show_alert=True, cache_time=3 ) async def test_process_unlock_user_invalid_user_id_answers_error(self): """process_unlock_user при некорректном user_id отвечает ошибкой.""" call = Mock() call.data = "unlock_abc" call.answer = AsyncMock() await process_unlock_user(call) call.answer.assert_awaited_once_with( text="Ошибка: некорректный ID пользователя", show_alert=True, cache_time=3 ) if __name__ == "__main__": pytest.main([__file__])