fix linter, fix ci, fix tests
This commit is contained in:
@@ -13,10 +13,10 @@ if str(_project_root) not in sys.path:
|
||||
import pytest
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import Chat, Message, User
|
||||
from database.async_db import AsyncBotDB
|
||||
|
||||
# Импортируем моки в самом начале
|
||||
import tests.mocks
|
||||
from database.async_db import AsyncBotDB
|
||||
|
||||
# Настройка pytest-asyncio
|
||||
pytest_plugins = ("pytest_asyncio",)
|
||||
|
||||
@@ -3,7 +3,6 @@ import tempfile
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from database.models import UserMessage
|
||||
from database.repositories.message_repository import MessageRepository
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ from datetime import datetime
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from database.models import MessageContentLink, PostContent, TelegramPost
|
||||
from database.repositories.post_repository import PostRepository
|
||||
|
||||
|
||||
@@ -5,12 +5,8 @@
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from helper_bot.handlers.admin.dependencies import (
|
||||
AdminAccessMiddleware,
|
||||
get_bot_db,
|
||||
get_settings,
|
||||
)
|
||||
from helper_bot.handlers.admin.dependencies import (AdminAccessMiddleware,
|
||||
get_bot_db, get_settings)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -28,8 +24,12 @@ class TestAdminAccessMiddleware:
|
||||
"""Мок handler."""
|
||||
return AsyncMock(return_value="handler_result")
|
||||
|
||||
@patch("helper_bot.handlers.admin.dependencies.check_access", new_callable=AsyncMock)
|
||||
async def test_access_granted_calls_handler(self, mock_check_access, middleware, mock_handler):
|
||||
@patch(
|
||||
"helper_bot.handlers.admin.dependencies.check_access", new_callable=AsyncMock
|
||||
)
|
||||
async def test_access_granted_calls_handler(
|
||||
self, mock_check_access, middleware, mock_handler
|
||||
):
|
||||
"""При доступе разрешён вызывается handler с event и data."""
|
||||
mock_check_access.return_value = True
|
||||
event = MagicMock()
|
||||
@@ -44,7 +44,9 @@ class TestAdminAccessMiddleware:
|
||||
mock_handler.assert_awaited_once_with(event, data)
|
||||
assert result == "handler_result"
|
||||
|
||||
@patch("helper_bot.handlers.admin.dependencies.check_access", new_callable=AsyncMock)
|
||||
@patch(
|
||||
"helper_bot.handlers.admin.dependencies.check_access", new_callable=AsyncMock
|
||||
)
|
||||
async def test_access_denied_answers_and_does_not_call_handler(
|
||||
self, mock_check_access, middleware, mock_handler
|
||||
):
|
||||
@@ -64,7 +66,9 @@ class TestAdminAccessMiddleware:
|
||||
mock_handler.assert_not_awaited()
|
||||
assert result is None
|
||||
|
||||
@patch("helper_bot.handlers.admin.dependencies.check_access", new_callable=AsyncMock)
|
||||
@patch(
|
||||
"helper_bot.handlers.admin.dependencies.check_access", new_callable=AsyncMock
|
||||
)
|
||||
@patch("helper_bot.handlers.admin.dependencies.get_global_instance")
|
||||
async def test_fallback_get_db_from_global_when_bot_db_missing(
|
||||
self, mock_get_global, mock_check_access, middleware, mock_handler
|
||||
@@ -86,11 +90,17 @@ class TestAdminAccessMiddleware:
|
||||
mock_bdf.get_db.assert_called_once()
|
||||
mock_check_access.assert_awaited_once_with(1, mock_bdf.get_db.return_value)
|
||||
|
||||
@patch("helper_bot.handlers.admin.dependencies.check_access", new_callable=AsyncMock)
|
||||
async def test_event_without_from_user_calls_handler(self, mock_check_access, middleware, mock_handler):
|
||||
@patch(
|
||||
"helper_bot.handlers.admin.dependencies.check_access", new_callable=AsyncMock
|
||||
)
|
||||
async def test_event_without_from_user_calls_handler(
|
||||
self, mock_check_access, middleware, mock_handler
|
||||
):
|
||||
"""Если у event нет from_user, handler вызывается (проверка доступа не выполняется)."""
|
||||
|
||||
class EventWithoutUser:
|
||||
pass
|
||||
|
||||
event = EventWithoutUser()
|
||||
data = {}
|
||||
|
||||
@@ -100,7 +110,9 @@ class TestAdminAccessMiddleware:
|
||||
mock_handler.assert_awaited_once_with(event, data)
|
||||
assert result == "handler_result"
|
||||
|
||||
@patch("helper_bot.handlers.admin.dependencies.check_access", new_callable=AsyncMock)
|
||||
@patch(
|
||||
"helper_bot.handlers.admin.dependencies.check_access", new_callable=AsyncMock
|
||||
)
|
||||
async def test_handler_typeerror_missing_data_calls_handler_without_data(
|
||||
self, mock_check_access, middleware
|
||||
):
|
||||
@@ -127,8 +139,12 @@ class TestAdminAccessMiddleware:
|
||||
assert call_count == 2
|
||||
assert result == "ok"
|
||||
|
||||
@patch("helper_bot.handlers.admin.dependencies.check_access", new_callable=AsyncMock)
|
||||
async def test_handler_other_exception_reraises(self, mock_check_access, middleware):
|
||||
@patch(
|
||||
"helper_bot.handlers.admin.dependencies.check_access", new_callable=AsyncMock
|
||||
)
|
||||
async def test_handler_other_exception_reraises(
|
||||
self, mock_check_access, middleware
|
||||
):
|
||||
"""При другом исключении в handler оно пробрасывается."""
|
||||
mock_check_access.return_value = True
|
||||
|
||||
|
||||
@@ -7,19 +7,16 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
import pytest
|
||||
from aiogram import types
|
||||
from aiogram.fsm.context import FSMContext
|
||||
|
||||
from helper_bot.handlers.admin.admin_handlers import (
|
||||
admin_panel,
|
||||
cancel_ban_process,
|
||||
confirm_ban,
|
||||
get_banned_users,
|
||||
get_last_users,
|
||||
get_ml_stats,
|
||||
process_ban_duration,
|
||||
process_ban_reason,
|
||||
process_ban_target,
|
||||
start_ban_process,
|
||||
)
|
||||
from helper_bot.handlers.admin.admin_handlers import (admin_panel,
|
||||
cancel_ban_process,
|
||||
confirm_ban,
|
||||
get_banned_users,
|
||||
get_last_users,
|
||||
get_ml_stats,
|
||||
process_ban_duration,
|
||||
process_ban_reason,
|
||||
process_ban_target,
|
||||
start_ban_process)
|
||||
from helper_bot.handlers.admin.services import User as AdminUser
|
||||
|
||||
|
||||
@@ -61,7 +58,9 @@ class TestAdminHandlers:
|
||||
return db
|
||||
|
||||
@patch("helper_bot.handlers.admin.admin_handlers.get_reply_keyboard_admin")
|
||||
async def test_admin_panel_sets_state_and_answers(self, mock_keyboard, mock_message, mock_state):
|
||||
async def test_admin_panel_sets_state_and_answers(
|
||||
self, mock_keyboard, mock_message, mock_state
|
||||
):
|
||||
"""admin_panel устанавливает состояние ADMIN и отправляет приветствие."""
|
||||
mock_keyboard.return_value = MagicMock()
|
||||
|
||||
@@ -69,10 +68,18 @@ class TestAdminHandlers:
|
||||
|
||||
mock_state.set_state.assert_awaited_once_with("ADMIN")
|
||||
mock_message.answer.assert_awaited_once()
|
||||
assert "админк" in mock_message.answer.call_args[0][0].lower() or "добро" in mock_message.answer.call_args[0][0].lower()
|
||||
assert (
|
||||
"админк" in mock_message.answer.call_args[0][0].lower()
|
||||
or "добро" in mock_message.answer.call_args[0][0].lower()
|
||||
)
|
||||
|
||||
@patch("helper_bot.handlers.admin.admin_handlers.return_to_admin_menu", new_callable=AsyncMock)
|
||||
async def test_cancel_ban_process_returns_to_menu(self, mock_return, mock_message, mock_state):
|
||||
@patch(
|
||||
"helper_bot.handlers.admin.admin_handlers.return_to_admin_menu",
|
||||
new_callable=AsyncMock,
|
||||
)
|
||||
async def test_cancel_ban_process_returns_to_menu(
|
||||
self, mock_return, mock_message, mock_state
|
||||
):
|
||||
"""cancel_ban_process вызывает return_to_admin_menu."""
|
||||
mock_state.get_state = AsyncMock(return_value="AWAIT_BAN_TARGET")
|
||||
|
||||
@@ -101,7 +108,10 @@ class TestAdminHandlers:
|
||||
mock_service.get_last_users.assert_awaited_once()
|
||||
mock_keyboard.assert_called_once()
|
||||
mock_message.answer.assert_awaited_once()
|
||||
assert "Список пользователей" in mock_message.answer.call_args[1]["text"] or "пользователей" in mock_message.answer.call_args[1]["text"]
|
||||
assert (
|
||||
"Список пользователей" in mock_message.answer.call_args[1]["text"]
|
||||
or "пользователей" in mock_message.answer.call_args[1]["text"]
|
||||
)
|
||||
|
||||
@patch("helper_bot.handlers.admin.admin_handlers.create_keyboard_with_pagination")
|
||||
@patch("helper_bot.handlers.admin.admin_handlers.AdminService")
|
||||
@@ -110,16 +120,23 @@ class TestAdminHandlers:
|
||||
):
|
||||
"""get_banned_users при пустом списке отправляет сообщение 'никого нет'."""
|
||||
mock_service = MagicMock()
|
||||
mock_service.get_banned_users_for_display = AsyncMock(return_value=("Текст", []))
|
||||
mock_service.get_banned_users_for_display = AsyncMock(
|
||||
return_value=("Текст", [])
|
||||
)
|
||||
mock_service_cls.return_value = mock_service
|
||||
|
||||
await get_banned_users(mock_message, mock_state, bot_db=mock_bot_db)
|
||||
|
||||
mock_message.answer.assert_awaited_once()
|
||||
assert "никого нет" in mock_message.answer.call_args[1]["text"] or "заблокированных" in mock_message.answer.call_args[1]["text"]
|
||||
assert (
|
||||
"никого нет" in mock_message.answer.call_args[1]["text"]
|
||||
or "заблокированных" in mock_message.answer.call_args[1]["text"]
|
||||
)
|
||||
|
||||
@patch("helper_bot.handlers.admin.admin_handlers.get_global_instance")
|
||||
async def test_get_ml_stats_disabled_answers_message(self, mock_get_global, mock_message, mock_state):
|
||||
async def test_get_ml_stats_disabled_answers_message(
|
||||
self, mock_get_global, mock_message, mock_state
|
||||
):
|
||||
"""get_ml_stats при отключённом scoring_manager отправляет сообщение об отключении."""
|
||||
mock_bdf = MagicMock()
|
||||
mock_bdf.get_scoring_manager.return_value = None
|
||||
@@ -128,16 +145,31 @@ class TestAdminHandlers:
|
||||
await get_ml_stats(mock_message, mock_state)
|
||||
|
||||
mock_message.answer.assert_awaited_once()
|
||||
assert "ML" in mock_message.answer.call_args[0][0] or "RAG" in mock_message.answer.call_args[0][0] or "отключен" in mock_message.answer.call_args[0][0].lower()
|
||||
assert (
|
||||
"ML" in mock_message.answer.call_args[0][0]
|
||||
or "RAG" in mock_message.answer.call_args[0][0]
|
||||
or "отключен" in mock_message.answer.call_args[0][0].lower()
|
||||
)
|
||||
|
||||
@patch("helper_bot.handlers.admin.admin_handlers.get_global_instance")
|
||||
async def test_get_ml_stats_with_rag_and_deepseek(self, mock_get_global, mock_message, mock_state):
|
||||
async def test_get_ml_stats_with_rag_and_deepseek(
|
||||
self, mock_get_global, mock_message, mock_state
|
||||
):
|
||||
"""get_ml_stats при включённом scoring возвращает статистику."""
|
||||
mock_scoring = MagicMock()
|
||||
mock_scoring.get_stats = AsyncMock(return_value={
|
||||
"rag": {"model_loaded": True, "vector_store": {"positive_count": 1, "negative_count": 0, "total_count": 1}},
|
||||
"deepseek": {"enabled": True, "model": "test", "timeout": 30},
|
||||
})
|
||||
mock_scoring.get_stats = AsyncMock(
|
||||
return_value={
|
||||
"rag": {
|
||||
"model_loaded": True,
|
||||
"vector_store": {
|
||||
"positive_count": 1,
|
||||
"negative_count": 0,
|
||||
"total_count": 1,
|
||||
},
|
||||
},
|
||||
"deepseek": {"enabled": True, "model": "test", "timeout": 30},
|
||||
}
|
||||
)
|
||||
mock_bdf = MagicMock()
|
||||
mock_bdf.get_scoring_manager.return_value = mock_scoring
|
||||
mock_get_global.return_value = mock_bdf
|
||||
@@ -148,7 +180,9 @@ class TestAdminHandlers:
|
||||
text = mock_message.answer.call_args[0][0]
|
||||
assert "ML" in text or "RAG" in text or "DeepSeek" in text
|
||||
|
||||
async def test_start_ban_process_by_nick_sets_state_await_target(self, mock_message, mock_state):
|
||||
async def test_start_ban_process_by_nick_sets_state_await_target(
|
||||
self, mock_message, mock_state
|
||||
):
|
||||
"""start_ban_process при 'Бан по нику' устанавливает ban_type username и AWAIT_BAN_TARGET."""
|
||||
mock_message.text = "Бан по нику"
|
||||
|
||||
@@ -159,9 +193,14 @@ class TestAdminHandlers:
|
||||
assert call_kw.get("ban_type") == "username"
|
||||
mock_state.set_state.assert_awaited_once_with("AWAIT_BAN_TARGET")
|
||||
mock_message.answer.assert_awaited_once()
|
||||
assert "username" in mock_message.answer.call_args[0][0].lower() or "ник" in mock_message.answer.call_args[0][0].lower()
|
||||
assert (
|
||||
"username" in mock_message.answer.call_args[0][0].lower()
|
||||
or "ник" in mock_message.answer.call_args[0][0].lower()
|
||||
)
|
||||
|
||||
async def test_start_ban_process_by_id_sets_ban_type_id(self, mock_message, mock_state):
|
||||
async def test_start_ban_process_by_id_sets_ban_type_id(
|
||||
self, mock_message, mock_state
|
||||
):
|
||||
"""start_ban_process при 'Бан по ID' устанавливает ban_type id."""
|
||||
mock_message.text = "Бан по ID"
|
||||
|
||||
@@ -172,11 +211,20 @@ class TestAdminHandlers:
|
||||
|
||||
@patch("helper_bot.handlers.admin.admin_handlers.create_keyboard_for_ban_reason")
|
||||
@patch("helper_bot.handlers.admin.admin_handlers.format_user_info")
|
||||
@patch("helper_bot.handlers.admin.admin_handlers.return_to_admin_menu", new_callable=AsyncMock)
|
||||
@patch(
|
||||
"helper_bot.handlers.admin.admin_handlers.return_to_admin_menu",
|
||||
new_callable=AsyncMock,
|
||||
)
|
||||
@patch("helper_bot.handlers.admin.admin_handlers.AdminService")
|
||||
async def test_process_ban_target_user_not_found_returns_to_menu(
|
||||
self, mock_service_cls, mock_return, mock_format, mock_keyboard,
|
||||
mock_message, mock_state, mock_bot_db
|
||||
self,
|
||||
mock_service_cls,
|
||||
mock_return,
|
||||
mock_format,
|
||||
mock_keyboard,
|
||||
mock_message,
|
||||
mock_state,
|
||||
mock_bot_db,
|
||||
):
|
||||
"""process_ban_target при ненайденном пользователе по username возвращает в меню."""
|
||||
mock_service = MagicMock()
|
||||
@@ -196,8 +244,7 @@ class TestAdminHandlers:
|
||||
@patch("helper_bot.handlers.admin.admin_handlers.format_user_info")
|
||||
@patch("helper_bot.handlers.admin.admin_handlers.AdminService")
|
||||
async def test_process_ban_reason_sets_state_await_duration(
|
||||
self, mock_service_cls, mock_format, mock_keyboard,
|
||||
mock_message, mock_state
|
||||
self, mock_service_cls, mock_format, mock_keyboard, mock_message, mock_state
|
||||
):
|
||||
"""process_ban_reason сохраняет причину и переводит в AWAIT_BAN_DURATION."""
|
||||
mock_state.get_state = AsyncMock(return_value="AWAIT_BAN_DETAILS")
|
||||
@@ -218,12 +265,14 @@ class TestAdminHandlers:
|
||||
self, mock_format, mock_keyboard, mock_message, mock_state
|
||||
):
|
||||
"""process_ban_duration при 'Навсегда' устанавливает ban_days=None."""
|
||||
mock_state.get_data = AsyncMock(return_value={
|
||||
"target_user_id": 1,
|
||||
"target_username": "u",
|
||||
"target_full_name": "U",
|
||||
"ban_reason": "Спам",
|
||||
})
|
||||
mock_state.get_data = AsyncMock(
|
||||
return_value={
|
||||
"target_user_id": 1,
|
||||
"target_username": "u",
|
||||
"target_full_name": "U",
|
||||
"ban_reason": "Спам",
|
||||
}
|
||||
)
|
||||
mock_message.text = "Навсегда"
|
||||
mock_format.return_value = "Подтверждение"
|
||||
mock_keyboard.return_value = MagicMock()
|
||||
|
||||
@@ -3,7 +3,6 @@ from datetime import datetime
|
||||
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from database.models import Admin
|
||||
from database.repositories.admin_repository import AdminRepository
|
||||
|
||||
|
||||
@@ -5,15 +5,12 @@
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from helper_bot.handlers.admin.exceptions import AdminError
|
||||
from helper_bot.handlers.admin.utils import (
|
||||
escape_html,
|
||||
format_ban_confirmation,
|
||||
format_user_info,
|
||||
handle_admin_error,
|
||||
return_to_admin_menu,
|
||||
)
|
||||
from helper_bot.handlers.admin.utils import (escape_html,
|
||||
format_ban_confirmation,
|
||||
format_user_info,
|
||||
handle_admin_error,
|
||||
return_to_admin_menu)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -124,7 +121,9 @@ class TestReturnToAdminMenu:
|
||||
state.set_state.assert_called_once_with("ADMIN")
|
||||
mock_kb.assert_called_once()
|
||||
assert message.answer.call_count == 1
|
||||
message.answer.assert_called_with("Вернулись в меню", reply_markup="keyboard_markup")
|
||||
message.answer.assert_called_with(
|
||||
"Вернулись в меню", reply_markup="keyboard_markup"
|
||||
)
|
||||
|
||||
async def test_additional_message_sent_first(self):
|
||||
"""При additional_message сначала отправляется оно, затем меню."""
|
||||
@@ -145,7 +144,9 @@ class TestReturnToAdminMenu:
|
||||
|
||||
assert message.answer.call_count == 2
|
||||
message.answer.assert_any_call("Дополнительный текст")
|
||||
message.answer.assert_any_call("Вернулись в меню", reply_markup="keyboard_markup")
|
||||
message.answer.assert_any_call(
|
||||
"Вернулись в меню", reply_markup="keyboard_markup"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -190,7 +191,5 @@ class TestHandleAdminError:
|
||||
message, ValueError("Что-то пошло не так"), state, "test"
|
||||
)
|
||||
|
||||
message.answer.assert_any_call(
|
||||
"Произошла внутренняя ошибка. Попробуйте позже."
|
||||
)
|
||||
message.answer.assert_any_call("Произошла внутренняя ошибка. Попробуйте позже.")
|
||||
state.set_state.assert_called_once_with("ADMIN")
|
||||
|
||||
@@ -6,8 +6,8 @@ import asyncio
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from helper_bot.middlewares.album_middleware import AlbumGetter, AlbumMiddleware
|
||||
from helper_bot.middlewares.album_middleware import (AlbumGetter,
|
||||
AlbumMiddleware)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -64,7 +64,9 @@ class TestAlbumMiddleware:
|
||||
"""Мок handler."""
|
||||
return AsyncMock(return_value="ok")
|
||||
|
||||
async def test_no_media_group_id_calls_handler_immediately(self, middleware, mock_handler):
|
||||
async def test_no_media_group_id_calls_handler_immediately(
|
||||
self, middleware, mock_handler
|
||||
):
|
||||
"""Сообщение без media_group_id передаётся в handler сразу."""
|
||||
event = MagicMock()
|
||||
event.media_group_id = None
|
||||
@@ -94,7 +96,9 @@ class TestAlbumMiddleware:
|
||||
assert data["album_getter"].media_group_id == "group_123"
|
||||
assert result == "ok"
|
||||
|
||||
async def test_second_media_group_message_does_not_call_handler(self, middleware, mock_handler):
|
||||
async def test_second_media_group_message_does_not_call_handler(
|
||||
self, middleware, mock_handler
|
||||
):
|
||||
"""Второе сообщение той же медиагруппы: handler не вызывается."""
|
||||
event1 = MagicMock()
|
||||
event1.media_group_id = "group_456"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from database.async_db import AsyncBotDB
|
||||
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ 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.exceptions import (DatabaseError,
|
||||
FileOperationError)
|
||||
from helper_bot.handlers.voice.services import AudioFileService
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ from datetime import datetime, timezone
|
||||
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from database.models import AudioListenRecord, AudioMessage, AudioModerate
|
||||
from database.repositories.audio_repository import AudioRepository
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ from datetime import datetime, timezone
|
||||
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from database.repositories.audio_repository import AudioRepository
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ from datetime import datetime, timedelta, timezone
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from helper_bot.utils.auto_unban_scheduler import AutoUnbanScheduler
|
||||
|
||||
|
||||
|
||||
@@ -3,11 +3,8 @@ from datetime import datetime, timedelta, timezone
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from helper_bot.utils.auto_unban_scheduler import (
|
||||
AutoUnbanScheduler,
|
||||
get_auto_unban_scheduler,
|
||||
)
|
||||
from helper_bot.utils.auto_unban_scheduler import (AutoUnbanScheduler,
|
||||
get_auto_unban_scheduler)
|
||||
|
||||
|
||||
class TestAutoUnbanScheduler:
|
||||
|
||||
@@ -3,11 +3,9 @@ from datetime import datetime
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from database.models import BlacklistHistoryRecord
|
||||
from database.repositories.blacklist_history_repository import (
|
||||
BlacklistHistoryRepository,
|
||||
)
|
||||
from database.repositories.blacklist_history_repository import \
|
||||
BlacklistHistoryRepository
|
||||
|
||||
|
||||
class TestBlacklistHistoryRepository:
|
||||
|
||||
@@ -6,7 +6,6 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
|
||||
from helper_bot.middlewares.blacklist_middleware import BlacklistMiddleware
|
||||
|
||||
|
||||
@@ -109,4 +108,4 @@ class TestBlacklistMiddleware:
|
||||
result = await middleware(mock_handler, event, data)
|
||||
|
||||
mock_handler.assert_called_once_with(event, data)
|
||||
assert result == "handler_ok"
|
||||
assert result == "handler_ok"
|
||||
|
||||
@@ -3,7 +3,6 @@ from datetime import datetime
|
||||
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from database.models import BlacklistUser
|
||||
from database.repositories.blacklist_repository import BlacklistRepository
|
||||
|
||||
|
||||
@@ -5,12 +5,10 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from helper_bot.handlers.callback.dependency_factory import (
|
||||
get_ban_service,
|
||||
get_post_publish_service,
|
||||
)
|
||||
from helper_bot.handlers.callback.services import BanService, PostPublishService
|
||||
get_ban_service, get_post_publish_service)
|
||||
from helper_bot.handlers.callback.services import (BanService,
|
||||
PostPublishService)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -20,7 +18,13 @@ class TestGetPostPublishService:
|
||||
def test_returns_post_publish_service_with_dependencies_from_factory(self):
|
||||
"""Возвращается PostPublishService с db, settings, s3_storage, scoring_manager из get_global_instance."""
|
||||
mock_db = MagicMock()
|
||||
mock_settings = {"Telegram": {"group_for_posts": "-100", "main_public": "@ch", "important_logs": "-200"}}
|
||||
mock_settings = {
|
||||
"Telegram": {
|
||||
"group_for_posts": "-100",
|
||||
"main_public": "@ch",
|
||||
"important_logs": "-200",
|
||||
}
|
||||
}
|
||||
mock_s3 = MagicMock()
|
||||
mock_scoring = MagicMock()
|
||||
mock_bdf = MagicMock()
|
||||
|
||||
@@ -3,17 +3,9 @@ from datetime import datetime
|
||||
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from helper_bot.handlers.callback.callback_handlers import (
|
||||
change_page,
|
||||
delete_voice_message,
|
||||
process_ban_user,
|
||||
process_unlock_user,
|
||||
return_to_main_menu,
|
||||
|
||||
save_voice_message,
|
||||
,
|
||||
)
|
||||
change_page, delete_voice_message, process_ban_user, process_unlock_user,
|
||||
return_to_main_menu, save_voice_message)
|
||||
from helper_bot.handlers.voice.constants import CALLBACK_DELETE, CALLBACK_SAVE
|
||||
|
||||
|
||||
@@ -407,7 +399,9 @@ class TestReturnToMainMenu:
|
||||
return call
|
||||
|
||||
@patch("helper_bot.handlers.callback.callback_handlers.get_reply_keyboard_admin")
|
||||
async def test_return_to_main_menu_deletes_and_answers(self, mock_keyboard, mock_call):
|
||||
async def test_return_to_main_menu_deletes_and_answers(
|
||||
self, mock_keyboard, mock_call
|
||||
):
|
||||
"""return_to_main_menu удаляет сообщение и отправляет приветствие."""
|
||||
mock_keyboard.return_value = MagicMock()
|
||||
|
||||
@@ -415,7 +409,10 @@ class TestReturnToMainMenu:
|
||||
|
||||
mock_call.message.delete.assert_called_once()
|
||||
mock_call.message.answer.assert_called_once()
|
||||
assert "админк" in mock_call.message.answer.call_args[0][0].lower() or "добро" in mock_call.message.answer.call_args[0][0].lower()
|
||||
assert (
|
||||
"админк" in mock_call.message.answer.call_args[0][0].lower()
|
||||
or "добро" in mock_call.message.answer.call_args[0][0].lower()
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -450,7 +447,9 @@ class TestChangePage:
|
||||
db.get_last_users = AsyncMock(return_value=[("U1", 1), ("U2", 2)])
|
||||
return db
|
||||
|
||||
@patch("helper_bot.handlers.callback.callback_handlers.create_keyboard_with_pagination")
|
||||
@patch(
|
||||
"helper_bot.handlers.callback.callback_handlers.create_keyboard_with_pagination"
|
||||
)
|
||||
async def test_change_page_list_users_edits_markup(
|
||||
self, mock_keyboard, mock_call_list_users, mock_bot_db_for_page
|
||||
):
|
||||
@@ -462,9 +461,17 @@ class TestChangePage:
|
||||
mock_bot_db_for_page.get_last_users.assert_awaited_once_with(30)
|
||||
mock_call_list_users.bot.edit_message_reply_markup.assert_awaited_once()
|
||||
|
||||
@patch("helper_bot.handlers.callback.callback_handlers.get_banned_users_buttons", new_callable=AsyncMock)
|
||||
@patch("helper_bot.handlers.callback.callback_handlers.get_banned_users_list", new_callable=AsyncMock)
|
||||
@patch("helper_bot.handlers.callback.callback_handlers.create_keyboard_with_pagination")
|
||||
@patch(
|
||||
"helper_bot.handlers.callback.callback_handlers.get_banned_users_buttons",
|
||||
new_callable=AsyncMock,
|
||||
)
|
||||
@patch(
|
||||
"helper_bot.handlers.callback.callback_handlers.get_banned_users_list",
|
||||
new_callable=AsyncMock,
|
||||
)
|
||||
@patch(
|
||||
"helper_bot.handlers.callback.callback_handlers.create_keyboard_with_pagination"
|
||||
)
|
||||
async def test_change_page_banned_list_edits_text_and_markup(
|
||||
self, mock_keyboard, mock_get_list, mock_get_buttons, mock_bot_db_for_page
|
||||
):
|
||||
@@ -491,7 +498,9 @@ class TestChangePage:
|
||||
call.bot.edit_message_text.assert_awaited_once()
|
||||
call.bot.edit_message_reply_markup.assert_awaited_once()
|
||||
|
||||
async def test_change_page_invalid_page_number_answers_error(self, mock_bot_db_for_page):
|
||||
async def test_change_page_invalid_page_number_answers_error(
|
||||
self, mock_bot_db_for_page
|
||||
):
|
||||
"""change_page при некорректном номере страницы отвечает ошибкой."""
|
||||
call = Mock()
|
||||
call.data = "page_abc"
|
||||
@@ -533,11 +542,19 @@ class TestProcessBanUser:
|
||||
state.set_state = AsyncMock()
|
||||
return state
|
||||
|
||||
@patch("helper_bot.handlers.callback.callback_handlers.create_keyboard_for_ban_reason")
|
||||
@patch(
|
||||
"helper_bot.handlers.callback.callback_handlers.create_keyboard_for_ban_reason"
|
||||
)
|
||||
@patch("helper_bot.handlers.callback.callback_handlers.format_user_info")
|
||||
@patch("helper_bot.handlers.callback.callback_handlers.get_ban_service")
|
||||
async def test_process_ban_user_success_sets_state_await_details(
|
||||
self, mock_get_ban, mock_format, mock_keyboard, mock_call, mock_state, mock_bot_db_ban
|
||||
self,
|
||||
mock_get_ban,
|
||||
mock_format,
|
||||
mock_keyboard,
|
||||
mock_call,
|
||||
mock_state,
|
||||
mock_bot_db_ban,
|
||||
):
|
||||
"""process_ban_user при успехе переводит в AWAIT_BAN_DETAILS."""
|
||||
mock_ban = Mock()
|
||||
@@ -559,6 +576,7 @@ class TestProcessBanUser:
|
||||
):
|
||||
"""process_ban_user при UserNotFoundError возвращает в админ-меню."""
|
||||
from helper_bot.handlers.callback.exceptions import UserNotFoundError
|
||||
|
||||
mock_ban = Mock()
|
||||
mock_ban.ban_user = AsyncMock(side_effect=UserNotFoundError("not found"))
|
||||
mock_get_ban.return_value = mock_ban
|
||||
@@ -569,7 +587,9 @@ class TestProcessBanUser:
|
||||
mock_call.message.answer.assert_awaited_once()
|
||||
mock_state.set_state.assert_awaited_once_with("ADMIN")
|
||||
|
||||
async def test_process_ban_user_invalid_user_id_answers_error(self, mock_call, mock_state, mock_bot_db_ban):
|
||||
async def test_process_ban_user_invalid_user_id_answers_error(
|
||||
self, mock_call, mock_state, mock_bot_db_ban
|
||||
):
|
||||
"""process_ban_user при некорректном user_id отвечает ошибкой."""
|
||||
mock_call.data = "ban_abc"
|
||||
|
||||
@@ -599,12 +619,16 @@ class TestProcessUnlockUser:
|
||||
|
||||
mock_ban.unlock_user.assert_awaited_once_with("123")
|
||||
call.answer.assert_awaited_once()
|
||||
assert "username" in call.answer.call_args[0][0] or "разблокирован" in call.answer.call_args[0][0].lower()
|
||||
assert (
|
||||
"username" in call.answer.call_args[0][0]
|
||||
or "разблокирован" in call.answer.call_args[0][0].lower()
|
||||
)
|
||||
|
||||
@patch("helper_bot.handlers.callback.callback_handlers.get_ban_service")
|
||||
async def test_process_unlock_user_not_found_answers_error(self, mock_get_ban):
|
||||
"""process_unlock_user при UserNotFoundError отвечает что пользователь не найден."""
|
||||
from helper_bot.handlers.callback.exceptions import UserNotFoundError
|
||||
|
||||
call = Mock()
|
||||
call.data = "unlock_999"
|
||||
call.answer = AsyncMock()
|
||||
@@ -614,7 +638,9 @@ class TestProcessUnlockUser:
|
||||
|
||||
await process_unlock_user(call)
|
||||
|
||||
call.answer.assert_awaited_once_with(text="Пользователь не найден в базе", show_alert=True, cache_time=3)
|
||||
call.answer.assert_awaited_once_with(
|
||||
text="Пользователь не найден в базе", show_alert=True, cache_time=3
|
||||
)
|
||||
|
||||
async def test_process_unlock_user_invalid_user_id_answers_error(self):
|
||||
"""process_unlock_user при некорректном user_id отвечает ошибкой."""
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -6,15 +6,15 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from aiogram import types
|
||||
|
||||
from helper_bot.handlers.group.decorators import error_handler as group_error_handler
|
||||
from helper_bot.handlers.private.decorators import (
|
||||
error_handler as private_error_handler,
|
||||
)
|
||||
from helper_bot.handlers.group.decorators import \
|
||||
error_handler as group_error_handler
|
||||
from helper_bot.handlers.private.decorators import \
|
||||
error_handler as private_error_handler
|
||||
|
||||
|
||||
class FakeMessage:
|
||||
"""Класс-маркер, чтобы мок проходил isinstance(..., types.Message) в декораторе."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ class TestGroupErrorHandler:
|
||||
|
||||
async def test_success_returns_result(self):
|
||||
"""При успешном выполнении возвращается результат функции."""
|
||||
|
||||
@group_error_handler
|
||||
async def sample_handler():
|
||||
return "ok"
|
||||
@@ -34,6 +35,7 @@ class TestGroupErrorHandler:
|
||||
|
||||
async def test_exception_is_reraised(self):
|
||||
"""При исключении оно пробрасывается дальше."""
|
||||
|
||||
@group_error_handler
|
||||
async def failing_handler():
|
||||
raise ValueError("test error")
|
||||
@@ -44,6 +46,7 @@ class TestGroupErrorHandler:
|
||||
@patch("helper_bot.handlers.group.decorators.logger")
|
||||
async def test_exception_is_logged(self, mock_logger):
|
||||
"""При исключении вызывается logger.error."""
|
||||
|
||||
@group_error_handler
|
||||
async def failing_handler():
|
||||
raise RuntimeError("logged error")
|
||||
@@ -95,6 +98,7 @@ class TestPrivateErrorHandler:
|
||||
|
||||
async def test_success_returns_result(self):
|
||||
"""При успешном выполнении возвращается результат функции."""
|
||||
|
||||
@private_error_handler
|
||||
async def sample_handler():
|
||||
return 42
|
||||
@@ -104,6 +108,7 @@ class TestPrivateErrorHandler:
|
||||
|
||||
async def test_exception_is_reraised(self):
|
||||
"""При исключении оно пробрасывается дальше."""
|
||||
|
||||
@private_error_handler
|
||||
async def failing_handler():
|
||||
raise TypeError("private error")
|
||||
@@ -114,6 +119,7 @@ class TestPrivateErrorHandler:
|
||||
@patch("helper_bot.handlers.private.decorators.logger")
|
||||
async def test_exception_is_logged(self, mock_logger):
|
||||
"""При исключении вызывается logger.error."""
|
||||
|
||||
@private_error_handler
|
||||
async def failing_handler():
|
||||
raise KeyError("key missing")
|
||||
@@ -155,6 +161,7 @@ class TestPrivateErrorHandler:
|
||||
|
||||
async def test_no_message_in_args_no_send(self):
|
||||
"""Если в args нет Message, send_message не вызывается (только логирование)."""
|
||||
|
||||
@private_error_handler
|
||||
async def failing_handler():
|
||||
raise ValueError("no message")
|
||||
|
||||
@@ -5,13 +5,10 @@
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from helper_bot.services.scoring.deepseek_service import DeepSeekService
|
||||
from helper_bot.services.scoring.exceptions import (
|
||||
DeepSeekAPIError,
|
||||
ScoringError,
|
||||
TextTooShortError,
|
||||
)
|
||||
from helper_bot.services.scoring.exceptions import (DeepSeekAPIError,
|
||||
ScoringError,
|
||||
TextTooShortError)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -20,7 +17,9 @@ class TestDeepSeekServiceInit:
|
||||
|
||||
def test_init_with_api_key_enabled(self):
|
||||
"""При переданном api_key сервис включён."""
|
||||
with patch("helper_bot.services.scoring.deepseek_service.httpx.AsyncClient", None):
|
||||
with patch(
|
||||
"helper_bot.services.scoring.deepseek_service.httpx.AsyncClient", None
|
||||
):
|
||||
service = DeepSeekService(api_key="key")
|
||||
assert service.is_enabled is True
|
||||
assert service.source_name == "deepseek"
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from helper_bot.middlewares.dependencies_middleware import DependenciesMiddleware
|
||||
from helper_bot.middlewares.dependencies_middleware import \
|
||||
DependenciesMiddleware
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -48,7 +48,9 @@ class TestDependenciesMiddleware:
|
||||
assert result == "ok"
|
||||
|
||||
@patch("helper_bot.middlewares.dependencies_middleware.get_global_instance")
|
||||
async def test_exception_does_not_break_chain(self, mock_get_global, middleware, mock_handler):
|
||||
async def test_exception_does_not_break_chain(
|
||||
self, mock_get_global, middleware, mock_handler
|
||||
):
|
||||
"""При исключении в get_global_instance handler всё равно вызывается."""
|
||||
mock_get_global.side_effect = RuntimeError("No global instance")
|
||||
|
||||
|
||||
@@ -8,13 +8,9 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
from aiogram import types
|
||||
|
||||
from helper_bot.utils.helper_func import (
|
||||
add_in_db_media,
|
||||
add_in_db_media_mediagroup,
|
||||
download_file,
|
||||
send_media_group_message_to_private_chat,
|
||||
)
|
||||
add_in_db_media, add_in_db_media_mediagroup, download_file,
|
||||
send_media_group_message_to_private_chat)
|
||||
|
||||
|
||||
class TestDownloadFile:
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
from aiogram.types import (
|
||||
InlineKeyboardButton,
|
||||
InlineKeyboardMarkup,
|
||||
KeyboardButton,
|
||||
ReplyKeyboardMarkup,
|
||||
)
|
||||
|
||||
from aiogram.types import (InlineKeyboardButton, InlineKeyboardMarkup,
|
||||
KeyboardButton, ReplyKeyboardMarkup)
|
||||
from database.async_db import AsyncBotDB
|
||||
from helper_bot.filters.main import ChatTypeFilter
|
||||
from helper_bot.keyboards.keyboards import (
|
||||
create_keyboard_with_pagination,
|
||||
get_reply_keyboard,
|
||||
get_reply_keyboard_admin,
|
||||
get_reply_keyboard_for_post,
|
||||
get_reply_keyboard_leave_chat,
|
||||
)
|
||||
from helper_bot.keyboards.keyboards import (create_keyboard_with_pagination,
|
||||
get_reply_keyboard,
|
||||
get_reply_keyboard_admin,
|
||||
get_reply_keyboard_for_post,
|
||||
get_reply_keyboard_leave_chat)
|
||||
|
||||
|
||||
class TestKeyboards:
|
||||
|
||||
@@ -6,7 +6,6 @@ import asyncio
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from helper_bot.main import start_bot, start_bot_with_retry
|
||||
|
||||
|
||||
@@ -15,11 +14,15 @@ from helper_bot.main import start_bot, start_bot_with_retry
|
||||
class TestStartBotWithRetry:
|
||||
"""Тесты для start_bot_with_retry."""
|
||||
|
||||
async def test_success_on_first_try_exits_immediately(self, mock_bot, mock_dispatcher):
|
||||
async def test_success_on_first_try_exits_immediately(
|
||||
self, mock_bot, mock_dispatcher
|
||||
):
|
||||
"""При успешном start_polling с первой попытки цикл завершается без повторов."""
|
||||
mock_dispatcher.start_polling = AsyncMock()
|
||||
await start_bot_with_retry(mock_bot, mock_dispatcher, max_retries=3)
|
||||
mock_dispatcher.start_polling.assert_awaited_once_with(mock_bot, skip_updates=True)
|
||||
mock_dispatcher.start_polling.assert_awaited_once_with(
|
||||
mock_bot, skip_updates=True
|
||||
)
|
||||
|
||||
@patch("helper_bot.main.asyncio.sleep", new_callable=AsyncMock)
|
||||
async def test_network_error_retries_then_succeeds(
|
||||
@@ -41,15 +44,15 @@ class TestStartBotWithRetry:
|
||||
self, mock_bot, mock_dispatcher
|
||||
):
|
||||
"""При не-сетевой ошибке исключение пробрасывается без повторов."""
|
||||
mock_dispatcher.start_polling = AsyncMock(
|
||||
side_effect=ValueError("critical")
|
||||
)
|
||||
mock_dispatcher.start_polling = AsyncMock(side_effect=ValueError("critical"))
|
||||
with pytest.raises(ValueError, match="critical"):
|
||||
await start_bot_with_retry(mock_bot, mock_dispatcher, max_retries=3)
|
||||
mock_dispatcher.start_polling.assert_awaited_once()
|
||||
|
||||
@patch("helper_bot.main.asyncio.sleep", new_callable=AsyncMock)
|
||||
async def test_max_retries_exceeded_raises(self, mock_sleep, mock_bot, mock_dispatcher):
|
||||
async def test_max_retries_exceeded_raises(
|
||||
self, mock_sleep, mock_bot, mock_dispatcher
|
||||
):
|
||||
"""При исчерпании попыток из-за сетевых ошибок исключение пробрасывается."""
|
||||
mock_dispatcher.start_polling = AsyncMock(
|
||||
side_effect=ConnectionError("network error")
|
||||
|
||||
@@ -3,7 +3,6 @@ from datetime import datetime
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from database.models import UserMessage
|
||||
from database.repositories.message_repository import MessageRepository
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import tempfile
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from database.models import UserMessage
|
||||
from database.repositories.message_repository import MessageRepository
|
||||
|
||||
|
||||
@@ -7,12 +7,8 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from aiogram.types import Message
|
||||
|
||||
from helper_bot.middlewares.metrics_middleware import (
|
||||
DatabaseMetricsMiddleware,
|
||||
ErrorMetricsMiddleware,
|
||||
MetricsMiddleware,
|
||||
)
|
||||
DatabaseMetricsMiddleware, ErrorMetricsMiddleware, MetricsMiddleware)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -30,13 +26,17 @@ class TestMetricsMiddleware:
|
||||
@pytest.fixture
|
||||
def mock_handler(self):
|
||||
"""Мок handler."""
|
||||
|
||||
async def sample_handler(event, data):
|
||||
return "result"
|
||||
|
||||
sample_handler.__name__ = "sample_handler"
|
||||
return sample_handler
|
||||
|
||||
@patch("helper_bot.middlewares.metrics_middleware.metrics")
|
||||
async def test_handler_success_records_metrics(self, mock_metrics, middleware, mock_handler):
|
||||
async def test_handler_success_records_metrics(
|
||||
self, mock_metrics, middleware, mock_handler
|
||||
):
|
||||
"""При успешном выполнении handler вызываются record_method_duration и record_middleware."""
|
||||
event = MagicMock(spec=Message)
|
||||
event.message = None
|
||||
@@ -64,10 +64,14 @@ class TestMetricsMiddleware:
|
||||
assert call_args[2] == "success"
|
||||
|
||||
@patch("helper_bot.middlewares.metrics_middleware.metrics")
|
||||
async def test_handler_exception_records_error_and_reraises(self, mock_metrics, middleware):
|
||||
async def test_handler_exception_records_error_and_reraises(
|
||||
self, mock_metrics, middleware
|
||||
):
|
||||
"""При исключении в handler записываются метрики ошибки и исключение пробрасывается."""
|
||||
|
||||
async def failing_handler(event, data):
|
||||
raise ValueError("test error")
|
||||
|
||||
failing_handler.__name__ = "failing_handler"
|
||||
|
||||
event = MagicMock(spec=Message)
|
||||
@@ -96,8 +100,10 @@ class TestMetricsMiddleware:
|
||||
|
||||
def test_get_handler_name_returns_function_name(self, middleware):
|
||||
"""_get_handler_name возвращает __name__ функции."""
|
||||
|
||||
def named_handler():
|
||||
pass
|
||||
|
||||
assert middleware._get_handler_name(named_handler) == "named_handler"
|
||||
|
||||
def test_get_handler_name_for_lambda_returns_qualname_or_unknown(self, middleware):
|
||||
@@ -108,7 +114,9 @@ class TestMetricsMiddleware:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("helper_bot.middlewares.metrics_middleware.metrics")
|
||||
async def test_record_comprehensive_message_metrics_photo(self, mock_metrics, middleware):
|
||||
async def test_record_comprehensive_message_metrics_photo(
|
||||
self, mock_metrics, middleware
|
||||
):
|
||||
"""_record_comprehensive_message_metrics для сообщения с фото записывает message_type photo."""
|
||||
message = MagicMock()
|
||||
message.photo = [MagicMock()]
|
||||
@@ -126,13 +134,17 @@ class TestMetricsMiddleware:
|
||||
|
||||
result = await middleware._record_comprehensive_message_metrics(message)
|
||||
|
||||
mock_metrics.record_message.assert_called_once_with("photo", "private", "message_handler")
|
||||
mock_metrics.record_message.assert_called_once_with(
|
||||
"photo", "private", "message_handler"
|
||||
)
|
||||
assert result["message_type"] == "photo"
|
||||
assert result["chat_type"] == "private"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("helper_bot.middlewares.metrics_middleware.metrics")
|
||||
async def test_record_comprehensive_message_metrics_voice(self, mock_metrics, middleware):
|
||||
async def test_record_comprehensive_message_metrics_voice(
|
||||
self, mock_metrics, middleware
|
||||
):
|
||||
"""_record_comprehensive_message_metrics для voice записывает message_type voice."""
|
||||
message = MagicMock()
|
||||
message.photo = None
|
||||
@@ -150,12 +162,16 @@ class TestMetricsMiddleware:
|
||||
|
||||
result = await middleware._record_comprehensive_message_metrics(message)
|
||||
|
||||
mock_metrics.record_message.assert_called_once_with("voice", "supergroup", "message_handler")
|
||||
mock_metrics.record_message.assert_called_once_with(
|
||||
"voice", "supergroup", "message_handler"
|
||||
)
|
||||
assert result["message_type"] == "voice"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("helper_bot.middlewares.metrics_middleware.metrics")
|
||||
async def test_record_comprehensive_callback_metrics(self, mock_metrics, middleware):
|
||||
async def test_record_comprehensive_callback_metrics(
|
||||
self, mock_metrics, middleware
|
||||
):
|
||||
"""_record_comprehensive_callback_metrics записывает callback_query и возвращает данные."""
|
||||
callback = MagicMock()
|
||||
callback.data = "publish"
|
||||
@@ -165,7 +181,9 @@ class TestMetricsMiddleware:
|
||||
|
||||
result = await middleware._record_comprehensive_callback_metrics(callback)
|
||||
|
||||
mock_metrics.record_message.assert_called_once_with("callback_query", "callback", "callback_handler")
|
||||
mock_metrics.record_message.assert_called_once_with(
|
||||
"callback_query", "callback", "callback_handler"
|
||||
)
|
||||
assert result["callback_data"] == "publish"
|
||||
assert result["user_id"] == 10
|
||||
|
||||
@@ -178,7 +196,9 @@ class TestMetricsMiddleware:
|
||||
|
||||
result = await middleware._record_unknown_event_metrics(event)
|
||||
|
||||
mock_metrics.record_message.assert_called_once_with("unknown", "unknown", "unknown_handler")
|
||||
mock_metrics.record_message.assert_called_once_with(
|
||||
"unknown", "unknown", "unknown_handler"
|
||||
)
|
||||
assert "event_type" in result
|
||||
|
||||
def test_extract_command_info_slash_command_returns_mapping(self, middleware):
|
||||
@@ -214,7 +234,9 @@ class TestMetricsMiddleware:
|
||||
result = middleware._extract_callback_command_info_with_fallback(callback)
|
||||
assert result is None
|
||||
|
||||
def test_extract_callback_command_info_ban_pattern_returns_callback_ban(self, middleware):
|
||||
def test_extract_callback_command_info_ban_pattern_returns_callback_ban(
|
||||
self, middleware
|
||||
):
|
||||
"""_extract_callback_command_info_with_fallback для ban_123 возвращает callback_ban."""
|
||||
callback = MagicMock()
|
||||
callback.data = "ban_123456"
|
||||
@@ -224,7 +246,9 @@ class TestMetricsMiddleware:
|
||||
assert result["command"] == "callback_ban" or "ban" in result["command"]
|
||||
assert "handler_type" in result
|
||||
|
||||
def test_extract_callback_command_info_page_pattern_returns_callback_page(self, middleware):
|
||||
def test_extract_callback_command_info_page_pattern_returns_callback_page(
|
||||
self, middleware
|
||||
):
|
||||
"""_extract_callback_command_info_with_fallback для page_2 возвращает callback_page."""
|
||||
callback = MagicMock()
|
||||
callback.data = "page_2"
|
||||
@@ -236,14 +260,18 @@ class TestMetricsMiddleware:
|
||||
@pytest.mark.asyncio
|
||||
@patch("helper_bot.middlewares.metrics_middleware.metrics")
|
||||
@patch("helper_bot.utils.base_dependency_factory.get_global_instance")
|
||||
async def test_update_active_users_metric_sets_metrics(self, mock_get_global, mock_metrics, middleware):
|
||||
async def test_update_active_users_metric_sets_metrics(
|
||||
self, mock_get_global, mock_metrics, middleware
|
||||
):
|
||||
"""_update_active_users_metric вызывает fetch_one и устанавливает метрики."""
|
||||
mock_bdf = MagicMock()
|
||||
mock_db = MagicMock()
|
||||
mock_db.fetch_one = AsyncMock(side_effect=[
|
||||
{"total": 100},
|
||||
{"daily": 10},
|
||||
])
|
||||
mock_db.fetch_one = AsyncMock(
|
||||
side_effect=[
|
||||
{"total": 100},
|
||||
{"daily": 10},
|
||||
]
|
||||
)
|
||||
mock_bdf.get_db.return_value = mock_db
|
||||
mock_get_global.return_value = mock_bdf
|
||||
|
||||
@@ -257,7 +285,9 @@ class TestMetricsMiddleware:
|
||||
@pytest.mark.asyncio
|
||||
@patch("helper_bot.middlewares.metrics_middleware.metrics")
|
||||
@patch("helper_bot.utils.base_dependency_factory.get_global_instance")
|
||||
async def test_update_active_users_metric_on_exception_sets_fallback(self, mock_get_global, mock_metrics, middleware):
|
||||
async def test_update_active_users_metric_on_exception_sets_fallback(
|
||||
self, mock_get_global, mock_metrics, middleware
|
||||
):
|
||||
"""_update_active_users_metric при исключении устанавливает fallback 1."""
|
||||
mock_get_global.side_effect = RuntimeError("no bdf")
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ from datetime import datetime
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from database.models import MessageContentLink, PostContent, TelegramPost
|
||||
from database.repositories.post_repository import PostRepository
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import tempfile
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from database.models import MessageContentLink, PostContent, TelegramPost
|
||||
from database.repositories.post_repository import PostRepository
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
from aiogram import types
|
||||
|
||||
from database.models import TelegramPost, User
|
||||
from helper_bot.handlers.private.services import BotSettings, PostService
|
||||
|
||||
|
||||
@@ -5,12 +5,9 @@
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from helper_bot.services.scoring.exceptions import (
|
||||
InsufficientExamplesError,
|
||||
ScoringError,
|
||||
TextTooShortError,
|
||||
)
|
||||
from helper_bot.services.scoring.exceptions import (InsufficientExamplesError,
|
||||
ScoringError,
|
||||
TextTooShortError)
|
||||
from helper_bot.services.scoring.rag_client import RagApiClient
|
||||
|
||||
|
||||
@@ -127,16 +124,13 @@ class TestRagApiClientCalculateScore:
|
||||
|
||||
async def test_timeout_raises_scoring_error(self, client):
|
||||
"""Таймаут запроса — ScoringError."""
|
||||
|
||||
class FakeTimeoutException(Exception):
|
||||
pass
|
||||
|
||||
with patch(
|
||||
"helper_bot.services.scoring.rag_client.httpx"
|
||||
) as mock_httpx:
|
||||
with patch("helper_bot.services.scoring.rag_client.httpx") as mock_httpx:
|
||||
mock_httpx.TimeoutException = FakeTimeoutException
|
||||
client._client.post = AsyncMock(
|
||||
side_effect=FakeTimeoutException("timeout")
|
||||
)
|
||||
client._client.post = AsyncMock(side_effect=FakeTimeoutException("timeout"))
|
||||
with pytest.raises(ScoringError, match="Таймаут"):
|
||||
await client.calculate_score("text")
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from aiogram.types import CallbackQuery, Message, Update
|
||||
|
||||
from helper_bot.middlewares.rate_limit_middleware import RateLimitMiddleware
|
||||
|
||||
|
||||
@@ -25,7 +24,9 @@ class TestRateLimitMiddleware:
|
||||
"""Мок handler."""
|
||||
return AsyncMock(return_value="handler_result")
|
||||
|
||||
async def test_event_with_message_calls_rate_limiter(self, middleware, mock_handler):
|
||||
async def test_event_with_message_calls_rate_limiter(
|
||||
self, middleware, mock_handler
|
||||
):
|
||||
"""При событии с message вызывается rate_limiter.send_with_rate_limit."""
|
||||
event = MagicMock(spec=Message)
|
||||
event.message = None
|
||||
@@ -49,7 +50,9 @@ class TestRateLimitMiddleware:
|
||||
mock_handler.assert_called_once_with(event, data)
|
||||
assert result == "rate_limited_result"
|
||||
|
||||
async def test_update_with_message_calls_rate_limiter(self, middleware, mock_handler):
|
||||
async def test_update_with_message_calls_rate_limiter(
|
||||
self, middleware, mock_handler
|
||||
):
|
||||
"""При Update с message извлекается chat_id и вызывается rate_limiter."""
|
||||
message = MagicMock(spec=Message)
|
||||
message.chat = MagicMock()
|
||||
@@ -69,7 +72,9 @@ class TestRateLimitMiddleware:
|
||||
mock_send.assert_called_once()
|
||||
assert mock_send.call_args[0][1] == 99999
|
||||
|
||||
async def test_event_without_message_calls_handler_directly(self, middleware, mock_handler):
|
||||
async def test_event_without_message_calls_handler_directly(
|
||||
self, middleware, mock_handler
|
||||
):
|
||||
"""При событии без message (например CallbackQuery) handler вызывается напрямую."""
|
||||
event = MagicMock(spec=CallbackQuery)
|
||||
event.message = None
|
||||
@@ -93,6 +98,7 @@ class TestRateLimitMiddleware:
|
||||
"send_with_rate_limit",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_send:
|
||||
|
||||
async def call_passed_handler(inner_handler, chat_id):
|
||||
return await inner_handler()
|
||||
|
||||
|
||||
@@ -7,13 +7,10 @@ from collections import deque
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from helper_bot.utils.rate_limit_monitor import (
|
||||
RateLimitMonitor,
|
||||
RateLimitStats,
|
||||
get_rate_limit_summary,
|
||||
record_rate_limit_request,
|
||||
)
|
||||
from helper_bot.utils.rate_limit_monitor import (RateLimitMonitor,
|
||||
RateLimitStats,
|
||||
get_rate_limit_summary,
|
||||
record_rate_limit_request)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -47,9 +44,7 @@ class TestRateLimitStats:
|
||||
|
||||
def test_average_wait_time(self):
|
||||
"""Среднее время ожидания считается корректно."""
|
||||
stats = RateLimitStats(
|
||||
chat_id=1, total_requests=4, total_wait_time=2.0
|
||||
)
|
||||
stats = RateLimitStats(chat_id=1, total_requests=4, total_wait_time=2.0)
|
||||
assert stats.average_wait_time == 0.5
|
||||
|
||||
def test_requests_per_minute_empty(self):
|
||||
@@ -60,7 +55,9 @@ class TestRateLimitStats:
|
||||
def test_requests_per_minute_recent(self):
|
||||
"""Подсчёт запросов за последнюю минуту."""
|
||||
now = time.time()
|
||||
stats = RateLimitStats(chat_id=1, request_times=deque([now, now - 30], maxlen=100))
|
||||
stats = RateLimitStats(
|
||||
chat_id=1, request_times=deque([now, now - 30], maxlen=100)
|
||||
)
|
||||
assert stats.requests_per_minute == 2
|
||||
|
||||
def test_requests_per_minute_old_ignored(self):
|
||||
|
||||
@@ -7,21 +7,15 @@ import time
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from helper_bot.config.rate_limit_config import RateLimitSettings, get_rate_limit_config
|
||||
from helper_bot.utils.rate_limit_monitor import (
|
||||
RateLimitMonitor,
|
||||
RateLimitStats,
|
||||
record_rate_limit_request,
|
||||
)
|
||||
from helper_bot.utils.rate_limiter import (
|
||||
ChatRateLimiter,
|
||||
GlobalRateLimiter,
|
||||
RateLimitConfig,
|
||||
RetryHandler,
|
||||
TelegramRateLimiter,
|
||||
send_with_rate_limit,
|
||||
)
|
||||
from helper_bot.config.rate_limit_config import (RateLimitSettings,
|
||||
get_rate_limit_config)
|
||||
from helper_bot.utils.rate_limit_monitor import (RateLimitMonitor,
|
||||
RateLimitStats,
|
||||
record_rate_limit_request)
|
||||
from helper_bot.utils.rate_limiter import (ChatRateLimiter, GlobalRateLimiter,
|
||||
RateLimitConfig, RetryHandler,
|
||||
TelegramRateLimiter,
|
||||
send_with_rate_limit)
|
||||
|
||||
|
||||
class TestRateLimitConfig:
|
||||
|
||||
@@ -3,12 +3,9 @@ from unittest.mock import AsyncMock, Mock, patch
|
||||
import pytest
|
||||
from aiogram import types
|
||||
from aiogram.fsm.context import FSMContext
|
||||
|
||||
from helper_bot.handlers.admin.exceptions import (
|
||||
InvalidInputError,
|
||||
UserAlreadyBannedError,
|
||||
UserNotFoundError,
|
||||
)
|
||||
from helper_bot.handlers.admin.exceptions import (InvalidInputError,
|
||||
UserAlreadyBannedError,
|
||||
UserNotFoundError)
|
||||
from helper_bot.handlers.admin.services import AdminService, BannedUser, User
|
||||
|
||||
|
||||
@@ -230,7 +227,9 @@ class TestAdminService:
|
||||
return_value=[(1, "спам", None), (2, "оскорбления", "2025-02-01")]
|
||||
)
|
||||
self.mock_db.get_username = AsyncMock(side_effect=["user1", "user2"])
|
||||
self.mock_db.get_full_name_by_id = AsyncMock(side_effect=["Name One", "Name Two"])
|
||||
self.mock_db.get_full_name_by_id = AsyncMock(
|
||||
side_effect=["Name One", "Name Two"]
|
||||
)
|
||||
|
||||
result = await self.admin_service.get_banned_users()
|
||||
|
||||
@@ -244,7 +243,9 @@ class TestAdminService:
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_banned_users_uses_user_id_fallback(self):
|
||||
"""get_banned_users при отсутствии username/full_name использует User_{id}."""
|
||||
self.mock_db.get_banned_users_from_db = AsyncMock(return_value=[(99, "reason", None)])
|
||||
self.mock_db.get_banned_users_from_db = AsyncMock(
|
||||
return_value=[(99, "reason", None)]
|
||||
)
|
||||
self.mock_db.get_username = AsyncMock(return_value=None)
|
||||
self.mock_db.get_full_name_by_id = AsyncMock(return_value=None)
|
||||
|
||||
@@ -256,8 +257,14 @@ class TestAdminService:
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_banned_users_for_display_success(self):
|
||||
"""Тест успешного получения данных для отображения забаненных."""
|
||||
with patch("helper_bot.handlers.admin.services.get_banned_users_list", new_callable=AsyncMock) as mock_list:
|
||||
with patch("helper_bot.handlers.admin.services.get_banned_users_buttons", new_callable=AsyncMock) as mock_buttons:
|
||||
with patch(
|
||||
"helper_bot.handlers.admin.services.get_banned_users_list",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_list:
|
||||
with patch(
|
||||
"helper_bot.handlers.admin.services.get_banned_users_buttons",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_buttons:
|
||||
mock_list.return_value = "Список забаненных"
|
||||
mock_buttons.return_value = []
|
||||
|
||||
|
||||
@@ -5,16 +5,11 @@ from unittest.mock import AsyncMock, MagicMock, Mock
|
||||
import pytest
|
||||
from aiogram import types
|
||||
from aiogram.fsm.context import FSMContext
|
||||
|
||||
from helper_bot.handlers.group.constants import ERROR_MESSAGES, FSM_STATES
|
||||
from helper_bot.handlers.group.exceptions import (
|
||||
NoReplyToMessageError,
|
||||
UserNotFoundError,
|
||||
)
|
||||
from helper_bot.handlers.group.group_handlers import (
|
||||
GroupHandlers,
|
||||
create_group_handlers,
|
||||
)
|
||||
from helper_bot.handlers.group.exceptions import (NoReplyToMessageError,
|
||||
UserNotFoundError)
|
||||
from helper_bot.handlers.group.group_handlers import (GroupHandlers,
|
||||
create_group_handlers)
|
||||
from helper_bot.handlers.group.services import AdminReplyService
|
||||
|
||||
|
||||
|
||||
@@ -5,12 +5,9 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||
import pytest
|
||||
from aiogram import types
|
||||
from aiogram.fsm.context import FSMContext
|
||||
|
||||
from helper_bot.handlers.private.constants import BUTTON_TEXTS, FSM_STATES
|
||||
from helper_bot.handlers.private.private_handlers import (
|
||||
PrivateHandlers,
|
||||
create_private_handlers,
|
||||
)
|
||||
PrivateHandlers, create_private_handlers)
|
||||
from helper_bot.handlers.private.services import BotSettings
|
||||
|
||||
|
||||
@@ -125,18 +122,19 @@ class TestPrivateHandlers:
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_emoji_message_no_emoji(self, mock_db, mock_settings, mock_message, mock_state):
|
||||
async def test_handle_emoji_message_no_emoji(
|
||||
self, mock_db, mock_settings, mock_message, mock_state
|
||||
):
|
||||
"""handle_emoji_message при user_emoji=None не отправляет ответ с эмодзи."""
|
||||
handlers = create_private_handlers(mock_db, mock_settings)
|
||||
with pytest.MonkeyPatch().context() as m:
|
||||
m.setattr('helper_bot.handlers.private.private_handlers.check_user_emoji', AsyncMock(return_value=None))
|
||||
m.setattr(
|
||||
"helper_bot.handlers.private.private_handlers.check_user_emoji",
|
||||
AsyncMock(return_value=None),
|
||||
)
|
||||
await handlers.handle_emoji_message(mock_message, mock_state)
|
||||
mock_state.set_state.assert_called_once_with(FSM_STATES["START"])
|
||||
mock_message.answer.assert_not_called()
|
||||
|
||||
mock_message.forward.assert_called_once_with(
|
||||
chat_id=mock_settings.group_for_logs
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_start_message(
|
||||
@@ -172,13 +170,24 @@ class TestPrivateHandlers:
|
||||
mock_db.update_user_date.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_restart_message(self, mock_db, mock_settings, mock_message, mock_state):
|
||||
async def test_handle_restart_message(
|
||||
self, mock_db, mock_settings, mock_message, mock_state
|
||||
):
|
||||
"""handle_restart_message перезапускает состояние и отправляет сообщение."""
|
||||
handlers = create_private_handlers(mock_db, mock_settings)
|
||||
with pytest.MonkeyPatch().context() as m:
|
||||
m.setattr('helper_bot.handlers.private.private_handlers.get_reply_keyboard', AsyncMock(return_value=Mock()))
|
||||
m.setattr('helper_bot.handlers.private.private_handlers.update_user_info', AsyncMock())
|
||||
m.setattr('helper_bot.handlers.private.private_handlers.check_user_emoji', AsyncMock())
|
||||
m.setattr(
|
||||
"helper_bot.handlers.private.private_handlers.get_reply_keyboard",
|
||||
AsyncMock(return_value=Mock()),
|
||||
)
|
||||
m.setattr(
|
||||
"helper_bot.handlers.private.private_handlers.update_user_info",
|
||||
AsyncMock(),
|
||||
)
|
||||
m.setattr(
|
||||
"helper_bot.handlers.private.private_handlers.check_user_emoji",
|
||||
AsyncMock(),
|
||||
)
|
||||
await handlers.handle_restart_message(mock_message, mock_state)
|
||||
mock_state.set_state.assert_called_once_with(FSM_STATES["START"])
|
||||
mock_message.answer.assert_called_once()
|
||||
@@ -188,7 +197,10 @@ class TestPrivateHandlers:
|
||||
"""suggest_post переводит в состояние SUGGEST и отправляет текст."""
|
||||
handlers = create_private_handlers(mock_db, mock_settings)
|
||||
with pytest.MonkeyPatch().context() as m:
|
||||
m.setattr('helper_bot.handlers.private.private_handlers.messages.get_message', lambda x, y: "Suggest text")
|
||||
m.setattr(
|
||||
"helper_bot.handlers.private.private_handlers.messages.get_message",
|
||||
lambda x, y: "Suggest text",
|
||||
)
|
||||
await handlers.suggest_post(mock_message, mock_state)
|
||||
mock_state.set_state.assert_called_once_with(FSM_STATES["SUGGEST"])
|
||||
mock_message.answer.assert_called_once()
|
||||
@@ -198,7 +210,10 @@ class TestPrivateHandlers:
|
||||
"""end_message отправляет прощание и переводит в START."""
|
||||
handlers = create_private_handlers(mock_db, mock_settings)
|
||||
with pytest.MonkeyPatch().context() as m:
|
||||
m.setattr('helper_bot.handlers.private.private_handlers.messages.get_message', lambda x, y: "Bye")
|
||||
m.setattr(
|
||||
"helper_bot.handlers.private.private_handlers.messages.get_message",
|
||||
lambda x, y: "Bye",
|
||||
)
|
||||
await handlers.end_message(mock_message, mock_state)
|
||||
mock_state.set_state.assert_called_once_with(FSM_STATES["START"])
|
||||
assert mock_message.answer.await_count >= 1
|
||||
@@ -208,55 +223,93 @@ class TestPrivateHandlers:
|
||||
"""stickers обновляет инфо о стикерах и отправляет ссылку."""
|
||||
handlers = create_private_handlers(mock_db, mock_settings)
|
||||
with pytest.MonkeyPatch().context() as m:
|
||||
m.setattr('helper_bot.handlers.private.private_handlers.get_reply_keyboard', AsyncMock(return_value=Mock()))
|
||||
m.setattr(
|
||||
"helper_bot.handlers.private.private_handlers.get_reply_keyboard",
|
||||
AsyncMock(return_value=Mock()),
|
||||
)
|
||||
await handlers.stickers(mock_message, mock_state)
|
||||
mock_db.update_stickers_info.assert_awaited_once_with(mock_message.from_user.id)
|
||||
mock_state.set_state.assert_called_once_with(FSM_STATES["START"])
|
||||
mock_message.answer.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_connect_with_admin(self, mock_db, mock_settings, mock_message, mock_state):
|
||||
async def test_connect_with_admin(
|
||||
self, mock_db, mock_settings, mock_message, mock_state
|
||||
):
|
||||
"""connect_with_admin переводит в PRE_CHAT и отправляет сообщение."""
|
||||
handlers = create_private_handlers(mock_db, mock_settings)
|
||||
with pytest.MonkeyPatch().context() as m:
|
||||
m.setattr('helper_bot.handlers.private.private_handlers.messages.get_message', lambda x, y: "Admin contact")
|
||||
m.setattr(
|
||||
"helper_bot.handlers.private.private_handlers.messages.get_message",
|
||||
lambda x, y: "Admin contact",
|
||||
)
|
||||
await handlers.connect_with_admin(mock_message, mock_state)
|
||||
mock_state.set_state.assert_called_once_with(FSM_STATES["PRE_CHAT"])
|
||||
mock_message.answer.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resend_message_in_group_pre_chat(self, mock_db, mock_settings, mock_message, mock_state):
|
||||
async def test_resend_message_in_group_pre_chat(
|
||||
self, mock_db, mock_settings, mock_message, mock_state
|
||||
):
|
||||
"""resend_message_in_group при PRE_CHAT переводит в START и отправляет question."""
|
||||
handlers = create_private_handlers(mock_db, mock_settings)
|
||||
mock_state.get_state = AsyncMock(return_value=FSM_STATES["PRE_CHAT"])
|
||||
with pytest.MonkeyPatch().context() as m:
|
||||
m.setattr('helper_bot.handlers.private.private_handlers.get_reply_keyboard', AsyncMock(return_value=Mock()))
|
||||
m.setattr('helper_bot.handlers.private.private_handlers.messages.get_message', lambda x, y: "Question?")
|
||||
m.setattr(
|
||||
"helper_bot.handlers.private.private_handlers.get_reply_keyboard",
|
||||
AsyncMock(return_value=Mock()),
|
||||
)
|
||||
m.setattr(
|
||||
"helper_bot.handlers.private.private_handlers.messages.get_message",
|
||||
lambda x, y: "Question?",
|
||||
)
|
||||
await handlers.resend_message_in_group_for_message(mock_message, mock_state)
|
||||
mock_message.forward.assert_called_once_with(chat_id=mock_settings.group_for_message)
|
||||
mock_message.forward.assert_called_once_with(
|
||||
chat_id=mock_settings.group_for_message
|
||||
)
|
||||
mock_state.set_state.assert_called_once_with(FSM_STATES["START"])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resend_message_in_group_chat(self, mock_db, mock_settings, mock_message, mock_state):
|
||||
async def test_resend_message_in_group_chat(
|
||||
self, mock_db, mock_settings, mock_message, mock_state
|
||||
):
|
||||
"""resend_message_in_group при CHAT оставляет в CHAT и отправляет question с leave markup."""
|
||||
handlers = create_private_handlers(mock_db, mock_settings)
|
||||
mock_state.get_state = AsyncMock(return_value=FSM_STATES["CHAT"])
|
||||
with pytest.MonkeyPatch().context() as m:
|
||||
m.setattr('helper_bot.handlers.private.private_handlers.get_reply_keyboard_leave_chat', lambda: Mock())
|
||||
m.setattr('helper_bot.handlers.private.private_handlers.messages.get_message', lambda x, y: "Question?")
|
||||
m.setattr(
|
||||
"helper_bot.handlers.private.private_handlers.get_reply_keyboard_leave_chat",
|
||||
lambda: Mock(),
|
||||
)
|
||||
m.setattr(
|
||||
"helper_bot.handlers.private.private_handlers.messages.get_message",
|
||||
lambda x, y: "Question?",
|
||||
)
|
||||
await handlers.resend_message_in_group_for_message(mock_message, mock_state)
|
||||
mock_message.forward.assert_called_once_with(chat_id=mock_settings.group_for_message)
|
||||
mock_message.forward.assert_called_once_with(
|
||||
chat_id=mock_settings.group_for_message
|
||||
)
|
||||
mock_message.answer.assert_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_suggest_router_answers_and_schedules_background(self, mock_db, mock_settings, mock_message, mock_state):
|
||||
async def test_suggest_router_answers_and_schedules_background(
|
||||
self, mock_db, mock_settings, mock_message, mock_state
|
||||
):
|
||||
"""suggest_router сразу отвечает и планирует фоновую обработку."""
|
||||
mock_message.media_group_id = None
|
||||
handlers = create_private_handlers(mock_db, mock_settings)
|
||||
with pytest.MonkeyPatch().context() as m:
|
||||
m.setattr('helper_bot.handlers.private.private_handlers.get_reply_keyboard', AsyncMock(return_value=Mock()))
|
||||
m.setattr('helper_bot.handlers.private.private_handlers.messages.get_message', lambda x, y: "Success")
|
||||
with patch.object(handlers.post_service, 'process_post', new_callable=AsyncMock):
|
||||
m.setattr(
|
||||
"helper_bot.handlers.private.private_handlers.get_reply_keyboard",
|
||||
AsyncMock(return_value=Mock()),
|
||||
)
|
||||
m.setattr(
|
||||
"helper_bot.handlers.private.private_handlers.messages.get_message",
|
||||
lambda x, y: "Success",
|
||||
)
|
||||
with patch.object(
|
||||
handlers.post_service, "process_post", new_callable=AsyncMock
|
||||
):
|
||||
await handlers.suggest_router(mock_message, mock_state)
|
||||
mock_state.set_state.assert_called_once_with(FSM_STATES["START"])
|
||||
mock_message.answer.assert_called_once()
|
||||
|
||||
@@ -7,7 +7,6 @@ from pathlib import Path
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from helper_bot.utils.s3_storage import S3StorageService
|
||||
|
||||
|
||||
@@ -90,7 +89,9 @@ class TestS3StorageServiceUploadDownload:
|
||||
mock_context.__aenter__.return_value = mock_s3
|
||||
mock_context.__aexit__.return_value = None
|
||||
mock_session.client.return_value = mock_context
|
||||
with patch("helper_bot.utils.s3_storage.aioboto3.Session", return_value=mock_session):
|
||||
with patch(
|
||||
"helper_bot.utils.s3_storage.aioboto3.Session", return_value=mock_session
|
||||
):
|
||||
s = S3StorageService(
|
||||
endpoint_url="http://s3",
|
||||
access_key="ak",
|
||||
@@ -179,7 +180,9 @@ class TestS3StorageServiceDownloadToTemp:
|
||||
mock_context.__aenter__.return_value = mock_s3
|
||||
mock_context.__aexit__.return_value = None
|
||||
mock_session.client.return_value = mock_context
|
||||
with patch("helper_bot.utils.s3_storage.aioboto3.Session", return_value=mock_session):
|
||||
with patch(
|
||||
"helper_bot.utils.s3_storage.aioboto3.Session", return_value=mock_session
|
||||
):
|
||||
service = S3StorageService(
|
||||
endpoint_url="http://s3",
|
||||
access_key="ak",
|
||||
@@ -204,7 +207,9 @@ class TestS3StorageServiceDownloadToTemp:
|
||||
mock_context.__aenter__.return_value = mock_s3
|
||||
mock_context.__aexit__.return_value = None
|
||||
mock_session.client.return_value = mock_context
|
||||
with patch("helper_bot.utils.s3_storage.aioboto3.Session", return_value=mock_session):
|
||||
with patch(
|
||||
"helper_bot.utils.s3_storage.aioboto3.Session", return_value=mock_session
|
||||
):
|
||||
service = S3StorageService(
|
||||
endpoint_url="http://s3",
|
||||
access_key="ak",
|
||||
|
||||
@@ -6,14 +6,11 @@ import json
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
# Импорты для тестирования базовых классов
|
||||
from helper_bot.services.scoring.base import CombinedScore, ScoringResult
|
||||
from helper_bot.services.scoring.exceptions import (
|
||||
InsufficientExamplesError,
|
||||
ScoringError,
|
||||
TextTooShortError,
|
||||
)
|
||||
from helper_bot.services.scoring.exceptions import (InsufficientExamplesError,
|
||||
ScoringError,
|
||||
TextTooShortError)
|
||||
|
||||
|
||||
class TestScoringResult:
|
||||
@@ -137,7 +134,6 @@ class TestVectorStore:
|
||||
"""Создает VectorStore для тестов."""
|
||||
try:
|
||||
import numpy as np
|
||||
|
||||
from helper_bot.services.scoring.vector_store import VectorStore
|
||||
|
||||
return VectorStore(vector_dim=768, max_examples=100)
|
||||
@@ -224,7 +220,8 @@ class TestDeepSeekService:
|
||||
@pytest.fixture
|
||||
def deepseek_service(self):
|
||||
"""Создает DeepSeekService для тестов."""
|
||||
from helper_bot.services.scoring.deepseek_service import DeepSeekService
|
||||
from helper_bot.services.scoring.deepseek_service import \
|
||||
DeepSeekService
|
||||
|
||||
return DeepSeekService(
|
||||
api_key="test_key",
|
||||
@@ -234,7 +231,8 @@ class TestDeepSeekService:
|
||||
|
||||
def test_service_disabled_without_key(self):
|
||||
"""Тест отключения сервиса без API ключа."""
|
||||
from helper_bot.services.scoring.deepseek_service import DeepSeekService
|
||||
from helper_bot.services.scoring.deepseek_service import \
|
||||
DeepSeekService
|
||||
|
||||
service = DeepSeekService(api_key=None, enabled=True)
|
||||
|
||||
@@ -266,7 +264,8 @@ class TestDeepSeekService:
|
||||
@pytest.mark.asyncio
|
||||
async def test_calculate_score_disabled(self):
|
||||
"""Тест расчета скора при отключенном сервисе."""
|
||||
from helper_bot.services.scoring.deepseek_service import DeepSeekService
|
||||
from helper_bot.services.scoring.deepseek_service import \
|
||||
DeepSeekService
|
||||
|
||||
service = DeepSeekService(api_key=None, enabled=False)
|
||||
|
||||
|
||||
@@ -6,12 +6,8 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from aiohttp import web
|
||||
|
||||
from helper_bot.server_prometheus import (
|
||||
MetricsServer,
|
||||
start_metrics_server,
|
||||
stop_metrics_server,
|
||||
)
|
||||
from helper_bot.server_prometheus import (MetricsServer, start_metrics_server,
|
||||
stop_metrics_server)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -40,7 +36,9 @@ class TestMetricsServer:
|
||||
self, mock_metrics_module
|
||||
):
|
||||
"""metrics_handler при успехе возвращает 200 и данные метрик."""
|
||||
mock_metrics_module.get_metrics.return_value = b"# TYPE bot_commands_total counter"
|
||||
mock_metrics_module.get_metrics.return_value = (
|
||||
b"# TYPE bot_commands_total counter"
|
||||
)
|
||||
server = MetricsServer(host="0.0.0.0", port=8080)
|
||||
request = MagicMock(spec=web.Request)
|
||||
|
||||
@@ -63,9 +61,7 @@ class TestMetricsServer:
|
||||
assert "Metrics not available" in response.text
|
||||
|
||||
@patch("helper_bot.server_prometheus.metrics")
|
||||
async def test_metrics_handler_on_exception_returns_500(
|
||||
self, mock_metrics_module
|
||||
):
|
||||
async def test_metrics_handler_on_exception_returns_500(self, mock_metrics_module):
|
||||
"""metrics_handler при исключении в get_metrics возвращает 500."""
|
||||
mock_metrics_module.get_metrics.side_effect = RuntimeError("metrics error")
|
||||
server = MetricsServer(host="0.0.0.0", port=8080)
|
||||
@@ -100,9 +96,7 @@ class TestMetricsServer:
|
||||
assert "Metrics not available" in response.text
|
||||
|
||||
@patch("helper_bot.server_prometheus.metrics")
|
||||
async def test_health_handler_empty_metrics_returns_503(
|
||||
self, mock_metrics_module
|
||||
):
|
||||
async def test_health_handler_empty_metrics_returns_503(self, mock_metrics_module):
|
||||
"""health_handler при пустых метриках возвращает 503."""
|
||||
mock_metrics_module.get_metrics.return_value = b""
|
||||
server = MetricsServer(host="0.0.0.0", port=8080)
|
||||
@@ -173,9 +167,7 @@ class TestMetricsServer:
|
||||
|
||||
@patch.object(MetricsServer, "start", new_callable=AsyncMock)
|
||||
@patch.object(MetricsServer, "stop", new_callable=AsyncMock)
|
||||
async def test_context_manager_enters_and_exits(
|
||||
self, mock_stop, mock_start
|
||||
):
|
||||
async def test_context_manager_enters_and_exits(self, mock_stop, mock_start):
|
||||
"""Использование как async context manager вызывает start и stop."""
|
||||
mock_start.return_value = None
|
||||
server = MetricsServer(host="0.0.0.0", port=8080)
|
||||
@@ -228,6 +220,7 @@ class TestStartStopMetricsServer:
|
||||
):
|
||||
"""stop_metrics_server при запущенном сервере останавливает его и обнуляет глобальную переменную."""
|
||||
import helper_bot.server_prometheus as mod
|
||||
|
||||
mock_instance = MagicMock()
|
||||
mock_instance.stop = AsyncMock()
|
||||
old_server = mod.metrics_server
|
||||
@@ -242,6 +235,7 @@ class TestStartStopMetricsServer:
|
||||
async def test_stop_metrics_server_when_none_does_not_raise(self):
|
||||
"""stop_metrics_server при metrics_server=None не падает."""
|
||||
import helper_bot.server_prometheus as mod
|
||||
|
||||
old_server = mod.metrics_server
|
||||
mod.metrics_server = None
|
||||
try:
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from helper_bot.middlewares.text_middleware import BulkTextMiddleware
|
||||
|
||||
|
||||
|
||||
@@ -2,41 +2,21 @@ import os
|
||||
from datetime import datetime
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
import helper_bot.utils.messages as messages # Import for patching constants
|
||||
import pytest
|
||||
from database.async_db import AsyncBotDB
|
||||
from helper_bot.utils.base_dependency_factory import (
|
||||
BaseDependencyFactory,
|
||||
get_global_instance,
|
||||
)
|
||||
from helper_bot.utils.base_dependency_factory import (BaseDependencyFactory,
|
||||
get_global_instance)
|
||||
from helper_bot.utils.helper_func import (
|
||||
add_days_to_date,
|
||||
add_in_db_media,
|
||||
add_in_db_media_mediagroup,
|
||||
check_access,
|
||||
check_user_emoji,
|
||||
check_username_and_full_name,
|
||||
delete_user_blacklist,
|
||||
determine_anonymity,
|
||||
download_file,
|
||||
get_banned_users_buttons,
|
||||
get_banned_users_list,
|
||||
get_first_name,
|
||||
get_random_emoji,
|
||||
get_text_message,
|
||||
prepare_media_group_from_middlewares,
|
||||
safe_html_escape,
|
||||
send_audio_message,
|
||||
send_media_group_message_to_private_chat,
|
||||
send_media_group_to_channel,
|
||||
send_photo_message,
|
||||
send_text_message,
|
||||
send_video_message,
|
||||
send_video_note_message,
|
||||
send_voice_message,
|
||||
update_user_info,
|
||||
)
|
||||
add_days_to_date, add_in_db_media, add_in_db_media_mediagroup,
|
||||
check_access, check_user_emoji, check_username_and_full_name,
|
||||
delete_user_blacklist, determine_anonymity, download_file,
|
||||
get_banned_users_buttons, get_banned_users_list, get_first_name,
|
||||
get_random_emoji, get_text_message, prepare_media_group_from_middlewares,
|
||||
safe_html_escape, send_audio_message,
|
||||
send_media_group_message_to_private_chat, send_media_group_to_channel,
|
||||
send_photo_message, send_text_message, send_video_message,
|
||||
send_video_note_message, send_voice_message, update_user_info)
|
||||
from helper_bot.utils.messages import get_message
|
||||
|
||||
|
||||
|
||||
@@ -3,14 +3,12 @@ from pathlib import Path
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from helper_bot.handlers.voice.exceptions import AudioProcessingError, VoiceMessageError
|
||||
from helper_bot.handlers.voice.exceptions import (AudioProcessingError,
|
||||
VoiceMessageError)
|
||||
from helper_bot.handlers.voice.services import VoiceBotService
|
||||
from helper_bot.handlers.voice.utils import (
|
||||
get_last_message_text,
|
||||
get_user_emoji_safe,
|
||||
validate_voice_message,
|
||||
)
|
||||
from helper_bot.handlers.voice.utils import (get_last_message_text,
|
||||
get_user_emoji_safe,
|
||||
validate_voice_message)
|
||||
|
||||
|
||||
class TestVoiceBotService:
|
||||
|
||||
@@ -1,22 +1,14 @@
|
||||
import pytest
|
||||
|
||||
from helper_bot.handlers.voice.constants import (
|
||||
BTN_LISTEN,
|
||||
BTN_SPEAK,
|
||||
BUTTON_COMMAND_MAPPING,
|
||||
CALLBACK_COMMAND_MAPPING,
|
||||
CALLBACK_DELETE,
|
||||
CALLBACK_SAVE,
|
||||
CMD_EMOJI,
|
||||
CMD_HELP,
|
||||
CMD_REFRESH,
|
||||
CMD_RESTART,
|
||||
CMD_START,
|
||||
COMMAND_MAPPING,
|
||||
STATE_STANDUP_WRITE,
|
||||
STATE_START,
|
||||
VOICE_BOT_NAME,
|
||||
)
|
||||
from helper_bot.handlers.voice.constants import (BTN_LISTEN, BTN_SPEAK,
|
||||
BUTTON_COMMAND_MAPPING,
|
||||
CALLBACK_COMMAND_MAPPING,
|
||||
CALLBACK_DELETE,
|
||||
CALLBACK_SAVE, CMD_EMOJI,
|
||||
CMD_HELP, CMD_REFRESH,
|
||||
CMD_RESTART, CMD_START,
|
||||
COMMAND_MAPPING,
|
||||
STATE_STANDUP_WRITE,
|
||||
STATE_START, VOICE_BOT_NAME)
|
||||
|
||||
|
||||
class TestVoiceConstants:
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import pytest
|
||||
|
||||
from helper_bot.handlers.voice.exceptions import (
|
||||
AudioProcessingError,
|
||||
VoiceBotError,
|
||||
VoiceMessageError,
|
||||
)
|
||||
from helper_bot.handlers.voice.exceptions import (AudioProcessingError,
|
||||
VoiceBotError,
|
||||
VoiceMessageError)
|
||||
|
||||
|
||||
class TestVoiceExceptions:
|
||||
|
||||
@@ -3,8 +3,8 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||
import pytest
|
||||
from aiogram import types
|
||||
from aiogram.fsm.context import FSMContext
|
||||
|
||||
from helper_bot.handlers.voice.constants import STATE_STANDUP_WRITE, STATE_START
|
||||
from helper_bot.handlers.voice.constants import (STATE_STANDUP_WRITE,
|
||||
STATE_START)
|
||||
from helper_bot.handlers.voice.voice_handler import VoiceHandlers
|
||||
|
||||
|
||||
@@ -146,113 +146,226 @@ class TestVoiceHandler:
|
||||
assert len(voice_handler.router.message.handlers) > 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_restart_function_sets_state_and_answers(self, voice_handler, mock_message, mock_state, mock_db, mock_settings):
|
||||
async def test_restart_function_sets_state_and_answers(
|
||||
self, voice_handler, mock_message, mock_state, mock_db, mock_settings
|
||||
):
|
||||
"""restart_function пересылает в логи, обновляет инфо и отправляет клавиатуру."""
|
||||
with patch('helper_bot.handlers.voice.voice_handler.update_user_info', new_callable=AsyncMock):
|
||||
with patch('helper_bot.handlers.voice.voice_handler.check_user_emoji', new_callable=AsyncMock):
|
||||
with patch('helper_bot.handlers.voice.voice_handler.get_main_keyboard') as mock_keyboard:
|
||||
with patch(
|
||||
"helper_bot.handlers.voice.voice_handler.update_user_info",
|
||||
new_callable=AsyncMock,
|
||||
):
|
||||
with patch(
|
||||
"helper_bot.handlers.voice.voice_handler.check_user_emoji",
|
||||
new_callable=AsyncMock,
|
||||
):
|
||||
with patch(
|
||||
"helper_bot.handlers.voice.voice_handler.get_main_keyboard"
|
||||
) as mock_keyboard:
|
||||
mock_keyboard.return_value = MagicMock()
|
||||
await voice_handler.restart_function(mock_message, mock_state, mock_db, mock_settings)
|
||||
mock_message.forward.assert_awaited_once_with(chat_id=mock_settings['Telegram']['group_for_logs'])
|
||||
await voice_handler.restart_function(
|
||||
mock_message, mock_state, mock_db, mock_settings
|
||||
)
|
||||
mock_message.forward.assert_awaited_once_with(
|
||||
chat_id=mock_settings["Telegram"]["group_for_logs"]
|
||||
)
|
||||
mock_state.set_state.assert_called_once_with(STATE_START)
|
||||
mock_message.answer.assert_called_once()
|
||||
assert 'Записывайся' in mock_message.answer.call_args[1]['text'] or 'слушай' in mock_message.answer.call_args[1]['text']
|
||||
assert (
|
||||
"Записывайся" in mock_message.answer.call_args[1]["text"]
|
||||
or "слушай" in mock_message.answer.call_args[1]["text"]
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_sets_state_and_sends_welcome(self, voice_handler, mock_message, mock_state, mock_db, mock_settings):
|
||||
async def test_start_sets_state_and_sends_welcome(
|
||||
self, voice_handler, mock_message, mock_state, mock_db, mock_settings
|
||||
):
|
||||
"""start устанавливает состояние и отправляет приветствие через VoiceBotService."""
|
||||
mock_db.mark_voice_bot_welcome_received = AsyncMock()
|
||||
with patch('helper_bot.handlers.voice.voice_handler.update_user_info', new_callable=AsyncMock):
|
||||
with patch('helper_bot.handlers.voice.voice_handler.get_user_emoji_safe', new_callable=AsyncMock, return_value='😊'):
|
||||
with patch('helper_bot.handlers.voice.voice_handler.VoiceBotService') as mock_svc_cls:
|
||||
mock_svc = MagicMock()
|
||||
mock_svc.send_welcome_messages = AsyncMock()
|
||||
mock_svc_cls.return_value = mock_svc
|
||||
await voice_handler.start(mock_message, mock_state, mock_db, mock_settings)
|
||||
mock_state.set_state.assert_called_once_with(STATE_START)
|
||||
mock_svc.send_welcome_messages.assert_awaited_once()
|
||||
mock_db.mark_voice_bot_welcome_received.assert_awaited_once_with(123)
|
||||
with patch(
|
||||
"helper_bot.handlers.voice.voice_handler.update_user_info",
|
||||
new_callable=AsyncMock,
|
||||
):
|
||||
with patch(
|
||||
"helper_bot.handlers.voice.voice_handler.get_user_emoji_safe",
|
||||
new_callable=AsyncMock,
|
||||
return_value="😊",
|
||||
):
|
||||
with patch(
|
||||
"helper_bot.handlers.voice.voice_handler.VoiceBotService"
|
||||
) as mock_svc_cls:
|
||||
mock_svc = MagicMock()
|
||||
mock_svc.send_welcome_messages = AsyncMock()
|
||||
mock_svc_cls.return_value = mock_svc
|
||||
await voice_handler.start(
|
||||
mock_message, mock_state, mock_db, mock_settings
|
||||
)
|
||||
mock_state.set_state.assert_called_once_with(STATE_START)
|
||||
mock_svc.send_welcome_messages.assert_awaited_once()
|
||||
mock_db.mark_voice_bot_welcome_received.assert_awaited_once_with(
|
||||
123
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_help_function_answers_help_message(self, voice_handler, mock_message, mock_state, mock_settings):
|
||||
async def test_help_function_answers_help_message(
|
||||
self, voice_handler, mock_message, mock_state, mock_settings
|
||||
):
|
||||
"""help_function пересылает в логи и отправляет HELP_MESSAGE."""
|
||||
with patch('helper_bot.handlers.voice.voice_handler.update_user_info', new_callable=AsyncMock):
|
||||
with patch('helper_bot.handlers.voice.voice_handler.messages.get_message') as mock_get:
|
||||
with patch(
|
||||
"helper_bot.handlers.voice.voice_handler.update_user_info",
|
||||
new_callable=AsyncMock,
|
||||
):
|
||||
with patch(
|
||||
"helper_bot.handlers.voice.voice_handler.messages.get_message"
|
||||
) as mock_get:
|
||||
mock_get.return_value = "Help text"
|
||||
await voice_handler.help_function(mock_message, mock_state, mock_settings)
|
||||
mock_message.forward.assert_awaited_once_with(chat_id=mock_settings['Telegram']['group_for_logs'])
|
||||
mock_message.answer.assert_called_once_with(text="Help text", disable_web_page_preview=not mock_settings['Telegram']['preview_link'])
|
||||
await voice_handler.help_function(
|
||||
mock_message, mock_state, mock_settings
|
||||
)
|
||||
mock_message.forward.assert_awaited_once_with(
|
||||
chat_id=mock_settings["Telegram"]["group_for_logs"]
|
||||
)
|
||||
mock_message.answer.assert_called_once_with(
|
||||
text="Help text",
|
||||
disable_web_page_preview=not mock_settings["Telegram"][
|
||||
"preview_link"
|
||||
],
|
||||
)
|
||||
mock_state.set_state.assert_called_once_with(STATE_START)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cancel_handler_returns_to_menu(self, voice_handler, mock_message, mock_state, mock_db, mock_settings):
|
||||
async def test_cancel_handler_returns_to_menu(
|
||||
self, voice_handler, mock_message, mock_state, mock_db, mock_settings
|
||||
):
|
||||
"""cancel_handler пересылает в логи и возвращает в главное меню."""
|
||||
with patch('helper_bot.handlers.voice.voice_handler.update_user_info', new_callable=AsyncMock):
|
||||
with patch('helper_bot.handlers.voice.voice_handler.get_reply_keyboard', new_callable=AsyncMock) as mock_kb:
|
||||
with patch(
|
||||
"helper_bot.handlers.voice.voice_handler.update_user_info",
|
||||
new_callable=AsyncMock,
|
||||
):
|
||||
with patch(
|
||||
"helper_bot.handlers.voice.voice_handler.get_reply_keyboard",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_kb:
|
||||
mock_kb.return_value = MagicMock()
|
||||
await voice_handler.cancel_handler(mock_message, mock_state, mock_db, mock_settings)
|
||||
await voice_handler.cancel_handler(
|
||||
mock_message, mock_state, mock_db, mock_settings
|
||||
)
|
||||
mock_message.forward.assert_awaited_once()
|
||||
mock_message.answer.assert_called_once()
|
||||
assert 'Добро пожаловать' in mock_message.answer.call_args[1]['text'] or 'меню' in mock_message.answer.call_args[1]['text']
|
||||
assert (
|
||||
"Добро пожаловать" in mock_message.answer.call_args[1]["text"]
|
||||
or "меню" in mock_message.answer.call_args[1]["text"]
|
||||
)
|
||||
mock_state.set_state.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_listen_function_clears_listenings(self, voice_handler, mock_message, mock_state, mock_db, mock_settings):
|
||||
async def test_refresh_listen_function_clears_listenings(
|
||||
self, voice_handler, mock_message, mock_state, mock_db, mock_settings
|
||||
):
|
||||
"""refresh_listen_function очищает прослушивания и отправляет сообщение."""
|
||||
with patch('helper_bot.handlers.voice.voice_handler.update_user_info', new_callable=AsyncMock):
|
||||
with patch('helper_bot.handlers.voice.voice_handler.get_main_keyboard') as mock_keyboard:
|
||||
with patch('helper_bot.handlers.voice.voice_handler.VoiceBotService') as mock_svc_cls:
|
||||
with patch(
|
||||
"helper_bot.handlers.voice.voice_handler.update_user_info",
|
||||
new_callable=AsyncMock,
|
||||
):
|
||||
with patch(
|
||||
"helper_bot.handlers.voice.voice_handler.get_main_keyboard"
|
||||
) as mock_keyboard:
|
||||
with patch(
|
||||
"helper_bot.handlers.voice.voice_handler.VoiceBotService"
|
||||
) as mock_svc_cls:
|
||||
mock_svc = MagicMock()
|
||||
mock_svc.clear_user_listenings = AsyncMock()
|
||||
mock_svc_cls.return_value = mock_svc
|
||||
with patch('helper_bot.handlers.voice.voice_handler.messages.get_message') as mock_get:
|
||||
with patch(
|
||||
"helper_bot.handlers.voice.voice_handler.messages.get_message"
|
||||
) as mock_get:
|
||||
mock_get.return_value = "Прослушивания сброшены"
|
||||
await voice_handler.refresh_listen_function(mock_message, mock_state, mock_db, mock_settings)
|
||||
await voice_handler.refresh_listen_function(
|
||||
mock_message, mock_state, mock_db, mock_settings
|
||||
)
|
||||
mock_svc.clear_user_listenings.assert_awaited_once_with(123)
|
||||
mock_message.answer.assert_called_once()
|
||||
mock_state.set_state.assert_called_once_with(STATE_START)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_suggest_voice_valid_sends_to_group_and_saves(self, voice_handler, mock_message, mock_state, mock_db, mock_settings):
|
||||
async def test_suggest_voice_valid_sends_to_group_and_saves(
|
||||
self, voice_handler, mock_message, mock_state, mock_db, mock_settings
|
||||
):
|
||||
"""suggest_voice при валидном голосовом отправляет в группу и сохраняет message_id."""
|
||||
mock_message.voice = MagicMock()
|
||||
mock_message.voice.file_id = "voice_123"
|
||||
with patch('helper_bot.handlers.voice.voice_handler.validate_voice_message', new_callable=AsyncMock, return_value=True):
|
||||
with patch('helper_bot.handlers.voice.voice_handler.send_voice_message', new_callable=AsyncMock) as mock_send:
|
||||
with patch(
|
||||
"helper_bot.handlers.voice.voice_handler.validate_voice_message",
|
||||
new_callable=AsyncMock,
|
||||
return_value=True,
|
||||
):
|
||||
with patch(
|
||||
"helper_bot.handlers.voice.voice_handler.send_voice_message",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_send:
|
||||
sent = MagicMock()
|
||||
sent.message_id = 999
|
||||
mock_send.return_value = sent
|
||||
mock_db.set_user_id_and_message_id_for_voice_bot = AsyncMock()
|
||||
with patch('helper_bot.handlers.voice.voice_handler.get_reply_keyboard_for_voice') as mock_kb:
|
||||
with patch(
|
||||
"helper_bot.handlers.voice.voice_handler.get_reply_keyboard_for_voice"
|
||||
) as mock_kb:
|
||||
mock_kb.return_value = MagicMock()
|
||||
with patch('helper_bot.handlers.voice.voice_handler.messages.get_message') as mock_get:
|
||||
with patch(
|
||||
"helper_bot.handlers.voice.voice_handler.messages.get_message"
|
||||
) as mock_get:
|
||||
mock_get.return_value = "Голос сохранён"
|
||||
await voice_handler.suggest_voice(mock_message, mock_state, mock_db, mock_settings)
|
||||
await voice_handler.suggest_voice(
|
||||
mock_message, mock_state, mock_db, mock_settings
|
||||
)
|
||||
mock_send.assert_awaited_once()
|
||||
mock_db.set_user_id_and_message_id_for_voice_bot.assert_awaited_once_with(999, 123)
|
||||
mock_db.set_user_id_and_message_id_for_voice_bot.assert_awaited_once_with(
|
||||
999, 123
|
||||
)
|
||||
mock_message.answer.assert_called_once()
|
||||
mock_state.set_state.assert_called_once_with(STATE_START)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_suggest_voice_invalid_keeps_state_standup_write(self, voice_handler, mock_message, mock_state, mock_db, mock_settings):
|
||||
async def test_suggest_voice_invalid_keeps_state_standup_write(
|
||||
self, voice_handler, mock_message, mock_state, mock_db, mock_settings
|
||||
):
|
||||
"""suggest_voice при невалидном голосовом оставляет состояние STANDUP_WRITE."""
|
||||
with patch('helper_bot.handlers.voice.voice_handler.validate_voice_message', new_callable=AsyncMock, return_value=False):
|
||||
with patch('helper_bot.handlers.voice.voice_handler.get_main_keyboard') as mock_keyboard:
|
||||
with patch(
|
||||
"helper_bot.handlers.voice.voice_handler.validate_voice_message",
|
||||
new_callable=AsyncMock,
|
||||
return_value=False,
|
||||
):
|
||||
with patch(
|
||||
"helper_bot.handlers.voice.voice_handler.get_main_keyboard"
|
||||
) as mock_keyboard:
|
||||
mock_keyboard.return_value = MagicMock()
|
||||
with patch('helper_bot.handlers.voice.voice_handler.messages.get_message') as mock_get:
|
||||
with patch(
|
||||
"helper_bot.handlers.voice.voice_handler.messages.get_message"
|
||||
) as mock_get:
|
||||
mock_get.return_value = "Неверный контент"
|
||||
await voice_handler.suggest_voice(mock_message, mock_state, mock_db, mock_settings)
|
||||
await voice_handler.suggest_voice(
|
||||
mock_message, mock_state, mock_db, mock_settings
|
||||
)
|
||||
mock_message.answer.assert_called()
|
||||
mock_state.set_state.assert_called_once_with(STATE_STANDUP_WRITE)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_emoji_message_answers_emoji(self, voice_handler, mock_message, mock_state, mock_settings):
|
||||
async def test_handle_emoji_message_answers_emoji(
|
||||
self, voice_handler, mock_message, mock_state, mock_settings
|
||||
):
|
||||
"""handle_emoji_message пересылает в логи и отвечает эмодзи или ничего."""
|
||||
with patch('helper_bot.handlers.voice.voice_handler.check_user_emoji', new_callable=AsyncMock, return_value='😊'):
|
||||
await voice_handler.handle_emoji_message(mock_message, mock_state, mock_settings)
|
||||
with patch(
|
||||
"helper_bot.handlers.voice.voice_handler.check_user_emoji",
|
||||
new_callable=AsyncMock,
|
||||
return_value="😊",
|
||||
):
|
||||
await voice_handler.handle_emoji_message(
|
||||
mock_message, mock_state, mock_settings
|
||||
)
|
||||
mock_message.forward.assert_awaited_once()
|
||||
mock_state.set_state.assert_called_once_with(STATE_START)
|
||||
mock_message.answer.assert_called_once_with(f'Твоя эмодзя - 😊', parse_mode='HTML')
|
||||
mock_message.answer.assert_called_once_with(
|
||||
f"Твоя эмодзя - 😊", parse_mode="HTML"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -3,8 +3,8 @@ from pathlib import Path
|
||||
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from helper_bot.handlers.voice.exceptions import AudioProcessingError, VoiceMessageError
|
||||
from helper_bot.handlers.voice.exceptions import (AudioProcessingError,
|
||||
VoiceMessageError)
|
||||
from helper_bot.handlers.voice.services import VoiceBotService
|
||||
|
||||
|
||||
@@ -275,9 +275,11 @@ class TestVoiceBotService:
|
||||
assert hasattr(voice_service, "send_welcome_messages")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_welcome_sticker_exception_returns_none(self, voice_service, mock_settings):
|
||||
async def test_get_welcome_sticker_exception_returns_none(
|
||||
self, voice_service, mock_settings
|
||||
):
|
||||
"""get_welcome_sticker при исключении возвращает None."""
|
||||
with patch('pathlib.Path.rglob') as mock_rglob:
|
||||
with patch("pathlib.Path.rglob") as mock_rglob:
|
||||
mock_rglob.side_effect = OSError("Permission denied")
|
||||
|
||||
sticker = await voice_service.get_welcome_sticker()
|
||||
@@ -285,11 +287,15 @@ class TestVoiceBotService:
|
||||
assert sticker is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_welcome_sticker_exception_sends_to_logs_when_enabled(self, voice_service, mock_settings):
|
||||
async def test_get_welcome_sticker_exception_sends_to_logs_when_enabled(
|
||||
self, voice_service, mock_settings
|
||||
):
|
||||
"""get_welcome_sticker при исключении и logs=True отправляет ошибку в логи."""
|
||||
voice_service.settings = {'Settings': {'logs': True}, 'Telegram': {}}
|
||||
with patch('pathlib.Path.rglob', side_effect=OSError("err")):
|
||||
with patch.object(voice_service, '_send_error_to_logs', new_callable=AsyncMock) as mock_send_logs:
|
||||
voice_service.settings = {"Settings": {"logs": True}, "Telegram": {}}
|
||||
with patch("pathlib.Path.rglob", side_effect=OSError("err")):
|
||||
with patch.object(
|
||||
voice_service, "_send_error_to_logs", new_callable=AsyncMock
|
||||
) as mock_send_logs:
|
||||
await voice_service.get_welcome_sticker()
|
||||
mock_send_logs.assert_awaited_once()
|
||||
|
||||
@@ -298,63 +304,90 @@ class TestVoiceBotService:
|
||||
"""get_random_audio при исключении выбрасывает AudioProcessingError."""
|
||||
mock_bot_db.check_listen_audio = AsyncMock(side_effect=Exception("DB error"))
|
||||
|
||||
with pytest.raises(AudioProcessingError, match="Не удалось получить случайное аудио"):
|
||||
with pytest.raises(
|
||||
AudioProcessingError, match="Не удалось получить случайное аудио"
|
||||
):
|
||||
await voice_service.get_random_audio(123)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mark_audio_as_listened_exception_raises(self, voice_service, mock_bot_db):
|
||||
async def test_mark_audio_as_listened_exception_raises(
|
||||
self, voice_service, mock_bot_db
|
||||
):
|
||||
"""mark_audio_as_listened при исключении выбрасывает DatabaseError."""
|
||||
from helper_bot.handlers.voice.exceptions import DatabaseError
|
||||
|
||||
mock_bot_db.mark_listened_audio = AsyncMock(side_effect=Exception("DB error"))
|
||||
|
||||
with pytest.raises(DatabaseError, match="Не удалось пометить аудио"):
|
||||
await voice_service.mark_audio_as_listened("file", 123)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_clear_user_listenings_exception_raises(self, voice_service, mock_bot_db):
|
||||
async def test_clear_user_listenings_exception_raises(
|
||||
self, voice_service, mock_bot_db
|
||||
):
|
||||
"""clear_user_listenings при исключении выбрасывает DatabaseError."""
|
||||
from helper_bot.handlers.voice.exceptions import DatabaseError
|
||||
mock_bot_db.delete_listen_count_for_user = AsyncMock(side_effect=Exception("DB error"))
|
||||
|
||||
mock_bot_db.delete_listen_count_for_user = AsyncMock(
|
||||
side_effect=Exception("DB error")
|
||||
)
|
||||
|
||||
with pytest.raises(DatabaseError, match="Не удалось очистить прослушивания"):
|
||||
await voice_service.clear_user_listenings(123)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_remaining_audio_count_exception_raises(self, voice_service, mock_bot_db):
|
||||
async def test_get_remaining_audio_count_exception_raises(
|
||||
self, voice_service, mock_bot_db
|
||||
):
|
||||
"""get_remaining_audio_count при исключении выбрасывает DatabaseError."""
|
||||
from helper_bot.handlers.voice.exceptions import DatabaseError
|
||||
|
||||
mock_bot_db.check_listen_audio = AsyncMock(side_effect=Exception("DB error"))
|
||||
|
||||
with pytest.raises(DatabaseError, match="Не удалось получить количество аудио"):
|
||||
await voice_service.get_remaining_audio_count(123)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_welcome_messages_exception_raises(self, voice_service, mock_bot_db, mock_settings):
|
||||
async def test_send_welcome_messages_exception_raises(
|
||||
self, voice_service, mock_bot_db, mock_settings
|
||||
):
|
||||
"""send_welcome_messages при исключении выбрасывает VoiceMessageError."""
|
||||
mock_message = Mock()
|
||||
mock_message.answer = AsyncMock(side_effect=Exception("Network error"))
|
||||
mock_message.answer_sticker = AsyncMock()
|
||||
with patch.object(voice_service, 'get_welcome_sticker', new_callable=AsyncMock, return_value=None):
|
||||
with patch.object(voice_service, '_get_main_keyboard', return_value=Mock()):
|
||||
with pytest.raises(VoiceMessageError, match="Не удалось отправить приветственные сообщения"):
|
||||
with patch.object(
|
||||
voice_service,
|
||||
"get_welcome_sticker",
|
||||
new_callable=AsyncMock,
|
||||
return_value=None,
|
||||
):
|
||||
with patch.object(voice_service, "_get_main_keyboard", return_value=Mock()):
|
||||
with pytest.raises(
|
||||
VoiceMessageError,
|
||||
match="Не удалось отправить приветственные сообщения",
|
||||
):
|
||||
await voice_service.send_welcome_messages(mock_message, "😊")
|
||||
|
||||
def test_get_main_keyboard_returns_keyboard(self, voice_service):
|
||||
"""_get_main_keyboard возвращает клавиатуру."""
|
||||
with patch('helper_bot.keyboards.keyboards.get_main_keyboard') as mock_kb:
|
||||
with patch("helper_bot.keyboards.keyboards.get_main_keyboard") as mock_kb:
|
||||
mock_kb.return_value = Mock()
|
||||
result = voice_service._get_main_keyboard()
|
||||
mock_kb.assert_called_once()
|
||||
assert result is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_error_to_logs_handles_exception(self, voice_service, mock_settings):
|
||||
async def test_send_error_to_logs_handles_exception(
|
||||
self, voice_service, mock_settings
|
||||
):
|
||||
"""_send_error_to_logs при ошибке отправки логирует и не падает."""
|
||||
voice_service.settings = {
|
||||
'Settings': {},
|
||||
'Telegram': {'important_logs': '-123'},
|
||||
"Settings": {},
|
||||
"Telegram": {"important_logs": "-123"},
|
||||
}
|
||||
with patch('helper_bot.utils.helper_func.send_voice_message', new_callable=AsyncMock) as mock_send:
|
||||
with patch(
|
||||
"helper_bot.utils.helper_func.send_voice_message", new_callable=AsyncMock
|
||||
) as mock_send:
|
||||
mock_send.side_effect = Exception("Send failed")
|
||||
await voice_service._send_error_to_logs("Test error")
|
||||
mock_send.assert_awaited_once()
|
||||
|
||||
@@ -3,14 +3,10 @@ from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
from aiogram import types
|
||||
|
||||
from helper_bot.handlers.voice.utils import (
|
||||
format_time_ago,
|
||||
get_last_message_text,
|
||||
get_user_emoji_safe,
|
||||
plural_time,
|
||||
validate_voice_message,
|
||||
)
|
||||
from helper_bot.handlers.voice.utils import (format_time_ago,
|
||||
get_last_message_text,
|
||||
get_user_emoji_safe, plural_time,
|
||||
validate_voice_message)
|
||||
|
||||
|
||||
class TestVoiceUtils:
|
||||
|
||||
Reference in New Issue
Block a user