Files
telegram-helper-bot/tests/test_callback_handlers.py
2026-02-02 00:54:23 +03:00

666 lines
26 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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__])