fix linter, fix ci, fix tests

This commit is contained in:
2026-02-02 00:46:44 +03:00
parent 68041037bd
commit d87d4e492e
93 changed files with 1042 additions and 862 deletions

View File

@@ -6,15 +6,13 @@ 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,
)
from helper_bot.handlers.callback.exceptions import (PostNotFoundError,
PublishError,
UserBlockedBotError,
UserNotFoundError)
from helper_bot.handlers.callback.services import (BanService,
PostPublishService)
@pytest.mark.unit
@@ -33,8 +31,12 @@ class TestPostPublishService:
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.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)
@@ -98,7 +100,9 @@ class TestPostPublishService:
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):
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)
@@ -113,21 +117,27 @@ class TestPostPublishService:
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):
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):
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):
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()
@@ -139,16 +149,22 @@ class TestPostPublishService:
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):
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):
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.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)
@@ -158,7 +174,9 @@ class TestPostPublishService:
)
@patch("helper_bot.handlers.callback.services.send_text_message")
async def test_save_published_post_content_empty_content(self, mock_send, service, mock_db):
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=[])
@@ -168,10 +186,14 @@ class TestPostPublishService:
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):
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.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)
@@ -179,7 +201,9 @@ class TestPostPublishService:
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):
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"
@@ -304,7 +328,9 @@ class TestPostPublishService:
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):
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)
@@ -312,7 +338,9 @@ class TestPostPublishService:
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):
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)
@@ -320,9 +348,13 @@ class TestPostPublishService:
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):
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))
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
@@ -332,16 +364,21 @@ class TestPostPublishService:
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):
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):
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()
@@ -365,7 +402,9 @@ class TestPostPublishService:
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):
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()
@@ -377,7 +416,9 @@ class TestPostPublishService:
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):
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])
@@ -389,7 +430,9 @@ class TestPostPublishService:
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):
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=[])
@@ -400,7 +443,9 @@ class TestPostPublishService:
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):
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=[])
@@ -422,13 +467,21 @@ class TestPostPublishService:
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_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.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.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()
@@ -453,16 +506,26 @@ class TestPostPublishService:
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.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_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.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.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()
@@ -476,10 +539,14 @@ class TestPostPublishService:
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")
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):
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)
@@ -492,7 +559,9 @@ class TestPostPublishService:
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):
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)
@@ -511,20 +580,28 @@ class TestPostPublishService:
await service.decline_post(call)
mock_db.update_status_for_media_group_by_helper_id.assert_awaited_once_with(10, "declined")
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):
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="Неподдерживаемый тип контента для отклонения"):
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):
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)
@@ -532,18 +609,24 @@ class TestPostPublishService:
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):
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):
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
@@ -561,7 +644,9 @@ class TestPostPublishService:
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):
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)
@@ -649,7 +734,9 @@ class TestBanService:
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_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")
@@ -658,6 +745,7 @@ class TestBanService:
):
"""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):
@@ -675,7 +763,9 @@ class TestBanService:
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):
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)
@@ -689,16 +779,26 @@ class TestBanService:
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):
@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):
@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")