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

View File

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

View File

@@ -43,6 +43,11 @@ class BaseDependencyFactory:
'test': self._parse_bool(os.getenv('TEST', 'false')) '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: def _parse_bool(self, value: str) -> bool:
"""Парсит строковое значение в boolean.""" """Парсит строковое значение в boolean."""
return value.lower() in ('true', '1', 'yes', 'on') 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.set_bot(auto_unban_bot)
auto_unban_scheduler.start_scheduler() auto_unban_scheduler.start_scheduler()
# Инициализируем метрики ПОСЛЕ импорта всех модулей # Метрики запускаются в main.py через server_prometheus.py
# Это гарантирует, что global instance полностью инициализирован # Здесь не нужно дублировать функциональность
from helper_bot.utils.metrics_exporter import MetricsManager
metrics_manager = MetricsManager(host="0.0.0.0", port=8000)
# Флаг для корректного завершения # Флаг для корректного завершения
shutdown_event = asyncio.Event() shutdown_event = asyncio.Event()
@@ -50,9 +48,8 @@ async def main():
signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGTERM, signal_handler)
# Запускаем бота и метрики # Запускаем бота (метрики запускаются внутри start_bot)
bot_task = asyncio.create_task(start_bot(bdf)) bot_task = asyncio.create_task(start_bot(bdf))
metrics_task = asyncio.create_task(metrics_manager.start())
main_bot = None main_bot = None
@@ -67,21 +64,16 @@ async def main():
logger.info("Останавливаем планировщик автоматического разбана...") logger.info("Останавливаем планировщик автоматического разбана...")
auto_unban_scheduler.stop_scheduler() auto_unban_scheduler.stop_scheduler()
logger.info("Останавливаем метрики...") # Метрики останавливаются в main.py
try:
await metrics_manager.stop()
except Exception as e:
logger.error(f"Ошибка при остановке метрик: {e}")
logger.info("Останавливаем задачи...") logger.info("Останавливаем задачи...")
# Отменяем задачи # Отменяем задачу бота
bot_task.cancel() bot_task.cancel()
metrics_task.cancel()
# Ждем завершения задач и получаем результат main bot # Ждем завершения задачи бота и получаем результат main bot
try: try:
results = await asyncio.gather(bot_task, metrics_task, return_exceptions=True) results = await asyncio.gather(bot_task, return_exceptions=True)
# Первый результат - это main bot # Результат - это main bot
if results[0] and not isinstance(results[0], Exception): if results[0] and not isinstance(results[0], Exception):
main_bot = results[0] main_bot = results[0]
except Exception as e: except Exception as e:

View File

