Dev 8 #10
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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")
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,10 +335,14 @@ class TestAudioRepositoryEdgeCases:
|
||||
listen_count=0
|
||||
)
|
||||
|
||||
# Должно вызвать TypeError при попытке преобразования None
|
||||
with pytest.raises(TypeError):
|
||||
# Метод обрабатывает 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,10 +360,14 @@ class TestAudioRepositoryEdgeCases:
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_audio_record_simple_none_date(self, audio_repository):
|
||||
"""Тест упрощенного добавления с None датой"""
|
||||
# Должно вызвать TypeError при попытке преобразования None
|
||||
with pytest.raises(TypeError):
|
||||
# Метод обрабатывает 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):
|
||||
"""Тест получения даты для timestamp = 0 (1970-01-01)"""
|
||||
@@ -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):
|
||||
|
||||
@@ -32,18 +32,18 @@ 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), # Разблокируется завтра
|
||||
(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), # Навсегда заблокирован
|
||||
]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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 == "Ошибка"
|
||||
|
||||
|
||||
@@ -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__':
|
||||
|
||||
@@ -63,9 +63,13 @@ 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:
|
||||
with patch('helper_bot.handlers.voice.voice_handler.update_user_info') as mock_update_user:
|
||||
mock_update_user.return_value = None
|
||||
|
||||
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)
|
||||
@@ -74,7 +78,8 @@ class TestVoiceHandler:
|
||||
@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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
"""Тест форматирования времени в минутах"""
|
||||
|
||||
Reference in New Issue
Block a user