Enhance bot functionality with new features and improvements

- Added a new `/status` endpoint in `server_prometheus.py` to provide process status information, including uptime and resource usage metrics.
- Implemented a PID manager in `run_helper.py` to track the bot's process, improving monitoring capabilities.
- Introduced a method to delete audio moderation records in `audio_repository.py`, enhancing database management.
- Updated voice message handling in callback handlers to ensure proper deletion of audio moderation records.
- Improved error handling and logging in various services, ensuring better tracking of media processing and file downloads.
- Refactored media handling functions to streamline operations and improve code readability.
- Enhanced metrics tracking for file downloads and media processing, providing better insights into bot performance.
This commit is contained in:
2025-09-04 00:46:45 +03:00
parent ae7bd476bb
commit fc0517c011
17 changed files with 1421 additions and 84 deletions

View File

@@ -0,0 +1,297 @@
import pytest
from unittest.mock import Mock, AsyncMock, patch, MagicMock
from datetime import datetime
import time
from helper_bot.handlers.callback.callback_handlers import (
save_voice_message,
delete_voice_message
)
from helper_bot.handlers.voice.constants import CALLBACK_SAVE, CALLBACK_DELETE
@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)
if __name__ == '__main__':
pytest.main([__file__])