@@ -132,7 +132,8 @@ class TestAudioRepository:
# Проверяем, что метод вызван # Проверяем, что метод вызван
audio_repository._execute_query.assert_called_once() audio_repository._execute_query.assert_called_once()
call_args = audio_repository._execute_query.call_args 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 assert isinstance(call_args[0][1][2], int) # timestamp
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -251,7 +252,7 @@ class TestAudioRepository:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_date_by_file_name(self, audio_repository): 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,)] audio_repository._execute_query_with_result.return_value = [(timestamp,)]
result = await audio_repository.get_date_by_file_name("test_audio.ogg") result = await audio_repository.get_date_by_file_name("test_audio.ogg")
@@ -357,7 +358,7 @@ class TestAudioRepository:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_date_by_file_name_logging(self, audio_repository): 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,)] audio_repository._execute_query_with_result.return_value = [(timestamp,)]
await audio_repository.get_date_by_file_name("test_audio.ogg") 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") result = await audio_repository.get_date_by_file_name("test_audio.ogg")
# Должна вернуться читаемая дата в формате dd.mm.yyyy HH:MM # Должна вернуться читаемая дата в формате dd.mm.yyyy HH:MM
assert result == "17.01.2022 10:30" assert result == "15.01.2022 15:10"
assert isinstance(result, str) assert isinstance(result, str)
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -184,7 +184,7 @@ class TestAudioRepositoryNewSchema:
result = await audio_repository.get_date_by_file_name("test_audio.ogg") 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 @pytest.mark.asyncio
async def test_get_date_by_file_name_midnight(self, audio_repository): 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") 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 @pytest.mark.asyncio
async def test_get_date_by_file_name_year_end(self, audio_repository): 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") 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 @pytest.mark.asyncio
async def test_foreign_keys_enabled_called(self, audio_repository): async def test_foreign_keys_enabled_called(self, audio_repository):
@@ -271,7 +271,7 @@ class TestAudioRepositoryNewSchema:
log_message = log_call[0][0] log_message = log_call[0][0]
assert "Получена дата" in log_message 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 assert "test_audio.ogg" in log_message
@@ -335,10 +335,14 @@ class TestAudioRepositoryEdgeCases:
listen_count=0 listen_count=0
) )
# Должно вызвать TypeError при попытке преобразования None # Метод обрабатывает None как timestamp без преобразования
with pytest.raises(TypeError): await audio_repository.add_audio_record(audio_msg)
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 @pytest.mark.asyncio
async def test_add_audio_record_simple_empty_string_date(self, audio_repository): async def test_add_audio_record_simple_empty_string_date(self, audio_repository):
"""Тест упрощенного добавления с пустой строковой датой""" """Тест упрощенного добавления с пустой строковой датой"""
@@ -356,9 +360,13 @@ class TestAudioRepositoryEdgeCases:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_add_audio_record_simple_none_date(self, audio_repository): async def test_add_audio_record_simple_none_date(self, audio_repository):
"""Тест упрощенного добавления с None датой""" """Тест упрощенного добавления с None датой"""
# Должно вызвать TypeError при попытке преобразования None # Метод обрабатывает None как timestamp без преобразования
with pytest.raises(TypeError): await audio_repository.add_audio_record_simple("test_audio.ogg", 12345, None)
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 @pytest.mark.asyncio
async def test_get_date_by_file_name_zero_timestamp(self, audio_repository): 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") 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 @pytest.mark.asyncio
async def test_get_date_by_file_name_negative_timestamp(self, audio_repository): 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") 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 @pytest.mark.asyncio
async def test_get_date_by_file_name_future_timestamp(self, audio_repository): 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_id INTEGER PRIMARY KEY,
user_name TEXT, user_name TEXT,
message_for_user TEXT, message_for_user TEXT,
date_to_unban TEXT date_to_unban INTEGER
) )
''') ''')
# Добавляем тестовые данные # Добавляем тестовые данные
today = datetime.now(timezone(timedelta(hours=3))).strftime("%Y-%m-%d") today_timestamp = int(datetime.now(timezone(timedelta(hours=3))).timestamp())
tomorrow = (datetime.now(timezone(timedelta(hours=3))) + timedelta(days=1)).strftime("%Y-%m-%d") tomorrow_timestamp = int((datetime.now(timezone(timedelta(hours=3))) + timedelta(days=1)).timestamp())
test_data = [ test_data = [
(123, "test_user1", "Test ban 1", today), # Разблокируется сегодня (123, "test_user1", "Test ban 1", today_timestamp), # Разблокируется сегодня
(456, "test_user2", "Test ban 2", today), # Разблокируется сегодня (456, "test_user2", "Test ban 2", today_timestamp), # Разблокируется сегодня
(789, "test_user3", "Test ban 3", tomorrow), # Разблокируется завтра (789, "test_user3", "Test ban 3", tomorrow_timestamp), # Разблокируется завтра
(999, "test_user4", "Test ban 4", None), # Навсегда заблокирован (999, "test_user4", "Test ban 4", None), # Навсегда заблокирован
] ]
cursor.executemany( cursor.executemany(
@@ -73,10 +73,9 @@ class TestAutoUnbanIntegration:
} }
# Создаем реальный экземпляр базы данных с тестовым файлом # Создаем реальный экземпляр базы данных с тестовым файлом
from database.db import BotDB from database.async_db import AsyncBotDB
import os import os
current_dir = os.getcwd() mock_factory.database = AsyncBotDB(test_db_path)
mock_factory.database = BotDB(current_dir, test_db_path)
return mock_factory return mock_factory
@@ -110,14 +109,15 @@ class TestAutoUnbanIntegration:
await scheduler.auto_unban_users() await scheduler.auto_unban_users()
# Проверяем, что пользователи с сегодняшней датой разблокированы # Проверяем, что пользователи с сегодняшней датой разблокированы
cursor.execute("SELECT COUNT(*) FROM blacklist WHERE date_to_unban = ?", current_timestamp = int(datetime.now(timezone(timedelta(hours=3))).timestamp())
(datetime.now(timezone(timedelta(hours=3))).strftime("%Y-%m-%d"),)) cursor.execute("SELECT COUNT(*) FROM blacklist WHERE date_to_unban IS NOT NULL AND date_to_unban <= ?",
(current_timestamp,))
today_count = cursor.fetchone()[0] today_count = cursor.fetchone()[0]
assert today_count == 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 IS NOT NULL AND date_to_unban > ?",
cursor.execute("SELECT COUNT(*) FROM blacklist WHERE date_to_unban = ?", (tomorrow,)) (current_timestamp,))
tomorrow_count = cursor.fetchone()[0] tomorrow_count = cursor.fetchone()[0]
assert tomorrow_count == 1 assert tomorrow_count == 1
@@ -146,8 +146,8 @@ class TestAutoUnbanIntegration:
# Удаляем пользователей с сегодняшней датой # Удаляем пользователей с сегодняшней датой
conn = sqlite3.connect(setup_test_db) conn = sqlite3.connect(setup_test_db)
cursor = conn.cursor() cursor = conn.cursor()
today = datetime.now(timezone(timedelta(hours=3))).strftime("%Y-%m-%d") current_timestamp = int(datetime.now(timezone(timedelta(hours=3))).timestamp())
cursor.execute("DELETE FROM blacklist WHERE date_to_unban = ?", (today,)) cursor.execute("DELETE FROM blacklist WHERE date_to_unban IS NOT NULL AND date_to_unban <= ?", (current_timestamp,))
conn.commit() conn.commit()
conn.close() conn.close()
@@ -195,7 +195,7 @@ class TestAutoUnbanIntegration:
scheduler = AutoUnbanScheduler() scheduler = AutoUnbanScheduler()
scheduler.bot_db = mock_bdf.database scheduler.bot_db = mock_bdf.database
# Проверяем, что дата в базе соответствует ожидаемому формату # Проверяем, что дата в базе соответствует ожидаемому формату (timestamp)
conn = sqlite3.connect(setup_test_db) conn = sqlite3.connect(setup_test_db)
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("SELECT date_to_unban FROM blacklist WHERE date_to_unban IS NOT NULL LIMIT 1") 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() conn.close()
if result and result[0]: if result and result[0]:
date_str = result[0] timestamp = result[0]
# Проверяем формат YYYY-MM-DD # Проверяем, что это валидный timestamp (целое число)
assert len(date_str) == 10 assert isinstance(timestamp, int)
assert date_str.count('-') == 2 assert timestamp > 0
assert date_str[:4].isdigit() # Год # Проверяем, что timestamp можно преобразовать в дату
assert date_str[5:7].isdigit() # Месяц date_obj = datetime.fromtimestamp(timestamp)
assert date_str[8:10].isdigit() # День assert isinstance(date_obj, datetime)
class TestSchedulerLifecycle: class TestSchedulerLifecycle:

