348 lines
14 KiB
Python
348 lines
14 KiB
Python
import time
|
||
from datetime import datetime
|
||
from unittest.mock import AsyncMock, MagicMock, Mock, mock_open, patch
|
||
|
||
import pytest
|
||
|
||
from helper_bot.handlers.voice.exceptions import (DatabaseError,
|
||
FileOperationError)
|
||
from helper_bot.handlers.voice.services import AudioFileService
|
||
|
||
|
||
@pytest.fixture
|
||
def mock_bot_db():
|
||
"""Мок для базы данных"""
|
||
mock_db = Mock()
|
||
mock_db.get_user_audio_records_count = AsyncMock(return_value=0)
|
||
mock_db.get_path_for_audio_record = AsyncMock(return_value=None)
|
||
mock_db.add_audio_record_simple = AsyncMock()
|
||
return mock_db
|
||
|
||
|
||
@pytest.fixture
|
||
def audio_service(mock_bot_db):
|
||
"""Экземпляр AudioFileService для тестов"""
|
||
return AudioFileService(mock_bot_db)
|
||
|
||
|
||
@pytest.fixture
|
||
def sample_datetime():
|
||
"""Тестовая дата"""
|
||
return datetime(2025, 1, 15, 14, 30, 0)
|
||
|
||
|
||
@pytest.fixture
|
||
def mock_bot():
|
||
"""Мок для бота"""
|
||
bot = Mock()
|
||
bot.get_file = AsyncMock()
|
||
bot.download_file = AsyncMock()
|
||
return bot
|
||
|
||
|
||
@pytest.fixture
|
||
def mock_message():
|
||
"""Мок для сообщения"""
|
||
message = Mock()
|
||
message.voice = Mock()
|
||
message.voice.file_id = "test_file_id"
|
||
return message
|
||
|
||
|
||
@pytest.fixture
|
||
def mock_file_info():
|
||
"""Мок для информации о файле"""
|
||
file_info = Mock()
|
||
file_info.file_path = "voice/test_file_id.ogg"
|
||
return file_info
|
||
|
||
|
||
class TestGenerateFileName:
|
||
"""Тесты для метода generate_file_name"""
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_generate_file_name_first_record(self, audio_service, mock_bot_db):
|
||
"""Тест генерации имени файла для первой записи пользователя"""
|
||
mock_bot_db.get_user_audio_records_count.return_value = 0
|
||
|
||
result = await audio_service.generate_file_name(12345)
|
||
|
||
assert result == "message_from_12345_number_1"
|
||
mock_bot_db.get_user_audio_records_count.assert_called_once_with(user_id=12345)
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_generate_file_name_existing_records(
|
||
self, audio_service, mock_bot_db
|
||
):
|
||
"""Тест генерации имени файла для существующих записей"""
|
||
mock_bot_db.get_user_audio_records_count.return_value = 3
|
||
mock_bot_db.get_path_for_audio_record.return_value = (
|
||
"message_from_12345_number_3"
|
||
)
|
||
|
||
result = await audio_service.generate_file_name(12345)
|
||
|
||
assert result == "message_from_12345_number_4"
|
||
mock_bot_db.get_user_audio_records_count.assert_called_once_with(user_id=12345)
|
||
mock_bot_db.get_path_for_audio_record.assert_called_once_with(user_id=12345)
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_generate_file_name_no_last_record(self, audio_service, mock_bot_db):
|
||
"""Тест генерации имени файла когда нет последней записи"""
|
||
mock_bot_db.get_user_audio_records_count.return_value = 2
|
||
mock_bot_db.get_path_for_audio_record.return_value = None
|
||
|
||
result = await audio_service.generate_file_name(12345)
|
||
|
||
assert result == "message_from_12345_number_3"
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_generate_file_name_invalid_last_record_format(
|
||
self, audio_service, mock_bot_db
|
||
):
|
||
"""Тест генерации имени файла с некорректным форматом последней записи"""
|
||
mock_bot_db.get_user_audio_records_count.return_value = 2
|
||
mock_bot_db.get_path_for_audio_record.return_value = "invalid_format"
|
||
|
||
result = await audio_service.generate_file_name(12345)
|
||
|
||
assert result == "message_from_12345_number_3"
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_generate_file_name_exception_handling(
|
||
self, audio_service, mock_bot_db
|
||
):
|
||
"""Тест обработки исключений при генерации имени файла"""
|
||
mock_bot_db.get_user_audio_records_count.side_effect = Exception(
|
||
"Database error"
|
||
)
|
||
|
||
with pytest.raises(FileOperationError) as exc_info:
|
||
await audio_service.generate_file_name(12345)
|
||
|
||
assert "Не удалось сгенерировать имя файла" in str(exc_info.value)
|
||
|
||
|
||
class TestSaveAudioFile:
|
||
"""Тесты для метода save_audio_file"""
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_save_audio_file_success(
|
||
self, audio_service, mock_bot_db, sample_datetime
|
||
):
|
||
"""Тест успешного сохранения аудио файла"""
|
||
file_name = "test_audio"
|
||
user_id = 12345
|
||
file_id = "test_file_id"
|
||
|
||
# Мокаем verify_file_exists чтобы он возвращал True
|
||
with patch.object(audio_service, "verify_file_exists", return_value=True):
|
||
await audio_service.save_audio_file(
|
||
file_name, user_id, sample_datetime, file_id
|
||
)
|
||
|
||
mock_bot_db.add_audio_record_simple.assert_called_once_with(
|
||
file_name, user_id, sample_datetime
|
||
)
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_save_audio_file_with_string_date(self, audio_service, mock_bot_db):
|
||
"""Тест сохранения аудио файла со строковой датой"""
|
||
file_name = "test_audio"
|
||
user_id = 12345
|
||
date_string = "2025-01-15 14:30:00"
|
||
file_id = "test_file_id"
|
||
|
||
# Мокаем verify_file_exists чтобы он возвращал True
|
||
with patch.object(audio_service, "verify_file_exists", return_value=True):
|
||
await audio_service.save_audio_file(
|
||
file_name, user_id, date_string, file_id
|
||
)
|
||
|
||
mock_bot_db.add_audio_record_simple.assert_called_once_with(
|
||
file_name, user_id, date_string
|
||
)
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_save_audio_file_exception_handling(
|
||
self, audio_service, mock_bot_db, sample_datetime
|
||
):
|
||
"""Тест обработки исключений при сохранении аудио файла"""
|
||
mock_bot_db.add_audio_record_simple.side_effect = Exception("Database error")
|
||
|
||
# Мокаем verify_file_exists чтобы он возвращал True
|
||
with patch.object(audio_service, "verify_file_exists", return_value=True):
|
||
with pytest.raises(DatabaseError) as exc_info:
|
||
await audio_service.save_audio_file(
|
||
"test", 12345, sample_datetime, "file_id"
|
||
)
|
||
|
||
assert "Не удалось сохранить аудио файл в БД" in str(exc_info.value)
|
||
|
||
|
||
class TestDownloadAndSaveAudio:
|
||
"""Тесты для метода download_and_save_audio"""
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_download_and_save_audio_success(
|
||
self, audio_service, mock_bot, mock_message, mock_file_info
|
||
):
|
||
"""Тест успешного скачивания и сохранения аудио"""
|
||
mock_bot.get_file.return_value = mock_file_info
|
||
|
||
# Мокаем скачанный файл
|
||
mock_downloaded_file = Mock()
|
||
mock_downloaded_file.tell.return_value = 0
|
||
mock_downloaded_file.seek = Mock()
|
||
mock_downloaded_file.read.return_value = b"audio_data"
|
||
|
||
# Настраиваем поведение tell() для получения размера файла
|
||
def mock_tell():
|
||
return 0 if mock_downloaded_file.seek.call_count == 0 else 1024
|
||
|
||
mock_downloaded_file.tell = Mock(side_effect=mock_tell)
|
||
|
||
mock_bot.download_file.return_value = mock_downloaded_file
|
||
|
||
with patch("builtins.open", mock_open()) as mock_file:
|
||
with patch("os.makedirs"):
|
||
with patch("os.path.exists", return_value=True):
|
||
with patch("os.path.getsize", return_value=1024):
|
||
await audio_service.download_and_save_audio(
|
||
mock_bot, mock_message, "test_audio"
|
||
)
|
||
|
||
mock_bot.get_file.assert_called_once_with(
|
||
file_id="test_file_id"
|
||
)
|
||
mock_bot.download_file.assert_called_once_with(
|
||
file_path="voice/test_file_id.ogg"
|
||
)
|
||
mock_file.assert_called_once()
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_download_and_save_audio_no_message(self, audio_service, mock_bot):
|
||
"""Тест скачивания когда сообщение отсутствует."""
|
||
with patch(
|
||
"helper_bot.handlers.voice.services.asyncio.sleep", new_callable=AsyncMock
|
||
):
|
||
with pytest.raises(FileOperationError) as exc_info:
|
||
await audio_service.download_and_save_audio(
|
||
mock_bot, None, "test_audio"
|
||
)
|
||
|
||
assert "Сообщение или голосовое сообщение не найдено" in str(exc_info.value)
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_download_and_save_audio_no_voice(self, audio_service, mock_bot):
|
||
"""Тест скачивания когда у сообщения нет voice атрибута."""
|
||
message = Mock()
|
||
message.voice = None
|
||
|
||
with patch(
|
||
"helper_bot.handlers.voice.services.asyncio.sleep", new_callable=AsyncMock
|
||
):
|
||
with pytest.raises(FileOperationError) as exc_info:
|
||
await audio_service.download_and_save_audio(
|
||
mock_bot, message, "test_audio"
|
||
)
|
||
|
||
assert "Сообщение или голосовое сообщение не найдено" in str(exc_info.value)
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_download_and_save_audio_download_failed(
|
||
self, audio_service, mock_bot, mock_message, mock_file_info
|
||
):
|
||
"""Тест скачивания когда загрузка не удалась."""
|
||
mock_bot.get_file.return_value = mock_file_info
|
||
mock_bot.download_file.return_value = None
|
||
|
||
with patch(
|
||
"helper_bot.handlers.voice.services.asyncio.sleep", new_callable=AsyncMock
|
||
):
|
||
with pytest.raises(FileOperationError) as exc_info:
|
||
await audio_service.download_and_save_audio(
|
||
mock_bot, mock_message, "test_audio"
|
||
)
|
||
|
||
assert "Не удалось скачать файл" in str(exc_info.value)
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_download_and_save_audio_exception_handling(
|
||
self, audio_service, mock_bot, mock_message
|
||
):
|
||
"""Тест обработки исключений при скачивании."""
|
||
mock_bot.get_file.side_effect = Exception("Network error")
|
||
|
||
with patch(
|
||
"helper_bot.handlers.voice.services.asyncio.sleep", new_callable=AsyncMock
|
||
):
|
||
with pytest.raises(FileOperationError) as exc_info:
|
||
await audio_service.download_and_save_audio(
|
||
mock_bot, mock_message, "test_audio"
|
||
)
|
||
|
||
assert "Не удалось скачать и сохранить аудио" in str(exc_info.value)
|
||
|
||
|
||
class TestAudioFileServiceIntegration:
|
||
"""Интеграционные тесты для AudioFileService"""
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_full_audio_processing_workflow(self, mock_bot_db):
|
||
"""Тест полного рабочего процесса обработки аудио"""
|
||
service = AudioFileService(mock_bot_db)
|
||
|
||
# Настраиваем моки
|
||
mock_bot_db.get_user_audio_records_count.return_value = 1
|
||
mock_bot_db.get_path_for_audio_record.return_value = (
|
||
"message_from_12345_number_1"
|
||
)
|
||
mock_bot_db.add_audio_record_simple = AsyncMock()
|
||
|
||
# Тестируем генерацию имени файла
|
||
file_name = await service.generate_file_name(12345)
|
||
assert file_name == "message_from_12345_number_2"
|
||
|
||
# Тестируем сохранение в БД
|
||
test_date = datetime.now()
|
||
with patch.object(service, "verify_file_exists", return_value=True):
|
||
await service.save_audio_file(file_name, 12345, test_date, "test_file_id")
|
||
|
||
# Проверяем вызовы
|
||
mock_bot_db.get_user_audio_records_count.assert_called_once_with(user_id=12345)
|
||
mock_bot_db.get_path_for_audio_record.assert_called_once_with(user_id=12345)
|
||
mock_bot_db.add_audio_record_simple.assert_called_once_with(
|
||
file_name, 12345, test_date
|
||
)
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_file_name_generation_sequence(self, mock_bot_db):
|
||
"""Тест последовательности генерации имен файлов"""
|
||
service = AudioFileService(mock_bot_db)
|
||
|
||
# Первая запись
|
||
mock_bot_db.get_user_audio_records_count.return_value = 0
|
||
file_name_1 = await service.generate_file_name(12345)
|
||
assert file_name_1 == "message_from_12345_number_1"
|
||
|
||
# Вторая запись
|
||
mock_bot_db.get_user_audio_records_count.return_value = 1
|
||
mock_bot_db.get_path_for_audio_record.return_value = (
|
||
"message_from_12345_number_1"
|
||
)
|
||
file_name_2 = await service.generate_file_name(12345)
|
||
assert file_name_2 == "message_from_12345_number_2"
|
||
|
||
# Третья запись
|
||
mock_bot_db.get_user_audio_records_count.return_value = 2
|
||
mock_bot_db.get_path_for_audio_record.return_value = (
|
||
"message_from_12345_number_2"
|
||
)
|
||
file_name_3 = await service.generate_file_name(12345)
|
||
assert file_name_3 == "message_from_12345_number_3"
|
||
|
||
|
||
if __name__ == "__main__":
|
||
pytest.main([__file__])
|