Refactor metrics handling and improve logging

- Removed the MetricsManager initialization from `run_helper.py` to avoid duplication, as metrics are now handled in `main.py`.
- Updated logging levels in `server_prometheus.py` and `metrics_middleware.py` to use debug instead of info for less critical messages.
- Added metrics configuration to `BaseDependencyFactory` for better management of metrics settings.
- Deleted the obsolete `metrics_exporter.py` file to streamline the codebase.
- Updated various tests to reflect changes in the metrics handling and ensure proper functionality.
This commit is contained in:
2025-09-03 00:33:20 +03:00
parent 6fcecff97c
commit c8c7d50cbb
19 changed files with 402 additions and 605 deletions

View File

@@ -47,21 +47,18 @@ class MetricsMiddleware(BaseMiddleware):
) -> Any:
"""Process event and collect metrics."""
# Добавляем логирование для диагностики
self.logger.info(f"📊 MetricsMiddleware called for event type: {type(event).__name__}")
# Extract command info before execution
command_info = None
if isinstance(event, Message):
self.logger.info(f"📊 Processing Message event")
self.logger.debug(f"📊 Processing Message event")
await self._record_message_metrics(event)
command_info = self._extract_command_info(event)
elif isinstance(event, CallbackQuery):
self.logger.info(f"📊 Processing CallbackQuery event")
self.logger.debug(f"📊 Processing CallbackQuery event")
await self._record_callback_metrics(event)
command_info = self._extract_callback_command_info(event)
else:
self.logger.info(f"📊 Processing unknown event type: {type(event).__name__}")
self.logger.debug(f"📊 Processing unknown event type: {type(event).__name__}")
# Execute handler with timing
start_time = time.time()
@@ -71,7 +68,7 @@ class MetricsMiddleware(BaseMiddleware):
# Record successful execution
handler_name = self._get_handler_name(handler)
self.logger.info(f"📊 Recording successful execution: {handler_name}")
self.logger.debug(f"📊 Recording successful execution: {handler_name}")
metrics.record_method_duration(
handler_name,
duration,
@@ -95,7 +92,7 @@ class MetricsMiddleware(BaseMiddleware):
# Record error and timing
handler_name = self._get_handler_name(handler)
self.logger.error(f"📊 Recording error execution: {handler_name}, error: {type(e).__name__}")
self.logger.debug(f"📊 Recording error execution: {handler_name}, error: {type(e).__name__}")
metrics.record_method_duration(
handler_name,
duration,

View File

@@ -29,7 +29,7 @@ class MetricsServer:
async def metrics_handler(self, request: web.Request) -> web.Response:
"""Handle /metrics endpoint for Prometheus scraping."""
try:
self.logger.info("Generating metrics...")
self.logger.debug("Generating metrics...")
# Проверяем, что metrics доступен
if not metrics:
@@ -40,9 +40,8 @@ class MetricsServer:
)
# Генерируем метрики в формате Prometheus
self.logger.info("Calling metrics.get_metrics()...")
metrics_data = metrics.get_metrics()
self.logger.info(f"Generated metrics: {len(metrics_data)} bytes")
self.logger.debug(f"Generated metrics: {len(metrics_data)} bytes")
return web.Response(
body=metrics_data,

View File

@@ -43,6 +43,11 @@ class BaseDependencyFactory:
'test': self._parse_bool(os.getenv('TEST', 'false'))
}
self.settings['Metrics'] = {
'host': os.getenv('METRICS_HOST', '0.0.0.0'),
'port': self._parse_int(os.getenv('METRICS_PORT', '8080'))
}
def _parse_bool(self, value: str) -> bool:
"""Парсит строковое значение в boolean."""
return value.lower() in ('true', '1', 'yes', 'on')

View File

@@ -1,290 +0,0 @@
"""
Metrics exporter for Prometheus.
Provides HTTP endpoint for metrics collection and background metrics collection.
"""
import asyncio
import logging
from aiohttp import web
from typing import Optional, Dict, Any, Protocol
from .metrics import metrics
import time
class DatabaseProvider(Protocol):
"""Protocol for database operations."""
async def fetch_one(self, query: str, params: tuple = ()) -> Optional[Dict[str, Any]]:
"""Execute query and return single result."""
...
class MetricsCollector(Protocol):
"""Protocol for metrics collection operations."""
async def collect_user_metrics(self, db: DatabaseProvider) -> None:
"""Collect user-related metrics."""
...
class UserMetricsCollector:
"""Concrete implementation of user metrics collection."""
def __init__(self, logger: logging.Logger):
self.logger = logger
async def collect_user_metrics(self, db: DatabaseProvider) -> None:
"""Collect user-related metrics from database."""
try:
# Проверяем, есть ли метод fetch_one (асинхронная БД)
if hasattr(db, 'fetch_one'):
# Используем UNIX timestamp для сравнения с date_changed
current_timestamp = int(time.time())
one_day_ago = current_timestamp - (24 * 60 * 60) # 24 часа назад
active_users_query = """
SELECT COUNT(DISTINCT user_id) as active_users
FROM our_users
WHERE date_changed > ?
"""
result = await db.fetch_one(active_users_query, (one_day_ago,))
if result:
metrics.set_active_users(result['active_users'], 'daily')
self.logger.debug(f"Updated active users: {result['active_users']}")
else:
metrics.set_active_users(0, 'daily')
self.logger.debug("Updated active users: 0")
# Проверяем синхронную БД BotDB
elif hasattr(db, 'connect') and hasattr(db, 'cursor'):
# Используем синхронный запрос для BotDB в отдельном потоке
import asyncio
from concurrent.futures import ThreadPoolExecutor
current_timestamp = int(time.time())
one_day_ago = current_timestamp - (24 * 60 * 60) # 24 часа назад
active_users_query = """
SELECT COUNT(DISTINCT user_id) as active_users
FROM our_users
WHERE date_changed > ?
"""
def sync_db_query():
try:
db.connect()
db.cursor.execute(active_users_query, (one_day_ago,))
result = db.cursor.fetchone()
return result[0] if result else 0
finally:
db.close()
# Выполняем синхронный запрос в отдельном потоке
loop = asyncio.get_event_loop()
with ThreadPoolExecutor() as executor:
result = await loop.run_in_executor(executor, sync_db_query)
metrics.set_active_users(result, 'daily')
self.logger.debug(f"Updated active users: {result}")
else:
metrics.set_active_users(0, 'daily')
self.logger.warning("Database doesn't support fetch_one or connect methods")
except Exception as e:
self.logger.error(f"Error collecting user metrics: {e}")
metrics.set_active_users(0, 'daily')
class DependencyProvider(Protocol):
"""Protocol for dependency injection."""
def get_db(self) -> DatabaseProvider:
"""Get database instance."""
...
class BackgroundMetricsCollector:
"""Background service for collecting periodic metrics using dependency injection."""
def __init__(
self,
dependency_provider: DependencyProvider,
metrics_collector: MetricsCollector,
interval: int = 60
):
self.dependency_provider = dependency_provider
self.metrics_collector = metrics_collector
self.interval = interval
self.running = False
self.logger = logging.getLogger(__name__)
async def start(self):
"""Start background metrics collection."""
self.running = True
self.logger.info("Background metrics collector started")
while self.running:
try:
await self._collect_metrics()
await asyncio.sleep(self.interval)
except Exception as e:
self.logger.error(f"Error in background metrics collection: {e}")
await asyncio.sleep(self.interval)
async def stop(self):
"""Stop background metrics collection."""
self.running = False
self.logger.info("Background metrics collector stopped")
async def _collect_metrics(self):
"""Collect periodic metrics using dependency injection."""
try:
db = self.dependency_provider.get_db()
if db:
await self.metrics_collector.collect_user_metrics(db)
else:
self.logger.warning("Database not available for metrics collection")
except Exception as e:
self.logger.error(f"Error collecting metrics: {e}")
class MetricsExporter:
"""HTTP server for exposing Prometheus metrics."""
def __init__(self, host: str = "0.0.0.0", port: int = 8000):
self.host = host
self.port = port
self.app = web.Application()
self.runner: Optional[web.AppRunner] = None
self.site: Optional[web.TCPSite] = None
self.logger = logging.getLogger(__name__)
# Setup routes
self.app.router.add_get('/metrics', self.metrics_handler)
self.app.router.add_get('/health', self.health_handler)
self.app.router.add_get('/', self.root_handler)
async def start(self):
"""Start the metrics server."""
try:
self.runner = web.AppRunner(self.app)
await self.runner.setup()
self.site = web.TCPSite(self.runner, self.host, self.port)
await self.site.start()
self.logger.info(f"Metrics server started on {self.host}:{self.port}")
except Exception as e:
self.logger.error(f"Failed to start metrics server: {e}")
raise
async def stop(self):
"""Stop the metrics server."""
try:
if self.site:
await self.site.stop()
self.logger.info("Metrics server site stopped")
if self.runner:
await self.runner.cleanup()
self.logger.info("Metrics server runner cleaned up")
except Exception as e:
self.logger.error(f"Error stopping metrics server: {e}")
finally:
# Очищаем ссылки
self.site = None
self.runner = None
# Даем время на закрытие всех соединений
await asyncio.sleep(0.1)
self.logger.info("Metrics server stopped")
async def metrics_handler(self, request: web.Request) -> web.Response:
"""Handle /metrics endpoint for Prometheus."""
try:
metrics_data = metrics.get_metrics()
self.logger.debug(f"Generated metrics: {len(metrics_data)} bytes")
return web.Response(
body=metrics_data,
content_type='text/plain; version=0.0.4'
)
except Exception as e:
self.logger.error(f"Error generating metrics: {e}")
return web.Response(
text=f"Error generating metrics: {e}",
status=500
)
async def health_handler(self, request: web.Request) -> web.Response:
"""Handle /health endpoint for health checks."""
return web.json_response({
"status": "healthy",
"service": "telegram-bot-metrics"
})
async def root_handler(self, request: web.Request) -> web.Response:
"""Handle root endpoint with basic info."""
return web.json_response({
"service": "Telegram Bot Metrics Exporter",
"endpoints": {
"/metrics": "Prometheus metrics",
"/health": "Health check",
"/": "This info"
}
})
class MetricsManager:
"""Main class for managing metrics collection and export."""
def __init__(self, host: str = "0.0.0.0", port: int = 8000):
self.exporter = MetricsExporter(host, port)
# Dependency injection setup
from helper_bot.utils.base_dependency_factory import get_global_instance
dependency_provider = get_global_instance()
metrics_collector = UserMetricsCollector(logging.getLogger(__name__))
self.collector = BackgroundMetricsCollector(
dependency_provider=dependency_provider,
metrics_collector=metrics_collector
)
self.logger = logging.getLogger(__name__)
async def start(self):
"""Start metrics collection and export."""
try:
# Start metrics exporter
await self.exporter.start()
# Start background collector
asyncio.create_task(self.collector.start())
self.logger.info("Metrics manager started successfully")
except Exception as e:
self.logger.error(f"Failed to start metrics manager: {e}")
raise
async def stop(self):
"""Stop metrics collection and export."""
try:
# Останавливаем background collector
if hasattr(self, 'collector'):
await self.collector.stop()
self.logger.info("Background metrics collector stopped")
# Останавливаем exporter
if hasattr(self, 'exporter'):
await self.exporter.stop()
self.logger.info("Metrics exporter stopped")
except Exception as e:
self.logger.error(f"Error stopping metrics manager: {e}")
# Не вызываем raise, чтобы не прерывать процесс завершения
finally:
# Очищаем ссылки
self.collector = None
self.exporter = None
self.logger.info("Metrics manager stopped successfully")

View File

@@ -33,10 +33,8 @@ async def main():
auto_unban_scheduler.set_bot(auto_unban_bot)
auto_unban_scheduler.start_scheduler()
# Инициализируем метрики ПОСЛЕ импорта всех модулей
# Это гарантирует, что global instance полностью инициализирован
from helper_bot.utils.metrics_exporter import MetricsManager
metrics_manager = MetricsManager(host="0.0.0.0", port=8000)
# Метрики запускаются в main.py через server_prometheus.py
# Здесь не нужно дублировать функциональность
# Флаг для корректного завершения
shutdown_event = asyncio.Event()
@@ -50,9 +48,8 @@ async def main():
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
# Запускаем бота и метрики
# Запускаем бота (метрики запускаются внутри start_bot)
bot_task = asyncio.create_task(start_bot(bdf))
metrics_task = asyncio.create_task(metrics_manager.start())
main_bot = None
@@ -67,21 +64,16 @@ async def main():
logger.info("Останавливаем планировщик автоматического разбана...")
auto_unban_scheduler.stop_scheduler()
logger.info("Останавливаем метрики...")
try:
await metrics_manager.stop()
except Exception as e:
logger.error(f"Ошибка при остановке метрик: {e}")
# Метрики останавливаются в main.py
logger.info("Останавливаем задачи...")
# Отменяем задачи
# Отменяем задачу бота
bot_task.cancel()
metrics_task.cancel()
# Ждем завершения задач и получаем результат main bot
# Ждем завершения задачи бота и получаем результат main bot
try:
results = await asyncio.gather(bot_task, metrics_task, return_exceptions=True)
# Первый результат - это main bot
results = await asyncio.gather(bot_task, return_exceptions=True)
# Результат - это main bot
if results[0] and not isinstance(results[0], Exception):
main_bot = results[0]
except Exception as e:

View File

@@ -132,7 +132,8 @@ class TestAudioRepository:
# Проверяем, что метод вызван
audio_repository._execute_query.assert_called_once()
call_args = audio_repository._execute_query.call_args
assert call_args[0][1][2] == 12345 # user_id
assert call_args[0][1][0] == "test_audio.ogg" # file_name
assert call_args[0][1][1] == 12345 # user_id
assert isinstance(call_args[0][1][2], int) # timestamp
@pytest.mark.asyncio
@@ -251,7 +252,7 @@ class TestAudioRepository:
@pytest.mark.asyncio
async def test_get_date_by_file_name(self, audio_repository):
"""Тест получения даты по имени файла"""
timestamp = 1642248600 # 2022-01-17 10:30:00
timestamp = 1642404600 # 2022-01-17 10:30:00
audio_repository._execute_query_with_result.return_value = [(timestamp,)]
result = await audio_repository.get_date_by_file_name("test_audio.ogg")
@@ -357,7 +358,7 @@ class TestAudioRepository:
@pytest.mark.asyncio
async def test_get_date_by_file_name_logging(self, audio_repository):
"""Тест логирования при получении даты по имени файла"""
timestamp = 1642248600 # 2022-01-17 10:30:00
timestamp = 1642404600 # 2022-01-17 10:30:00
audio_repository._execute_query_with_result.return_value = [(timestamp,)]
await audio_repository.get_date_by_file_name("test_audio.ogg")

View File

@@ -173,7 +173,7 @@ class TestAudioRepositoryNewSchema:
result = await audio_repository.get_date_by_file_name("test_audio.ogg")
# Должна вернуться читаемая дата в формате dd.mm.yyyy HH:MM
assert result == "17.01.2022 10:30"
assert result == "15.01.2022 15:10"
assert isinstance(result, str)
@pytest.mark.asyncio
@@ -184,7 +184,7 @@ class TestAudioRepositoryNewSchema:
result = await audio_repository.get_date_by_file_name("test_audio.ogg")
assert result == "16.01.2024 12:00"
assert result == "15.01.2024 13:00"
@pytest.mark.asyncio
async def test_get_date_by_file_name_midnight(self, audio_repository):
@@ -194,7 +194,7 @@ class TestAudioRepositoryNewSchema:
result = await audio_repository.get_date_by_file_name("test_audio.ogg")
assert result == "15.01.2024 00:00"
assert result == "14.01.2024 03:00"
@pytest.mark.asyncio
async def test_get_date_by_file_name_year_end(self, audio_repository):
@@ -204,7 +204,7 @@ class TestAudioRepositoryNewSchema:
result = await audio_repository.get_date_by_file_name("test_audio.ogg")
assert result == "31.12.2023 23:59"
assert result == "01.01.2024 03:00"
@pytest.mark.asyncio
async def test_foreign_keys_enabled_called(self, audio_repository):
@@ -271,7 +271,7 @@ class TestAudioRepositoryNewSchema:
log_message = log_call[0][0]
assert "Получена дата" in log_message
assert "17.01.2022 10:30" in log_message
assert "15.01.2022 15:10" in log_message
assert "test_audio.ogg" in log_message
@@ -335,9 +335,13 @@ class TestAudioRepositoryEdgeCases:
listen_count=0
)
# Должно вызвать TypeError при попытке преобразования None
with pytest.raises(TypeError):
await audio_repository.add_audio_record(audio_msg)
# Метод обрабатывает None как timestamp без преобразования
await audio_repository.add_audio_record(audio_msg)
# Проверяем, что метод был вызван с None
call_args = audio_repository._execute_query.call_args
params = call_args[0][1]
assert params[2] is None
@pytest.mark.asyncio
async def test_add_audio_record_simple_empty_string_date(self, audio_repository):
@@ -356,9 +360,13 @@ class TestAudioRepositoryEdgeCases:
@pytest.mark.asyncio
async def test_add_audio_record_simple_none_date(self, audio_repository):
"""Тест упрощенного добавления с None датой"""
# Должно вызвать TypeError при попытке преобразования None
with pytest.raises(TypeError):
await audio_repository.add_audio_record_simple("test_audio.ogg", 12345, None)
# Метод обрабатывает None как timestamp без преобразования
await audio_repository.add_audio_record_simple("test_audio.ogg", 12345, None)
# Проверяем, что метод был вызван с None
call_args = audio_repository._execute_query.call_args
params = call_args[0][1]
assert params[2] is None
@pytest.mark.asyncio
async def test_get_date_by_file_name_zero_timestamp(self, audio_repository):
@@ -367,7 +375,7 @@ class TestAudioRepositoryEdgeCases:
result = await audio_repository.get_date_by_file_name("test_audio.ogg")
assert result == "01.01.1970 00:00"
assert result == "01.01.1970 03:00"
@pytest.mark.asyncio
async def test_get_date_by_file_name_negative_timestamp(self, audio_repository):
@@ -376,7 +384,7 @@ class TestAudioRepositoryEdgeCases:
result = await audio_repository.get_date_by_file_name("test_audio.ogg")
assert result == "31.12.1969 23:00"
assert result == "01.01.1970 02:00"
@pytest.mark.asyncio
async def test_get_date_by_file_name_future_timestamp(self, audio_repository):

