Dev 8 #10

Merged
KerradKerridi merged 15 commits from dev-8 into master 2025-09-03 22:00:36 +00:00
8 changed files with 998 additions and 104 deletions
Showing only changes of commit 013892dcb7 - Show all commits

View File

@@ -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

View File

@@ -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

View File

@@ -1,6 +1,7 @@
import pytest import pytest
from unittest.mock import Mock, AsyncMock, patch from unittest.mock import Mock, AsyncMock, patch
from datetime import datetime from datetime import datetime
from pathlib import Path
from helper_bot.handlers.voice.services import VoiceBotService from helper_bot.handlers.voice.services import VoiceBotService
from helper_bot.handlers.voice.exceptions import VoiceMessageError, AudioProcessingError from helper_bot.handlers.voice.exceptions import VoiceMessageError, AudioProcessingError
@@ -89,6 +90,57 @@ class TestVoiceBotService:
mock_bot_db.delete_listen_count_for_user.assert_called_once_with(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: class TestVoiceHandlers:
"""Тесты для VoiceHandlers""" """Тесты для VoiceHandlers"""
@@ -149,6 +201,7 @@ class TestUtils:
"""Тест валидации голосового сообщения""" """Тест валидации голосового сообщения"""
mock_message = Mock() mock_message = Mock()
mock_message.content_type = 'voice' mock_message.content_type = 'voice'
mock_message.voice = Mock()
result = validate_voice_message(mock_message) result = validate_voice_message(mock_message)
@@ -172,6 +225,22 @@ class TestUtils:
assert result == "😊" assert result == "😊"
mock_bot_db.check_emoji_for_user.assert_called_once_with(123) 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: class TestExceptions:
"""Тесты для исключений""" """Тесты для исключений"""

View File

@@ -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__])

View File

@@ -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__])

117
tests/test_voice_handler.py Normal file
View File

@@ -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__])

View File

@@ -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__])

200
tests/test_voice_utils.py Normal file
View File

@@ -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__])