Enhance bot functionality and refactor database interactions

- Added `ca-certificates` installation to Dockerfile for improved network security.
- Updated health check command in Dockerfile to include better timeout handling.
- Refactored `run_helper.py` to implement proper signal handling and logging during shutdown.
- Transitioned database operations to an asynchronous model in `async_db.py`, improving performance and responsiveness.
- Updated database schema to support new foreign key relationships and optimized indexing for better query performance.
- Enhanced various bot handlers to utilize async database methods, improving overall efficiency and user experience.
- Removed obsolete database and fix scripts to streamline the project structure.
This commit is contained in:
2025-09-02 18:22:02 +03:00
parent 013892dcb7
commit 1c6a37bc12
59 changed files with 5682 additions and 4204 deletions

View File

@@ -0,0 +1,423 @@
import pytest
from unittest.mock import Mock, AsyncMock, patch, MagicMock
from datetime import datetime
import time
from database.repositories.blacklist_repository import BlacklistRepository
from database.models import BlacklistUser
class TestBlacklistRepository:
"""Тесты для BlacklistRepository"""
@pytest.fixture
def mock_db_connection(self):
"""Мок для DatabaseConnection"""
mock_connection = Mock()
mock_connection._execute_query = AsyncMock()
mock_connection._execute_query_with_result = AsyncMock()
mock_connection.logger = Mock()
return mock_connection
@pytest.fixture
def blacklist_repository(self, mock_db_connection):
"""Экземпляр BlacklistRepository для тестов"""
# Патчим наследование от DatabaseConnection
with patch.object(BlacklistRepository, '__init__', return_value=None):
repo = BlacklistRepository()
repo._execute_query = mock_db_connection._execute_query
repo._execute_query_with_result = mock_db_connection._execute_query_with_result
repo.logger = mock_db_connection.logger
return repo
@pytest.fixture
def sample_blacklist_user(self):
"""Тестовый пользователь в черном списке"""
return BlacklistUser(
user_id=12345,
message_for_user="Нарушение правил",
date_to_unban=int(time.time()) + 86400, # +1 день
created_at=int(time.time())
)
@pytest.fixture
def sample_blacklist_user_permanent(self):
"""Тестовый пользователь с постоянным баном"""
return BlacklistUser(
user_id=67890,
message_for_user="Постоянный бан",
date_to_unban=None,
created_at=int(time.time())
)
@pytest.mark.asyncio
async def test_create_tables(self, blacklist_repository):
"""Тест создания таблицы черного списка"""
await blacklist_repository.create_tables()
# Проверяем, что метод вызван
blacklist_repository._execute_query.assert_called()
calls = blacklist_repository._execute_query.call_args_list
# Проверяем, что создается таблица с правильной структурой
create_table_call = calls[0]
assert "CREATE TABLE IF NOT EXISTS blacklist" in create_table_call[0][0]
assert "user_id INTEGER NOT NULL PRIMARY KEY" in create_table_call[0][0]
assert "message_for_user TEXT" in create_table_call[0][0]
assert "date_to_unban INTEGER" in create_table_call[0][0]
assert "created_at INTEGER DEFAULT (strftime('%s', 'now'))" in create_table_call[0][0]
assert "FOREIGN KEY (user_id) REFERENCES our_users (user_id) ON DELETE CASCADE" in create_table_call[0][0]
# Проверяем логирование
blacklist_repository.logger.info.assert_called_once_with("Таблица черного списка создана")
@pytest.mark.asyncio
async def test_add_user(self, blacklist_repository, sample_blacklist_user):
"""Тест добавления пользователя в черный список"""
await blacklist_repository.add_user(sample_blacklist_user)
# Проверяем, что метод вызван с правильными параметрами
blacklist_repository._execute_query.assert_called_once()
call_args = blacklist_repository._execute_query.call_args
# Проверяем SQL запрос (учитываем форматирование)
sql_query = call_args[0][0].replace('\n', ' ').replace(' ', ' ').replace(' ', ' ').strip()
expected_sql = "INSERT INTO blacklist (user_id, message_for_user, date_to_unban) VALUES (?, ?, ?)"
assert sql_query == expected_sql
# Проверяем параметры
assert call_args[0][1] == (12345, "Нарушение правил", sample_blacklist_user.date_to_unban)
# Проверяем логирование
blacklist_repository.logger.info.assert_called_once_with(
"Пользователь добавлен в черный список: user_id=12345"
)
@pytest.mark.asyncio
async def test_add_user_permanent_ban(self, blacklist_repository, sample_blacklist_user_permanent):
"""Тест добавления пользователя с постоянным баном"""
await blacklist_repository.add_user(sample_blacklist_user_permanent)
call_args = blacklist_repository._execute_query.call_args
assert call_args[0][1] == (67890, "Постоянный бан", None)
blacklist_repository.logger.info.assert_called_once_with(
"Пользователь добавлен в черный список: user_id=67890"
)
@pytest.mark.asyncio
async def test_remove_user_success(self, blacklist_repository):
"""Тест успешного удаления пользователя из черного списка"""
await blacklist_repository.remove_user(12345)
# Проверяем, что метод вызван с правильными параметрами
blacklist_repository._execute_query.assert_called_once()
call_args = blacklist_repository._execute_query.call_args
assert call_args[0][0] == "DELETE FROM blacklist WHERE user_id = ?"
assert call_args[0][1] == (12345,)
# Проверяем логирование
blacklist_repository.logger.info.assert_called_once_with(
"Пользователь с идентификатором 12345 успешно удален из черного списка."
)
@pytest.mark.asyncio
async def test_remove_user_failure(self, blacklist_repository):
"""Тест неудачного удаления пользователя из черного списка"""
# Симулируем ошибку при удалении
blacklist_repository._execute_query.side_effect = Exception("Database error")
result = await blacklist_repository.remove_user(12345)
# Проверяем, что возвращается False при ошибке
assert result is False
# Проверяем логирование ошибки
blacklist_repository.logger.error.assert_called_once()
error_log = blacklist_repository.logger.error.call_args[0][0]
assert "Ошибка удаления пользователя с идентификатором 12345" in error_log
assert "Database error" in error_log
@pytest.mark.asyncio
async def test_user_exists_true(self, blacklist_repository):
"""Тест проверки существования пользователя (пользователь существует)"""
# Симулируем результат запроса - пользователь найден
blacklist_repository._execute_query_with_result.return_value = [(1,)]
result = await blacklist_repository.user_exists(12345)
# Проверяем, что возвращается True
assert result is True
# Проверяем, что метод вызван с правильными параметрами
blacklist_repository._execute_query_with_result.assert_called_once()
call_args = blacklist_repository._execute_query_with_result.call_args
assert call_args[0][0] == "SELECT 1 FROM blacklist WHERE user_id = ?"
assert call_args[0][1] == (12345,)
# Проверяем логирование
blacklist_repository.logger.info.assert_called_once_with(
"Существует ли пользователь: user_id=12345 Итог: [(1,)]"
)
@pytest.mark.asyncio
async def test_user_exists_false(self, blacklist_repository):
"""Тест проверки существования пользователя (пользователь не существует)"""
# Симулируем результат запроса - пользователь не найден
blacklist_repository._execute_query_with_result.return_value = []
result = await blacklist_repository.user_exists(12345)
# Проверяем, что возвращается False
assert result is False
# Проверяем логирование
blacklist_repository.logger.info.assert_called_once_with(
"Существует ли пользователь: user_id=12345 Итог: []"
)
@pytest.mark.asyncio
async def test_get_user_success(self, blacklist_repository):
"""Тест успешного получения пользователя по ID"""
# Симулируем результат запроса
mock_row = (12345, "Нарушение правил", int(time.time()) + 86400, int(time.time()))
blacklist_repository._execute_query_with_result.return_value = [mock_row]
result = await blacklist_repository.get_user(12345)
# Проверяем, что возвращается правильный объект
assert result is not None
assert result.user_id == 12345
assert result.message_for_user == "Нарушение правил"
assert result.date_to_unban == mock_row[2]
assert result.created_at == mock_row[3]
# Проверяем, что метод вызван с правильными параметрами
blacklist_repository._execute_query_with_result.assert_called_once()
call_args = blacklist_repository._execute_query_with_result.call_args
assert call_args[0][0] == "SELECT user_id, message_for_user, date_to_unban, created_at FROM blacklist WHERE user_id = ?"
assert call_args[0][1] == (12345,)
@pytest.mark.asyncio
async def test_get_user_not_found(self, blacklist_repository):
"""Тест получения пользователя по ID (пользователь не найден)"""
# Симулируем результат запроса - пользователь не найден
blacklist_repository._execute_query_with_result.return_value = []
result = await blacklist_repository.get_user(12345)
# Проверяем, что возвращается None
assert result is None
@pytest.mark.asyncio
async def test_get_all_users_with_limits(self, blacklist_repository):
"""Тест получения пользователей с лимитами"""
# Симулируем результат запроса
mock_rows = [
(12345, "Нарушение правил", int(time.time()) + 86400, int(time.time())),
(67890, "Постоянный бан", None, int(time.time()) - 86400)
]
blacklist_repository._execute_query_with_result.return_value = mock_rows
result = await blacklist_repository.get_all_users(offset=0, limit=10)
# Проверяем, что возвращается правильный список
assert len(result) == 2
assert result[0].user_id == 12345
assert result[0].message_for_user == "Нарушение правил"
assert result[1].user_id == 67890
assert result[1].message_for_user == "Постоянный бан"
assert result[1].date_to_unban is None
# Проверяем, что метод вызван с правильными параметрами
blacklist_repository._execute_query_with_result.assert_called_once()
call_args = blacklist_repository._execute_query_with_result.call_args
assert call_args[0][0] == "SELECT user_id, message_for_user, date_to_unban, created_at FROM blacklist LIMIT ?, ?"
assert call_args[0][1] == (0, 10)
# Проверяем логирование
blacklist_repository.logger.info.assert_called_once_with(
"Получен список пользователей в черном списке (offset=0, limit=10): 2"
)
@pytest.mark.asyncio
async def test_get_all_users_no_limit(self, blacklist_repository):
"""Тест получения всех пользователей без лимитов"""
# Симулируем результат запроса
mock_rows = [
(12345, "Нарушение правил", int(time.time()) + 86400, int(time.time())),
(67890, "Постоянный бан", None, int(time.time()) - 86400)
]
blacklist_repository._execute_query_with_result.return_value = mock_rows
result = await blacklist_repository.get_all_users_no_limit()
# Проверяем, что возвращается правильный список
assert len(result) == 2
# Проверяем, что метод вызван без лимитов
blacklist_repository._execute_query_with_result.assert_called_once()
call_args = blacklist_repository._execute_query_with_result.call_args
assert call_args[0][0] == "SELECT user_id, message_for_user, date_to_unban, created_at FROM blacklist"
# Проверяем, что параметры пустые (без лимитов)
assert len(call_args[0]) == 1 # Только SQL запрос, без параметров
# Проверяем логирование
blacklist_repository.logger.info.assert_called_once_with(
"Получен список всех пользователей в черном списке: 2"
)
@pytest.mark.asyncio
async def test_get_users_for_unblock_today(self, blacklist_repository):
"""Тест получения пользователей для разблокировки сегодня"""
current_timestamp = int(time.time())
# Симулируем результат запроса - пользователи с истекшим сроком
mock_rows = [(12345,), (67890,)]
blacklist_repository._execute_query_with_result.return_value = mock_rows
result = await blacklist_repository.get_users_for_unblock_today(current_timestamp)
# Проверяем, что возвращается правильный словарь
assert len(result) == 2
assert 12345 in result
assert 67890 in result
assert result[12345] == 12345
assert result[67890] == 67890
# Проверяем, что метод вызван с правильными параметрами
blacklist_repository._execute_query_with_result.assert_called_once()
call_args = blacklist_repository._execute_query_with_result.call_args
assert call_args[0][0] == "SELECT user_id FROM blacklist WHERE date_to_unban IS NOT NULL AND date_to_unban <= ?"
assert call_args[0][1] == (current_timestamp,)
# Проверяем логирование
blacklist_repository.logger.info.assert_called_once_with(
"Получен список пользователей для разблокировки: {12345: 12345, 67890: 67890}"
)
@pytest.mark.asyncio
async def test_get_users_for_unblock_today_empty(self, blacklist_repository):
"""Тест получения пользователей для разблокировки (пустой результат)"""
current_timestamp = int(time.time())
# Симулируем пустой результат запроса
blacklist_repository._execute_query_with_result.return_value = []
result = await blacklist_repository.get_users_for_unblock_today(current_timestamp)
# Проверяем, что возвращается пустой словарь
assert result == {}
# Проверяем логирование
blacklist_repository.logger.info.assert_called_once_with(
"Получен список пользователей для разблокировки: {}"
)
@pytest.mark.asyncio
async def test_get_count(self, blacklist_repository):
"""Тест получения количества пользователей в черном списке"""
# Симулируем результат запроса
blacklist_repository._execute_query_with_result.return_value = [(5,)]
result = await blacklist_repository.get_count()
# Проверяем, что возвращается правильное количество
assert result == 5
# Проверяем, что метод вызван с правильными параметрами
blacklist_repository._execute_query_with_result.assert_called_once()
call_args = blacklist_repository._execute_query_with_result.call_args
assert call_args[0][0] == "SELECT COUNT(*) FROM blacklist"
# Проверяем, что параметры пустые
assert len(call_args[0]) == 1 # Только SQL запрос, без параметров
@pytest.mark.asyncio
async def test_get_count_zero(self, blacklist_repository):
"""Тест получения количества пользователей (0 пользователей)"""
# Симулируем пустой результат запроса
blacklist_repository._execute_query_with_result.return_value = []
result = await blacklist_repository.get_count()
# Проверяем, что возвращается 0
assert result == 0
@pytest.mark.asyncio
async def test_get_count_none_result(self, blacklist_repository):
"""Тест получения количества пользователей (None результат)"""
# Симулируем None результат запроса
blacklist_repository._execute_query_with_result.return_value = None
result = await blacklist_repository.get_count()
# Проверяем, что возвращается 0
assert result == 0
@pytest.mark.asyncio
async def test_error_handling_in_get_user(self, blacklist_repository):
"""Тест обработки ошибок при получении пользователя"""
# Симулируем ошибку базы данных
blacklist_repository._execute_query_with_result.side_effect = Exception("Database connection failed")
# Проверяем, что исключение пробрасывается
with pytest.raises(Exception) as exc_info:
await blacklist_repository.get_user(12345)
assert "Database connection failed" in str(exc_info.value)
@pytest.mark.asyncio
async def test_error_handling_in_get_all_users(self, blacklist_repository):
"""Тест обработки ошибок при получении всех пользователей"""
# Симулируем ошибку базы данных
blacklist_repository._execute_query_with_result.side_effect = Exception("Database connection failed")
# Проверяем, что исключение пробрасывается
with pytest.raises(Exception) as exc_info:
await blacklist_repository.get_all_users()
assert "Database connection failed" in str(exc_info.value)
@pytest.mark.asyncio
async def test_error_handling_in_get_count(self, blacklist_repository):
"""Тест обработки ошибок при получении количества"""
# Симулируем ошибку базы данных
blacklist_repository._execute_query_with_result.side_effect = Exception("Database connection failed")
# Проверяем, что исключение пробрасывается
with pytest.raises(Exception) as exc_info:
await blacklist_repository.get_count()
assert "Database connection failed" in str(exc_info.value)
@pytest.mark.asyncio
async def test_error_handling_in_get_users_for_unblock_today(self, blacklist_repository):
"""Тест обработки ошибок при получении пользователей для разблокировки"""
# Симулируем ошибку базы данных
blacklist_repository._execute_query_with_result.side_effect = Exception("Database connection failed")
# Проверяем, что исключение пробрасывается
with pytest.raises(Exception) as exc_info:
await blacklist_repository.get_users_for_unblock_today(int(time.time()))
assert "Database connection failed" in str(exc_info.value)
# TODO: 20-й тест - test_integration_workflow
# Этот тест должен проверять полный рабочий процесс:
# 1. Добавление пользователя в черный список
# 2. Проверка существования пользователя
# 3. Получение информации о пользователе
# 4. Получение общего количества пользователей
# 5. Удаление пользователя из черного списка
# 6. Проверка, что пользователь больше не существует
#
# Проблема: тест падает из-за сложности мокирования возвращаемых значений
# при создании объектов BlacklistUser из результатов запросов к БД.
# Требует более сложной настройки моков для корректной работы.