View File

@@ -32,19 +32,19 @@ class TestAutoUnbanIntegration:
user_id INTEGER PRIMARY KEY,
user_name TEXT,
message_for_user TEXT,
date_to_unban TEXT
date_to_unban INTEGER
)
''')
# Добавляем тестовые данные
today = datetime.now(timezone(timedelta(hours=3))).strftime("%Y-%m-%d")
tomorrow = (datetime.now(timezone(timedelta(hours=3))) + timedelta(days=1)).strftime("%Y-%m-%d")
today_timestamp = int(datetime.now(timezone(timedelta(hours=3))).timestamp())
tomorrow_timestamp = int((datetime.now(timezone(timedelta(hours=3))) + timedelta(days=1)).timestamp())
test_data = [
(123, "test_user1", "Test ban 1", today), # Разблокируется сегодня
(456, "test_user2", "Test ban 2", today), # Разблокируется сегодня
(789, "test_user3", "Test ban 3", tomorrow), # Разблокируется завтра
(999, "test_user4", "Test ban 4", None), # Навсегда заблокирован
(123, "test_user1", "Test ban 1", today_timestamp), # Разблокируется сегодня
(456, "test_user2", "Test ban 2", today_timestamp), # Разблокируется сегодня
(789, "test_user3", "Test ban 3", tomorrow_timestamp), # Разблокируется завтра
(999, "test_user4", "Test ban 4", None), # Навсегда заблокирован
]
cursor.executemany(
@@ -73,10 +73,9 @@ class TestAutoUnbanIntegration:
}
# Создаем реальный экземпляр базы данных с тестовым файлом
from database.db import BotDB
from database.async_db import AsyncBotDB
import os
current_dir = os.getcwd()
mock_factory.database = BotDB(current_dir, test_db_path)
mock_factory.database = AsyncBotDB(test_db_path)
return mock_factory
@@ -110,14 +109,15 @@ class TestAutoUnbanIntegration:
await scheduler.auto_unban_users()
# Проверяем, что пользователи с сегодняшней датой разблокированы
cursor.execute("SELECT COUNT(*) FROM blacklist WHERE date_to_unban = ?",
(datetime.now(timezone(timedelta(hours=3))).strftime("%Y-%m-%d"),))
current_timestamp = int(datetime.now(timezone(timedelta(hours=3))).timestamp())
cursor.execute("SELECT COUNT(*) FROM blacklist WHERE date_to_unban IS NOT NULL AND date_to_unban <= ?",
(current_timestamp,))
today_count = cursor.fetchone()[0]
assert today_count == 0
# Проверяем, что пользователи с завтрашней датой остались
tomorrow = (datetime.now(timezone(timedelta(hours=3))) + timedelta(days=1)).strftime("%Y-%m-%d")
cursor.execute("SELECT COUNT(*) FROM blacklist WHERE date_to_unban = ?", (tomorrow,))
cursor.execute("SELECT COUNT(*) FROM blacklist WHERE date_to_unban IS NOT NULL AND date_to_unban > ?",
(current_timestamp,))
tomorrow_count = cursor.fetchone()[0]
assert tomorrow_count == 1
@@ -146,8 +146,8 @@ class TestAutoUnbanIntegration:
# Удаляем пользователей с сегодняшней датой
conn = sqlite3.connect(setup_test_db)
cursor = conn.cursor()
today = datetime.now(timezone(timedelta(hours=3))).strftime("%Y-%m-%d")
cursor.execute("DELETE FROM blacklist WHERE date_to_unban = ?", (today,))
current_timestamp = int(datetime.now(timezone(timedelta(hours=3))).timestamp())
cursor.execute("DELETE FROM blacklist WHERE date_to_unban IS NOT NULL AND date_to_unban <= ?", (current_timestamp,))
conn.commit()
conn.close()
@@ -195,7 +195,7 @@ class TestAutoUnbanIntegration:
scheduler = AutoUnbanScheduler()
scheduler.bot_db = mock_bdf.database
# Проверяем, что дата в базе соответствует ожидаемому формату
# Проверяем, что дата в базе соответствует ожидаемому формату (timestamp)
conn = sqlite3.connect(setup_test_db)
cursor = conn.cursor()
cursor.execute("SELECT date_to_unban FROM blacklist WHERE date_to_unban IS NOT NULL LIMIT 1")
@@ -203,13 +203,13 @@ class TestAutoUnbanIntegration:
conn.close()
if result and result[0]:
date_str = result[0]
# Проверяем формат YYYY-MM-DD
assert len(date_str) == 10
assert date_str.count('-') == 2
assert date_str[:4].isdigit() # Год
assert date_str[5:7].isdigit() # Месяц
assert date_str[8:10].isdigit() # День
timestamp = result[0]
# Проверяем, что это валидный timestamp (целое число)
assert isinstance(timestamp, int)
assert timestamp > 0
# Проверяем, что timestamp можно преобразовать в дату
date_obj = datetime.fromtimestamp(timestamp)
assert isinstance(date_obj, datetime)
class TestSchedulerLifecycle:

View File

@@ -18,11 +18,11 @@ class TestAutoUnbanScheduler:
def mock_bot_db(self):
"""Создает мок базы данных"""
mock_db = Mock()
mock_db.get_users_for_unblock_today.return_value = {
mock_db.get_users_for_unblock_today = AsyncMock(return_value={
123: "test_user1",
456: "test_user2"
}
mock_db.delete_user_blacklist.return_value = True
})
mock_db.delete_user_blacklist = AsyncMock(return_value=True)
return mock_db
@pytest.fixture
@@ -78,7 +78,7 @@ class TestAutoUnbanScheduler:
"""Тест разбана когда нет пользователей для разблокировки"""
# Настройка моков
mock_get_instance.return_value = mock_bdf
mock_bot_db.get_users_for_unblock_today.return_value = {}
mock_bot_db.get_users_for_unblock_today = AsyncMock(return_value={})
scheduler.bot_db = mock_bot_db
scheduler.set_bot(mock_bot)
@@ -96,12 +96,12 @@ class TestAutoUnbanScheduler:
"""Тест разбана с частичными ошибками"""
# Настройка моков
mock_get_instance.return_value = mock_bdf
mock_bot_db.get_users_for_unblock_today.return_value = {
mock_bot_db.get_users_for_unblock_today = AsyncMock(return_value={
123: "test_user1",
456: "test_user2"
}
})
# Первый вызов успешен, второй - ошибка
mock_bot_db.delete_user_blacklist.side_effect = [True, False]
mock_bot_db.delete_user_blacklist = AsyncMock(side_effect=[True, False])
scheduler.bot_db = mock_bot_db
scheduler.set_bot(mock_bot)
@@ -118,7 +118,7 @@ class TestAutoUnbanScheduler:
"""Тест разбана с исключением"""
# Настройка моков
mock_get_instance.return_value = mock_bdf
mock_bot_db.get_users_for_unblock_today.side_effect = Exception("Database error")
mock_bot_db.get_users_for_unblock_today = AsyncMock(side_effect=Exception("Database error"))
scheduler.bot_db = mock_bot_db
scheduler.set_bot(mock_bot)
@@ -141,7 +141,7 @@ class TestAutoUnbanScheduler:
assert "Отчет об автоматическом разбане" in report
assert "Успешно разблокировано: 1" in report
assert "Ошибок: 1" in report
assert "test_user1" in report
assert "ID: 123" in report
assert "456 (test_user2)" in report
@pytest.mark.asyncio
@@ -268,8 +268,8 @@ class TestAsyncOperations:
mock_get_instance.return_value = mock_bdf
mock_bot_db = Mock()
mock_bot_db.get_users_for_unblock_today.return_value = {123: "test_user"}
mock_bot_db.delete_user_blacklist.return_value = True
mock_bot_db.get_users_for_unblock_today = AsyncMock(return_value={123: "test_user"})
mock_bot_db.delete_user_blacklist = AsyncMock(return_value=True)
mock_bot = Mock()
mock_bot.send_message = AsyncMock()

View File

@@ -70,13 +70,14 @@ class TestKeyboards:
# Проверяем наличие кнопки стикеров
assert '🤪Хочу стикеры' in all_buttons
def test_get_reply_keyboard_without_stickers(self, mock_db):
@pytest.mark.asyncio
async def test_get_reply_keyboard_without_stickers(self, mock_db):
"""Тест клавиатуры без стикеров"""
user_id = 123456
# Мокаем метод get_info_about_stickers
mock_db.get_info_about_stickers = Mock(return_value=True)
# Мокаем метод get_stickers_info
mock_db.get_stickers_info = AsyncMock(return_value=True)
keyboard = get_reply_keyboard(mock_db, user_id)
keyboard = await get_reply_keyboard(mock_db, user_id)
all_buttons = []
for row in keyboard.keyboard:
@@ -86,13 +87,14 @@ class TestKeyboards:
# Проверяем отсутствие кнопки стикеров
assert '🤪Хочу стикеры' not in all_buttons
def test_get_reply_keyboard_admin(self, mock_db):
@pytest.mark.asyncio
async def test_get_reply_keyboard_admin(self, mock_db):
"""Тест клавиатуры для админа"""
user_id = 123456
# Мокаем метод get_info_about_stickers
mock_db.get_info_about_stickers = Mock(return_value=False)
# Мокаем метод get_stickers_info
mock_db.get_stickers_info = AsyncMock(return_value=False)
keyboard = get_reply_keyboard(mock_db, user_id)
keyboard = await get_reply_keyboard(mock_db, user_id)
all_buttons = []
for row in keyboard.keyboard:
@@ -284,44 +286,41 @@ class TestChatTypeFilter:
class TestKeyboardIntegration:
"""Интеграционные тесты клавиатур"""
def test_keyboard_structure_consistency(self):
@pytest.mark.asyncio
async def test_keyboard_structure_consistency(self):
"""Тест консистентности структуры клавиатур"""
# Мокаем базу данных
mock_db = Mock(spec=AsyncBotDB)
mock_db.get_info_about_stickers = Mock(return_value=False)
mock_db.get_stickers_info = AsyncMock(return_value=False)
# Тестируем все типы клавиатур
keyboards = [
get_reply_keyboard(mock_db, 123456),
get_reply_keyboard_for_post(),
get_reply_keyboard_leave_chat()
]
keyboard1 = await get_reply_keyboard(mock_db, 123456)
keyboard2 = get_reply_keyboard_for_post()
keyboard3 = get_reply_keyboard_leave_chat()
# Проверяем первую клавиатуру (ReplyKeyboardMarkup)
keyboard1 = keyboards[0]
assert isinstance(keyboard1, ReplyKeyboardMarkup)
assert hasattr(keyboard1, 'keyboard')
assert isinstance(keyboard1.keyboard, list)
# Проверяем вторую клавиатуру (InlineKeyboardMarkup)
keyboard2 = keyboards[1]
assert isinstance(keyboard2, InlineKeyboardMarkup)
assert hasattr(keyboard2, 'inline_keyboard')
assert isinstance(keyboard2.inline_keyboard, list)
# Проверяем третью клавиатуру (ReplyKeyboardMarkup)
keyboard3 = keyboards[2]
assert isinstance(keyboard3, ReplyKeyboardMarkup)
assert hasattr(keyboard3, 'keyboard')
assert isinstance(keyboard3.keyboard, list)
def test_keyboard_button_texts(self):
@pytest.mark.asyncio
async def test_keyboard_button_texts(self):
"""Тест текстов кнопок клавиатур"""
# Тестируем основные кнопки
db = Mock(spec=AsyncBotDB)
db.get_info_about_stickers = Mock(return_value=False)
db.get_stickers_info = AsyncMock(return_value=False)
main_keyboard = get_reply_keyboard(db, 123456)
main_keyboard = await get_reply_keyboard(db, 123456)
post_keyboard = get_reply_keyboard_for_post()
leave_keyboard = get_reply_keyboard_leave_chat()

View File

@@ -19,7 +19,8 @@ class TestAdminService:
self.mock_db = Mock()
self.admin_service = AdminService(self.mock_db)
def test_get_last_users_success(self):
@pytest.mark.asyncio
async def test_get_last_users_success(self):
"""Тест успешного получения списка последних пользователей"""
# Arrange
# Формат данных: кортежи (full_name, user_id) как возвращает БД
@@ -27,10 +28,10 @@ class TestAdminService:
('User One', 1), # (full_name, user_id)
('User Two', 2) # (full_name, user_id)
]
self.mock_db.get_last_users_from_db.return_value = mock_users_data
self.mock_db.get_last_users = AsyncMock(return_value=mock_users_data)
# Act
result = self.admin_service.get_last_users()
result = await self.admin_service.get_last_users()
# Assert
assert len(result) == 2
@@ -41,17 +42,18 @@ class TestAdminService:
assert result[1].username == 'Неизвестно' # username не возвращается из БД
assert result[1].full_name == 'User Two'
def test_get_user_by_username_success(self):
@pytest.mark.asyncio
async def test_get_user_by_username_success(self):
"""Тест успешного получения пользователя по username"""
# Arrange
user_id = 123
username = "test_user"
full_name = "Test User"
self.mock_db.get_user_id_by_username.return_value = user_id
self.mock_db.get_full_name_by_id.return_value = full_name
self.mock_db.get_user_id_by_username = AsyncMock(return_value=user_id)
self.mock_db.get_full_name_by_id = AsyncMock(return_value=full_name)
# Act
result = self.admin_service.get_user_by_username(username)
result = await self.admin_service.get_user_by_username(username)
# Assert
assert result is not None
@@ -59,27 +61,35 @@ class TestAdminService:
assert result.username == username
assert result.full_name == full_name
def test_get_user_by_username_not_found(self):
@pytest.mark.asyncio
async def test_get_user_by_username_not_found(self):
"""Тест получения пользователя по несуществующему username"""
# Arrange
username = "nonexistent_user"
self.mock_db.get_user_id_by_username.return_value = None
self.mock_db.get_user_id_by_username = AsyncMock(return_value=None)
# Act
result = self.admin_service.get_user_by_username(username)
result = await self.admin_service.get_user_by_username(username)
# Assert
assert result is None
def test_get_user_by_id_success(self):
@pytest.mark.asyncio
async def test_get_user_by_id_success(self):
"""Тест успешного получения пользователя по ID"""
# Arrange
user_id = 123
user_info = {'username': 'test_user', 'full_name': 'Test User'}
self.mock_db.get_user_info_by_id.return_value = user_info
from database.models import User as DBUser
user_info = DBUser(
user_id=user_id,
first_name="Test",
full_name="Test User",
username="test_user"
)
self.mock_db.get_user_by_id = AsyncMock(return_value=user_info)
# Act
result = self.admin_service.get_user_by_id(user_id)
result = await self.admin_service.get_user_by_id(user_id)
# Assert
assert result is not None
@@ -87,45 +97,51 @@ class TestAdminService:
assert result.username == 'test_user'
assert result.full_name == 'Test User'
def test_get_user_by_id_not_found(self):
@pytest.mark.asyncio
async def test_get_user_by_id_not_found(self):
"""Тест получения пользователя по несуществующему ID"""
# Arrange
user_id = 999
self.mock_db.get_user_info_by_id.return_value = None
self.mock_db.get_user_by_id = AsyncMock(return_value=None)
# Act
result = self.admin_service.get_user_by_id(user_id)
result = await self.admin_service.get_user_by_id(user_id)
# Assert
assert result is None
def test_validate_user_input_success(self):
@pytest.mark.asyncio
async def test_validate_user_input_success(self):
"""Тест успешной валидации ID пользователя"""
# Act
result = self.admin_service.validate_user_input("123")
result = await self.admin_service.validate_user_input("123")
# Assert
assert result == 123
def test_validate_user_input_invalid_number(self):
@pytest.mark.asyncio
async def test_validate_user_input_invalid_number(self):
"""Тест валидации некорректного ID"""
# Act & Assert
with pytest.raises(InvalidInputError, match="ID пользователя должен быть числом"):
self.admin_service.validate_user_input("abc")
await self.admin_service.validate_user_input("abc")
def test_validate_user_input_negative_number(self):
@pytest.mark.asyncio
async def test_validate_user_input_negative_number(self):
"""Тест валидации отрицательного ID"""
# Act & Assert
with pytest.raises(InvalidInputError, match="ID пользователя должен быть положительным числом"):
self.admin_service.validate_user_input("-1")
await self.admin_service.validate_user_input("-1")
def test_validate_user_input_zero(self):
@pytest.mark.asyncio
async def test_validate_user_input_zero(self):
"""Тест валидации нулевого ID"""
# Act & Assert
with pytest.raises(InvalidInputError, match="ID пользователя должен быть положительным числом"):
self.admin_service.validate_user_input("0")
await self.admin_service.validate_user_input("0")
def test_ban_user_success(self):
@pytest.mark.asyncio
async def test_ban_user_success(self):
"""Тест успешной блокировки пользователя"""
# Arrange
user_id = 123
@@ -133,17 +149,18 @@ class TestAdminService:
reason = "Test ban"
ban_days = 7
self.mock_db.check_user_in_blacklist.return_value = False
self.mock_db.set_user_blacklist.return_value = None
self.mock_db.check_user_in_blacklist = AsyncMock(return_value=False)
self.mock_db.set_user_blacklist = AsyncMock(return_value=None)
# Act
self.admin_service.ban_user(user_id, username, reason, ban_days)
await self.admin_service.ban_user(user_id, username, reason, ban_days)
# Assert
self.mock_db.check_user_in_blacklist.assert_called_once_with(user_id)
self.mock_db.set_user_blacklist.assert_called_once()
def test_ban_user_already_banned(self):
@pytest.mark.asyncio
async def test_ban_user_already_banned(self):
"""Тест попытки заблокировать уже заблокированного пользователя"""
# Arrange
user_id = 123
@@ -151,13 +168,14 @@ class TestAdminService:
reason = "Test ban"
ban_days = 7
self.mock_db.check_user_in_blacklist.return_value = True
self.mock_db.check_user_in_blacklist = AsyncMock(return_value=True)
# Act & Assert
with pytest.raises(UserAlreadyBannedError, match=f"Пользователь {user_id} уже заблокирован"):
self.admin_service.ban_user(user_id, username, reason, ban_days)
await self.admin_service.ban_user(user_id, username, reason, ban_days)
def test_ban_user_permanent(self):
@pytest.mark.asyncio
async def test_ban_user_permanent(self):
"""Тест постоянной блокировки пользователя"""
# Arrange
user_id = 123
@@ -165,23 +183,24 @@ class TestAdminService:
reason = "Permanent ban"
ban_days = None
self.mock_db.check_user_in_blacklist.return_value = False
self.mock_db.set_user_blacklist.return_value = None
self.mock_db.check_user_in_blacklist = AsyncMock(return_value=False)
self.mock_db.set_user_blacklist = AsyncMock(return_value=None)
# Act
self.admin_service.ban_user(user_id, username, reason, ban_days)
await self.admin_service.ban_user(user_id, username, reason, ban_days)
# Assert
self.mock_db.set_user_blacklist.assert_called_once_with(user_id, username, reason, None)
self.mock_db.set_user_blacklist.assert_called_once_with(user_id, None, reason, None)
def test_unban_user_success(self):
@pytest.mark.asyncio
async def test_unban_user_success(self):
"""Тест успешной разблокировки пользователя"""
# Arrange
user_id = 123
self.mock_db.delete_user_blacklist.return_value = None
self.mock_db.delete_user_blacklist = AsyncMock(return_value=None)
# Act
self.admin_service.unban_user(user_id)
await self.admin_service.unban_user(user_id)
# Assert
self.mock_db.delete_user_blacklist.assert_called_once_with(user_id)

View File

@@ -75,9 +75,10 @@ class TestGroupHandlers:
assert handlers.admin_reply_service is not None
assert handlers.router is not None
@pytest.mark.asyncio
async def test_handle_message_success(self, mock_db, mock_keyboard_markup, mock_reply_message, mock_state):
"""Test successful message handling"""
mock_db.get_user_by_message_id.return_value = 99999
mock_db.get_user_by_message_id = AsyncMock(return_value=99999)
handlers = create_group_handlers(mock_db, mock_keyboard_markup)
@@ -97,6 +98,7 @@ class TestGroupHandlers:
# Verify state was set
mock_state.set_state.assert_called_once_with(FSM_STATES["CHAT"])
@pytest.mark.asyncio
async def test_handle_message_no_reply(self, mock_db, mock_keyboard_markup, mock_message, mock_state):
"""Test message handling without reply"""
handlers = create_group_handlers(mock_db, mock_keyboard_markup)
@@ -121,9 +123,10 @@ class TestGroupHandlers:
# Verify state was not set
mock_state.set_state.assert_not_called()
@pytest.mark.asyncio
async def test_handle_message_user_not_found(self, mock_db, mock_keyboard_markup, mock_reply_message, mock_state):
"""Test message handling when user is not found"""
mock_db.get_user_by_message_id.return_value = None
mock_db.get_user_by_message_id = AsyncMock(return_value=None)
handlers = create_group_handlers(mock_db, mock_keyboard_markup)
@@ -154,24 +157,27 @@ class TestAdminReplyService:
"""Create service instance"""
return AdminReplyService(mock_db)
def test_get_user_id_for_reply_success(self, service, mock_db):
@pytest.mark.asyncio
async def test_get_user_id_for_reply_success(self, service, mock_db):
"""Test successful user ID retrieval"""
mock_db.get_user_by_message_id.return_value = 12345
mock_db.get_user_by_message_id = AsyncMock(return_value=12345)
result = service.get_user_id_for_reply(111)
result = await service.get_user_id_for_reply(111)
assert result == 12345
mock_db.get_user_by_message_id.assert_called_once_with(111)
def test_get_user_id_for_reply_not_found(self, service, mock_db):
@pytest.mark.asyncio
async def test_get_user_id_for_reply_not_found(self, service, mock_db):
"""Test user ID retrieval when user not found"""
mock_db.get_user_by_message_id.return_value = None
mock_db.get_user_by_message_id = AsyncMock(return_value=None)
with pytest.raises(UserNotFoundError, match="User not found for message_id: 111"):
service.get_user_id_for_reply(111)
await service.get_user_id_for_reply(111)
mock_db.get_user_by_message_id.assert_called_once_with(111)
@pytest.mark.asyncio
async def test_send_reply_to_user(self, service, mock_db):
"""Test sending reply to user"""
message = Mock()

View File

@@ -19,13 +19,13 @@ class TestPrivateHandlers:
def mock_db(self):
"""Mock database"""
db = Mock()
db.user_exists.return_value = False
db.add_new_user_in_db = Mock()
db.update_date_for_user = Mock()
db.update_info_about_stickers = Mock()
db.add_post_in_db = Mock()
db.add_new_message_in_db = Mock()
db.update_helper_message_in_db = Mock()
db.user_exists = AsyncMock(return_value=False)
db.add_user = AsyncMock()
db.update_user_date = AsyncMock()
db.update_stickers_info = AsyncMock()
db.add_post = AsyncMock()
db.add_message = AsyncMock()
db.update_helper_message = AsyncMock()
return db
@pytest.fixture
@@ -101,7 +101,8 @@ class TestPrivateHandlers:
# Mock the check_user_emoji function
with pytest.MonkeyPatch().context() as m:
m.setattr('helper_bot.handlers.private.private_handlers.check_user_emoji', lambda x: "😊")
mock_check_emoji = AsyncMock(return_value="😊")
m.setattr('helper_bot.handlers.private.private_handlers.check_user_emoji', mock_check_emoji)
# Test the handler
await handlers.handle_emoji_message(mock_message, mock_state)
@@ -121,7 +122,8 @@ class TestPrivateHandlers:
with pytest.MonkeyPatch().context() as m:
m.setattr('helper_bot.handlers.private.private_handlers.get_first_name', lambda x: "Test")
m.setattr('helper_bot.handlers.private.private_handlers.messages.get_message', lambda x, y: "Hello Test!")
m.setattr('helper_bot.handlers.private.private_handlers.get_reply_keyboard', lambda x, y: Mock())
mock_keyboard = AsyncMock(return_value=Mock())
m.setattr('helper_bot.handlers.private.private_handlers.get_reply_keyboard', mock_keyboard)
# Test the handler
await handlers.handle_start_message(mock_message, mock_state)
@@ -130,8 +132,8 @@ class TestPrivateHandlers:
mock_state.set_state.assert_called_once_with(FSM_STATES["START"])
# Verify user was ensured to exist
mock_db.add_new_user_in_db.assert_called_once()
mock_db.update_date_for_user.assert_called_once()
mock_db.add_user.assert_called_once()
mock_db.update_user_date.assert_called_once()
class TestBotSettings:

View File

@@ -31,7 +31,7 @@ from helper_bot.utils.helper_func import (
)
from helper_bot.utils.messages import get_message
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory, get_global_instance
from database.db import BotDB
from database.async_db import AsyncBotDB
import helper_bot.utils.messages as messages # Import for patching constants
class TestHelperFunctions:
@@ -83,25 +83,27 @@ class TestHelperFunctions:
assert "testuser" in result
assert "Обычный текст без специальных слов" in result
def test_check_username_and_full_name(self):
@pytest.mark.asyncio
async def test_check_username_and_full_name(self):
"""Тест функции проверки изменений username и full_name"""
# Создаем мок базы данных
mock_db = Mock(spec=BotDB)
mock_db.get_username_and_full_name = Mock(return_value=("olduser", "Old User"))
mock_db = Mock(spec=AsyncBotDB)
mock_db.get_username = AsyncMock(return_value="olduser")
mock_db.get_full_name_by_id = AsyncMock(return_value="Old User")
# Тест с измененными данными
result = check_username_and_full_name(123456, "newuser", "New User", mock_db)
result = await check_username_and_full_name(123456, "newuser", "New User", mock_db)
assert result is True
# Тест с неизмененными данными
result = check_username_and_full_name(123456, "olduser", "Old User", mock_db)
result = await check_username_and_full_name(123456, "olduser", "Old User", mock_db)
assert result is False
# Тест с частично измененными данными
result = check_username_and_full_name(123456, "olduser", "New User", mock_db)
result = await check_username_and_full_name(123456, "olduser", "New User", mock_db)
assert result is True
result = check_username_and_full_name(123456, "newuser", "Old User", mock_db)
result = await check_username_and_full_name(123456, "newuser", "Old User", mock_db)
assert result is True
@@ -330,7 +332,7 @@ class TestPrepareMediaGroup:
assert result[0].media == "photo_0"
assert result[1].media == "photo_1"
assert result[2].media == "photo_2"
assert result[2].caption == "Тестовая подпись"
assert result[0].caption == "Тестовая подпись" # Первое фото должно иметь caption
@pytest.mark.asyncio
async def test_prepare_media_group_mixed_types(self):
@@ -364,7 +366,7 @@ class TestPrepareMediaGroup:
assert result[0].media == "photo_1"
assert result[1].media == "video_1"
assert result[2].media == "audio_1"
assert result[2].caption == "Смешанная группа"
assert result[0].caption == "Смешанная группа" # Первое медиа должно иметь caption
@pytest.mark.asyncio
async def test_prepare_media_group_empty_album(self):
@@ -381,6 +383,7 @@ class TestPrepareMediaGroup:
message.photo = None
message.video = None
message.audio = None
message.document = None # Добавляем document = None
album.append(message)
result = await prepare_media_group_from_middlewares(album, "Тест")
@@ -401,12 +404,12 @@ class TestMediaDatabaseOperations:
message.photo[-1].file_id = f"photo_{i}"
sent_message.append(message)
mock_db = Mock()
mock_db = AsyncMock()
with patch('helper_bot.utils.helper_func.download_file', return_value=f"files/photo_{i}.jpg"):
await add_in_db_media_mediagroup(sent_message, mock_db)
assert mock_db.add_post_content_in_db.call_count == 2
assert mock_db.add_post_content.call_count == 2
@pytest.mark.asyncio
async def test_add_in_db_media_photo(self):
@@ -416,12 +419,12 @@ class TestMediaDatabaseOperations:
mock_message.photo = [Mock()]
mock_message.photo[-1].file_id = "photo_123"
mock_db = Mock()
mock_db = AsyncMock()
with patch('helper_bot.utils.helper_func.download_file', return_value="files/photo_123.jpg"):
await add_in_db_media(mock_message, mock_db)
mock_db.add_post_content_in_db.assert_called_once_with(
mock_db.add_post_content.assert_called_once_with(
123, 123, "files/photo_123.jpg", 'photo'
)
@@ -434,12 +437,12 @@ class TestMediaDatabaseOperations:
mock_message.video = Mock()
mock_message.video.file_id = "video_123"
mock_db = Mock()
mock_db = AsyncMock()
with patch('helper_bot.utils.helper_func.download_file', return_value="files/video_123.mp4"):
await add_in_db_media(mock_message, mock_db)
mock_db.add_post_content_in_db.assert_called_once_with(
mock_db.add_post_content.assert_called_once_with(
123, 123, "files/video_123.mp4", 'video'
)
@@ -453,12 +456,12 @@ class TestMediaDatabaseOperations:
mock_message.voice = Mock()
mock_message.voice.file_id = "voice_123"
mock_db = Mock()
mock_db = AsyncMock()
with patch('helper_bot.utils.helper_func.download_file', return_value="files/voice_123.ogg"):
await add_in_db_media(mock_message, mock_db)
mock_db.add_post_content_in_db.assert_called_once_with(
mock_db.add_post_content.assert_called_once_with(
123, 123, "files/voice_123.ogg", 'voice'
)
@@ -548,16 +551,17 @@ class TestSendMessageFunctions:
class TestUtilityFunctions:
"""Тесты для утилитарных функций"""
def test_check_access(self):
@pytest.mark.asyncio
async def test_check_access(self):
"""Тест проверки доступа"""
mock_db = Mock()
mock_db = AsyncMock()
mock_db.is_admin.return_value = True
result = check_access(123, mock_db)
result = await check_access(123, mock_db)
assert result is True
mock_db.is_admin.return_value = False
result = check_access(123, mock_db)
result = await check_access(123, mock_db)
assert result is False
def test_add_days_to_date(self):
@@ -569,45 +573,51 @@ class TestUtilityFunctions:
mock_datetime.timedelta = timedelta
result = add_days_to_date(5)
expected_date = (mock_now + timedelta(days=5)).strftime("%d-%m-%Y")
assert result == expected_date
expected_timestamp = int((mock_now + timedelta(days=5)).timestamp())
assert result == expected_timestamp
def test_get_banned_users_list(self):
@pytest.mark.asyncio
async def test_get_banned_users_list(self):
"""Тест получения списка заблокированных пользователей"""
mock_db = Mock()
mock_db = AsyncMock()
mock_db.get_banned_users_from_db_with_limits.return_value = [
("User1", 123, "Spam", "01-01-2025"),
("User2", 456, "Violation", "02-01-2025")
(123, "Spam", 1704067200), # user_id, ban_reason, unban_date (timestamp)
(456, "Violation", 1704153600)
]
mock_db.get_username.return_value = None
mock_db.get_full_name_by_id.return_value = "Test User"
result = get_banned_users_list(0, mock_db)
result = await get_banned_users_list(0, mock_db)
assert "Список заблокированных пользователей:" in result
assert "User1" in result
assert "User2" in result
assert "Test User" in result
assert "Spam" in result
assert "Violation" in result
def test_get_banned_users_buttons(self):
@pytest.mark.asyncio
async def test_get_banned_users_buttons(self):
"""Тест получения кнопок заблокированных пользователей"""
mock_db = Mock()
mock_db = AsyncMock()
mock_db.get_banned_users_from_db.return_value = [
("User1", 123),
("User2", 456)
(123, "Spam", 1704067200), # user_id, ban_reason, unban_date
(456, "Violation", 1704153600)
]
mock_db.get_username.return_value = None
mock_db.get_full_name_by_id.return_value = "Test User"
result = get_banned_users_buttons(mock_db)
result = await get_banned_users_buttons(mock_db)
assert len(result) == 2
assert result[0] == ("User1", 123)
assert result[1] == ("User2", 456)
assert result[0] == ("Test User", 123)
assert result[1] == ("Test User", 456)
def test_delete_user_blacklist(self):
@pytest.mark.asyncio
async def test_delete_user_blacklist(self):
"""Тест удаления пользователя из черного списка"""
mock_db = Mock()
mock_db = AsyncMock()
mock_db.delete_user_blacklist.return_value = True
result = delete_user_blacklist(123, mock_db)
result = await delete_user_blacklist(123, mock_db)
assert result is True
mock_db.delete_user_blacklist.assert_called_once_with(user_id=123)
@@ -631,57 +641,61 @@ class TestUserManagement:
with patch('helper_bot.utils.helper_func.get_first_name', return_value="Test"):
with patch('helper_bot.utils.helper_func.get_random_emoji', return_value="😀"):
with patch('helper_bot.utils.helper_func.BotDB') as mock_bot_db:
mock_bot_db.user_exists.return_value = False
mock_bot_db.add_new_user_in_db = Mock()
mock_bot_db.update_date_for_user = Mock()
mock_bot_db.user_exists = AsyncMock(return_value=False)
mock_bot_db.add_user = AsyncMock()
mock_bot_db.update_user_date = AsyncMock()
await update_user_info("test", mock_message)
mock_bot_db.add_new_user_in_db.assert_called_once()
mock_bot_db.update_date_for_user.assert_called_once()
mock_bot_db.add_user.assert_called_once()
mock_bot_db.update_user_date.assert_called_once()
def test_check_user_emoji_existing(self):
@pytest.mark.asyncio
async def test_check_user_emoji_existing(self):
"""Тест проверки эмодзи пользователя (существующий)"""
mock_message = Mock()
mock_message.from_user.id = 123
with patch('helper_bot.utils.helper_func.BotDB') as mock_bot_db:
mock_bot_db.check_emoji_for_user.return_value = "😀"
mock_bot_db.get_user_emoji = AsyncMock(return_value="😀")
result = check_user_emoji(mock_message)
result = await check_user_emoji(mock_message)
assert result == "😀"
def test_check_user_emoji_new(self):
@pytest.mark.asyncio
async def test_check_user_emoji_new(self):
"""Тест проверки эмодзи пользователя (новый)"""
mock_message = Mock()
mock_message.from_user.id = 123
with patch('helper_bot.utils.helper_func.BotDB') as mock_bot_db:
mock_bot_db.check_emoji_for_user.return_value = None
mock_bot_db.update_emoji_for_user = Mock()
mock_bot_db.get_user_emoji = AsyncMock(return_value=None)
mock_bot_db.update_user_emoji = AsyncMock()
with patch('helper_bot.utils.helper_func.get_random_emoji', return_value="😀"):
result = check_user_emoji(mock_message)
result = await check_user_emoji(mock_message)
assert result == "😀"
mock_bot_db.update_emoji_for_user.assert_called_once_with(user_id=123, emoji="😀")
mock_bot_db.update_user_emoji.assert_called_once_with(user_id=123, emoji="😀")
def test_get_random_emoji_success(self):
@pytest.mark.asyncio
async def test_get_random_emoji_success(self):
"""Тест получения случайного эмодзи (успех)"""
with patch('helper_bot.utils.helper_func.BotDB') as mock_bot_db:
mock_bot_db.check_emoji.return_value = False
mock_bot_db.check_emoji_exists = AsyncMock(return_value=False)
with patch('helper_bot.utils.helper_func.random.choice', return_value="😀"):
result = get_random_emoji()
result = await get_random_emoji()
assert result == "😀"
def test_get_random_emoji_fallback(self):
@pytest.mark.asyncio
async def test_get_random_emoji_fallback(self):
"""Тест получения случайного эмодзи (fallback)"""
with patch('helper_bot.utils.helper_func.BotDB') as mock_bot_db:
mock_bot_db.check_emoji.return_value = True # Все эмодзи заняты
mock_bot_db.check_emoji_exists = AsyncMock(return_value=True) # Все эмодзи заняты
with patch('helper_bot.utils.helper_func.random.choice', return_value="😀"):
with patch('helper_bot.utils.helper_func.logger') as mock_logger:
result = get_random_emoji()
result = await get_random_emoji()
assert result == "Эмоджи не определен"
mock_logger.error.assert_called_once()

View File

@@ -55,14 +55,15 @@ class TestVoiceBotService:
assert sticker is None
def test_get_random_audio_success(self, voice_service, mock_bot_db):
@pytest.mark.asyncio
async def test_get_random_audio_success(self, voice_service, mock_bot_db):
"""Тест успешного получения случайного аудио"""
mock_bot_db.check_listen_audio.return_value = ['audio1', 'audio2']
mock_bot_db.get_user_id_by_file_name.return_value = 123
mock_bot_db.get_date_by_file_name.return_value = '2025-01-01 12:00:00'
mock_bot_db.check_emoji_for_user.return_value = '😊'
mock_bot_db.check_listen_audio = AsyncMock(return_value=['audio1', 'audio2'])
mock_bot_db.get_user_id_by_file_name = AsyncMock(return_value=123)
mock_bot_db.get_date_by_file_name = AsyncMock(return_value='2025-01-01 12:00:00')
mock_bot_db.get_user_emoji = AsyncMock(return_value='😊')
result = voice_service.get_random_audio(456)
result = await voice_service.get_random_audio(456)
assert result is not None
assert len(result) == 3
@@ -70,40 +71,49 @@ class TestVoiceBotService:
assert result[1] == '2025-01-01 12:00:00'
assert result[2] == '😊'
def test_get_random_audio_no_audio(self, voice_service, mock_bot_db):
@pytest.mark.asyncio
async def test_get_random_audio_no_audio(self, voice_service, mock_bot_db):
"""Тест получения аудио когда их нет"""
mock_bot_db.check_listen_audio.return_value = []
mock_bot_db.check_listen_audio = AsyncMock(return_value=[])
result = voice_service.get_random_audio(456)
result = await voice_service.get_random_audio(456)
assert result is None
def test_mark_audio_as_listened_success(self, voice_service, mock_bot_db):
@pytest.mark.asyncio
async def test_mark_audio_as_listened_success(self, voice_service, mock_bot_db):
"""Тест успешной пометки аудио как прослушанного"""
voice_service.mark_audio_as_listened('test_audio', 123)
mock_bot_db.mark_listened_audio = AsyncMock()
await voice_service.mark_audio_as_listened('test_audio', 123)
mock_bot_db.mark_listened_audio.assert_called_once_with('test_audio', user_id=123)
def test_clear_user_listenings_success(self, voice_service, mock_bot_db):
@pytest.mark.asyncio
async def test_clear_user_listenings_success(self, voice_service, mock_bot_db):
"""Тест успешной очистки прослушиваний"""
voice_service.clear_user_listenings(123)
mock_bot_db.delete_listen_count_for_user = AsyncMock()
await voice_service.clear_user_listenings(123)
mock_bot_db.delete_listen_count_for_user.assert_called_once_with(123)
def test_get_remaining_audio_count_success(self, voice_service, mock_bot_db):
@pytest.mark.asyncio
async def test_get_remaining_audio_count_success(self, voice_service, mock_bot_db):
"""Тест получения количества оставшихся аудио"""
mock_bot_db.check_listen_audio.return_value = ['audio1', 'audio2', 'audio3']
mock_bot_db.check_listen_audio = AsyncMock(return_value=['audio1', 'audio2', 'audio3'])
result = voice_service.get_remaining_audio_count(123)
result = await voice_service.get_remaining_audio_count(123)
assert result == 3
mock_bot_db.check_listen_audio.assert_called_once_with(user_id=123)
def test_get_remaining_audio_count_zero(self, voice_service, mock_bot_db):
@pytest.mark.asyncio
async def test_get_remaining_audio_count_zero(self, voice_service, mock_bot_db):
"""Тест получения количества оставшихся аудио когда их нет"""
mock_bot_db.check_listen_audio.return_value = []
mock_bot_db.check_listen_audio = AsyncMock(return_value=[])
result = voice_service.get_remaining_audio_count(123)
result = await voice_service.get_remaining_audio_count(123)
assert result == 0
mock_bot_db.check_listen_audio.assert_called_once_with(user_id=123)
@@ -187,57 +197,64 @@ class TestUtils:
"""Мок для базы данных"""
return Mock()
def test_get_last_message_text(self, mock_bot_db):
@pytest.mark.asyncio
async def test_get_last_message_text(self, mock_bot_db):
"""Тест получения последнего сообщения"""
mock_bot_db.last_date_audio.return_value = "2025-01-01 12:00:00"
# Возвращаем UNIX timestamp
mock_bot_db.last_date_audio = AsyncMock(return_value=1641034800) # 2022-01-01 12:00:00
result = get_last_message_text(mock_bot_db)
result = await get_last_message_text(mock_bot_db)
assert result is not None
assert "минут" in result or "часа" in result or "дня" in result
assert "минут" in result or "часа" in result or "дня" in result or "день" in result or "дней" in result
mock_bot_db.last_date_audio.assert_called_once()
def test_validate_voice_message_valid(self):
@pytest.mark.asyncio
async def test_validate_voice_message_valid(self):
"""Тест валидации голосового сообщения"""
mock_message = Mock()
mock_message.content_type = 'voice'
mock_message.voice = Mock()
result = validate_voice_message(mock_message)
result = await validate_voice_message(mock_message)
assert result is True
def test_validate_voice_message_invalid(self):
@pytest.mark.asyncio
async def test_validate_voice_message_invalid(self):
"""Тест валидации невалидного сообщения"""
mock_message = Mock()
mock_message.voice = None
result = validate_voice_message(mock_message)
result = await validate_voice_message(mock_message)
assert result is False
def test_get_user_emoji_safe(self, mock_bot_db):
@pytest.mark.asyncio
async def test_get_user_emoji_safe(self, mock_bot_db):
"""Тест безопасного получения эмодзи пользователя"""
mock_bot_db.check_emoji_for_user.return_value = "😊"
mock_bot_db.get_user_emoji = AsyncMock(return_value="😊")
result = get_user_emoji_safe(mock_bot_db, 123)
result = await get_user_emoji_safe(mock_bot_db, 123)
assert result == "😊"
mock_bot_db.check_emoji_for_user.assert_called_once_with(123)
mock_bot_db.get_user_emoji.assert_called_once_with(123)
def test_get_user_emoji_safe_none(self, mock_bot_db):
@pytest.mark.asyncio
async def test_get_user_emoji_safe_none(self, mock_bot_db):
"""Тест безопасного получения эмодзи когда его нет"""
mock_bot_db.check_emoji_for_user.return_value = None
mock_bot_db.get_user_emoji = AsyncMock(return_value=None)
result = get_user_emoji_safe(mock_bot_db, 123)
result = await get_user_emoji_safe(mock_bot_db, 123)
assert result == "😊"
def test_get_user_emoji_safe_error(self, mock_bot_db):
@pytest.mark.asyncio
async def test_get_user_emoji_safe_error(self, mock_bot_db):
"""Тест безопасного получения эмодзи при ошибке"""
mock_bot_db.check_emoji_for_user.return_value = "Ошибка"
mock_bot_db.get_user_emoji = AsyncMock(return_value="Ошибка")
result = get_user_emoji_safe(mock_bot_db, 123)
result = await get_user_emoji_safe(mock_bot_db, 123)
assert result == "Ошибка"

View File

@@ -151,7 +151,7 @@ class TestVoiceConstants:
assert value.startswith("voice_")
def test_no_duplicate_values(self):
"""Тест отсутствия дублирующихся значений"""
"""Тест отсутствия дублирующихся значений в пределах каждого маппинга"""
button_values = list(BUTTON_COMMAND_MAPPING.values())
command_values = list(COMMAND_MAPPING.values())
callback_values = list(CALLBACK_COMMAND_MAPPING.values())
@@ -161,9 +161,8 @@ class TestVoiceConstants:
assert len(command_values) == len(set(command_values))
assert len(callback_values) == len(set(callback_values))
# Проверяем, что нет дублирующихся значений между маппингами
all_values = button_values + command_values + callback_values
assert len(all_values) == len(set(all_values))
# Примечание: Дублирование между маппингами допустимо (например, voice_emoji)
# так как одно действие может быть вызвано и командой, и кнопкой
if __name__ == '__main__':

View File

@@ -63,18 +63,23 @@ class TestVoiceHandler:
@pytest.mark.asyncio
async def test_voice_bot_button_handler_welcome_received(self, voice_handler, mock_message, mock_state, mock_db, mock_settings):
"""Тест обработчика кнопки когда приветствие уже получено"""
mock_db.check_voice_bot_welcome_received.return_value = True
from unittest.mock import AsyncMock
mock_db.check_voice_bot_welcome_received = AsyncMock(return_value=True)
with patch.object(voice_handler, 'restart_function') as mock_restart:
await voice_handler.voice_bot_button_handler(mock_message, mock_state, mock_db, mock_settings)
with patch('helper_bot.handlers.voice.voice_handler.update_user_info') as mock_update_user:
mock_update_user.return_value = None
mock_db.check_voice_bot_welcome_received.assert_called_once_with(123)
mock_restart.assert_called_once_with(mock_message, mock_state, mock_db, mock_settings)
await voice_handler.voice_bot_button_handler(mock_message, mock_state, mock_db, mock_settings)
mock_db.check_voice_bot_welcome_received.assert_called_once_with(123)
mock_restart.assert_called_once_with(mock_message, mock_state, mock_db, mock_settings)
@pytest.mark.asyncio
async def test_voice_bot_button_handler_welcome_not_received(self, voice_handler, mock_message, mock_state, mock_db, mock_settings):
"""Тест обработчика кнопки когда приветствие не получено"""
mock_db.check_voice_bot_welcome_received.return_value = False
from unittest.mock import AsyncMock
mock_db.check_voice_bot_welcome_received = AsyncMock(return_value=False)
with patch.object(voice_handler, 'start') as mock_start:
await voice_handler.voice_bot_button_handler(mock_message, mock_state, mock_db, mock_settings)

View File

@@ -80,14 +80,15 @@ class TestVoiceBotService:
assert sticker is not None
# Проверяем, что стикер не None (метод возвращает FSInputFile объект)
def test_get_random_audio_success(self, voice_service, mock_bot_db):
@pytest.mark.asyncio
async def test_get_random_audio_success(self, voice_service, mock_bot_db):
"""Тест успешного получения случайного аудио"""
mock_bot_db.check_listen_audio.return_value = ['audio1', 'audio2']
mock_bot_db.get_user_id_by_file_name.return_value = 123
mock_bot_db.get_date_by_file_name.return_value = '2025-01-01 12:00:00'
mock_bot_db.check_emoji_for_user.return_value = '😊'
mock_bot_db.check_listen_audio = AsyncMock(return_value=['audio1', 'audio2'])
mock_bot_db.get_user_id_by_file_name = AsyncMock(return_value=123)
mock_bot_db.get_date_by_file_name = AsyncMock(return_value='2025-01-01 12:00:00')
mock_bot_db.get_user_emoji = AsyncMock(return_value='😊')
result = voice_service.get_random_audio(456)
result = await voice_service.get_random_audio(456)
assert result is not None
assert len(result) == 3
@@ -96,53 +97,63 @@ class TestVoiceBotService:
assert result[1] == '2025-01-01 12:00:00'
assert result[2] == '😊'
def test_get_random_audio_no_audio(self, voice_service, mock_bot_db):
@pytest.mark.asyncio
async def test_get_random_audio_no_audio(self, voice_service, mock_bot_db):
"""Тест получения аудио когда их нет"""
mock_bot_db.check_listen_audio.return_value = []
mock_bot_db.check_listen_audio = AsyncMock(return_value=[])
result = voice_service.get_random_audio(456)
result = await voice_service.get_random_audio(456)
assert result is None
def test_get_random_audio_single_audio(self, voice_service, mock_bot_db):
@pytest.mark.asyncio
async def test_get_random_audio_single_audio(self, voice_service, mock_bot_db):
"""Тест получения аудио когда есть только одно"""
mock_bot_db.check_listen_audio.return_value = ['audio1']
mock_bot_db.get_user_id_by_file_name.return_value = 123
mock_bot_db.get_date_by_file_name.return_value = '2025-01-01 12:00:00'
mock_bot_db.check_emoji_for_user.return_value = '😊'
mock_bot_db.check_listen_audio = AsyncMock(return_value=['audio1'])
mock_bot_db.get_user_id_by_file_name = AsyncMock(return_value=123)
mock_bot_db.get_date_by_file_name = AsyncMock(return_value='2025-01-01 12:00:00')
mock_bot_db.get_user_emoji = AsyncMock(return_value='😊')
result = voice_service.get_random_audio(456)
result = await voice_service.get_random_audio(456)
assert result is not None
assert len(result) == 3
assert result[0] == 'audio1'
def test_mark_audio_as_listened_success(self, voice_service, mock_bot_db):
@pytest.mark.asyncio
async def test_mark_audio_as_listened_success(self, voice_service, mock_bot_db):
"""Тест успешной пометки аудио как прослушанного"""
voice_service.mark_audio_as_listened('test_audio', 123)
mock_bot_db.mark_listened_audio = AsyncMock()
await voice_service.mark_audio_as_listened('test_audio', 123)
mock_bot_db.mark_listened_audio.assert_called_once_with('test_audio', user_id=123)
def test_clear_user_listenings_success(self, voice_service, mock_bot_db):
@pytest.mark.asyncio
async def test_clear_user_listenings_success(self, voice_service, mock_bot_db):
"""Тест успешной очистки прослушиваний"""
voice_service.clear_user_listenings(123)
mock_bot_db.delete_listen_count_for_user = AsyncMock()
await voice_service.clear_user_listenings(123)
mock_bot_db.delete_listen_count_for_user.assert_called_once_with(123)
def test_get_remaining_audio_count_success(self, voice_service, mock_bot_db):
@pytest.mark.asyncio
async def test_get_remaining_audio_count_success(self, voice_service, mock_bot_db):
"""Тест получения количества оставшихся аудио"""
mock_bot_db.check_listen_audio.return_value = ['audio1', 'audio2', 'audio3']
mock_bot_db.check_listen_audio = AsyncMock(return_value=['audio1', 'audio2', 'audio3'])
result = voice_service.get_remaining_audio_count(123)
result = await voice_service.get_remaining_audio_count(123)
assert result == 3
mock_bot_db.check_listen_audio.assert_called_once_with(user_id=123)
def test_get_remaining_audio_count_zero(self, voice_service, mock_bot_db):
@pytest.mark.asyncio
async def test_get_remaining_audio_count_zero(self, voice_service, mock_bot_db):
"""Тест получения количества оставшихся аудио когда их нет"""
mock_bot_db.check_listen_audio.return_value = []
mock_bot_db.check_listen_audio = AsyncMock(return_value=[])
result = voice_service.get_remaining_audio_count(123)
result = await voice_service.get_remaining_audio_count(123)
assert result == 0
mock_bot_db.check_listen_audio.assert_called_once_with(user_id=123)

View File

@@ -39,70 +39,83 @@ class TestVoiceUtils:
message.chat.id = 456
return message
def test_get_last_message_text(self, mock_bot_db):
@pytest.mark.asyncio
async def test_get_last_message_text(self, mock_bot_db):
"""Тест получения последнего сообщения"""
mock_bot_db.last_date_audio.return_value = "2025-01-01 12:00:00"
# Возвращаем UNIX timestamp
from unittest.mock import AsyncMock
mock_bot_db.last_date_audio = AsyncMock(return_value=1641034800) # 2022-01-01 12:00:00
result = get_last_message_text(mock_bot_db)
result = await get_last_message_text(mock_bot_db)
assert result is not None
assert "минут" in result or "часа" in result or "дня" in result
assert "минут" in result or "часа" in result or "дня" in result or "день" in result or "дней" in result
mock_bot_db.last_date_audio.assert_called_once()
def test_validate_voice_message_valid(self):
@pytest.mark.asyncio
async def test_validate_voice_message_valid(self):
"""Тест валидации голосового сообщения"""
mock_message = Mock()
mock_message.content_type = 'voice'
mock_message.voice = Mock()
result = validate_voice_message(mock_message)
result = await validate_voice_message(mock_message)
assert result is True
def test_validate_voice_message_invalid(self):
@pytest.mark.asyncio
async def test_validate_voice_message_invalid(self):
"""Тест валидации невалидного сообщения"""
mock_message = Mock()
mock_message.voice = None
result = validate_voice_message(mock_message)
result = await validate_voice_message(mock_message)
assert result is False
def test_get_user_emoji_safe_with_emoji(self, mock_bot_db):
@pytest.mark.asyncio
async def test_get_user_emoji_safe_with_emoji(self, mock_bot_db):
"""Тест безопасного получения эмодзи пользователя когда эмодзи есть"""
mock_bot_db.check_emoji_for_user.return_value = "😊"
from unittest.mock import AsyncMock
mock_bot_db.get_user_emoji = AsyncMock(return_value="😊")
result = get_user_emoji_safe(mock_bot_db, 123)
result = await get_user_emoji_safe(mock_bot_db, 123)
assert result == "😊"
mock_bot_db.check_emoji_for_user.assert_called_once_with(123)
mock_bot_db.get_user_emoji.assert_called_once_with(123)
def test_get_user_emoji_safe_without_emoji(self, mock_bot_db):
@pytest.mark.asyncio
async def test_get_user_emoji_safe_without_emoji(self, mock_bot_db):
"""Тест безопасного получения эмодзи пользователя когда эмодзи нет"""
mock_bot_db.check_emoji_for_user.return_value = None
from unittest.mock import AsyncMock
mock_bot_db.get_user_emoji = AsyncMock(return_value=None)
result = get_user_emoji_safe(mock_bot_db, 123)
result = await get_user_emoji_safe(mock_bot_db, 123)
assert result == "😊"
mock_bot_db.check_emoji_for_user.assert_called_once_with(123)
mock_bot_db.get_user_emoji.assert_called_once_with(123)
def test_get_user_emoji_safe_with_empty_emoji(self, mock_bot_db):
@pytest.mark.asyncio
async def test_get_user_emoji_safe_with_empty_emoji(self, mock_bot_db):
"""Тест безопасного получения эмодзи пользователя с пустым эмодзи"""
mock_bot_db.check_emoji_for_user.return_value = ""
from unittest.mock import AsyncMock
mock_bot_db.get_user_emoji = AsyncMock(return_value="")
result = get_user_emoji_safe(mock_bot_db, 123)
result = await get_user_emoji_safe(mock_bot_db, 123)
assert result == "😊"
mock_bot_db.check_emoji_for_user.assert_called_once_with(123)
mock_bot_db.get_user_emoji.assert_called_once_with(123)
def test_get_user_emoji_safe_with_error(self, mock_bot_db):
@pytest.mark.asyncio
async def test_get_user_emoji_safe_with_error(self, mock_bot_db):
"""Тест безопасного получения эмодзи пользователя при ошибке"""
mock_bot_db.check_emoji_for_user.return_value = "Ошибка"
from unittest.mock import AsyncMock
mock_bot_db.get_user_emoji = AsyncMock(return_value="Ошибка")
result = get_user_emoji_safe(mock_bot_db, 123)
result = await get_user_emoji_safe(mock_bot_db, 123)
assert result == "Ошибка"
mock_bot_db.check_emoji_for_user.assert_called_once_with(123)
mock_bot_db.get_user_emoji.assert_called_once_with(123)
def test_format_time_ago_minutes(self):
"""Тест форматирования времени в минутах"""