View File

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

View File

@@ -70,13 +70,14 @@ class TestKeyboards:
# Проверяем наличие кнопки стикеров # Проверяем наличие кнопки стикеров
assert '🤪Хочу стикеры' in all_buttons 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 user_id = 123456
# Мокаем метод get_info_about_stickers # Мокаем метод get_stickers_info
mock_db.get_info_about_stickers = Mock(return_value=True) 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 = [] all_buttons = []
for row in keyboard.keyboard: for row in keyboard.keyboard:
@@ -86,13 +87,14 @@ class TestKeyboards:
# Проверяем отсутствие кнопки стикеров # Проверяем отсутствие кнопки стикеров
assert '🤪Хочу стикеры' not in all_buttons 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 user_id = 123456
# Мокаем метод get_info_about_stickers # Мокаем метод get_stickers_info
mock_db.get_info_about_stickers = Mock(return_value=False) 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 = [] all_buttons = []
for row in keyboard.keyboard: for row in keyboard.keyboard:
@@ -284,44 +286,41 @@ class TestChatTypeFilter:
class TestKeyboardIntegration: 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 = Mock(spec=AsyncBotDB)
mock_db.get_info_about_stickers = Mock(return_value=False) mock_db.get_stickers_info = AsyncMock(return_value=False)
# Тестируем все типы клавиатур # Тестируем все типы клавиатур
keyboards = [ keyboard1 = await get_reply_keyboard(mock_db, 123456)
get_reply_keyboard(mock_db, 123456), keyboard2 = get_reply_keyboard_for_post()
get_reply_keyboard_for_post(), keyboard3 = get_reply_keyboard_leave_chat()
get_reply_keyboard_leave_chat()
]
# Проверяем первую клавиатуру (ReplyKeyboardMarkup) # Проверяем первую клавиатуру (ReplyKeyboardMarkup)
keyboard1 = keyboards[0]
assert isinstance(keyboard1, ReplyKeyboardMarkup) assert isinstance(keyboard1, ReplyKeyboardMarkup)
assert hasattr(keyboard1, 'keyboard') assert hasattr(keyboard1, 'keyboard')
assert isinstance(keyboard1.keyboard, list) assert isinstance(keyboard1.keyboard, list)
# Проверяем вторую клавиатуру (InlineKeyboardMarkup) # Проверяем вторую клавиатуру (InlineKeyboardMarkup)
keyboard2 = keyboards[1]
assert isinstance(keyboard2, InlineKeyboardMarkup) assert isinstance(keyboard2, InlineKeyboardMarkup)
assert hasattr(keyboard2, 'inline_keyboard') assert hasattr(keyboard2, 'inline_keyboard')
assert isinstance(keyboard2.inline_keyboard, list) assert isinstance(keyboard2.inline_keyboard, list)
# Проверяем третью клавиатуру (ReplyKeyboardMarkup) # Проверяем третью клавиатуру (ReplyKeyboardMarkup)
keyboard3 = keyboards[2]
assert isinstance(keyboard3, ReplyKeyboardMarkup) assert isinstance(keyboard3, ReplyKeyboardMarkup)
assert hasattr(keyboard3, 'keyboard') assert hasattr(keyboard3, 'keyboard')
assert isinstance(keyboard3.keyboard, list) 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 = 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() post_keyboard = get_reply_keyboard_for_post()
leave_keyboard = get_reply_keyboard_leave_chat() leave_keyboard = get_reply_keyboard_leave_chat()

View File

@@ -19,7 +19,8 @@ class TestAdminService:
self.mock_db = Mock() self.mock_db = Mock()
self.admin_service = AdminService(self.mock_db) 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 # Arrange
# Формат данных: кортежи (full_name, user_id) как возвращает БД # Формат данных: кортежи (full_name, user_id) как возвращает БД
@@ -27,10 +28,10 @@ class TestAdminService:
('User One', 1), # (full_name, user_id) ('User One', 1), # (full_name, user_id)
('User Two', 2) # (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 # Act
result = self.admin_service.get_last_users() result = await self.admin_service.get_last_users()
# Assert # Assert
assert len(result) == 2 assert len(result) == 2
@@ -41,17 +42,18 @@ class TestAdminService:
assert result[1].username == 'Неизвестно' # username не возвращается из БД assert result[1].username == 'Неизвестно' # username не возвращается из БД
assert result[1].full_name == 'User Two' 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""" """Тест успешного получения пользователя по username"""
# Arrange # Arrange
user_id = 123 user_id = 123
username = "test_user" username = "test_user"
full_name = "Test User" full_name = "Test User"
self.mock_db.get_user_id_by_username.return_value = user_id self.mock_db.get_user_id_by_username = AsyncMock(return_value=user_id)
self.mock_db.get_full_name_by_id.return_value = full_name self.mock_db.get_full_name_by_id = AsyncMock(return_value=full_name)
# Act # Act
result = self.admin_service.get_user_by_username(username) result = await self.admin_service.get_user_by_username(username)
# Assert # Assert
assert result is not None assert result is not None
@@ -59,27 +61,35 @@ class TestAdminService:
assert result.username == username assert result.username == username
assert result.full_name == full_name 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""" """Тест получения пользователя по несуществующему username"""
# Arrange # Arrange
username = "nonexistent_user" 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 # Act
result = self.admin_service.get_user_by_username(username) result = await self.admin_service.get_user_by_username(username)
# Assert # Assert
assert result is None 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""" """Тест успешного получения пользователя по ID"""
# Arrange # Arrange
user_id = 123 user_id = 123
user_info = {'username': 'test_user', 'full_name': 'Test User'} from database.models import User as DBUser
self.mock_db.get_user_info_by_id.return_value = user_info 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 # Act
result = self.admin_service.get_user_by_id(user_id) result = await self.admin_service.get_user_by_id(user_id)
# Assert # Assert
assert result is not None assert result is not None
@@ -87,45 +97,51 @@ class TestAdminService:
assert result.username == 'test_user' assert result.username == 'test_user'
assert result.full_name == '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""" """Тест получения пользователя по несуществующему ID"""
# Arrange # Arrange
user_id = 999 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 # Act
result = self.admin_service.get_user_by_id(user_id) result = await self.admin_service.get_user_by_id(user_id)
# Assert # Assert
assert result is None assert result is None
def test_validate_user_input_success(self): @pytest.mark.asyncio
async def test_validate_user_input_success(self):
"""Тест успешной валидации ID пользователя""" """Тест успешной валидации ID пользователя"""
# Act # Act
result = self.admin_service.validate_user_input("123") result = await self.admin_service.validate_user_input("123")
# Assert # Assert
assert result == 123 assert result == 123
def test_validate_user_input_invalid_number(self): @pytest.mark.asyncio
async def test_validate_user_input_invalid_number(self):
"""Тест валидации некорректного ID""" """Тест валидации некорректного ID"""
# Act & Assert # Act & Assert
with pytest.raises(InvalidInputError, match="ID пользователя должен быть числом"): 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""" """Тест валидации отрицательного ID"""
# Act & Assert # Act & Assert
with pytest.raises(InvalidInputError, match="ID пользователя должен быть положительным числом"): 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""" """Тест валидации нулевого ID"""
# Act & Assert # Act & Assert
with pytest.raises(InvalidInputError, match="ID пользователя должен быть положительным числом"): 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 # Arrange
user_id = 123 user_id = 123
@@ -133,17 +149,18 @@ class TestAdminService:
reason = "Test ban" reason = "Test ban"
ban_days = 7 ban_days = 7
self.mock_db.check_user_in_blacklist.return_value = False self.mock_db.check_user_in_blacklist = AsyncMock(return_value=False)
self.mock_db.set_user_blacklist.return_value = None self.mock_db.set_user_blacklist = AsyncMock(return_value=None)
# Act # 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 # Assert
self.mock_db.check_user_in_blacklist.assert_called_once_with(user_id) self.mock_db.check_user_in_blacklist.assert_called_once_with(user_id)
self.mock_db.set_user_blacklist.assert_called_once() 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 # Arrange
user_id = 123 user_id = 123
@@ -151,13 +168,14 @@ class TestAdminService:
reason = "Test ban" reason = "Test ban"
ban_days = 7 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 # Act & Assert
with pytest.raises(UserAlreadyBannedError, match=f"Пользователь {user_id} уже заблокирован"): 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 # Arrange
user_id = 123 user_id = 123
@@ -165,23 +183,24 @@ class TestAdminService:
reason = "Permanent ban" reason = "Permanent ban"
ban_days = None ban_days = None
self.mock_db.check_user_in_blacklist.return_value = False self.mock_db.check_user_in_blacklist = AsyncMock(return_value=False)
self.mock_db.set_user_blacklist.return_value = None self.mock_db.set_user_blacklist = AsyncMock(return_value=None)
# Act # 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 # 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 # Arrange
user_id = 123 user_id = 123
self.mock_db.delete_user_blacklist.return_value = None self.mock_db.delete_user_blacklist = AsyncMock(return_value=None)
# Act # Act
self.admin_service.unban_user(user_id) await self.admin_service.unban_user(user_id)
# Assert # Assert
self.mock_db.delete_user_blacklist.assert_called_once_with(user_id) 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.admin_reply_service is not None
assert handlers.router 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): async def test_handle_message_success(self, mock_db, mock_keyboard_markup, mock_reply_message, mock_state):
"""Test successful message handling""" """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) handlers = create_group_handlers(mock_db, mock_keyboard_markup)
@@ -97,6 +98,7 @@ class TestGroupHandlers:
# Verify state was set # Verify state was set
mock_state.set_state.assert_called_once_with(FSM_STATES["CHAT"]) 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): async def test_handle_message_no_reply(self, mock_db, mock_keyboard_markup, mock_message, mock_state):
"""Test message handling without reply""" """Test message handling without reply"""
handlers = create_group_handlers(mock_db, mock_keyboard_markup) handlers = create_group_handlers(mock_db, mock_keyboard_markup)
@@ -121,9 +123,10 @@ class TestGroupHandlers:
# Verify state was not set # Verify state was not set
mock_state.set_state.assert_not_called() 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): 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""" """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) handlers = create_group_handlers(mock_db, mock_keyboard_markup)
@@ -154,24 +157,27 @@ class TestAdminReplyService:
"""Create service instance""" """Create service instance"""
return AdminReplyService(mock_db) 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""" """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 assert result == 12345
mock_db.get_user_by_message_id.assert_called_once_with(111) 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""" """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"): 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) 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): async def test_send_reply_to_user(self, service, mock_db):
"""Test sending reply to user""" """Test sending reply to user"""
message = Mock() message = Mock()

View File

@@ -19,13 +19,13 @@ class TestPrivateHandlers:
def mock_db(self): def mock_db(self):
"""Mock database""" """Mock database"""
db = Mock() db = Mock()
db.user_exists.return_value = False db.user_exists = AsyncMock(return_value=False)
db.add_new_user_in_db = Mock() db.add_user = AsyncMock()
db.update_date_for_user = Mock() db.update_user_date = AsyncMock()
db.update_info_about_stickers = Mock() db.update_stickers_info = AsyncMock()
db.add_post_in_db = Mock() db.add_post = AsyncMock()
db.add_new_message_in_db = Mock() db.add_message = AsyncMock()
db.update_helper_message_in_db = Mock() db.update_helper_message = AsyncMock()
return db return db
@pytest.fixture @pytest.fixture
@@ -101,7 +101,8 @@ class TestPrivateHandlers:
# Mock the check_user_emoji function # Mock the check_user_emoji function
with pytest.MonkeyPatch().context() as m: 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 # Test the handler
await handlers.handle_emoji_message(mock_message, mock_state) await handlers.handle_emoji_message(mock_message, mock_state)
@@ -121,7 +122,8 @@ class TestPrivateHandlers:
with pytest.MonkeyPatch().context() as m: 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.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.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 # Test the handler
await handlers.handle_start_message(mock_message, mock_state) 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"]) mock_state.set_state.assert_called_once_with(FSM_STATES["START"])
# Verify user was ensured to exist # Verify user was ensured to exist
mock_db.add_new_user_in_db.assert_called_once() mock_db.add_user.assert_called_once()
mock_db.update_date_for_user.assert_called_once() mock_db.update_user_date.assert_called_once()
class TestBotSettings: 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.messages import get_message
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 database.db import BotDB from database.async_db import AsyncBotDB
import helper_bot.utils.messages as messages # Import for patching constants import helper_bot.utils.messages as messages # Import for patching constants
class TestHelperFunctions: class TestHelperFunctions:
@@ -83,25 +83,27 @@ class TestHelperFunctions:
assert "testuser" in result assert "testuser" in result
assert "Обычный текст без специальных слов" 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""" """Тест функции проверки изменений username и full_name"""
# Создаем мок базы данных # Создаем мок базы данных
mock_db = Mock(spec=BotDB) mock_db = Mock(spec=AsyncBotDB)
mock_db.get_username_and_full_name = Mock(return_value=("olduser", "Old User")) 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 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 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 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 assert result is True
@@ -330,7 +332,7 @@ class TestPrepareMediaGroup:
assert result[0].media == "photo_0" assert result[0].media == "photo_0"
assert result[1].media == "photo_1" assert result[1].media == "photo_1"
assert result[2].media == "photo_2" assert result[2].media == "photo_2"
assert result[2].caption == "Тестовая подпись" assert result[0].caption == "Тестовая подпись" # Первое фото должно иметь caption
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_prepare_media_group_mixed_types(self): async def test_prepare_media_group_mixed_types(self):
@@ -364,7 +366,7 @@ class TestPrepareMediaGroup:
assert result[0].media == "photo_1" assert result[0].media == "photo_1"
assert result[1].media == "video_1" assert result[1].media == "video_1"
assert result[2].media == "audio_1" assert result[2].media == "audio_1"
assert result[2].caption == "Смешанная группа" assert result[0].caption == "Смешанная группа" # Первое медиа должно иметь caption
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_prepare_media_group_empty_album(self): async def test_prepare_media_group_empty_album(self):
@@ -381,6 +383,7 @@ class TestPrepareMediaGroup:
message.photo = None message.photo = None
message.video = None message.video = None
message.audio = None message.audio = None
message.document = None # Добавляем document = None
album.append(message) album.append(message)
result = await prepare_media_group_from_middlewares(album, "Тест") result = await prepare_media_group_from_middlewares(album, "Тест")
@@ -401,12 +404,12 @@ class TestMediaDatabaseOperations:
message.photo[-1].file_id = f"photo_{i}" message.photo[-1].file_id = f"photo_{i}"
sent_message.append(message) 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"): 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) 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 @pytest.mark.asyncio
async def test_add_in_db_media_photo(self): async def test_add_in_db_media_photo(self):
@@ -416,12 +419,12 @@ class TestMediaDatabaseOperations:
mock_message.photo = [Mock()] mock_message.photo = [Mock()]
mock_message.photo[-1].file_id = "photo_123" 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"): with patch('helper_bot.utils.helper_func.download_file', return_value="files/photo_123.jpg"):
await add_in_db_media(mock_message, mock_db) 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' 123, 123, "files/photo_123.jpg", 'photo'
) )
@@ -434,12 +437,12 @@ class TestMediaDatabaseOperations:
mock_message.video = Mock() mock_message.video = Mock()
mock_message.video.file_id = "video_123" 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"): with patch('helper_bot.utils.helper_func.download_file', return_value="files/video_123.mp4"):
await add_in_db_media(mock_message, mock_db) 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' 123, 123, "files/video_123.mp4", 'video'
) )
@@ -453,12 +456,12 @@ class TestMediaDatabaseOperations:
mock_message.voice = Mock() mock_message.voice = Mock()
mock_message.voice.file_id = "voice_123" 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"): with patch('helper_bot.utils.helper_func.download_file', return_value="files/voice_123.ogg"):
await add_in_db_media(mock_message, mock_db) 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' 123, 123, "files/voice_123.ogg", 'voice'
) )
@@ -548,16 +551,17 @@ class TestSendMessageFunctions:
class TestUtilityFunctions: 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 mock_db.is_admin.return_value = True
result = check_access(123, mock_db) result = await check_access(123, mock_db)
assert result is True assert result is True
mock_db.is_admin.return_value = False mock_db.is_admin.return_value = False
result = check_access(123, mock_db) result = await check_access(123, mock_db)
assert result is False assert result is False
def test_add_days_to_date(self): def test_add_days_to_date(self):
@@ -569,45 +573,51 @@ class TestUtilityFunctions:
mock_datetime.timedelta = timedelta mock_datetime.timedelta = timedelta
result = add_days_to_date(5) result = add_days_to_date(5)
expected_date = (mock_now + timedelta(days=5)).strftime("%d-%m-%Y") expected_timestamp = int((mock_now + timedelta(days=5)).timestamp())
assert result == expected_date 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 = [ mock_db.get_banned_users_from_db_with_limits.return_value = [
("User1", 123, "Spam", "01-01-2025"), (123, "Spam", 1704067200), # user_id, ban_reason, unban_date (timestamp)
("User2", 456, "Violation", "02-01-2025") (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 "Список заблокированных пользователей:" in result
assert "User1" in result assert "Test User" in result
assert "User2" in result
assert "Spam" in result assert "Spam" in result
assert "Violation" 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 = [ mock_db.get_banned_users_from_db.return_value = [
("User1", 123), (123, "Spam", 1704067200), # user_id, ban_reason, unban_date
("User2", 456) (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 len(result) == 2
assert result[0] == ("User1", 123) assert result[0] == ("Test User", 123)
assert result[1] == ("User2", 456) 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 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 assert result is True
mock_db.delete_user_blacklist.assert_called_once_with(user_id=123) 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_first_name', return_value="Test"):
with patch('helper_bot.utils.helper_func.get_random_emoji', return_value="😀"): with patch('helper_bot.utils.helper_func.get_random_emoji', return_value="😀"):
with patch('helper_bot.utils.helper_func.BotDB') as mock_bot_db: with patch('helper_bot.utils.helper_func.BotDB') as mock_bot_db:
mock_bot_db.user_exists.return_value = False mock_bot_db.user_exists = AsyncMock(return_value=False)
mock_bot_db.add_new_user_in_db = Mock() mock_bot_db.add_user = AsyncMock()
mock_bot_db.update_date_for_user = Mock() mock_bot_db.update_user_date = AsyncMock()
await update_user_info("test", mock_message) await update_user_info("test", mock_message)
mock_bot_db.add_new_user_in_db.assert_called_once() mock_bot_db.add_user.assert_called_once()
mock_bot_db.update_date_for_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 = Mock()
mock_message.from_user.id = 123 mock_message.from_user.id = 123
with patch('helper_bot.utils.helper_func.BotDB') as mock_bot_db: 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 == "😀" 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 = Mock()
mock_message.from_user.id = 123 mock_message.from_user.id = 123
with patch('helper_bot.utils.helper_func.BotDB') as mock_bot_db: 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.get_user_emoji = AsyncMock(return_value=None)
mock_bot_db.update_emoji_for_user = Mock() mock_bot_db.update_user_emoji = AsyncMock()
with patch('helper_bot.utils.helper_func.get_random_emoji', return_value="😀"): 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 == "😀" 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: 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="😀"): with patch('helper_bot.utils.helper_func.random.choice', return_value="😀"):
result = get_random_emoji() result = await get_random_emoji()
assert result == "😀" assert result == "😀"
def test_get_random_emoji_fallback(self): @pytest.mark.asyncio
async def test_get_random_emoji_fallback(self):
"""Тест получения случайного эмодзи (fallback)""" """Тест получения случайного эмодзи (fallback)"""
with patch('helper_bot.utils.helper_func.BotDB') as mock_bot_db: 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.random.choice', return_value="😀"):
with patch('helper_bot.utils.helper_func.logger') as mock_logger: with patch('helper_bot.utils.helper_func.logger') as mock_logger:
result = get_random_emoji() result = await get_random_emoji()
assert result == "Эмоджи не определен" assert result == "Эмоджи не определен"
mock_logger.error.assert_called_once() mock_logger.error.assert_called_once()

View File

@@ -55,14 +55,15 @@ class TestVoiceBotService:
assert sticker is None 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.check_listen_audio = AsyncMock(return_value=['audio1', 'audio2'])
mock_bot_db.get_user_id_by_file_name.return_value = 123 mock_bot_db.get_user_id_by_file_name = AsyncMock(return_value=123)
mock_bot_db.get_date_by_file_name.return_value = '2025-01-01 12:00:00' mock_bot_db.get_date_by_file_name = AsyncMock(return_value='2025-01-01 12:00:00')
mock_bot_db.check_emoji_for_user.return_value = '😊' 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 result is not None
assert len(result) == 3 assert len(result) == 3
@@ -70,40 +71,49 @@ class TestVoiceBotService:
assert result[1] == '2025-01-01 12:00:00' assert result[1] == '2025-01-01 12:00:00'
assert result[2] == '😊' 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 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) 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) 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 assert result == 3
mock_bot_db.check_listen_audio.assert_called_once_with(user_id=123) 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 assert result == 0
mock_bot_db.check_listen_audio.assert_called_once_with(user_id=123) mock_bot_db.check_listen_audio.assert_called_once_with(user_id=123)
@@ -187,57 +197,64 @@ class TestUtils:
"""Мок для базы данных""" """Мок для базы данных"""
return Mock() 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 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() 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 = Mock()
mock_message.content_type = 'voice' mock_message.content_type = 'voice'
mock_message.voice = Mock() mock_message.voice = Mock()
result = validate_voice_message(mock_message) result = await validate_voice_message(mock_message)
assert result is True 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 = Mock()
mock_message.voice = None mock_message.voice = None
result = validate_voice_message(mock_message) result = await validate_voice_message(mock_message)
assert result is False 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 == "😊" 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 == "😊" 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 == "Ошибка" assert result == "Ошибка"

View File

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

View File

@@ -63,18 +63,23 @@ class TestVoiceHandler:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_voice_bot_button_handler_welcome_received(self, voice_handler, mock_message, mock_state, mock_db, mock_settings): 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: 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 @pytest.mark.asyncio
async def test_voice_bot_button_handler_welcome_not_received(self, voice_handler, mock_message, mock_state, mock_db, mock_settings): 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: with patch.object(voice_handler, 'start') as mock_start:
await voice_handler.voice_bot_button_handler(mock_message, mock_state, mock_db, mock_settings) 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 assert sticker is not None
# Проверяем, что стикер не None (метод возвращает FSInputFile объект) # Проверяем, что стикер не 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.check_listen_audio = AsyncMock(return_value=['audio1', 'audio2'])
mock_bot_db.get_user_id_by_file_name.return_value = 123 mock_bot_db.get_user_id_by_file_name = AsyncMock(return_value=123)
mock_bot_db.get_date_by_file_name.return_value = '2025-01-01 12:00:00' mock_bot_db.get_date_by_file_name = AsyncMock(return_value='2025-01-01 12:00:00')
mock_bot_db.check_emoji_for_user.return_value = '😊' 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 result is not None
assert len(result) == 3 assert len(result) == 3
@@ -96,53 +97,63 @@ class TestVoiceBotService:
assert result[1] == '2025-01-01 12:00:00' assert result[1] == '2025-01-01 12:00:00'
assert result[2] == '😊' 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 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.check_listen_audio = AsyncMock(return_value=['audio1'])
mock_bot_db.get_user_id_by_file_name.return_value = 123 mock_bot_db.get_user_id_by_file_name = AsyncMock(return_value=123)
mock_bot_db.get_date_by_file_name.return_value = '2025-01-01 12:00:00' mock_bot_db.get_date_by_file_name = AsyncMock(return_value='2025-01-01 12:00:00')
mock_bot_db.check_emoji_for_user.return_value = '😊' 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 result is not None
assert len(result) == 3 assert len(result) == 3
assert result[0] == 'audio1' 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) 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) 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 assert result == 3
mock_bot_db.check_listen_audio.assert_called_once_with(user_id=123) 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 assert result == 0
mock_bot_db.check_listen_audio.assert_called_once_with(user_id=123) 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 message.chat.id = 456
return message 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 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() 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 = Mock()
mock_message.content_type = 'voice' mock_message.content_type = 'voice'
mock_message.voice = Mock() mock_message.voice = Mock()
result = validate_voice_message(mock_message) result = await validate_voice_message(mock_message)
assert result is True 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 = Mock()
mock_message.voice = None mock_message.voice = None
result = validate_voice_message(mock_message) result = await validate_voice_message(mock_message)
assert result is False 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 == "😊" 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 == "😊" 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 == "😊" 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 == "Ошибка" 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): def test_format_time_ago_minutes(self):
"""Тест форматирования времени в минутах""" """Тест форматирования времени в минутах"""