811 lines
35 KiB
Python
811 lines
35 KiB
Python
"""
|
||
Тесты для 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.exceptions import (
|
||
PostNotFoundError,
|
||
PublishError,
|
||
UserBlockedBotError,
|
||
UserNotFoundError,
|
||
)
|
||
from helper_bot.handlers.callback.services import BanService, PostPublishService
|
||
|
||
|
||
@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"
|