From d87d4e492ec44b1fdc1a920c6476ae79aff17d0c Mon Sep 17 00:00:00 2001 From: Andrey Date: Mon, 2 Feb 2026 00:46:44 +0300 Subject: [PATCH] fix linter, fix ci, fix tests --- .github/workflows/ci.yml | 4 +- database/__init__.py | 16 +- database/async_db.py | 14 +- database/base.py | 1 - database/repositories/migration_repository.py | 1 - database/repository_factory.py | 5 +- helper_bot/handlers/admin/__init__.py | 18 +- helper_bot/handlers/admin/admin_handlers.py | 32 +-- helper_bot/handlers/admin/dependencies.py | 1 - .../handlers/admin/rate_limit_handlers.py | 15 +- helper_bot/handlers/admin/services.py | 15 +- helper_bot/handlers/admin/utils.py | 1 - helper_bot/handlers/callback/__init__.py | 19 +- .../handlers/callback/callback_handlers.py | 49 ++-- .../handlers/callback/dependency_factory.py | 1 - helper_bot/handlers/callback/services.py | 57 ++--- helper_bot/handlers/group/__init__.py | 1 - helper_bot/handlers/group/decorators.py | 6 +- helper_bot/handlers/group/group_handlers.py | 3 - helper_bot/handlers/group/services.py | 2 - helper_bot/handlers/private/__init__.py | 4 +- helper_bot/handlers/private/decorators.py | 6 +- .../handlers/private/private_handlers.py | 13 +- helper_bot/handlers/private/services.py | 42 ++-- helper_bot/handlers/voice/services.py | 28 +-- helper_bot/handlers/voice/voice_handler.py | 35 +-- helper_bot/keyboards/keyboards.py | 1 - helper_bot/main.py | 13 +- .../middlewares/blacklist_middleware.py | 1 - .../middlewares/dependencies_middleware.py | 1 - helper_bot/middlewares/metrics_middleware.py | 16 +- .../middlewares/rate_limit_middleware.py | 4 +- helper_bot/services/scoring/__init__.py | 11 +- .../services/scoring/deepseek_service.py | 1 - helper_bot/services/scoring/rag_client.py | 16 +- .../services/scoring/scoring_manager.py | 3 +- helper_bot/utils/auto_unban_scheduler.py | 1 - helper_bot/utils/base_dependency_factory.py | 10 +- helper_bot/utils/helper_func.py | 25 +- helper_bot/utils/metrics.py | 9 +- helper_bot/utils/rate_limiter.py | 4 +- helper_bot/utils/s3_storage.py | 1 - run_helper.py | 3 +- tests/conftest.py | 2 +- tests/conftest_message_repository.py | 1 - tests/conftest_post_repository.py | 1 - tests/test_admin_dependencies.py | 46 ++-- tests/test_admin_handlers.py | 131 +++++++---- tests/test_admin_repository.py | 1 - tests/test_admin_utils.py | 25 +- tests/test_album_middleware.py | 12 +- tests/test_async_db.py | 1 - tests/test_audio_file_service.py | 4 +- tests/test_audio_repository.py | 1 - tests/test_audio_repository_schema.py | 1 - tests/test_auto_unban_integration.py | 1 - tests/test_auto_unban_scheduler.py | 7 +- tests/test_blacklist_history_repository.py | 6 +- tests/test_blacklist_middleware.py | 3 +- tests/test_blacklist_repository.py | 1 - tests/test_callback_dependency_factory.py | 16 +- tests/test_callback_handlers.py | 70 ++++-- tests/test_callback_services.py | 212 ++++++++++++----- tests/test_decorators.py | 17 +- tests/test_deepseek_service.py | 13 +- tests/test_dependencies_middleware.py | 8 +- tests/test_improved_media_processing.py | 8 +- tests/test_keyboards_and_filters.py | 21 +- tests/test_main.py | 17 +- tests/test_message_repository.py | 1 - tests/test_message_repository_integration.py | 1 - tests/test_metrics_middleware.py | 74 ++++-- tests/test_post_repository.py | 1 - tests/test_post_repository_integration.py | 1 - tests/test_post_service.py | 1 - tests/test_rag_client.py | 18 +- tests/test_rate_limit_middleware.py | 14 +- tests/test_rate_limit_monitor.py | 19 +- tests/test_rate_limiter.py | 24 +- tests/test_refactored_admin_handlers.py | 27 ++- tests/test_refactored_group_handlers.py | 13 +- tests/test_refactored_private_handlers.py | 115 ++++++--- tests/test_s3_storage.py | 13 +- tests/test_scoring_services.py | 19 +- tests/test_server_prometheus.py | 26 +-- tests/test_text_middleware.py | 1 - tests/test_utils.py | 44 +--- tests/test_voice_bot_architecture.py | 12 +- tests/test_voice_constants.py | 28 +-- tests/test_voice_exceptions.py | 9 +- tests/test_voice_handler.py | 219 +++++++++++++----- tests/test_voice_services.py | 77 ++++-- tests/test_voice_utils.py | 12 +- 93 files changed, 1042 insertions(+), 862 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ac6315..3e23323 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI pipeline on: push: - branches: [ 'dev-*', 'feature-*' ] + branches: [ 'dev-*', 'feature-*', 'fix-*' ] pull_request: - branches: [ 'dev-*', 'feature-*', 'main' ] + branches: [ 'dev-*', 'feature-*', 'fix-*', 'main' ] workflow_dispatch: jobs: diff --git a/database/__init__.py b/database/__init__.py index ae0cdc6..766c4da 100644 --- a/database/__init__.py +++ b/database/__init__.py @@ -11,19 +11,9 @@ from .async_db import AsyncBotDB from .base import DatabaseConnection -from .models import ( - Admin, - AudioListenRecord, - AudioMessage, - AudioModerate, - BlacklistUser, - MessageContentLink, - Migration, - PostContent, - TelegramPost, - User, - UserMessage, -) +from .models import (Admin, AudioListenRecord, AudioMessage, AudioModerate, + BlacklistUser, MessageContentLink, Migration, PostContent, + TelegramPost, User, UserMessage) from .repository_factory import RepositoryFactory # Для обратной совместимости экспортируем старый интерфейс diff --git a/database/async_db.py b/database/async_db.py index 39bdf94..8767e4d 100644 --- a/database/async_db.py +++ b/database/async_db.py @@ -2,17 +2,9 @@ from datetime import datetime from typing import Any, Dict, List, Optional, Tuple import aiosqlite - -from database.models import ( - Admin, - AudioMessage, - BlacklistHistoryRecord, - BlacklistUser, - PostContent, - TelegramPost, - User, - UserMessage, -) +from database.models import (Admin, AudioMessage, BlacklistHistoryRecord, + BlacklistUser, PostContent, TelegramPost, User, + UserMessage) from database.repository_factory import RepositoryFactory diff --git a/database/base.py b/database/base.py index ca32425..4fb6fb0 100644 --- a/database/base.py +++ b/database/base.py @@ -2,7 +2,6 @@ import os from typing import Optional import aiosqlite - from logs.custom_logger import logger diff --git a/database/repositories/migration_repository.py b/database/repositories/migration_repository.py index 8c7a02b..628ecee 100644 --- a/database/repositories/migration_repository.py +++ b/database/repositories/migration_repository.py @@ -1,7 +1,6 @@ """Репозиторий для работы с миграциями базы данных.""" import aiosqlite - from database.base import DatabaseConnection diff --git a/database/repository_factory.py b/database/repository_factory.py index d218f21..e2c462f 100644 --- a/database/repository_factory.py +++ b/database/repository_factory.py @@ -2,9 +2,8 @@ from typing import Optional from database.repositories.admin_repository import AdminRepository from database.repositories.audio_repository import AudioRepository -from database.repositories.blacklist_history_repository import ( - BlacklistHistoryRepository, -) +from database.repositories.blacklist_history_repository import \ + BlacklistHistoryRepository from database.repositories.blacklist_repository import BlacklistRepository from database.repositories.message_repository import MessageRepository from database.repositories.migration_repository import MigrationRepository diff --git a/helper_bot/handlers/admin/__init__.py b/helper_bot/handlers/admin/__init__.py index 4b9d6f2..dbcf322 100644 --- a/helper_bot/handlers/admin/__init__.py +++ b/helper_bot/handlers/admin/__init__.py @@ -1,20 +1,10 @@ from .admin_handlers import admin_router from .dependencies import AdminAccessMiddleware, BotDB, Settings -from .exceptions import ( - AdminAccessDeniedError, - AdminError, - InvalidInputError, - UserAlreadyBannedError, - UserNotFoundError, -) +from .exceptions import (AdminAccessDeniedError, AdminError, InvalidInputError, + UserAlreadyBannedError, UserNotFoundError) from .services import AdminService, BannedUser, User -from .utils import ( - escape_html, - format_ban_confirmation, - format_user_info, - handle_admin_error, - return_to_admin_menu, -) +from .utils import (escape_html, format_ban_confirmation, format_user_info, + handle_admin_error, return_to_admin_menu) __all__ = [ "admin_router", diff --git a/helper_bot/handlers/admin/admin_handlers.py b/helper_bot/handlers/admin/admin_handlers.py index 66d0519..063777e 100644 --- a/helper_bot/handlers/admin/admin_handlers.py +++ b/helper_bot/handlers/admin/admin_handlers.py @@ -1,30 +1,22 @@ from aiogram import F, Router, types from aiogram.filters import Command, MagicData, StateFilter from aiogram.fsm.context import FSMContext - from helper_bot.filters.main import ChatTypeFilter from helper_bot.handlers.admin.dependencies import AdminAccessMiddleware -from helper_bot.handlers.admin.exceptions import ( - InvalidInputError, - UserAlreadyBannedError, -) +from helper_bot.handlers.admin.exceptions import (InvalidInputError, + UserAlreadyBannedError) from helper_bot.handlers.admin.services import AdminService -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.keyboards.keyboards import ( - create_keyboard_for_approve_ban, - create_keyboard_for_ban_days, - create_keyboard_for_ban_reason, - create_keyboard_with_pagination, - get_reply_keyboard_admin, -) +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.keyboards.keyboards import (create_keyboard_for_approve_ban, + create_keyboard_for_ban_days, + create_keyboard_for_ban_reason, + create_keyboard_with_pagination, + get_reply_keyboard_admin) from helper_bot.utils.base_dependency_factory import get_global_instance - # Local imports - metrics from helper_bot.utils.metrics import db_query_time, track_errors, track_time from logs.custom_logger import logger diff --git a/helper_bot/handlers/admin/dependencies.py b/helper_bot/handlers/admin/dependencies.py index 89a486f..9774837 100644 --- a/helper_bot/handlers/admin/dependencies.py +++ b/helper_bot/handlers/admin/dependencies.py @@ -7,7 +7,6 @@ except ImportError: from aiogram import BaseMiddleware from aiogram.types import TelegramObject - from helper_bot.utils.base_dependency_factory import get_global_instance from helper_bot.utils.helper_func import check_access from logs.custom_logger import logger diff --git a/helper_bot/handlers/admin/rate_limit_handlers.py b/helper_bot/handlers/admin/rate_limit_handlers.py index 2837121..8ec4e5c 100644 --- a/helper_bot/handlers/admin/rate_limit_handlers.py +++ b/helper_bot/handlers/admin/rate_limit_handlers.py @@ -6,20 +6,15 @@ from aiogram import F, Router, types from aiogram.filters import Command, MagicData from aiogram.fsm.context import FSMContext from aiogram.types import FSInputFile - from helper_bot.filters.main import ChatTypeFilter -from helper_bot.middlewares.dependencies_middleware import DependenciesMiddleware - +from helper_bot.middlewares.dependencies_middleware import \ + DependenciesMiddleware # Local imports - metrics from helper_bot.utils.metrics import track_errors, track_time from helper_bot.utils.rate_limit_metrics import ( - get_rate_limit_metrics_summary, - update_rate_limit_gauges, -) -from helper_bot.utils.rate_limit_monitor import ( - get_rate_limit_summary, - rate_limit_monitor, -) + get_rate_limit_metrics_summary, update_rate_limit_gauges) +from helper_bot.utils.rate_limit_monitor import (get_rate_limit_summary, + rate_limit_monitor) from logs.custom_logger import logger diff --git a/helper_bot/handlers/admin/services.py b/helper_bot/handlers/admin/services.py index 6eb625e..e8b760f 100644 --- a/helper_bot/handlers/admin/services.py +++ b/helper_bot/handlers/admin/services.py @@ -1,16 +1,11 @@ from datetime import datetime from typing import List, Optional -from helper_bot.handlers.admin.exceptions import ( - InvalidInputError, - UserAlreadyBannedError, -) -from helper_bot.utils.helper_func import ( - add_days_to_date, - get_banned_users_buttons, - get_banned_users_list, -) - +from helper_bot.handlers.admin.exceptions import (InvalidInputError, + UserAlreadyBannedError) +from helper_bot.utils.helper_func import (add_days_to_date, + get_banned_users_buttons, + get_banned_users_list) # Local imports - metrics from helper_bot.utils.metrics import track_errors, track_time from logs.custom_logger import logger diff --git a/helper_bot/handlers/admin/utils.py b/helper_bot/handlers/admin/utils.py index 74fea5c..b9cf51c 100644 --- a/helper_bot/handlers/admin/utils.py +++ b/helper_bot/handlers/admin/utils.py @@ -3,7 +3,6 @@ from typing import Optional from aiogram import types from aiogram.fsm.context import FSMContext - from helper_bot.handlers.admin.exceptions import AdminError from helper_bot.keyboards.keyboards import get_reply_keyboard_admin from logs.custom_logger import logger diff --git a/helper_bot/handlers/callback/__init__.py b/helper_bot/handlers/callback/__init__.py index ccb7ddd..0feae62 100644 --- a/helper_bot/handlers/callback/__init__.py +++ b/helper_bot/handlers/callback/__init__.py @@ -1,19 +1,8 @@ from .callback_handlers import callback_router -from .constants import ( - CALLBACK_BAN, - CALLBACK_DECLINE, - CALLBACK_PAGE, - CALLBACK_PUBLISH, - CALLBACK_RETURN, - CALLBACK_UNLOCK, -) -from .exceptions import ( - BanError, - PostNotFoundError, - PublishError, - UserBlockedBotError, - UserNotFoundError, -) +from .constants import (CALLBACK_BAN, CALLBACK_DECLINE, CALLBACK_PAGE, + CALLBACK_PUBLISH, CALLBACK_RETURN, CALLBACK_UNLOCK) +from .exceptions import (BanError, PostNotFoundError, PublishError, + UserBlockedBotError, UserNotFoundError) from .services import BanService, PostPublishService __all__ = [ diff --git a/helper_bot/handlers/callback/callback_handlers.py b/helper_bot/handlers/callback/callback_handlers.py index d156184..fdafe03 100644 --- a/helper_bot/handlers/callback/callback_handlers.py +++ b/helper_bot/handlers/callback/callback_handlers.py @@ -7,49 +7,28 @@ from aiogram import F, Router from aiogram.filters import MagicData from aiogram.fsm.context import FSMContext from aiogram.types import CallbackQuery - from helper_bot.handlers.admin.utils import format_user_info from helper_bot.handlers.voice.constants import CALLBACK_DELETE, CALLBACK_SAVE from helper_bot.handlers.voice.services import AudioFileService -from helper_bot.keyboards.keyboards import ( - create_keyboard_for_ban_reason, - create_keyboard_with_pagination, - get_reply_keyboard_admin, -) +from helper_bot.keyboards.keyboards import (create_keyboard_for_ban_reason, + create_keyboard_with_pagination, + get_reply_keyboard_admin) from helper_bot.utils.base_dependency_factory import get_global_instance -from helper_bot.utils.helper_func import get_banned_users_buttons, get_banned_users_list - +from helper_bot.utils.helper_func import (get_banned_users_buttons, + get_banned_users_list) # Local imports - metrics -from helper_bot.utils.metrics import ( - db_query_time, - track_errors, - track_file_operations, - track_time, -) +from helper_bot.utils.metrics import (db_query_time, track_errors, + track_file_operations, track_time) from logs.custom_logger import logger -from .constants import ( - CALLBACK_BAN, - CALLBACK_DECLINE, - CALLBACK_PAGE, - CALLBACK_PUBLISH, - CALLBACK_RETURN, - CALLBACK_UNLOCK, - ERROR_BOT_BLOCKED, - MESSAGE_DECLINED, - MESSAGE_ERROR, - MESSAGE_PUBLISHED, - MESSAGE_USER_BANNED, - MESSAGE_USER_UNLOCKED, -) +from .constants import (CALLBACK_BAN, CALLBACK_DECLINE, CALLBACK_PAGE, + CALLBACK_PUBLISH, CALLBACK_RETURN, CALLBACK_UNLOCK, + ERROR_BOT_BLOCKED, MESSAGE_DECLINED, MESSAGE_ERROR, + MESSAGE_PUBLISHED, MESSAGE_USER_BANNED, + MESSAGE_USER_UNLOCKED) from .dependency_factory import get_ban_service, get_post_publish_service -from .exceptions import ( - BanError, - PostNotFoundError, - PublishError, - UserBlockedBotError, - UserNotFoundError, -) +from .exceptions import (BanError, PostNotFoundError, PublishError, + UserBlockedBotError, UserNotFoundError) callback_router = Router() diff --git a/helper_bot/handlers/callback/dependency_factory.py b/helper_bot/handlers/callback/dependency_factory.py index a8b376f..3a4ca99 100644 --- a/helper_bot/handlers/callback/dependency_factory.py +++ b/helper_bot/handlers/callback/dependency_factory.py @@ -3,7 +3,6 @@ from typing import Callable from aiogram import Bot from aiogram.client.default import DefaultBotProperties from aiogram.fsm.context import FSMContext - from helper_bot.utils.base_dependency_factory import get_global_instance from .services import BanService, PostPublishService diff --git a/helper_bot/handlers/callback/services.py b/helper_bot/handlers/callback/services.py index f9aff0d..7088a5d 100644 --- a/helper_bot/handlers/callback/services.py +++ b/helper_bot/handlers/callback/services.py @@ -4,49 +4,28 @@ from typing import Any, Dict from aiogram import Bot, types from aiogram.types import CallbackQuery - from helper_bot.keyboards.keyboards import create_keyboard_for_ban_reason -from helper_bot.utils.helper_func import ( - delete_user_blacklist, - get_text_message, - send_audio_message, - send_media_group_to_channel, - send_photo_message, - send_text_message, - send_video_message, - send_video_note_message, - send_voice_message, -) - +from helper_bot.utils.helper_func import (delete_user_blacklist, + get_text_message, send_audio_message, + send_media_group_to_channel, + send_photo_message, + send_text_message, + send_video_message, + send_video_note_message, + send_voice_message) # Local imports - metrics -from helper_bot.utils.metrics import ( - db_query_time, - track_errors, - track_media_processing, - track_time, -) +from helper_bot.utils.metrics import (db_query_time, track_errors, + track_media_processing, track_time) from logs.custom_logger import logger -from .constants import ( - CONTENT_TYPE_AUDIO, - CONTENT_TYPE_MEDIA_GROUP, - CONTENT_TYPE_PHOTO, - CONTENT_TYPE_TEXT, - CONTENT_TYPE_VIDEO, - CONTENT_TYPE_VIDEO_NOTE, - CONTENT_TYPE_VOICE, - ERROR_BOT_BLOCKED, - MESSAGE_POST_DECLINED, - MESSAGE_POST_PUBLISHED, - MESSAGE_USER_BANNED_SPAM, -) -from .exceptions import ( - BanError, - PostNotFoundError, - PublishError, - UserBlockedBotError, - UserNotFoundError, -) +from .constants import (CONTENT_TYPE_AUDIO, CONTENT_TYPE_MEDIA_GROUP, + CONTENT_TYPE_PHOTO, CONTENT_TYPE_TEXT, + CONTENT_TYPE_VIDEO, CONTENT_TYPE_VIDEO_NOTE, + CONTENT_TYPE_VOICE, ERROR_BOT_BLOCKED, + MESSAGE_POST_DECLINED, MESSAGE_POST_PUBLISHED, + MESSAGE_USER_BANNED_SPAM) +from .exceptions import (BanError, PostNotFoundError, PublishError, + UserBlockedBotError, UserNotFoundError) class PostPublishService: diff --git a/helper_bot/handlers/group/__init__.py b/helper_bot/handlers/group/__init__.py index a060f32..e838c10 100644 --- a/helper_bot/handlers/group/__init__.py +++ b/helper_bot/handlers/group/__init__.py @@ -6,7 +6,6 @@ from .constants import ERROR_MESSAGES, FSM_STATES from .decorators import error_handler from .exceptions import NoReplyToMessageError, UserNotFoundError from .group_handlers import GroupHandlers, create_group_handlers, group_router - # Local imports - services from .services import AdminReplyService, DatabaseProtocol diff --git a/helper_bot/handlers/group/decorators.py b/helper_bot/handlers/group/decorators.py index b1511a0..b451b43 100644 --- a/helper_bot/handlers/group/decorators.py +++ b/helper_bot/handlers/group/decorators.py @@ -6,7 +6,6 @@ from typing import Any, Callable # Third-party imports from aiogram import types - # Local imports from logs.custom_logger import logger @@ -25,9 +24,8 @@ def error_handler(func: Callable[..., Any]) -> Callable[..., Any]: (arg for arg in args if isinstance(arg, types.Message)), None ) if message and hasattr(message, "bot"): - from helper_bot.utils.base_dependency_factory import ( - get_global_instance, - ) + from helper_bot.utils.base_dependency_factory import \ + get_global_instance bdf = get_global_instance() important_logs = bdf.settings["Telegram"]["important_logs"] diff --git a/helper_bot/handlers/group/group_handlers.py b/helper_bot/handlers/group/group_handlers.py index c21fddc..1ee2f70 100644 --- a/helper_bot/handlers/group/group_handlers.py +++ b/helper_bot/handlers/group/group_handlers.py @@ -3,14 +3,11 @@ # Third-party imports from aiogram import Router, types from aiogram.fsm.context import FSMContext - # Local imports - filters from database.async_db import AsyncBotDB from helper_bot.filters.main import ChatTypeFilter - # Local imports - metrics from helper_bot.utils.metrics import metrics, track_errors, track_time - # Local imports - utilities from logs.custom_logger import logger diff --git a/helper_bot/handlers/group/services.py b/helper_bot/handlers/group/services.py index 934887b..4600329 100644 --- a/helper_bot/handlers/group/services.py +++ b/helper_bot/handlers/group/services.py @@ -5,10 +5,8 @@ from typing import Optional, Protocol # Third-party imports from aiogram import types - # Local imports from helper_bot.utils.helper_func import send_text_message - # Local imports - metrics from helper_bot.utils.metrics import db_query_time, track_errors, track_time from logs.custom_logger import logger diff --git a/helper_bot/handlers/private/__init__.py b/helper_bot/handlers/private/__init__.py index 0a8be50..e5c549b 100644 --- a/helper_bot/handlers/private/__init__.py +++ b/helper_bot/handlers/private/__init__.py @@ -4,8 +4,8 @@ # Local imports - constants and utilities from .constants import BUTTON_TEXTS, ERROR_MESSAGES, FSM_STATES from .decorators import error_handler -from .private_handlers import PrivateHandlers, create_private_handlers, private_router - +from .private_handlers import (PrivateHandlers, create_private_handlers, + private_router) # Local imports - services from .services import BotSettings, PostService, StickerService, UserService diff --git a/helper_bot/handlers/private/decorators.py b/helper_bot/handlers/private/decorators.py index 1adabc0..3629e24 100644 --- a/helper_bot/handlers/private/decorators.py +++ b/helper_bot/handlers/private/decorators.py @@ -6,7 +6,6 @@ from typing import Any, Callable # Third-party imports from aiogram import types - # Local imports from logs.custom_logger import logger @@ -25,9 +24,8 @@ def error_handler(func: Callable[..., Any]) -> Callable[..., Any]: (arg for arg in args if isinstance(arg, types.Message)), None ) if message and hasattr(message, "bot"): - from helper_bot.utils.base_dependency_factory import ( - get_global_instance, - ) + from helper_bot.utils.base_dependency_factory import \ + get_global_instance bdf = get_global_instance() important_logs = bdf.settings["Telegram"]["important_logs"] diff --git a/helper_bot/handlers/private/private_handlers.py b/helper_bot/handlers/private/private_handlers.py index c8d0f8b..b8a7d11 100644 --- a/helper_bot/handlers/private/private_handlers.py +++ b/helper_bot/handlers/private/private_handlers.py @@ -8,23 +8,18 @@ from datetime import datetime from aiogram import F, Router, types from aiogram.filters import Command, StateFilter from aiogram.fsm.context import FSMContext - # Local imports - filters and middlewares from database.async_db import AsyncBotDB from helper_bot.filters.main import ChatTypeFilter - # Local imports - utilities -from helper_bot.keyboards import get_reply_keyboard, get_reply_keyboard_for_post +from helper_bot.keyboards import (get_reply_keyboard, + get_reply_keyboard_for_post) from helper_bot.keyboards.keyboards import get_reply_keyboard_leave_chat from helper_bot.middlewares.album_middleware import AlbumMiddleware from helper_bot.middlewares.blacklist_middleware import BlacklistMiddleware from helper_bot.utils import messages -from helper_bot.utils.helper_func import ( - check_user_emoji, - get_first_name, - update_user_info, -) - +from helper_bot.utils.helper_func import (check_user_emoji, get_first_name, + update_user_info) # Local imports - metrics from helper_bot.utils.metrics import db_query_time, track_errors, track_time diff --git a/helper_bot/handlers/private/services.py b/helper_bot/handlers/private/services.py index 5fbaef4..9aec98b 100644 --- a/helper_bot/handlers/private/services.py +++ b/helper_bot/handlers/private/services.py @@ -12,35 +12,19 @@ from typing import Any, Callable, Dict, Protocol, Union # Third-party imports from aiogram import types from aiogram.types import FSInputFile - from database.models import TelegramPost, User from helper_bot.keyboards import get_reply_keyboard_for_post - # Local imports - utilities from helper_bot.utils.helper_func import ( - add_in_db_media, - check_username_and_full_name, - determine_anonymity, - get_first_name, - get_text_message, - prepare_media_group_from_middlewares, - send_audio_message, - send_media_group_message_to_private_chat, - send_photo_message, - send_text_message, - send_video_message, - send_video_note_message, - send_voice_message, -) - + add_in_db_media, check_username_and_full_name, determine_anonymity, + get_first_name, get_text_message, prepare_media_group_from_middlewares, + send_audio_message, send_media_group_message_to_private_chat, + send_photo_message, send_text_message, send_video_message, + send_video_note_message, send_voice_message) # Local imports - metrics -from helper_bot.utils.metrics import ( - db_query_time, - track_errors, - track_file_operations, - track_media_processing, - track_time, -) +from helper_bot.utils.metrics import (db_query_time, track_errors, + track_file_operations, + track_media_processing, track_time) from logs.custom_logger import logger @@ -536,7 +520,7 @@ class PostService: rag_score_pos_only, ml_scores_json, ) = await self._get_scores(raw_text) - + logger.debug( f"PostService.handle_text_post: Передача скоров в get_text_message - " f"rag_score={rag_score} (type: {type(rag_score).__name__ if rag_score is not None else 'None'}), " @@ -594,7 +578,7 @@ class PostService: rag_score_pos_only, ml_scores_json, ) = await self._get_scores(raw_caption) - + logger.debug( f"PostService.handle_photo_post: Передача скоров в get_text_message - " f"rag_score={rag_score} (type: {type(rag_score).__name__ if rag_score is not None else 'None'}), " @@ -660,7 +644,7 @@ class PostService: rag_score_pos_only, ml_scores_json, ) = await self._get_scores(raw_caption) - + logger.debug( f"PostService.handle_video_post: Передача скоров в get_text_message - " f"rag_score={rag_score} (type: {type(rag_score).__name__ if rag_score is not None else 'None'}), " @@ -753,7 +737,7 @@ class PostService: rag_score_pos_only, ml_scores_json, ) = await self._get_scores(raw_caption) - + logger.debug( f"PostService.handle_audio_post: Передача скоров в get_text_message - " f"rag_score={rag_score} (type: {type(rag_score).__name__ if rag_score is not None else 'None'}), " @@ -854,7 +838,7 @@ class PostService: rag_score_pos_only, ml_scores_json, ) = await self._get_scores(raw_caption) - + logger.debug( f"PostService.handle_media_group_post: Передача скоров в get_text_message - " f"rag_score={rag_score} (type: {type(rag_score).__name__ if rag_score is not None else 'None'}), " diff --git a/helper_bot/handlers/voice/services.py b/helper_bot/handlers/voice/services.py index 36808ee..5ec8890 100644 --- a/helper_bot/handlers/voice/services.py +++ b/helper_bot/handlers/voice/services.py @@ -7,24 +7,16 @@ from pathlib import Path from typing import List, Optional, Tuple from aiogram.types import FSInputFile - -from helper_bot.handlers.voice.constants import ( - MESSAGE_DELAY_1, - MESSAGE_DELAY_2, - MESSAGE_DELAY_3, - MESSAGE_DELAY_4, - STICK_DIR, - STICK_PATTERN, - STICKER_DELAY, - VOICE_USERS_DIR, -) -from helper_bot.handlers.voice.exceptions import ( - AudioProcessingError, - DatabaseError, - FileOperationError, - VoiceMessageError, -) - +from helper_bot.handlers.voice.constants import (MESSAGE_DELAY_1, + MESSAGE_DELAY_2, + MESSAGE_DELAY_3, + MESSAGE_DELAY_4, STICK_DIR, + STICK_PATTERN, STICKER_DELAY, + VOICE_USERS_DIR) +from helper_bot.handlers.voice.exceptions import (AudioProcessingError, + DatabaseError, + FileOperationError, + VoiceMessageError) # Local imports - metrics from helper_bot.utils.metrics import db_query_time, track_errors, track_time from logs.custom_logger import logger diff --git a/helper_bot/handlers/voice/voice_handler.py b/helper_bot/handlers/voice/voice_handler.py index 3ea223e..5f2193e 100644 --- a/helper_bot/handlers/voice/voice_handler.py +++ b/helper_bot/handlers/voice/voice_handler.py @@ -6,38 +6,25 @@ from aiogram import F, Router, types from aiogram.filters import Command, MagicData, StateFilter from aiogram.fsm.context import FSMContext from aiogram.types import FSInputFile - from helper_bot.filters.main import ChatTypeFilter from helper_bot.handlers.private.constants import BUTTON_TEXTS, FSM_STATES from helper_bot.handlers.voice.constants import * 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) from helper_bot.keyboards import get_reply_keyboard -from helper_bot.keyboards.keyboards import ( - get_main_keyboard, - get_reply_keyboard_for_voice, -) +from helper_bot.keyboards.keyboards import (get_main_keyboard, + get_reply_keyboard_for_voice) from helper_bot.middlewares.blacklist_middleware import BlacklistMiddleware -from helper_bot.middlewares.dependencies_middleware import DependenciesMiddleware +from helper_bot.middlewares.dependencies_middleware import \ + DependenciesMiddleware from helper_bot.utils import messages -from helper_bot.utils.helper_func import ( - check_user_emoji, - get_first_name, - send_voice_message, - update_user_info, -) - +from helper_bot.utils.helper_func import (check_user_emoji, get_first_name, + send_voice_message, update_user_info) # Local imports - metrics -from helper_bot.utils.metrics import ( - db_query_time, - track_errors, - track_file_operations, - track_time, -) +from helper_bot.utils.metrics import (db_query_time, track_errors, + track_file_operations, track_time) from logs.custom_logger import logger diff --git a/helper_bot/keyboards/keyboards.py b/helper_bot/keyboards/keyboards.py index ed605ad..bb12f54 100644 --- a/helper_bot/keyboards/keyboards.py +++ b/helper_bot/keyboards/keyboards.py @@ -1,6 +1,5 @@ from aiogram import types from aiogram.utils.keyboard import InlineKeyboardBuilder, ReplyKeyboardBuilder - # Local imports - metrics from helper_bot.utils.metrics import track_errors, track_time diff --git a/helper_bot/main.py b/helper_bot/main.py index f055db6..120d25d 100644 --- a/helper_bot/main.py +++ b/helper_bot/main.py @@ -6,20 +6,19 @@ from aiogram import Bot, Dispatcher from aiogram.client.default import DefaultBotProperties from aiogram.fsm.storage.memory import MemoryStorage from aiogram.fsm.strategy import FSMStrategy - from helper_bot.handlers.admin import admin_router from helper_bot.handlers.callback import callback_router from helper_bot.handlers.group import group_router from helper_bot.handlers.private import private_router from helper_bot.handlers.voice import VoiceHandlers from helper_bot.middlewares.blacklist_middleware import BlacklistMiddleware -from helper_bot.middlewares.dependencies_middleware import DependenciesMiddleware -from helper_bot.middlewares.metrics_middleware import ( - ErrorMetricsMiddleware, - MetricsMiddleware, -) +from helper_bot.middlewares.dependencies_middleware import \ + DependenciesMiddleware +from helper_bot.middlewares.metrics_middleware import (ErrorMetricsMiddleware, + MetricsMiddleware) from helper_bot.middlewares.rate_limit_middleware import RateLimitMiddleware -from helper_bot.server_prometheus import start_metrics_server, stop_metrics_server +from helper_bot.server_prometheus import (start_metrics_server, + stop_metrics_server) async def start_bot_with_retry( diff --git a/helper_bot/middlewares/blacklist_middleware.py b/helper_bot/middlewares/blacklist_middleware.py index 32279e2..05bdd17 100644 --- a/helper_bot/middlewares/blacklist_middleware.py +++ b/helper_bot/middlewares/blacklist_middleware.py @@ -4,7 +4,6 @@ from typing import Any, Dict from aiogram import BaseMiddleware, types from aiogram.types import CallbackQuery, Message, TelegramObject - from helper_bot.utils.base_dependency_factory import get_global_instance from logs.custom_logger import logger diff --git a/helper_bot/middlewares/dependencies_middleware.py b/helper_bot/middlewares/dependencies_middleware.py index d18c28c..ce266c0 100644 --- a/helper_bot/middlewares/dependencies_middleware.py +++ b/helper_bot/middlewares/dependencies_middleware.py @@ -2,7 +2,6 @@ from typing import Any, Dict from aiogram import BaseMiddleware from aiogram.types import TelegramObject - from helper_bot.utils.base_dependency_factory import get_global_instance from logs.custom_logger import logger diff --git a/helper_bot/middlewares/metrics_middleware.py b/helper_bot/middlewares/metrics_middleware.py index 2564b86..b5465a9 100644 --- a/helper_bot/middlewares/metrics_middleware.py +++ b/helper_bot/middlewares/metrics_middleware.py @@ -16,16 +16,16 @@ from ..utils.metrics import metrics # Import button command mapping try: - from ..handlers.admin.constants import ADMIN_BUTTON_COMMAND_MAPPING, ADMIN_COMMANDS + from ..handlers.admin.constants import (ADMIN_BUTTON_COMMAND_MAPPING, + ADMIN_COMMANDS) from ..handlers.callback.constants import CALLBACK_COMMAND_MAPPING from ..handlers.private.constants import BUTTON_COMMAND_MAPPING - from ..handlers.voice.constants import ( - BUTTON_COMMAND_MAPPING as VOICE_BUTTON_COMMAND_MAPPING, - ) - from ..handlers.voice.constants import ( - CALLBACK_COMMAND_MAPPING as VOICE_CALLBACK_COMMAND_MAPPING, - ) - from ..handlers.voice.constants import COMMAND_MAPPING as VOICE_COMMAND_MAPPING + from ..handlers.voice.constants import \ + BUTTON_COMMAND_MAPPING as VOICE_BUTTON_COMMAND_MAPPING + from ..handlers.voice.constants import \ + CALLBACK_COMMAND_MAPPING as VOICE_CALLBACK_COMMAND_MAPPING + from ..handlers.voice.constants import \ + COMMAND_MAPPING as VOICE_COMMAND_MAPPING except ImportError: # Fallback if constants not available BUTTON_COMMAND_MAPPING = {} diff --git a/helper_bot/middlewares/rate_limit_middleware.py b/helper_bot/middlewares/rate_limit_middleware.py index c50ef88..8800e73 100644 --- a/helper_bot/middlewares/rate_limit_middleware.py +++ b/helper_bot/middlewares/rate_limit_middleware.py @@ -6,8 +6,8 @@ from typing import Any, Awaitable, Callable, Dict, Union from aiogram import BaseMiddleware from aiogram.exceptions import TelegramAPIError, TelegramRetryAfter -from aiogram.types import CallbackQuery, ChatMemberUpdated, InlineQuery, Message, Update - +from aiogram.types import (CallbackQuery, ChatMemberUpdated, InlineQuery, + Message, Update) from helper_bot.utils.rate_limiter import telegram_rate_limiter from logs.custom_logger import logger diff --git a/helper_bot/services/scoring/__init__.py b/helper_bot/services/scoring/__init__.py index 33ee5f7..6a1d156 100644 --- a/helper_bot/services/scoring/__init__.py +++ b/helper_bot/services/scoring/__init__.py @@ -9,14 +9,9 @@ from .base import CombinedScore, ScoringResult, ScoringServiceProtocol from .deepseek_service import DeepSeekService -from .exceptions import ( - DeepSeekAPIError, - InsufficientExamplesError, - ModelNotLoadedError, - ScoringError, - TextTooShortError, - VectorStoreError, -) +from .exceptions import (DeepSeekAPIError, InsufficientExamplesError, + ModelNotLoadedError, ScoringError, TextTooShortError, + VectorStoreError) from .rag_client import RagApiClient from .scoring_manager import ScoringManager diff --git a/helper_bot/services/scoring/deepseek_service.py b/helper_bot/services/scoring/deepseek_service.py index 4f9cc23..a365323 100644 --- a/helper_bot/services/scoring/deepseek_service.py +++ b/helper_bot/services/scoring/deepseek_service.py @@ -9,7 +9,6 @@ import json from typing import List, Optional import httpx - from helper_bot.utils.metrics import track_errors, track_time from logs.custom_logger import logger diff --git a/helper_bot/services/scoring/rag_client.py b/helper_bot/services/scoring/rag_client.py index fc2f362..b0f868f 100644 --- a/helper_bot/services/scoring/rag_client.py +++ b/helper_bot/services/scoring/rag_client.py @@ -7,12 +7,12 @@ HTTP клиент для взаимодействия с внешним RAG се from typing import Any, Dict, Optional import httpx - from helper_bot.utils.metrics import track_errors, track_time from logs.custom_logger import logger from .base import ScoringResult -from .exceptions import InsufficientExamplesError, ScoringError, TextTooShortError +from .exceptions import (InsufficientExamplesError, ScoringError, + TextTooShortError) class RagApiClient: @@ -160,11 +160,19 @@ class RagApiClient: else None ) rag_score_pos_only_raw = data.get("rag_score_pos_only") - rag_score_pos_only = float(rag_score_pos_only_raw) if rag_score_pos_only_raw is not None else None + rag_score_pos_only = ( + float(rag_score_pos_only_raw) + if rag_score_pos_only_raw is not None + else None + ) # Форматируем confidence для логирования confidence_str = f"{confidence:.4f}" if confidence is not None else "None" - rag_score_pos_only_str = f"{rag_score_pos_only:.4f}" if rag_score_pos_only is not None else "None" + rag_score_pos_only_str = ( + f"{rag_score_pos_only:.4f}" + if rag_score_pos_only is not None + else "None" + ) logger.info( f"RagApiClient: Скор успешно получен из API - " diff --git a/helper_bot/services/scoring/scoring_manager.py b/helper_bot/services/scoring/scoring_manager.py index 6761176..3c0c968 100644 --- a/helper_bot/services/scoring/scoring_manager.py +++ b/helper_bot/services/scoring/scoring_manager.py @@ -13,7 +13,8 @@ from logs.custom_logger import logger from .base import CombinedScore, ScoringResult from .deepseek_service import DeepSeekService -from .exceptions import InsufficientExamplesError, ScoringError, TextTooShortError +from .exceptions import (InsufficientExamplesError, ScoringError, + TextTooShortError) from .rag_client import RagApiClient diff --git a/helper_bot/utils/auto_unban_scheduler.py b/helper_bot/utils/auto_unban_scheduler.py index 91550b5..d46a688 100644 --- a/helper_bot/utils/auto_unban_scheduler.py +++ b/helper_bot/utils/auto_unban_scheduler.py @@ -4,7 +4,6 @@ from typing import Optional from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.triggers.cron import CronTrigger - from helper_bot.utils.base_dependency_factory import get_global_instance from logs.custom_logger import logger diff --git a/helper_bot/utils/base_dependency_factory.py b/helper_bot/utils/base_dependency_factory.py index e3e1971..2edb100 100644 --- a/helper_bot/utils/base_dependency_factory.py +++ b/helper_bot/utils/base_dependency_factory.py @@ -2,9 +2,8 @@ import os import sys from typing import Optional -from dotenv import load_dotenv - from database.async_db import AsyncBotDB +from dotenv import load_dotenv from helper_bot.utils.s3_storage import S3StorageService from logs.custom_logger import logger @@ -142,11 +141,8 @@ class BaseDependencyFactory: Вызывается лениво при первом обращении к get_scoring_manager(). """ - from helper_bot.services.scoring import ( - DeepSeekService, - RagApiClient, - ScoringManager, - ) + from helper_bot.services.scoring import (DeepSeekService, RagApiClient, + ScoringManager) scoring_config = self.settings["Scoring"] diff --git a/helper_bot/utils/helper_func.py b/helper_bot/utils/helper_func.py index 39aab76..483672e 100644 --- a/helper_bot/utils/helper_func.py +++ b/helper_bot/utils/helper_func.py @@ -17,29 +17,16 @@ except ImportError: _emoji_lib_available = False from aiogram import types -from aiogram.types import ( - FSInputFile, - InputMediaAudio, - InputMediaDocument, - InputMediaPhoto, - InputMediaVideo, -) - +from aiogram.types import (FSInputFile, InputMediaAudio, InputMediaDocument, + InputMediaPhoto, InputMediaVideo) from database.models import TelegramPost -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 logs.custom_logger import logger # Local imports - metrics -from .metrics import ( - db_query_time, - track_errors, - track_file_operations, - track_media_processing, - track_time, -) +from .metrics import (db_query_time, track_errors, track_file_operations, + track_media_processing, track_time) bdf = get_global_instance() # TODO: поменять архитектуру и подключить правильный BotDB diff --git a/helper_bot/utils/metrics.py b/helper_bot/utils/metrics.py index b416b8c..18c2f42 100644 --- a/helper_bot/utils/metrics.py +++ b/helper_bot/utils/metrics.py @@ -10,13 +10,8 @@ from contextlib import asynccontextmanager from functools import wraps from typing import Any, Dict, Optional -from prometheus_client import ( - CONTENT_TYPE_LATEST, - Counter, - Gauge, - Histogram, - generate_latest, -) +from prometheus_client import (CONTENT_TYPE_LATEST, Counter, Gauge, Histogram, + generate_latest) from prometheus_client.core import CollectorRegistry # Метрики rate limiter теперь создаются в основном классе diff --git a/helper_bot/utils/rate_limiter.py b/helper_bot/utils/rate_limiter.py index 78d891f..290f33b 100644 --- a/helper_bot/utils/rate_limiter.py +++ b/helper_bot/utils/rate_limiter.py @@ -8,7 +8,6 @@ from dataclasses import dataclass from typing import Any, Callable, Dict, Optional from aiogram.exceptions import TelegramAPIError, TelegramRetryAfter - from logs.custom_logger import logger from .metrics import metrics @@ -188,7 +187,8 @@ class TelegramRateLimiter: # Глобальный экземпляр rate limiter -from helper_bot.config.rate_limit_config import RateLimitSettings, get_rate_limit_config +from helper_bot.config.rate_limit_config import (RateLimitSettings, + get_rate_limit_config) def _create_rate_limit_config(settings: RateLimitSettings) -> RateLimitConfig: diff --git a/helper_bot/utils/s3_storage.py b/helper_bot/utils/s3_storage.py index dbbf2d6..5b5dfc6 100644 --- a/helper_bot/utils/s3_storage.py +++ b/helper_bot/utils/s3_storage.py @@ -8,7 +8,6 @@ from pathlib import Path from typing import Optional import aioboto3 - from logs.custom_logger import logger diff --git a/run_helper.py b/run_helper.py index 8715db1..98222fa 100644 --- a/run_helper.py +++ b/run_helper.py @@ -68,7 +68,8 @@ async def main(): # Останавливаем планировщик метрик try: - from helper_bot.utils.metrics_scheduler import stop_metrics_scheduler + from helper_bot.utils.metrics_scheduler import \ + stop_metrics_scheduler stop_metrics_scheduler() logger.info("Планировщик метрик остановлен") diff --git a/tests/conftest.py b/tests/conftest.py index 96cb8d7..041e950 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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",) diff --git a/tests/conftest_message_repository.py b/tests/conftest_message_repository.py index 90f7b8b..0793b59 100644 --- a/tests/conftest_message_repository.py +++ b/tests/conftest_message_repository.py @@ -3,7 +3,6 @@ import tempfile from datetime import datetime import pytest - from database.models import UserMessage from database.repositories.message_repository import MessageRepository diff --git a/tests/conftest_post_repository.py b/tests/conftest_post_repository.py index 8c660ce..56c26b5 100644 --- a/tests/conftest_post_repository.py +++ b/tests/conftest_post_repository.py @@ -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 diff --git a/tests/test_admin_dependencies.py b/tests/test_admin_dependencies.py index 51694b5..0ffb6f1 100644 --- a/tests/test_admin_dependencies.py +++ b/tests/test_admin_dependencies.py @@ -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 diff --git a/tests/test_admin_handlers.py b/tests/test_admin_handlers.py index 081520a..f53ea65 100644 --- a/tests/test_admin_handlers.py +++ b/tests/test_admin_handlers.py @@ -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() diff --git a/tests/test_admin_repository.py b/tests/test_admin_repository.py index 1eee060..5e9f5c9 100644 --- a/tests/test_admin_repository.py +++ b/tests/test_admin_repository.py @@ -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 diff --git a/tests/test_admin_utils.py b/tests/test_admin_utils.py index f7c90a3..1509bb2 100644 --- a/tests/test_admin_utils.py +++ b/tests/test_admin_utils.py @@ -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") diff --git a/tests/test_album_middleware.py b/tests/test_album_middleware.py index 58a894c..6213f79 100644 --- a/tests/test_album_middleware.py +++ b/tests/test_album_middleware.py @@ -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" diff --git a/tests/test_async_db.py b/tests/test_async_db.py index 02b9b3f..fdb2cab 100644 --- a/tests/test_async_db.py +++ b/tests/test_async_db.py @@ -1,7 +1,6 @@ from unittest.mock import AsyncMock, Mock, patch import pytest - from database.async_db import AsyncBotDB diff --git a/tests/test_audio_file_service.py b/tests/test_audio_file_service.py index 7d298ed..5811683 100644 --- a/tests/test_audio_file_service.py +++ b/tests/test_audio_file_service.py @@ -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 diff --git a/tests/test_audio_repository.py b/tests/test_audio_repository.py index 5ed86fe..0ee248f 100644 --- a/tests/test_audio_repository.py +++ b/tests/test_audio_repository.py @@ -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 diff --git a/tests/test_audio_repository_schema.py b/tests/test_audio_repository_schema.py index ed57604..d084df3 100644 --- a/tests/test_audio_repository_schema.py +++ b/tests/test_audio_repository_schema.py @@ -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 diff --git a/tests/test_auto_unban_integration.py b/tests/test_auto_unban_integration.py index 1bb82b6..1b7bd99 100644 --- a/tests/test_auto_unban_integration.py +++ b/tests/test_auto_unban_integration.py @@ -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 diff --git a/tests/test_auto_unban_scheduler.py b/tests/test_auto_unban_scheduler.py index 7f64bef..05d10d4 100644 --- a/tests/test_auto_unban_scheduler.py +++ b/tests/test_auto_unban_scheduler.py @@ -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: diff --git a/tests/test_blacklist_history_repository.py b/tests/test_blacklist_history_repository.py index 828a222..82c6493 100644 --- a/tests/test_blacklist_history_repository.py +++ b/tests/test_blacklist_history_repository.py @@ -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: diff --git a/tests/test_blacklist_middleware.py b/tests/test_blacklist_middleware.py index 884f48e..5bda9b6 100644 --- a/tests/test_blacklist_middleware.py +++ b/tests/test_blacklist_middleware.py @@ -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" \ No newline at end of file + assert result == "handler_ok" diff --git a/tests/test_blacklist_repository.py b/tests/test_blacklist_repository.py index 97caf4f..f7133bd 100644 --- a/tests/test_blacklist_repository.py +++ b/tests/test_blacklist_repository.py @@ -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 diff --git a/tests/test_callback_dependency_factory.py b/tests/test_callback_dependency_factory.py index 00dc098..5757157 100644 --- a/tests/test_callback_dependency_factory.py +++ b/tests/test_callback_dependency_factory.py @@ -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() diff --git a/tests/test_callback_handlers.py b/tests/test_callback_handlers.py index b011e82..3196fba 100644 --- a/tests/test_callback_handlers.py +++ b/tests/test_callback_handlers.py @@ -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 отвечает ошибкой.""" diff --git a/tests/test_callback_services.py b/tests/test_callback_services.py index 46a3ae1..2433b66 100644 --- a/tests/test_callback_services.py +++ b/tests/test_callback_services.py @@ -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") diff --git a/tests/test_decorators.py b/tests/test_decorators.py index 039c103..c05c9aa 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -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") diff --git a/tests/test_deepseek_service.py b/tests/test_deepseek_service.py index 10dfee3..54d42ca 100644 --- a/tests/test_deepseek_service.py +++ b/tests/test_deepseek_service.py @@ -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" diff --git a/tests/test_dependencies_middleware.py b/tests/test_dependencies_middleware.py index 98a69f1..4bc3369 100644 --- a/tests/test_dependencies_middleware.py +++ b/tests/test_dependencies_middleware.py @@ -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") diff --git a/tests/test_improved_media_processing.py b/tests/test_improved_media_processing.py index 05d78e7..99c9c38 100644 --- a/tests/test_improved_media_processing.py +++ b/tests/test_improved_media_processing.py @@ -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: diff --git a/tests/test_keyboards_and_filters.py b/tests/test_keyboards_and_filters.py index 48de3c1..975bc8f 100644 --- a/tests/test_keyboards_and_filters.py +++ b/tests/test_keyboards_and_filters.py @@ -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: diff --git a/tests/test_main.py b/tests/test_main.py index ebf66d8..0a492c2 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -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") diff --git a/tests/test_message_repository.py b/tests/test_message_repository.py index b8f2557..2fdbceb 100644 --- a/tests/test_message_repository.py +++ b/tests/test_message_repository.py @@ -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 diff --git a/tests/test_message_repository_integration.py b/tests/test_message_repository_integration.py index 84c3b3a..8e129ab 100644 --- a/tests/test_message_repository_integration.py +++ b/tests/test_message_repository_integration.py @@ -4,7 +4,6 @@ import tempfile from datetime import datetime import pytest - from database.models import UserMessage from database.repositories.message_repository import MessageRepository diff --git a/tests/test_metrics_middleware.py b/tests/test_metrics_middleware.py index 76f0a4f..c291286 100644 --- a/tests/test_metrics_middleware.py +++ b/tests/test_metrics_middleware.py @@ -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") diff --git a/tests/test_post_repository.py b/tests/test_post_repository.py index 797be28..dd982bd 100644 --- a/tests/test_post_repository.py +++ b/tests/test_post_repository.py @@ -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 diff --git a/tests/test_post_repository_integration.py b/tests/test_post_repository_integration.py index d485fec..43bb399 100644 --- a/tests/test_post_repository_integration.py +++ b/tests/test_post_repository_integration.py @@ -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 diff --git a/tests/test_post_service.py b/tests/test_post_service.py index ffabd2b..591fb6b 100644 --- a/tests/test_post_service.py +++ b/tests/test_post_service.py @@ -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 diff --git a/tests/test_rag_client.py b/tests/test_rag_client.py index 35244cd..4870b08 100644 --- a/tests/test_rag_client.py +++ b/tests/test_rag_client.py @@ -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") diff --git a/tests/test_rate_limit_middleware.py b/tests/test_rate_limit_middleware.py index f3caad6..640b52f 100644 --- a/tests/test_rate_limit_middleware.py +++ b/tests/test_rate_limit_middleware.py @@ -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() diff --git a/tests/test_rate_limit_monitor.py b/tests/test_rate_limit_monitor.py index 6e76e4b..dfbbff8 100644 --- a/tests/test_rate_limit_monitor.py +++ b/tests/test_rate_limit_monitor.py @@ -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): diff --git a/tests/test_rate_limiter.py b/tests/test_rate_limiter.py index ed3e7bd..fdac278 100644 --- a/tests/test_rate_limiter.py +++ b/tests/test_rate_limiter.py @@ -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: diff --git a/tests/test_refactored_admin_handlers.py b/tests/test_refactored_admin_handlers.py index 5e9406b..f0aad6e 100644 --- a/tests/test_refactored_admin_handlers.py +++ b/tests/test_refactored_admin_handlers.py @@ -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 = [] diff --git a/tests/test_refactored_group_handlers.py b/tests/test_refactored_group_handlers.py index 464c95f..972b08f 100644 --- a/tests/test_refactored_group_handlers.py +++ b/tests/test_refactored_group_handlers.py @@ -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 diff --git a/tests/test_refactored_private_handlers.py b/tests/test_refactored_private_handlers.py index 057068f..8062e8d 100644 --- a/tests/test_refactored_private_handlers.py +++ b/tests/test_refactored_private_handlers.py @@ -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() diff --git a/tests/test_s3_storage.py b/tests/test_s3_storage.py index a02cf3f..ac5bab8 100644 --- a/tests/test_s3_storage.py +++ b/tests/test_s3_storage.py @@ -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", diff --git a/tests/test_scoring_services.py b/tests/test_scoring_services.py index c7d427d..9065a4d 100644 --- a/tests/test_scoring_services.py +++ b/tests/test_scoring_services.py @@ -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) diff --git a/tests/test_server_prometheus.py b/tests/test_server_prometheus.py index b6c22dd..1cb4311 100644 --- a/tests/test_server_prometheus.py +++ b/tests/test_server_prometheus.py @@ -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: diff --git a/tests/test_text_middleware.py b/tests/test_text_middleware.py index 63c64b1..6187382 100644 --- a/tests/test_text_middleware.py +++ b/tests/test_text_middleware.py @@ -5,7 +5,6 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest - from helper_bot.middlewares.text_middleware import BulkTextMiddleware diff --git a/tests/test_utils.py b/tests/test_utils.py index d511122..72fcf51 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -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 diff --git a/tests/test_voice_bot_architecture.py b/tests/test_voice_bot_architecture.py index 9f7b7bc..d8b3adb 100644 --- a/tests/test_voice_bot_architecture.py +++ b/tests/test_voice_bot_architecture.py @@ -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: diff --git a/tests/test_voice_constants.py b/tests/test_voice_constants.py index b6774ef..5215d1b 100644 --- a/tests/test_voice_constants.py +++ b/tests/test_voice_constants.py @@ -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: diff --git a/tests/test_voice_exceptions.py b/tests/test_voice_exceptions.py index 38d8150..2225d8a 100644 --- a/tests/test_voice_exceptions.py +++ b/tests/test_voice_exceptions.py @@ -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: diff --git a/tests/test_voice_handler.py b/tests/test_voice_handler.py index 9456d30..a880abb 100644 --- a/tests/test_voice_handler.py +++ b/tests/test_voice_handler.py @@ -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__": diff --git a/tests/test_voice_services.py b/tests/test_voice_services.py index 515f7d1..4c14471 100644 --- a/tests/test_voice_services.py +++ b/tests/test_voice_services.py @@ -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() diff --git a/tests/test_voice_utils.py b/tests/test_voice_utils.py index 50de559..165697e 100644 --- a/tests/test_voice_utils.py +++ b/tests/test_voice_utils.py @@ -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: