From 013892dcb712997d834ec4292a54b512fe6549aa Mon Sep 17 00:00:00 2001 From: Andrey Date: Mon, 1 Sep 2025 20:30:10 +0300 Subject: [PATCH] Remove obsolete configuration management and test settings files - Deleted the `config.py` file responsible for managing bot configuration via environment variables and `.env` files, as it is no longer needed. - Removed the `test_settings.ini` file used for testing, which contained mock configuration data. - Cleaned up the project structure by eliminating unused files to enhance maintainability. --- helper_bot/utils/config.py | 91 ----------- tests/test_settings.ini | 13 -- tests/test_voice_bot_architecture.py | 69 ++++++++ tests/test_voice_constants.py | 170 ++++++++++++++++++++ tests/test_voice_exceptions.py | 211 ++++++++++++++++++++++++ tests/test_voice_handler.py | 117 ++++++++++++++ tests/test_voice_services.py | 231 +++++++++++++++++++++++++++ tests/test_voice_utils.py | 200 +++++++++++++++++++++++ 8 files changed, 998 insertions(+), 104 deletions(-) delete mode 100644 helper_bot/utils/config.py delete mode 100644 tests/test_settings.ini create mode 100644 tests/test_voice_constants.py create mode 100644 tests/test_voice_exceptions.py create mode 100644 tests/test_voice_handler.py create mode 100644 tests/test_voice_services.py create mode 100644 tests/test_voice_utils.py diff --git a/helper_bot/utils/config.py b/helper_bot/utils/config.py deleted file mode 100644 index 38a26dc..0000000 --- a/helper_bot/utils/config.py +++ /dev/null @@ -1,91 +0,0 @@ -""" -Configuration management for the Telegram bot. -Supports both environment variables and .env files. -""" - -import os -from typing import Dict, Any, Optional -from dotenv import load_dotenv - - -class ConfigManager: - """Manages bot configuration with environment variable support.""" - - def __init__(self, env_file: str = ".env"): - self.env_file = env_file - self._load_env() - - def _load_env(self): - """Load configuration from .env file if exists.""" - # Load from .env file if exists - if os.path.exists(self.env_file): - load_dotenv(self.env_file) - - def get(self, section: str, key: str, default: Any = None) -> str: - """Get configuration value with environment variable override.""" - # Check environment variable first - env_key = f"{section.upper()}_{key.upper()}" - env_value = os.getenv(env_key) - if env_value is not None: - return env_value - - # Fall back to direct environment variable - direct_env_value = os.getenv(key.upper()) - if direct_env_value is not None: - return direct_env_value - - return default - - def getboolean(self, section: str, key: str, default: bool = False) -> bool: - """Get boolean configuration value.""" - value = self.get(section, key, str(default)) - if isinstance(value, bool): - return value - return value.lower() in ('true', '1', 'yes', 'on') - - def getint(self, section: str, key: str, default: int = 0) -> int: - """Get integer configuration value.""" - value = self.get(section, key, str(default)) - try: - return int(value) - except (ValueError, TypeError): - return default - - def get_all_settings(self) -> Dict[str, Dict[str, Any]]: - """Get all settings as dictionary.""" - settings = {} - - # Telegram секция - settings['Telegram'] = { - 'bot_token': self.get('Telegram', 'bot_token', ''), - 'listen_bot_token': self.get('Telegram', 'listen_bot_token', ''), - 'test_bot_token': self.get('Telegram', 'test_bot_token', ''), - 'preview_link': self.getboolean('Telegram', 'preview_link', False), - 'main_public': self.get('Telegram', 'main_public', ''), - 'group_for_posts': self.getint('Telegram', 'group_for_posts', 0), - 'group_for_message': self.getint('Telegram', 'group_for_message', 0), - 'group_for_logs': self.getint('Telegram', 'group_for_logs', 0), - 'important_logs': self.getint('Telegram', 'important_logs', 0), - 'archive': self.getint('Telegram', 'archive', 0), - 'test_group': self.getint('Telegram', 'test_group', 0) - } - - # Settings секция - settings['Settings'] = { - 'logs': self.getboolean('Settings', 'logs', False), - 'test': self.getboolean('Settings', 'test', False) - } - - return settings - - -# Global config instance -_config_instance: Optional[ConfigManager] = None - - -def get_config() -> ConfigManager: - """Get global configuration instance.""" - global _config_instance - if _config_instance is None: - _config_instance = ConfigManager() - return _config_instance diff --git a/tests/test_settings.ini b/tests/test_settings.ini deleted file mode 100644 index e05cb42..0000000 --- a/tests/test_settings.ini +++ /dev/null @@ -1,13 +0,0 @@ -[Telegram] -bot_token = test_token_123 -preview_link = false -main_public = @test -group_for_posts = -1001234567890 -group_for_message = -1001234567891 -group_for_logs = -1001234567893 -important_logs = -1001234567894 -test_channel = -1001234567895 - -[Settings] -logs = true -test = false diff --git a/tests/test_voice_bot_architecture.py b/tests/test_voice_bot_architecture.py index a82214d..3bbf0a1 100644 --- a/tests/test_voice_bot_architecture.py +++ b/tests/test_voice_bot_architecture.py @@ -1,6 +1,7 @@ import pytest from unittest.mock import Mock, AsyncMock, patch from datetime import datetime +from pathlib import Path from helper_bot.handlers.voice.services import VoiceBotService from helper_bot.handlers.voice.exceptions import VoiceMessageError, AudioProcessingError @@ -88,6 +89,57 @@ class TestVoiceBotService: voice_service.clear_user_listenings(123) mock_bot_db.delete_listen_count_for_user.assert_called_once_with(123) + + def test_get_remaining_audio_count_success(self, voice_service, mock_bot_db): + """Тест получения количества оставшихся аудио""" + mock_bot_db.check_listen_audio.return_value = ['audio1', 'audio2', 'audio3'] + + result = voice_service.get_remaining_audio_count(123) + + assert result == 3 + mock_bot_db.check_listen_audio.assert_called_once_with(user_id=123) + + def test_get_remaining_audio_count_zero(self, voice_service, mock_bot_db): + """Тест получения количества оставшихся аудио когда их нет""" + mock_bot_db.check_listen_audio.return_value = [] + + result = voice_service.get_remaining_audio_count(123) + + assert result == 0 + mock_bot_db.check_listen_audio.assert_called_once_with(user_id=123) + + @pytest.mark.asyncio + async def test_send_welcome_messages_success(self, voice_service, mock_bot_db, mock_settings): + """Тест успешной отправки приветственных сообщений""" + mock_message = Mock() + mock_message.from_user.id = 123 + mock_message.answer = AsyncMock() + mock_message.answer.return_value = Mock() + mock_message.answer_sticker = AsyncMock() + + with patch.object(voice_service, 'get_welcome_sticker') as mock_sticker: + mock_sticker.return_value = 'test_sticker.tgs' + + await voice_service.send_welcome_messages(mock_message, '😊') + + # Проверяем, что сообщения отправлены + assert mock_message.answer.call_count >= 1 + + @pytest.mark.asyncio + async def test_send_welcome_messages_no_sticker(self, voice_service, mock_bot_db, mock_settings): + """Тест отправки приветственных сообщений без стикера""" + mock_message = Mock() + mock_message.from_user.id = 123 + mock_message.answer = AsyncMock() + mock_message.answer.return_value = Mock() + + with patch.object(voice_service, 'get_welcome_sticker') as mock_sticker: + mock_sticker.return_value = None + + await voice_service.send_welcome_messages(mock_message, '😊') + + # Проверяем, что сообщения отправлены + assert mock_message.answer.call_count >= 1 class TestVoiceHandlers: @@ -149,6 +201,7 @@ class TestUtils: """Тест валидации голосового сообщения""" mock_message = Mock() mock_message.content_type = 'voice' + mock_message.voice = Mock() result = validate_voice_message(mock_message) @@ -171,6 +224,22 @@ class TestUtils: assert result == "😊" mock_bot_db.check_emoji_for_user.assert_called_once_with(123) + + def test_get_user_emoji_safe_none(self, mock_bot_db): + """Тест безопасного получения эмодзи когда его нет""" + mock_bot_db.check_emoji_for_user.return_value = None + + result = get_user_emoji_safe(mock_bot_db, 123) + + assert result == "😊" + + def test_get_user_emoji_safe_error(self, mock_bot_db): + """Тест безопасного получения эмодзи при ошибке""" + mock_bot_db.check_emoji_for_user.return_value = "Ошибка" + + result = get_user_emoji_safe(mock_bot_db, 123) + + assert result == "Ошибка" class TestExceptions: diff --git a/tests/test_voice_constants.py b/tests/test_voice_constants.py new file mode 100644 index 0000000..cc6458b --- /dev/null +++ b/tests/test_voice_constants.py @@ -0,0 +1,170 @@ +import pytest +from helper_bot.handlers.voice.constants import ( + BUTTON_COMMAND_MAPPING, + COMMAND_MAPPING, + CALLBACK_COMMAND_MAPPING, + VOICE_BOT_NAME, + STATE_START, + STATE_STANDUP_WRITE, + BTN_SPEAK, + BTN_LISTEN, + CMD_START, + CMD_HELP, + CMD_RESTART, + CMD_EMOJI, + CMD_REFRESH, + CALLBACK_SAVE, + CALLBACK_DELETE +) + + +class TestVoiceConstants: + """Тесты для констант voice модуля""" + + def test_button_command_mapping_structure(self): + """Тест структуры BUTTON_COMMAND_MAPPING""" + assert isinstance(BUTTON_COMMAND_MAPPING, dict) + assert len(BUTTON_COMMAND_MAPPING) > 0 + + # Проверяем, что все значения являются строками + for key, value in BUTTON_COMMAND_MAPPING.items(): + assert isinstance(key, str) + assert isinstance(value, str) + + def test_button_command_mapping_specific_values(self): + """Тест конкретных значений в BUTTON_COMMAND_MAPPING""" + assert "🎤Высказаться" in BUTTON_COMMAND_MAPPING + assert "🎧Послушать" in BUTTON_COMMAND_MAPPING + + assert BUTTON_COMMAND_MAPPING["🎤Высказаться"] == "voice_speak" + assert BUTTON_COMMAND_MAPPING["🎧Послушать"] == "voice_listen" + + def test_command_mapping_structure(self): + """Тест структуры COMMAND_MAPPING""" + assert isinstance(COMMAND_MAPPING, dict) + assert len(COMMAND_MAPPING) > 0 + + # Проверяем, что все значения являются строками + for key, value in COMMAND_MAPPING.items(): + assert isinstance(key, str) + assert isinstance(value, str) + + def test_command_mapping_specific_values(self): + """Тест конкретных значений в COMMAND_MAPPING""" + assert "start" in COMMAND_MAPPING + assert "help" in COMMAND_MAPPING + assert "restart" in COMMAND_MAPPING + assert "emoji" in COMMAND_MAPPING + assert "refresh" in COMMAND_MAPPING + + assert COMMAND_MAPPING["start"] == "voice_start" + assert COMMAND_MAPPING["help"] == "voice_help" + assert COMMAND_MAPPING["restart"] == "voice_restart" + assert COMMAND_MAPPING["emoji"] == "voice_emoji" + assert COMMAND_MAPPING["refresh"] == "voice_refresh" + + def test_callback_command_mapping_structure(self): + """Тест структуры CALLBACK_COMMAND_MAPPING""" + assert isinstance(CALLBACK_COMMAND_MAPPING, dict) + assert len(CALLBACK_COMMAND_MAPPING) > 0 + + # Проверяем, что все значения являются строками + for key, value in CALLBACK_COMMAND_MAPPING.items(): + assert isinstance(key, str) + assert isinstance(value, str) + + def test_callback_command_mapping_specific_values(self): + """Тест конкретных значений в CALLBACK_COMMAND_MAPPING""" + assert "save" in CALLBACK_COMMAND_MAPPING + assert "delete" in CALLBACK_COMMAND_MAPPING + + assert CALLBACK_COMMAND_MAPPING["save"] == "voice_save" + assert CALLBACK_COMMAND_MAPPING["delete"] == "voice_delete" + + def test_voice_bot_name(self): + """Тест VOICE_BOT_NAME""" + assert isinstance(VOICE_BOT_NAME, str) + assert len(VOICE_BOT_NAME) > 0 + assert "voice" in VOICE_BOT_NAME.lower() + + def test_state_constants(self): + """Тест констант состояний""" + assert isinstance(STATE_START, str) + assert isinstance(STATE_STANDUP_WRITE, str) + assert len(STATE_START) > 0 + assert len(STATE_STANDUP_WRITE) > 0 + + def test_button_constants(self): + """Тест констант кнопок""" + assert isinstance(BTN_SPEAK, str) + assert isinstance(BTN_LISTEN, str) + assert len(BTN_SPEAK) > 0 + assert len(BTN_LISTEN) > 0 + + def test_command_constants(self): + """Тест констант команд""" + assert isinstance(CMD_START, str) + assert isinstance(CMD_HELP, str) + assert isinstance(CMD_RESTART, str) + assert isinstance(CMD_EMOJI, str) + assert isinstance(CMD_REFRESH, str) + + assert CMD_START == "start" + assert CMD_HELP == "help" + assert CMD_RESTART == "restart" + assert CMD_EMOJI == "emoji" + assert CMD_REFRESH == "refresh" + + def test_callback_constants(self): + """Тест констант callback""" + assert isinstance(CALLBACK_SAVE, str) + assert isinstance(CALLBACK_DELETE, str) + + assert CALLBACK_SAVE == "save" + assert CALLBACK_DELETE == "delete" + + def test_mapping_consistency(self): + """Тест согласованности маппингов""" + # Проверяем, что все ключи в маппингах соответствуют константам + assert "🎤Высказаться" in BUTTON_COMMAND_MAPPING + assert "🎧Послушать" in BUTTON_COMMAND_MAPPING + + assert "start" in COMMAND_MAPPING + assert "help" in COMMAND_MAPPING + assert "restart" in COMMAND_MAPPING + assert "emoji" in COMMAND_MAPPING + assert "refresh" in COMMAND_MAPPING + + assert "save" in CALLBACK_COMMAND_MAPPING + assert "delete" in CALLBACK_COMMAND_MAPPING + + def test_mapping_values_format(self): + """Тест формата значений в маппингах""" + # Проверяем, что все значения начинаются с 'voice_' + for value in BUTTON_COMMAND_MAPPING.values(): + assert value.startswith("voice_") + + for value in COMMAND_MAPPING.values(): + assert value.startswith("voice_") + + for value in CALLBACK_COMMAND_MAPPING.values(): + assert value.startswith("voice_") + + def test_no_duplicate_values(self): + """Тест отсутствия дублирующихся значений""" + button_values = list(BUTTON_COMMAND_MAPPING.values()) + command_values = list(COMMAND_MAPPING.values()) + callback_values = list(CALLBACK_COMMAND_MAPPING.values()) + + # Проверяем, что нет дублирующихся значений в каждом маппинге + assert len(button_values) == len(set(button_values)) + assert len(command_values) == len(set(command_values)) + assert len(callback_values) == len(set(callback_values)) + + # Проверяем, что нет дублирующихся значений между маппингами + all_values = button_values + command_values + callback_values + assert len(all_values) == len(set(all_values)) + + +if __name__ == '__main__': + pytest.main([__file__]) diff --git a/tests/test_voice_exceptions.py b/tests/test_voice_exceptions.py new file mode 100644 index 0000000..d60667d --- /dev/null +++ b/tests/test_voice_exceptions.py @@ -0,0 +1,211 @@ +import pytest +from helper_bot.handlers.voice.exceptions import ( + VoiceMessageError, + AudioProcessingError, + VoiceBotError +) + + +class TestVoiceExceptions: + """Тесты для исключений voice модуля""" + + def test_voice_message_error_inheritance(self): + """Тест наследования VoiceMessageError""" + assert issubclass(VoiceMessageError, Exception) + + def test_voice_message_error_message(self): + """Тест сообщения VoiceMessageError""" + error_message = "Тестовая ошибка голосового сообщения" + error = VoiceMessageError(error_message) + + assert str(error) == error_message + assert error.args == (error_message,) + + def test_voice_message_error_empty_message(self): + """Тест VoiceMessageError с пустым сообщением""" + error = VoiceMessageError("") + + assert str(error) == "" + assert error.args == ("",) + + def test_voice_message_error_none_message(self): + """Тест VoiceMessageError с None сообщением""" + error = VoiceMessageError(None) + + assert str(error) == "None" + assert error.args == (None,) + + def test_voice_message_error_multiple_args(self): + """Тест VoiceMessageError с несколькими аргументами""" + error = VoiceMessageError("Ошибка", "Дополнительная информация") + + assert str(error) == "('Ошибка', 'Дополнительная информация')" + assert error.args == ("Ошибка", "Дополнительная информация") + + def test_audio_processing_error_inheritance(self): + """Тест наследования AudioProcessingError""" + assert issubclass(AudioProcessingError, Exception) + + def test_audio_processing_error_message(self): + """Тест сообщения AudioProcessingError""" + error_message = "Ошибка обработки аудио файла" + error = AudioProcessingError(error_message) + + assert str(error) == error_message + assert error.args == (error_message,) + + def test_audio_processing_error_empty_message(self): + """Тест AudioProcessingError с пустым сообщением""" + error = AudioProcessingError("") + + assert str(error) == "" + assert error.args == ("",) + + def test_audio_processing_error_none_message(self): + """Тест AudioProcessingError с None сообщением""" + error = AudioProcessingError(None) + + assert str(error) == "None" + assert error.args == (None,) + + def test_voice_bot_error_inheritance(self): + """Тест наследования VoiceBotError""" + assert issubclass(VoiceBotError, Exception) + + def test_voice_bot_error_message(self): + """Тест сообщения VoiceBotError""" + error_message = "Общая ошибка voice бота" + error = VoiceBotError(error_message) + + assert str(error) == error_message + assert error.args == (error_message,) + + def test_exception_hierarchy(self): + """Тест иерархии исключений""" + # Проверяем, что все исключения наследуются от Exception + assert issubclass(VoiceMessageError, Exception) + assert issubclass(AudioProcessingError, Exception) + assert issubclass(VoiceBotError, Exception) + + # Проверяем, что VoiceMessageError и AudioProcessingError наследуются от VoiceBotError + assert issubclass(VoiceMessageError, VoiceBotError) + assert issubclass(AudioProcessingError, VoiceBotError) + + # Проверяем, что VoiceBotError не наследуется от других исключений + assert not issubclass(VoiceBotError, VoiceMessageError) + assert not issubclass(VoiceBotError, AudioProcessingError) + + # Проверяем, что VoiceMessageError и AudioProcessingError не наследуются друг от друга + assert not issubclass(VoiceMessageError, AudioProcessingError) + assert not issubclass(AudioProcessingError, VoiceMessageError) + + def test_exception_creation_without_args(self): + """Тест создания исключений без аргументов""" + # Должно работать без аргументов + voice_error = VoiceMessageError() + audio_error = AudioProcessingError() + bot_error = VoiceBotError() + + assert str(voice_error) == "" + assert str(audio_error) == "" + assert str(bot_error) == "" + + def test_exception_creation_with_int(self): + """Тест создания исключений с числовыми аргументами""" + voice_error = VoiceMessageError(123) + audio_error = AudioProcessingError(456) + bot_error = VoiceBotError(789) + + assert str(voice_error) == "123" + assert str(audio_error) == "456" + assert str(bot_error) == "789" + + def test_exception_creation_with_list(self): + """Тест создания исключений со списками""" + error_list = ["Ошибка 1", "Ошибка 2"] + voice_error = VoiceMessageError(error_list) + audio_error = AudioProcessingError(error_list) + bot_error = VoiceBotError(error_list) + + assert str(voice_error) == str(error_list) + assert str(audio_error) == str(error_list) + assert str(bot_error) == str(error_list) + + def test_exception_creation_with_dict(self): + """Тест создания исключений со словарями""" + error_dict = {"code": 500, "message": "Internal error"} + voice_error = VoiceMessageError(error_dict) + audio_error = AudioProcessingError(error_dict) + bot_error = VoiceBotError(error_dict) + + assert str(voice_error) == str(error_dict) + assert str(audio_error) == str(error_dict) + assert str(bot_error) == str(error_dict) + + def test_exception_attributes(self): + """Тест атрибутов исключений""" + error_message = "Тестовая ошибка" + voice_error = VoiceMessageError(error_message) + audio_error = AudioProcessingError(error_message) + bot_error = VoiceBotError(error_message) + + # Проверяем, что исключения имеют атрибут args + assert hasattr(voice_error, 'args') + assert hasattr(audio_error, 'args') + assert hasattr(bot_error, 'args') + + # Проверяем, что args содержит переданное сообщение + assert voice_error.args == (error_message,) + assert audio_error.args == (error_message,) + assert bot_error.args == (error_message,) + + def test_exception_string_representation(self): + """Тест строкового представления исключений""" + error_message = "Тестовая ошибка" + voice_error = VoiceMessageError(error_message) + audio_error = AudioProcessingError(error_message) + bot_error = VoiceBotError(error_message) + + # Проверяем, что str() возвращает сообщение + assert str(voice_error) == error_message + assert str(audio_error) == error_message + assert str(bot_error) == error_message + + # Проверяем, что repr() содержит имя класса + assert "VoiceMessageError" in repr(voice_error) + assert "AudioProcessingError" in repr(audio_error) + assert "VoiceBotError" in repr(bot_error) + + def test_exception_equality(self): + """Тест равенства исключений""" + error1 = VoiceMessageError("Ошибка") + error2 = VoiceMessageError("Ошибка") + error3 = VoiceMessageError("Другая ошибка") + + # Исключения с одинаковыми сообщениями не равны (разные объекты) + assert error1 != error2 + assert error1 != error3 + + # Но их строковые представления равны + assert str(error1) == str(error2) + assert str(error1) != str(error3) + + def test_exception_inheritance_chain(self): + """Тест цепочки наследования исключений""" + # Проверяем, что все исключения являются экземплярами Exception + voice_error = VoiceMessageError("Ошибка") + audio_error = AudioProcessingError("Ошибка") + bot_error = VoiceBotError("Ошибка") + + assert isinstance(voice_error, Exception) + assert isinstance(audio_error, Exception) + assert isinstance(bot_error, Exception) + + # Проверяем, что исключения являются экземплярами своих классов + assert isinstance(voice_error, VoiceMessageError) + assert isinstance(audio_error, AudioProcessingError) + assert isinstance(bot_error, VoiceBotError) + + +if __name__ == '__main__': + pytest.main([__file__]) diff --git a/tests/test_voice_handler.py b/tests/test_voice_handler.py new file mode 100644 index 0000000..43c5442 --- /dev/null +++ b/tests/test_voice_handler.py @@ -0,0 +1,117 @@ +import pytest +from unittest.mock import Mock, AsyncMock, patch, MagicMock +from aiogram import types +from aiogram.fsm.context import FSMContext + +from helper_bot.handlers.voice.voice_handler import VoiceHandlers +from helper_bot.handlers.voice.constants import STATE_START, STATE_STANDUP_WRITE + + +class TestVoiceHandler: + """Тесты для VoiceHandler""" + + @pytest.fixture + def mock_db(self): + """Мок для базы данных""" + mock_db = Mock() + mock_db.settings = { + 'Telegram': { + 'group_for_logs': 'test_logs_chat', + 'group_for_posts': 'test_posts_chat', + 'preview_link': True + } + } + return mock_db + + @pytest.fixture + def mock_settings(self): + """Мок для настроек""" + return { + 'Telegram': { + 'group_for_logs': 'test_logs_chat', + 'group_for_posts': 'test_posts_chat', + 'preview_link': True + } + } + + @pytest.fixture + def mock_message(self): + """Мок для сообщения""" + message = Mock(spec=types.Message) + message.from_user = Mock() + message.from_user.id = 123 + message.from_user.first_name = "Test" + message.from_user.full_name = "Test User" + message.chat = Mock() + message.chat.id = 456 + message.answer = AsyncMock() + message.forward = AsyncMock() + return message + + @pytest.fixture + def mock_state(self): + """Мок для состояния FSM""" + state = Mock(spec=FSMContext) + state.set_state = AsyncMock() + return state + + @pytest.fixture + def voice_handler(self, mock_db, mock_settings): + """Экземпляр VoiceHandler для тестов""" + return VoiceHandlers(mock_db, mock_settings) + + @pytest.mark.asyncio + async def test_voice_bot_button_handler_welcome_received(self, voice_handler, mock_message, mock_state, mock_db, mock_settings): + """Тест обработчика кнопки когда приветствие уже получено""" + mock_db.check_voice_bot_welcome_received.return_value = True + + with patch.object(voice_handler, 'restart_function') as mock_restart: + await voice_handler.voice_bot_button_handler(mock_message, mock_state, mock_db, mock_settings) + + mock_db.check_voice_bot_welcome_received.assert_called_once_with(123) + mock_restart.assert_called_once_with(mock_message, mock_state, mock_db, mock_settings) + + @pytest.mark.asyncio + async def test_voice_bot_button_handler_welcome_not_received(self, voice_handler, mock_message, mock_state, mock_db, mock_settings): + """Тест обработчика кнопки когда приветствие не получено""" + mock_db.check_voice_bot_welcome_received.return_value = False + + with patch.object(voice_handler, 'start') as mock_start: + await voice_handler.voice_bot_button_handler(mock_message, mock_state, mock_db, mock_settings) + + mock_db.check_voice_bot_welcome_received.assert_called_once_with(123) + mock_start.assert_called_once_with(mock_message, mock_state, mock_db, mock_settings) + + @pytest.mark.asyncio + async def test_voice_bot_button_handler_exception(self, voice_handler, mock_message, mock_state, mock_db, mock_settings): + """Тест обработчика кнопки при исключении""" + mock_db.check_voice_bot_welcome_received.side_effect = Exception("Test error") + + with patch.object(voice_handler, 'start') as mock_start: + await voice_handler.voice_bot_button_handler(mock_message, mock_state, mock_db, mock_settings) + + mock_start.assert_called_once_with(mock_message, mock_state, mock_db, mock_settings) + + # Упрощенные тесты для основных функций + @pytest.mark.asyncio + async def test_standup_write(self, voice_handler, mock_message, mock_state, mock_db, mock_settings): + """Тест функции standup_write""" + with patch('helper_bot.handlers.voice.voice_handler.messages.get_message') as mock_get_message: + mock_get_message.return_value = "Record voice message" + + await voice_handler.standup_write(mock_message, mock_state, mock_db, mock_settings) + + mock_state.set_state.assert_called_once_with(STATE_STANDUP_WRITE) + mock_message.answer.assert_called_once_with( + text="Record voice message", + reply_markup=types.ReplyKeyboardRemove() + ) + + def test_setup_handlers(self, voice_handler): + """Тест настройки обработчиков""" + # Проверяем, что роутер содержит обработчики + assert len(voice_handler.router.message.handlers) > 0 + + +if __name__ == '__main__': + pytest.main([__file__]) diff --git a/tests/test_voice_services.py b/tests/test_voice_services.py new file mode 100644 index 0000000..4ec48fe --- /dev/null +++ b/tests/test_voice_services.py @@ -0,0 +1,231 @@ +import pytest +from unittest.mock import Mock, AsyncMock, patch, MagicMock +from pathlib import Path +from datetime import datetime + +from helper_bot.handlers.voice.services import VoiceBotService +from helper_bot.handlers.voice.exceptions import VoiceMessageError, AudioProcessingError + + +class TestVoiceBotService: + """Тесты для VoiceBotService""" + + @pytest.fixture + def mock_bot_db(self): + """Мок для базы данных""" + mock_db = Mock() + mock_db.settings = { + 'Settings': {'logs': True}, + 'Telegram': {'important_logs': 'test_chat_id'} + } + return mock_db + + @pytest.fixture + def mock_settings(self): + """Мок для настроек""" + return { + 'Settings': {'logs': True}, + 'Telegram': {'preview_link': True} + } + + @pytest.fixture + def voice_service(self, mock_bot_db, mock_settings): + """Экземпляр VoiceBotService для тестов""" + return VoiceBotService(mock_bot_db, mock_settings) + + @pytest.mark.asyncio + async def test_get_welcome_sticker_success(self, voice_service, mock_settings): + """Тест успешного получения стикера""" + with patch('pathlib.Path.rglob') as mock_rglob: + mock_rglob.return_value = ['/path/to/sticker1.tgs', '/path/to/sticker2.tgs'] + + sticker = await voice_service.get_welcome_sticker() + + assert sticker is not None + mock_rglob.assert_called_once() + + @pytest.mark.asyncio + async def test_get_welcome_sticker_no_stickers(self, voice_service, mock_settings): + """Тест получения стикера когда их нет""" + with patch('pathlib.Path.rglob') as mock_rglob: + mock_rglob.return_value = [] + + sticker = await voice_service.get_welcome_sticker() + + assert sticker is None + + @pytest.mark.asyncio + async def test_get_welcome_sticker_only_webp_files(self, voice_service, mock_settings): + """Тест получения стикера когда есть только webp файлы""" + with patch('pathlib.Path.rglob') as mock_rglob: + mock_rglob.return_value = ['/path/to/sticker1.webp', '/path/to/sticker2.webp'] + + sticker = await voice_service.get_welcome_sticker() + + # Проверяем, что стикер не None (метод ищет файлы по паттерну Hello_*) + assert sticker is not None + + @pytest.mark.asyncio + async def test_get_welcome_sticker_mixed_files(self, voice_service, mock_settings): + """Тест получения стикера когда есть смешанные файлы""" + with patch('pathlib.Path.rglob') as mock_rglob: + mock_rglob.return_value = [ + '/path/to/sticker1.webp', + '/path/to/sticker2.tgs', + '/path/to/sticker3.webp' + ] + + sticker = await voice_service.get_welcome_sticker() + + assert sticker is not None + # Проверяем, что стикер не None (метод возвращает FSInputFile объект) + + def test_get_random_audio_success(self, voice_service, mock_bot_db): + """Тест успешного получения случайного аудио""" + mock_bot_db.check_listen_audio.return_value = ['audio1', 'audio2'] + mock_bot_db.get_user_id_by_file_name.return_value = 123 + mock_bot_db.get_date_by_file_name.return_value = '2025-01-01 12:00:00' + mock_bot_db.check_emoji_for_user.return_value = '😊' + + result = voice_service.get_random_audio(456) + + assert result is not None + assert len(result) == 3 + # Проверяем, что результат содержит ожидаемые данные, но не проверяем точное значение audio + assert result[0] in ['audio1', 'audio2'] + assert result[1] == '2025-01-01 12:00:00' + assert result[2] == '😊' + + def test_get_random_audio_no_audio(self, voice_service, mock_bot_db): + """Тест получения аудио когда их нет""" + mock_bot_db.check_listen_audio.return_value = [] + + result = voice_service.get_random_audio(456) + + assert result is None + + def test_get_random_audio_single_audio(self, voice_service, mock_bot_db): + """Тест получения аудио когда есть только одно""" + mock_bot_db.check_listen_audio.return_value = ['audio1'] + mock_bot_db.get_user_id_by_file_name.return_value = 123 + mock_bot_db.get_date_by_file_name.return_value = '2025-01-01 12:00:00' + mock_bot_db.check_emoji_for_user.return_value = '😊' + + result = voice_service.get_random_audio(456) + + assert result is not None + assert len(result) == 3 + assert result[0] == 'audio1' + + def test_mark_audio_as_listened_success(self, voice_service, mock_bot_db): + """Тест успешной пометки аудио как прослушанного""" + voice_service.mark_audio_as_listened('test_audio', 123) + + mock_bot_db.mark_listened_audio.assert_called_once_with('test_audio', user_id=123) + + def test_clear_user_listenings_success(self, voice_service, mock_bot_db): + """Тест успешной очистки прослушиваний""" + voice_service.clear_user_listenings(123) + + mock_bot_db.delete_listen_count_for_user.assert_called_once_with(123) + + def test_get_remaining_audio_count_success(self, voice_service, mock_bot_db): + """Тест получения количества оставшихся аудио""" + mock_bot_db.check_listen_audio.return_value = ['audio1', 'audio2', 'audio3'] + + result = voice_service.get_remaining_audio_count(123) + + assert result == 3 + mock_bot_db.check_listen_audio.assert_called_once_with(user_id=123) + + def test_get_remaining_audio_count_zero(self, voice_service, mock_bot_db): + """Тест получения количества оставшихся аудио когда их нет""" + mock_bot_db.check_listen_audio.return_value = [] + + result = voice_service.get_remaining_audio_count(123) + + assert result == 0 + mock_bot_db.check_listen_audio.assert_called_once_with(user_id=123) + + @pytest.mark.asyncio + async def test_send_welcome_messages_success(self, voice_service, mock_bot_db, mock_settings): + """Тест успешной отправки приветственных сообщений""" + mock_message = Mock() + mock_message.from_user.id = 123 + mock_message.answer = AsyncMock() + mock_message.answer.return_value = Mock() + mock_message.answer_sticker = AsyncMock() + + with patch.object(voice_service, 'get_welcome_sticker') as mock_sticker: + mock_sticker.return_value = 'test_sticker.tgs' + + await voice_service.send_welcome_messages(mock_message, '😊') + + # Проверяем, что сообщения отправлены + assert mock_message.answer.call_count >= 1 + + @pytest.mark.asyncio + async def test_send_welcome_messages_no_sticker(self, voice_service, mock_bot_db, mock_settings): + """Тест отправки приветственных сообщений без стикера""" + mock_message = Mock() + mock_message.from_user.id = 123 + mock_message.answer = AsyncMock() + mock_message.answer.return_value = Mock() + + with patch.object(voice_service, 'get_welcome_sticker') as mock_sticker: + mock_sticker.return_value = None + + await voice_service.send_welcome_messages(mock_message, '😊') + + # Проверяем, что сообщения отправлены + assert mock_message.answer.call_count >= 1 + + @pytest.mark.asyncio + async def test_send_welcome_messages_with_sticker(self, voice_service, mock_bot_db, mock_settings): + """Тест отправки приветственных сообщений со стикером""" + mock_message = Mock() + mock_message.from_user.id = 123 + mock_message.answer = AsyncMock() + mock_message.answer.return_value = Mock() + mock_message.answer_sticker = AsyncMock() + + with patch.object(voice_service, 'get_welcome_sticker') as mock_sticker: + mock_sticker.return_value = 'test_sticker.tgs' + + await voice_service.send_welcome_messages(mock_message, '😊') + + # Проверяем, что сообщения отправлены + assert mock_message.answer.call_count >= 1 + + @pytest.mark.asyncio + async def test_get_welcome_sticker_with_tgs_files(self, voice_service, mock_settings): + """Тест получения стикера когда есть .tgs файлы""" + with patch('pathlib.Path.rglob') as mock_rglob: + mock_rglob.return_value = ['/path/to/sticker1.tgs', '/path/to/sticker2.tgs'] + + sticker = await voice_service.get_welcome_sticker() + + assert sticker is not None + # Проверяем, что стикер не None (метод возвращает FSInputFile объект) + + def test_service_initialization(self, mock_bot_db, mock_settings): + """Тест инициализации сервиса""" + service = VoiceBotService(mock_bot_db, mock_settings) + + assert service.bot_db == mock_bot_db + assert service.settings == mock_settings + + def test_service_attributes(self, voice_service): + """Тест атрибутов сервиса""" + assert hasattr(voice_service, 'bot_db') + assert hasattr(voice_service, 'settings') + assert hasattr(voice_service, 'get_welcome_sticker') + assert hasattr(voice_service, 'get_random_audio') + assert hasattr(voice_service, 'mark_audio_as_listened') + assert hasattr(voice_service, 'clear_user_listenings') + assert hasattr(voice_service, 'get_remaining_audio_count') + assert hasattr(voice_service, 'send_welcome_messages') + + +if __name__ == '__main__': + pytest.main([__file__]) diff --git a/tests/test_voice_utils.py b/tests/test_voice_utils.py new file mode 100644 index 0000000..fb0f9c2 --- /dev/null +++ b/tests/test_voice_utils.py @@ -0,0 +1,200 @@ +import pytest +from unittest.mock import Mock, patch +from datetime import datetime, timedelta +from aiogram import types + +from helper_bot.handlers.voice.utils import ( + get_last_message_text, + validate_voice_message, + get_user_emoji_safe, + format_time_ago, + plural_time +) + + +class TestVoiceUtils: + """Тесты для утилит voice модуля""" + + @pytest.fixture + def mock_bot_db(self): + """Мок для базы данных""" + mock_db = Mock() + mock_db.settings = { + 'Telegram': { + 'group_for_logs': 'test_logs_chat' + } + } + return mock_db + + @pytest.fixture + def mock_message(self): + """Мок для сообщения""" + message = Mock(spec=types.Message) + message.from_user.id = 123 + message.from_user.first_name = "Test" + message.from_user.full_name = "Test User" + message.from_user.username = "testuser" + message.from_user.is_bot = False + message.from_user.language_code = "ru" + message.chat.id = 456 + return message + + def test_get_last_message_text(self, mock_bot_db): + """Тест получения последнего сообщения""" + mock_bot_db.last_date_audio.return_value = "2025-01-01 12:00:00" + + result = get_last_message_text(mock_bot_db) + + assert result is not None + assert "минут" in result or "часа" in result or "дня" in result + mock_bot_db.last_date_audio.assert_called_once() + + def test_validate_voice_message_valid(self): + """Тест валидации голосового сообщения""" + mock_message = Mock() + mock_message.content_type = 'voice' + mock_message.voice = Mock() + + result = validate_voice_message(mock_message) + + assert result is True + + def test_validate_voice_message_invalid(self): + """Тест валидации невалидного сообщения""" + mock_message = Mock() + mock_message.voice = None + + result = validate_voice_message(mock_message) + + assert result is False + + def test_get_user_emoji_safe_with_emoji(self, mock_bot_db): + """Тест безопасного получения эмодзи пользователя когда эмодзи есть""" + mock_bot_db.check_emoji_for_user.return_value = "😊" + + result = get_user_emoji_safe(mock_bot_db, 123) + + assert result == "😊" + mock_bot_db.check_emoji_for_user.assert_called_once_with(123) + + def test_get_user_emoji_safe_without_emoji(self, mock_bot_db): + """Тест безопасного получения эмодзи пользователя когда эмодзи нет""" + mock_bot_db.check_emoji_for_user.return_value = None + + result = get_user_emoji_safe(mock_bot_db, 123) + + assert result == "😊" + mock_bot_db.check_emoji_for_user.assert_called_once_with(123) + + def test_get_user_emoji_safe_with_empty_emoji(self, mock_bot_db): + """Тест безопасного получения эмодзи пользователя с пустым эмодзи""" + mock_bot_db.check_emoji_for_user.return_value = "" + + result = get_user_emoji_safe(mock_bot_db, 123) + + assert result == "😊" + mock_bot_db.check_emoji_for_user.assert_called_once_with(123) + + def test_get_user_emoji_safe_with_error(self, mock_bot_db): + """Тест безопасного получения эмодзи пользователя при ошибке""" + mock_bot_db.check_emoji_for_user.return_value = "Ошибка" + + result = get_user_emoji_safe(mock_bot_db, 123) + + assert result == "Ошибка" + mock_bot_db.check_emoji_for_user.assert_called_once_with(123) + + def test_format_time_ago_minutes(self): + """Тест форматирования времени в минутах""" + from datetime import datetime, timedelta + + # Создаем дату 30 минут назад + test_date = (datetime.now() - timedelta(minutes=30)).strftime("%Y-%m-%d %H:%M:%S") + + result = format_time_ago(test_date) + + assert result is not None + assert "минут" in result + assert "30" in result or "29" in result or "31" in result + + def test_format_time_ago_hours(self): + """Тест форматирования времени в часах""" + from datetime import datetime, timedelta + + # Создаем дату 2 часа назад + test_date = (datetime.now() - timedelta(hours=2)).strftime("%Y-%m-%d %H:%M:%S") + + result = format_time_ago(test_date) + + assert result is not None + assert "часа" in result or "часов" in result + + def test_format_time_ago_days(self): + """Тест форматирования времени в днях""" + from datetime import datetime, timedelta + + # Создаем дату 3 дня назад + test_date = (datetime.now() - timedelta(days=3)).strftime("%Y-%m-%d %H:%M:%S") + + result = format_time_ago(test_date) + + assert result is not None + assert "дня" in result or "дней" in result + + def test_format_time_ago_none(self): + """Тест форматирования времени с None""" + result = format_time_ago(None) + + assert result is None + + def test_format_time_ago_invalid_format(self): + """Тест форматирования времени с неверным форматом""" + result = format_time_ago("invalid_date_format") + + assert result is None + + def test_plural_time_minutes(self): + """Тест множественного числа для минут""" + assert "1 минуту" in plural_time(1, 1) + assert "2 минуты" in plural_time(1, 2) + assert "5 минут" in plural_time(1, 5) + assert "11 минут" in plural_time(1, 11) + assert "21 минуту" in plural_time(1, 21) + + def test_plural_time_hours(self): + """Тест множественного числа для часов""" + assert "1 час" in plural_time(2, 1) + assert "2 часа" in plural_time(2, 2) + assert "5 часов" in plural_time(2, 5) + assert "11 часов" in plural_time(2, 11) + assert "21 час" in plural_time(2, 21) + + def test_plural_time_days(self): + """Тест множественного числа для дней""" + assert "1 день" in plural_time(3, 1) + assert "2 дня" in plural_time(3, 2) + assert "5 дней" in plural_time(3, 5) + assert "11 дней" in plural_time(3, 11) + assert "21 день" in plural_time(3, 21) + + def test_plural_time_invalid_type(self): + """Тест множественного числа с неверным типом""" + result = plural_time(4, 5) + + assert result == "5" + + def test_plural_time_edge_cases(self): + """Тест граничных случаев для множественного числа""" + # Тест для 0 + assert "0 минут" in plural_time(1, 0) + assert "0 часов" in plural_time(2, 0) + assert "0 дней" in plural_time(3, 0) + + # Тест для больших чисел + assert "100 минут" in plural_time(1, 100) + assert "100 часов" in plural_time(2, 100) + assert "100 дней" in plural_time(3, 100) + + +if __name__ == '__main__': + pytest.main([__file__])