Переписал почти все тесты

feat: улучшено логирование и обработка скорингов в PostService и RagApiClient

- Добавлены отладочные сообщения для передачи скорингов в функции обработки постов.
- Обновлено логирование успешного получения скорингов из RAG API с дополнительной информацией.
- Оптимизирована обработка скорингов в функции get_text_message для улучшения отладки.
- Обновлены тесты для проверки новых функциональных возможностей и обработки ошибок.
This commit is contained in:
2026-01-30 00:55:47 +03:00
parent e87f4af82f
commit a5faa4bdc6
27 changed files with 4320 additions and 8 deletions

View File

@@ -0,0 +1,708 @@
"""
Тесты для helper_bot.handlers.callback.services: PostPublishService, BanService.
"""
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from aiogram.types import CallbackQuery, Message
from helper_bot.handlers.callback.constants import CONTENT_TYPE_MEDIA_GROUP
from helper_bot.handlers.callback.services import BanService, PostPublishService
from helper_bot.handlers.callback.exceptions import (
PostNotFoundError,
PublishError,
UserBlockedBotError,
UserNotFoundError,
)
@pytest.mark.unit
@pytest.mark.asyncio
class TestPostPublishService:
"""Тесты для PostPublishService."""
@pytest.fixture
def mock_bot(self):
bot = MagicMock()
bot.delete_message = AsyncMock()
return bot
@pytest.fixture
def mock_db(self):
db = MagicMock()
db.get_author_id_by_message_id = AsyncMock(return_value=123)
db.update_status_by_message_id = AsyncMock(return_value=1)
db.get_post_text_and_anonymity_by_message_id = AsyncMock(return_value=("text", False))
db.get_user_by_id = AsyncMock(return_value=MagicMock(first_name="U", username="u"))
db.update_published_message_id = AsyncMock()
db.get_post_content_by_message_id = AsyncMock(return_value=[("/path", "photo")])
db.add_published_post_content = AsyncMock(return_value=True)
db.get_post_text_by_message_id = AsyncMock(return_value="post text")
return db
@pytest.fixture
def settings(self):
return {
"Telegram": {
"group_for_posts": "-100",
"main_public": "-200",
"important_logs": "-300",
}
}
@pytest.fixture
def service(self, mock_bot, mock_db, settings):
return PostPublishService(mock_bot, mock_db, settings)
def test_get_bot_returns_bot_when_set(self, service, mock_bot):
"""_get_bot при установленном bot возвращает его."""
message = MagicMock()
assert service._get_bot(message) is mock_bot
def test_get_bot_returns_message_bot_when_bot_none(self, mock_db, settings):
"""_get_bot при bot=None возвращает message.bot."""
service = PostPublishService(None, mock_db, settings)
message = MagicMock()
message.bot = MagicMock()
assert service._get_bot(message) is message.bot
@pytest.fixture
def mock_call_text(self):
call = MagicMock(spec=CallbackQuery)
call.message = MagicMock(spec=Message)
call.message.message_id = 1
call.message.text = None
call.message.media_group_id = None
call.message.content_type = "text"
call.message.from_user = MagicMock()
call.message.from_user.full_name = "User"
call.message.from_user.id = 123
return call
@patch("helper_bot.handlers.callback.services.send_text_message")
@patch("helper_bot.handlers.callback.services.get_text_message")
async def test_publish_post_text_success(
self, mock_get_text, mock_send_text, service, mock_call_text, mock_db
):
"""publish_post для текстового поста вызывает _publish_text_post и отправляет в канал."""
mock_get_text.return_value = "Formatted"
sent = MagicMock()
sent.message_id = 999
mock_send_text.return_value = sent
await service.publish_post(mock_call_text)
mock_db.update_status_by_message_id.assert_awaited_once_with(1, "approved")
assert mock_send_text.await_count >= 1
mock_db.update_published_message_id.assert_awaited_once()
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_get_author_id_raises_when_not_found(self, mock_send, service, mock_db):
"""_get_author_id при отсутствии автора выбрасывает PostNotFoundError."""
mock_db.get_author_id_by_message_id = AsyncMock(return_value=None)
with pytest.raises(PostNotFoundError):
await service._get_author_id(1)
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_get_author_id_returns_author_id(self, mock_send, service, mock_db):
"""_get_author_id возвращает ID автора."""
mock_db.get_author_id_by_message_id = AsyncMock(return_value=456)
result = await service._get_author_id(1)
assert result == 456
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_get_author_id_for_media_group_returns_from_helper(self, mock_send, service, mock_db):
"""_get_author_id_for_media_group при нахождении по helper_id возвращает author_id."""
mock_db.get_author_id_by_helper_message_id = AsyncMock(return_value=789)
result = await service._get_author_id_for_media_group(100)
assert result == 789
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_train_on_published_skips_when_no_scoring_manager(self, mock_send, service, mock_db):
"""_train_on_published при отсутствии scoring_manager ничего не делает."""
service.scoring_manager = None
await service._train_on_published(1)
mock_db.get_post_text_by_message_id.assert_not_called()
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_train_on_published_calls_on_post_published(self, mock_send, service, mock_db):
"""_train_on_published при наличии scoring_manager вызывает on_post_published."""
mock_scoring = MagicMock()
mock_scoring.on_post_published = AsyncMock()
service.scoring_manager = mock_scoring
mock_db.get_post_text_by_message_id = AsyncMock(return_value="post text")
await service._train_on_published(1)
mock_scoring.on_post_published.assert_awaited_once_with("post text")
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_train_on_declined_skips_when_no_scoring_manager(self, mock_send, service):
"""_train_on_declined при отсутствии scoring_manager ничего не делает."""
service.scoring_manager = None
await service._train_on_declined(1)
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_save_published_post_content_copies_path(self, mock_send, service, mock_db):
"""_save_published_post_content копирует путь контента в published."""
published_message = MagicMock()
mock_db.get_post_content_by_message_id = AsyncMock(return_value=[("/path/file", "photo")])
mock_db.add_published_post_content = AsyncMock(return_value=True)
await service._save_published_post_content(published_message, 100, 1)
mock_db.add_published_post_content.assert_awaited_once_with(
published_message_id=100, content_path="/path/file", content_type="photo"
)
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_save_published_post_content_empty_content(self, mock_send, service, mock_db):
"""_save_published_post_content при пустом контенте не падает."""
published_message = MagicMock()
mock_db.get_post_content_by_message_id = AsyncMock(return_value=[])
await service._save_published_post_content(published_message, 100, 1)
mock_db.add_published_post_content.assert_not_called()
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_save_published_post_content_add_fails(self, mock_send, service, mock_db):
"""_save_published_post_content при add_published_post_content=False не падает."""
published_message = MagicMock()
mock_db.get_post_content_by_message_id = AsyncMock(return_value=[("/path", "photo")])
mock_db.add_published_post_content = AsyncMock(return_value=False)
await service._save_published_post_content(published_message, 100, 1)
mock_db.add_published_post_content.assert_awaited_once()
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_publish_post_unsupported_content_raises(self, mock_send, service, mock_call_text):
"""publish_post при неподдерживаемом типе контента выбрасывает PublishError."""
mock_call_text.message.content_type = "document"
with pytest.raises(PublishError, match="Неподдерживаемый тип контента"):
await service.publish_post(mock_call_text)
@patch("helper_bot.handlers.callback.services.send_photo_message")
@patch("helper_bot.handlers.callback.services.send_text_message")
@patch("helper_bot.handlers.callback.services.get_text_message")
async def test_publish_post_photo_success(
self, mock_get_text, mock_send_text, mock_send_photo, service, mock_db
):
"""publish_post для фото вызывает _publish_photo_post."""
mock_get_text.return_value = "Formatted"
call = MagicMock(spec=CallbackQuery)
call.message = MagicMock(spec=Message)
call.message.message_id = 1
call.message.text = None
call.message.media_group_id = None
call.message.content_type = "photo"
call.message.photo = [MagicMock(), MagicMock(file_id="fid")]
call.message.from_user = MagicMock(full_name="U", id=1)
sent = MagicMock()
sent.message_id = 999
mock_send_photo.return_value = sent
await service.publish_post(call)
mock_send_photo.assert_awaited_once()
mock_db.update_published_message_id.assert_awaited_once()
@patch("helper_bot.handlers.callback.services.send_video_message")
@patch("helper_bot.handlers.callback.services.send_text_message")
@patch("helper_bot.handlers.callback.services.get_text_message")
async def test_publish_post_video_success(
self, mock_get_text, mock_send_text, mock_send_video, service, mock_db
):
"""publish_post для видео вызывает _publish_video_post."""
mock_get_text.return_value = "Formatted"
call = MagicMock(spec=CallbackQuery)
call.message = MagicMock(spec=Message)
call.message.message_id = 1
call.message.text = None
call.message.media_group_id = None
call.message.content_type = "video"
call.message.video = MagicMock(file_id="vid")
call.message.from_user = MagicMock(full_name="U", id=1)
sent = MagicMock()
sent.message_id = 999
mock_send_video.return_value = sent
await service.publish_post(call)
mock_send_video.assert_awaited_once()
@patch("helper_bot.handlers.callback.services.send_video_note_message")
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_publish_post_video_note_success(
self, mock_send_text, mock_send_vn, service, mock_db
):
"""publish_post для кружка вызывает _publish_video_note_post."""
call = MagicMock(spec=CallbackQuery)
call.message = MagicMock(spec=Message)
call.message.message_id = 1
call.message.text = None
call.message.media_group_id = None
call.message.content_type = "video_note"
call.message.video_note = MagicMock(file_id="vnid")
call.message.from_user = MagicMock(full_name="U", id=1)
sent = MagicMock()
sent.message_id = 999
mock_send_vn.return_value = sent
await service.publish_post(call)
mock_send_vn.assert_awaited_once()
@patch("helper_bot.handlers.callback.services.send_audio_message")
@patch("helper_bot.handlers.callback.services.send_text_message")
@patch("helper_bot.handlers.callback.services.get_text_message")
async def test_publish_post_audio_success(
self, mock_get_text, mock_send_text, mock_send_audio, service, mock_db
):
"""publish_post для аудио вызывает _publish_audio_post."""
mock_get_text.return_value = "Formatted"
call = MagicMock(spec=CallbackQuery)
call.message = MagicMock(spec=Message)
call.message.message_id = 1
call.message.text = None
call.message.media_group_id = None
call.message.content_type = "audio"
call.message.audio = MagicMock(file_id="aid")
call.message.from_user = MagicMock(full_name="U", id=1)
sent = MagicMock()
sent.message_id = 999
mock_send_audio.return_value = sent
await service.publish_post(call)
mock_send_audio.assert_awaited_once()
@patch("helper_bot.handlers.callback.services.send_voice_message")
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_publish_post_voice_success(
self, mock_send_text, mock_send_voice, service, mock_db
):
"""publish_post для войса вызывает _publish_voice_post."""
call = MagicMock(spec=CallbackQuery)
call.message = MagicMock(spec=Message)
call.message.message_id = 1
call.message.text = None
call.message.media_group_id = None
call.message.content_type = "voice"
call.message.voice = MagicMock(file_id="vid")
call.message.from_user = MagicMock(full_name="U", id=1)
sent = MagicMock()
sent.message_id = 999
mock_send_voice.return_value = sent
await service.publish_post(call)
mock_send_voice.assert_awaited_once()
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_publish_text_post_updated_rows_zero_raises(self, mock_send, service, mock_call_text, mock_db):
"""_publish_text_post при updated_rows=0 выбрасывает PostNotFoundError."""
mock_db.update_status_by_message_id = AsyncMock(return_value=0)
with pytest.raises(PostNotFoundError, match="не найден в базе данных"):
await service._publish_text_post(mock_call_text)
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_publish_text_post_user_none_raises(self, mock_send, service, mock_call_text, mock_db):
"""_publish_text_post при отсутствии пользователя выбрасывает PostNotFoundError."""
mock_db.get_user_by_id = AsyncMock(return_value=None)
with pytest.raises(PostNotFoundError, match="не найден в базе данных"):
await service._publish_text_post(mock_call_text)
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_publish_text_post_raw_text_none_uses_empty(self, mock_send, service, mock_call_text, mock_db):
"""_publish_text_post при raw_text=None использует пустую строку."""
mock_db.get_post_text_and_anonymity_by_message_id = AsyncMock(return_value=(None, False))
sent = MagicMock()
sent.message_id = 999
mock_send.return_value = sent
await service._publish_text_post(mock_call_text)
mock_db.update_status_by_message_id.assert_awaited_once_with(1, "approved")
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_delete_post_and_notify_author_user_blocked_raises(self, mock_send, service, mock_call_text):
"""_delete_post_and_notify_author при заблокированном боте выбрасывает UserBlockedBotError."""
from helper_bot.handlers.callback.constants import ERROR_BOT_BLOCKED
mock_send.side_effect = Exception(ERROR_BOT_BLOCKED)
with pytest.raises(UserBlockedBotError):
await service._delete_post_and_notify_author(mock_call_text, 123)
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_train_on_published_skips_empty_text(self, mock_send, service, mock_db):
"""_train_on_published пропускает пустой текст."""
mock_scoring = MagicMock()
mock_scoring.on_post_published = AsyncMock()
service.scoring_manager = mock_scoring
mock_db.get_post_text_by_message_id = AsyncMock(return_value=" ")
await service._train_on_published(1)
mock_scoring.on_post_published.assert_not_called()
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_train_on_published_skips_caret(self, mock_send, service, mock_db):
"""_train_on_published пропускает текст '^'."""
mock_scoring = MagicMock()
mock_scoring.on_post_published = AsyncMock()
service.scoring_manager = mock_scoring
mock_db.get_post_text_by_message_id = AsyncMock(return_value="^")
await service._train_on_published(1)
mock_scoring.on_post_published.assert_not_called()
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_train_on_declined_calls_on_post_declined(self, mock_send, service, mock_db):
"""_train_on_declined вызывает on_post_declined."""
mock_scoring = MagicMock()
mock_scoring.on_post_declined = AsyncMock()
service.scoring_manager = mock_scoring
mock_db.get_post_text_by_message_id = AsyncMock(return_value="declined text")
await service._train_on_declined(1)
mock_scoring.on_post_declined.assert_awaited_once_with("declined text")
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_get_author_id_for_media_group_fallback_via_post_ids(self, mock_send, service, mock_db):
"""_get_author_id_for_media_group fallback через get_post_ids_from_telegram_by_last_id."""
mock_db.get_author_id_by_helper_message_id = AsyncMock(return_value=None)
mock_db.get_post_ids_from_telegram_by_last_id = AsyncMock(return_value=[50, 51])
mock_db.get_author_id_by_message_id = AsyncMock(side_effect=[None, 777])
result = await service._get_author_id_for_media_group(100)
assert result == 777
mock_db.get_author_id_by_message_id.assert_any_call(50)
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_get_author_id_for_media_group_fallback_direct(self, mock_send, service, mock_db):
"""_get_author_id_for_media_group fallback напрямую по message_id."""
mock_db.get_author_id_by_helper_message_id = AsyncMock(return_value=None)
mock_db.get_post_ids_from_telegram_by_last_id = AsyncMock(return_value=[])
mock_db.get_author_id_by_message_id = AsyncMock(return_value=888)
result = await service._get_author_id_for_media_group(100)
assert result == 888
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_get_author_id_for_media_group_raises_when_not_found(self, mock_send, service, mock_db):
"""_get_author_id_for_media_group при отсутствии автора выбрасывает PostNotFoundError."""
mock_db.get_author_id_by_helper_message_id = AsyncMock(return_value=None)
mock_db.get_post_ids_from_telegram_by_last_id = AsyncMock(return_value=[])
mock_db.get_author_id_by_message_id = AsyncMock(return_value=None)
with pytest.raises(PostNotFoundError, match="Автор не найден для медиагруппы"):
await service._get_author_id_for_media_group(100)
@patch("helper_bot.handlers.callback.services.send_media_group_to_channel")
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_publish_post_media_group_by_media_group_id(
self, mock_send_text, mock_send_media, service, mock_db
):
"""publish_post при media_group_id идёт в _publish_media_group."""
call = MagicMock(spec=CallbackQuery)
call.message = MagicMock(spec=Message)
call.message.message_id = 10
call.message.text = None
call.message.media_group_id = "mg_123"
call.message.from_user = MagicMock()
mock_db.get_post_ids_by_helper_id = AsyncMock(return_value=[1])
mock_db.get_post_content_by_helper_id = AsyncMock(return_value=[("/p", "photo")])
mock_db.get_post_text_and_anonymity_by_helper_id = AsyncMock(return_value=("", False))
mock_db.get_author_id_by_helper_message_id = AsyncMock(return_value=123)
mock_db.get_user_by_id = AsyncMock(return_value=MagicMock(first_name="U", username="u"))
mock_db.update_published_message_id = AsyncMock()
mock_db.update_status_for_media_group_by_helper_id = AsyncMock()
mock_db.get_post_content_by_message_id = AsyncMock(return_value=[("/path", "photo")])
mock_db.add_published_post_content = AsyncMock(return_value=True)
bot = MagicMock()
bot.delete_messages = AsyncMock()
bot.delete_message = AsyncMock()
call.message.bot = bot
service.bot = bot
mock_send_media.return_value = [MagicMock(message_id=101)]
mock_send_text.return_value = MagicMock()
await service.publish_post(call)
mock_send_media.assert_awaited_once()
@patch("helper_bot.handlers.callback.services.send_media_group_to_channel")
@patch("helper_bot.handlers.callback.services.send_text_message")
@patch("helper_bot.handlers.callback.services.get_text_message")
async def test_publish_media_group_success(
self, mock_get_text, mock_send_text, mock_send_media, service, mock_db
):
"""_publish_media_group успешно публикует медиагруппу."""
mock_get_text.return_value = "Formatted"
call = MagicMock(spec=CallbackQuery)
call.message = MagicMock(spec=Message)
call.message.message_id = 10
call.message.text = CONTENT_TYPE_MEDIA_GROUP # маршрутизация в _publish_media_group
call.message.from_user = MagicMock()
mock_db.get_post_ids_by_helper_id = AsyncMock(return_value=[1, 2])
mock_db.get_post_content_by_helper_id = AsyncMock(return_value=[("/p1", "photo"), ("/p2", "photo")])
mock_db.get_post_text_and_anonymity_by_helper_id = AsyncMock(return_value=("text", False))
mock_db.get_author_id_by_helper_message_id = AsyncMock(return_value=123)
mock_db.get_user_by_id = AsyncMock(return_value=MagicMock(first_name="U", username="u"))
mock_db.update_published_message_id = AsyncMock()
mock_db.update_status_for_media_group_by_helper_id = AsyncMock()
mock_db.get_post_content_by_message_id = AsyncMock(return_value=[("/path", "photo")])
mock_db.add_published_post_content = AsyncMock(return_value=True)
bot = MagicMock()
bot.delete_messages = AsyncMock()
bot.delete_message = AsyncMock()
call.message.bot = bot
service.bot = bot
sent_msgs = [MagicMock(message_id=101), MagicMock(message_id=102)]
mock_send_media.return_value = sent_msgs
mock_send_text.return_value = MagicMock()
await service.publish_post(call)
mock_send_media.assert_awaited_once()
mock_db.update_status_for_media_group_by_helper_id.assert_awaited_once_with(10, "approved")
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_publish_media_group_empty_ids_raises(self, mock_send, service, mock_db):
"""_publish_media_group при пустых media_group_message_ids выбрасывает PublishError."""
call = MagicMock(spec=CallbackQuery)
call.message = MagicMock(spec=Message)
call.message.message_id = 10
call.message.text = CONTENT_TYPE_MEDIA_GROUP
call.message.from_user = MagicMock()
mock_db.get_post_ids_by_helper_id = AsyncMock(return_value=[])
with pytest.raises(PublishError, match="Не найдены message_id медиагруппы"):
await service.publish_post(call)
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_decline_post_media_group_calls_decline_media_group(self, mock_send, service, mock_db):
"""decline_post для медиагруппы вызывает _decline_media_group."""
call = MagicMock(spec=CallbackQuery)
call.message = MagicMock(spec=Message)
call.message.message_id = 10
call.message.text = CONTENT_TYPE_MEDIA_GROUP
call.message.content_type = "text"
call.message.from_user = MagicMock(full_name="A", id=1)
mock_db.get_author_id_by_helper_message_id = AsyncMock(return_value=123)
mock_db.update_status_for_media_group_by_helper_id = AsyncMock()
mock_db.get_post_ids_by_helper_id = AsyncMock(return_value=[1, 2])
bot = MagicMock()
bot.delete_messages = AsyncMock()
call.message.bot = bot
service.bot = bot
mock_send.return_value = MagicMock()
await service.decline_post(call)
mock_db.update_status_for_media_group_by_helper_id.assert_awaited_once_with(10, "declined")
mock_send.assert_awaited_once()
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_decline_post_unsupported_type_raises(self, mock_send, service, mock_call_text):
"""decline_post при неподдерживаемом типе выбрасывает PublishError."""
mock_call_text.message.text = None
mock_call_text.message.content_type = "document"
with pytest.raises(PublishError, match="Неподдерживаемый тип контента для отклонения"):
await service.decline_post(mock_call_text)
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_decline_single_post_post_not_found_raises(self, mock_send, service, mock_call_text, mock_db):
"""_decline_single_post при updated_rows=0 выбрасывает PostNotFoundError."""
mock_db.update_status_by_message_id = AsyncMock(return_value=0)
with pytest.raises(PostNotFoundError, match="не найден в базе данных"):
await service._decline_single_post(mock_call_text)
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_decline_single_post_user_blocked_raises(self, mock_send, service, mock_call_text, mock_db):
"""_decline_single_post при заблокированном боте выбрасывает UserBlockedBotError."""
from helper_bot.handlers.callback.constants import ERROR_BOT_BLOCKED
mock_send.side_effect = Exception(ERROR_BOT_BLOCKED)
with pytest.raises(UserBlockedBotError):
await service._decline_single_post(mock_call_text)
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_decline_media_group_user_blocked_raises(self, mock_send, service, mock_db):
"""_decline_media_group при заблокированном боте выбрасывает UserBlockedBotError."""
from helper_bot.handlers.callback.constants import ERROR_BOT_BLOCKED
call = MagicMock(spec=CallbackQuery)
call.message = MagicMock(spec=Message)
call.message.message_id = 10
call.message.from_user = MagicMock()
mock_db.update_status_for_media_group_by_helper_id = AsyncMock()
mock_db.get_post_ids_by_helper_id = AsyncMock(return_value=[1])
mock_db.get_author_id_by_helper_message_id = AsyncMock(return_value=123)
bot = MagicMock()
bot.delete_messages = AsyncMock()
call.message.bot = bot
service.bot = bot
mock_send.side_effect = Exception(ERROR_BOT_BLOCKED)
with pytest.raises(UserBlockedBotError):
await service._decline_media_group(call)
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_delete_media_group_and_notify_author_success(self, mock_send, service, mock_db):
"""_delete_media_group_and_notify_author удаляет сообщения и уведомляет автора."""
call = MagicMock(spec=CallbackQuery)
call.message = MagicMock(spec=Message)
call.message.message_id = 10
mock_db.get_post_ids_by_helper_id = AsyncMock(return_value=[1, 2])
bot = MagicMock()
bot.delete_messages = AsyncMock()
call.message.bot = bot
service.bot = bot
mock_send.return_value = MagicMock()
await service._delete_media_group_and_notify_author(call, 123)
bot.delete_messages.assert_awaited_once()
mock_send.assert_awaited_once()
@pytest.mark.unit
@pytest.mark.asyncio
class TestBanService:
"""Тесты для BanService."""
@pytest.fixture
def mock_db(self):
db = MagicMock()
db.get_author_id_by_message_id = AsyncMock(return_value=111)
db.get_author_id_by_helper_message_id = AsyncMock(return_value=None)
db.set_user_blacklist = AsyncMock()
db.update_status_by_message_id = AsyncMock(return_value=1)
db.update_status_for_media_group_by_helper_id = AsyncMock(return_value=1)
db.get_username = AsyncMock(return_value="user")
return db
@pytest.fixture
def settings(self):
return {"Telegram": {"group_for_posts": "-100", "important_logs": "-200"}}
@pytest.fixture
def ban_service(self, mock_db, settings):
bot = MagicMock()
bot.delete_message = AsyncMock()
return BanService(bot, mock_db, settings)
def test_get_bot_returns_bot_when_set(self, ban_service):
"""_get_bot при установленном bot возвращает его."""
message = MagicMock()
assert ban_service._get_bot(message) is ban_service.bot
@pytest.fixture
def mock_call(self):
call = MagicMock(spec=CallbackQuery)
call.message = MagicMock()
call.message.message_id = 1
call.message.text = None
call.from_user = MagicMock()
call.from_user.id = 999
call.bot = MagicMock()
call.bot.delete_message = AsyncMock()
return call
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_ban_user_from_post_success(
self, mock_send, ban_service, mock_call, mock_db
):
"""ban_user_from_post устанавливает blacklist и обновляет статус поста."""
mock_call.message.text = None
await ban_service.ban_user_from_post(mock_call)
mock_db.set_user_blacklist.assert_awaited_once()
mock_db.update_status_by_message_id.assert_awaited_once_with(1, "declined")
mock_send.assert_awaited_once()
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_ban_user_from_post_media_group(
self, mock_send, ban_service, mock_call, mock_db
):
"""ban_user_from_post для медиагруппы обновляет статус по helper_id."""
mock_call.message.text = CONTENT_TYPE_MEDIA_GROUP
mock_call.message.message_id = 10
mock_db.get_author_id_by_helper_message_id = AsyncMock(return_value=111)
mock_db.get_author_id_by_message_id = AsyncMock(return_value=None)
mock_db.update_status_for_media_group_by_helper_id = AsyncMock(return_value=1)
await ban_service.ban_user_from_post(mock_call)
mock_db.set_user_blacklist.assert_awaited_once()
mock_db.update_status_for_media_group_by_helper_id.assert_awaited_once_with(10, "declined")
mock_send.assert_awaited_once()
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_ban_user_from_post_user_blocked_raises(
self, mock_send, ban_service, mock_call, mock_db
):
"""ban_user_from_post при заблокированном боте выбрасывает UserBlockedBotError."""
from helper_bot.handlers.callback.constants import ERROR_BOT_BLOCKED
mock_send.side_effect = Exception(ERROR_BOT_BLOCKED)
with pytest.raises(UserBlockedBotError):
await ban_service.ban_user_from_post(mock_call)
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_ban_user_from_post_author_not_found_raises(
self, mock_send, ban_service, mock_call, mock_db
):
"""ban_user_from_post при отсутствии автора выбрасывает UserNotFoundError."""
mock_db.get_author_id_by_message_id = AsyncMock(return_value=None)
mock_db.get_author_id_by_helper_message_id = AsyncMock(return_value=None)
with pytest.raises(UserNotFoundError, match="Автор не найден"):
await ban_service.ban_user_from_post(mock_call)
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_ban_user_raises_when_not_found(self, mock_send, ban_service, mock_db):
"""ban_user при отсутствии пользователя выбрасывает UserNotFoundError."""
mock_db.get_username = AsyncMock(return_value=None)
with pytest.raises(UserNotFoundError):
await ban_service.ban_user("999", "")
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_ban_user_returns_username(self, mock_send, ban_service, mock_db):
"""ban_user возвращает username пользователя."""
mock_db.get_username = AsyncMock(return_value="found_user")
result = await ban_service.ban_user("123", "")
assert result == "found_user"
@patch("helper_bot.handlers.callback.services.delete_user_blacklist", new_callable=AsyncMock)
async def test_unlock_user_raises_when_not_found(self, mock_delete, ban_service, mock_db):
"""unlock_user при отсутствии пользователя выбрасывает UserNotFoundError."""
mock_db.get_username = AsyncMock(return_value=None)
with pytest.raises(UserNotFoundError):
await ban_service.unlock_user("999")
@patch("helper_bot.handlers.callback.services.delete_user_blacklist", new_callable=AsyncMock)
async def test_unlock_user_returns_username(self, mock_delete, ban_service, mock_db):
"""unlock_user удаляет из blacklist и возвращает username."""
mock_db.get_username = AsyncMock(return_value="unlocked_user")
result = await ban_service.unlock_user("123")
mock_delete.assert_awaited_once()
assert result == "unlocked_user"