This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
- **Упрощение**: Заменена `extract_and_validate_token` на `authenticate_request`
|
||||
|
||||
#### 🏗️ Архитектурные улучшения
|
||||
- Используем redis connection pool
|
||||
- **Библиотечная цель**: Добавлена `lib.rs` для тестирования модулей
|
||||
- **Модульность**: Четкое разделение ответственности между модулями
|
||||
- **Единообразие**: Все handlers теперь используют одинаковую логику аутентификации
|
||||
|
||||
311
src/app_state.rs
311
src/app_state.rs
@@ -6,11 +6,139 @@ use aws_config::BehaviorVersion;
|
||||
use aws_sdk_s3::{Client as S3Client, config::Credentials};
|
||||
use log::warn;
|
||||
use redis::{AsyncCommands, Client as RedisClient, aio::MultiplexedConnection};
|
||||
use std::{env, time::Duration};
|
||||
use std::{env, sync::Arc, time::Duration};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
/// Redis Connection Pool
|
||||
#[derive(Clone)]
|
||||
pub struct RedisConnectionPool {
|
||||
client: RedisClient,
|
||||
max_connections: usize,
|
||||
connections: Arc<Mutex<Vec<MultiplexedConnection>>>,
|
||||
timeout: Duration,
|
||||
}
|
||||
|
||||
impl RedisConnectionPool {
|
||||
/// Создает новый connection pool
|
||||
pub async fn new(
|
||||
redis_url: String,
|
||||
max_connections: usize,
|
||||
timeout: Duration,
|
||||
) -> Result<Self, redis::RedisError> {
|
||||
let client = RedisClient::open(redis_url)?;
|
||||
let connections = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
let pool = Self {
|
||||
client,
|
||||
max_connections,
|
||||
connections,
|
||||
timeout,
|
||||
};
|
||||
|
||||
// Предварительно создаем несколько соединений
|
||||
pool.warm_up_connections(3).await?;
|
||||
|
||||
Ok(pool)
|
||||
}
|
||||
|
||||
/// Предварительно создает соединения для пула
|
||||
async fn warm_up_connections(&self, count: usize) -> Result<(), redis::RedisError> {
|
||||
let mut connections = self.connections.lock().await;
|
||||
|
||||
for _ in 0..count.min(self.max_connections) {
|
||||
match tokio::time::timeout(self.timeout, self.client.get_multiplexed_async_connection())
|
||||
.await
|
||||
{
|
||||
Ok(Ok(conn)) => {
|
||||
connections.push(conn);
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
warn!("Failed to create Redis connection during warm-up: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
Err(_) => {
|
||||
warn!("Timeout creating Redis connection during warm-up");
|
||||
return Err(redis::RedisError::from((
|
||||
redis::ErrorKind::IoError,
|
||||
"Connection timeout",
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
warn!(
|
||||
"✅ Redis connection pool warmed up with {} connections",
|
||||
connections.len()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Получает соединение из пула
|
||||
pub async fn get_connection(&self) -> Result<MultiplexedConnection, redis::RedisError> {
|
||||
let mut connections = self.connections.lock().await;
|
||||
|
||||
// Пытаемся взять существующее соединение
|
||||
if let Some(conn) = connections.pop() {
|
||||
return Ok(conn);
|
||||
}
|
||||
|
||||
// Если соединений нет, создаем новое
|
||||
drop(connections); // Освобождаем мьютекс
|
||||
|
||||
match tokio::time::timeout(self.timeout, self.client.get_multiplexed_async_connection())
|
||||
.await
|
||||
{
|
||||
Ok(Ok(conn)) => Ok(conn),
|
||||
Ok(Err(e)) => {
|
||||
warn!("Failed to create new Redis connection: {}", e);
|
||||
Err(e)
|
||||
}
|
||||
Err(_) => {
|
||||
warn!("Timeout creating new Redis connection");
|
||||
Err(redis::RedisError::from((
|
||||
redis::ErrorKind::IoError,
|
||||
"Connection timeout",
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Возвращает соединение обратно в пул
|
||||
pub async fn return_connection(&self, conn: MultiplexedConnection) {
|
||||
let mut connections = self.connections.lock().await;
|
||||
|
||||
if connections.len() < self.max_connections {
|
||||
connections.push(conn);
|
||||
}
|
||||
// Если пул полный, соединение просто закрывается
|
||||
}
|
||||
|
||||
/// Проверяет здоровье пула
|
||||
pub async fn health_check(&self) -> bool {
|
||||
match self.get_connection().await {
|
||||
Ok(mut conn) => {
|
||||
match tokio::time::timeout(Duration::from_secs(2), conn.ping::<String>()).await {
|
||||
Ok(Ok(_)) => {
|
||||
self.return_connection(conn).await;
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Получает статистику пула
|
||||
pub async fn get_stats(&self) -> (usize, usize) {
|
||||
let connections = self.connections.lock().await;
|
||||
(connections.len(), self.max_connections)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub redis: Option<MultiplexedConnection>,
|
||||
pub redis_pool: Option<RedisConnectionPool>,
|
||||
pub storj_client: S3Client,
|
||||
pub aws_client: S3Client,
|
||||
pub bucket: String,
|
||||
@@ -27,6 +155,43 @@ impl AppState {
|
||||
Self::new_with_config(security_config).await
|
||||
}
|
||||
|
||||
/// Получает соединение из Redis connection pool
|
||||
pub async fn get_redis_connection(&self) -> Result<MultiplexedConnection, redis::RedisError> {
|
||||
if let Some(ref pool) = self.redis_pool {
|
||||
pool.get_connection().await
|
||||
} else {
|
||||
Err(redis::RedisError::from((
|
||||
redis::ErrorKind::IoError,
|
||||
"Redis pool not available",
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Возвращает соединение обратно в пул
|
||||
pub async fn return_redis_connection(&self, conn: MultiplexedConnection) {
|
||||
if let Some(ref pool) = self.redis_pool {
|
||||
pool.return_connection(conn).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Проверяет здоровье Redis connection pool
|
||||
pub async fn redis_health_check(&self) -> bool {
|
||||
if let Some(ref pool) = self.redis_pool {
|
||||
pool.health_check().await
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Получает статистику Redis connection pool
|
||||
pub async fn redis_pool_stats(&self) -> Option<(usize, usize)> {
|
||||
if let Some(ref pool) = self.redis_pool {
|
||||
Some(pool.get_stats().await)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Инициализация с кастомной конфигурацией безопасности.
|
||||
pub async fn new_with_config(security_config: SecurityConfig) -> Self {
|
||||
log::warn!("🚀 Starting AppState initialization...");
|
||||
@@ -89,64 +254,33 @@ impl AppState {
|
||||
security_config: SecurityConfig,
|
||||
redis_url: String,
|
||||
) -> Self {
|
||||
let redis_client = match RedisClient::open(redis_url) {
|
||||
Ok(client) => {
|
||||
log::warn!("✅ Redis client created successfully");
|
||||
client
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("❌ Failed to create Redis client: {}", e);
|
||||
panic!("Redis client creation failed: {}", e);
|
||||
}
|
||||
};
|
||||
|
||||
// Устанавливаем таймаут для Redis операций с graceful fallback
|
||||
log::warn!(
|
||||
"🔄 Attempting Redis connection with timeout: {}s",
|
||||
security_config.request_timeout_seconds
|
||||
);
|
||||
|
||||
let redis_connection = match tokio::time::timeout(
|
||||
// Создаем Redis connection pool
|
||||
let redis_pool = match RedisConnectionPool::new(
|
||||
redis_url.clone(),
|
||||
20, // max_connections согласно руководству
|
||||
Duration::from_secs(security_config.request_timeout_seconds),
|
||||
redis_client.get_multiplexed_async_connection(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(mut conn)) => {
|
||||
log::warn!("✅ Redis connection established");
|
||||
|
||||
// Тестируем подключение простой командой
|
||||
match tokio::time::timeout(Duration::from_secs(2), conn.ping::<String>()).await {
|
||||
Ok(Ok(result)) => {
|
||||
log::warn!("✅ Redis PING successful: {}", result);
|
||||
Some(conn)
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
log::warn!("⚠️ Redis PING failed: {}", e);
|
||||
None
|
||||
}
|
||||
Err(_) => {
|
||||
log::warn!("⚠️ Redis PING timeout");
|
||||
None
|
||||
}
|
||||
}
|
||||
Ok(pool) => {
|
||||
log::warn!("✅ Redis connection pool created successfully");
|
||||
Some(pool)
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
log::warn!("⚠️ Redis connection failed: {}", e);
|
||||
log::warn!(" Error type: {:?}", e.kind());
|
||||
log::warn!("⚠️ Running in fallback mode without Redis (quotas disabled)");
|
||||
None
|
||||
}
|
||||
Err(_) => {
|
||||
log::warn!(
|
||||
"⚠️ Redis connection timeout after {} seconds",
|
||||
security_config.request_timeout_seconds
|
||||
);
|
||||
Err(e) => {
|
||||
log::warn!("⚠️ Redis connection pool creation failed: {}", e);
|
||||
log::warn!("⚠️ Running in fallback mode without Redis (quotas disabled)");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
// Одиночное соединение больше не нужно - используем только connection pool
|
||||
|
||||
// Получаем конфигурацию для S3 (Storj)
|
||||
let s3_access_key = env::var("STORJ_ACCESS_KEY").expect("STORJ_ACCESS_KEY must be set");
|
||||
let s3_secret_key = env::var("STORJ_SECRET_KEY").expect("STORJ_SECRET_KEY must be set");
|
||||
@@ -209,7 +343,7 @@ impl AppState {
|
||||
let aws_client = S3Client::new(&aws_config);
|
||||
|
||||
let app_state = AppState {
|
||||
redis: redis_connection,
|
||||
redis_pool,
|
||||
storj_client,
|
||||
aws_client,
|
||||
bucket,
|
||||
@@ -226,10 +360,13 @@ impl AppState {
|
||||
pub async fn cache_filelist(&self) {
|
||||
warn!("caching AWS filelist...");
|
||||
|
||||
// Проверяем доступность Redis
|
||||
let Some(mut redis) = self.redis.clone() else {
|
||||
warn!("⚠️ Redis not available, skipping filelist caching");
|
||||
return;
|
||||
// Получаем соединение из пула
|
||||
let mut redis = match self.get_redis_connection().await {
|
||||
Ok(conn) => conn,
|
||||
Err(_) => {
|
||||
warn!("⚠️ Redis pool not available, skipping filelist caching");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Запрашиваем список файлов из Storj S3
|
||||
@@ -249,31 +386,43 @@ impl AppState {
|
||||
}
|
||||
|
||||
warn!("cached {} files", filelist.len());
|
||||
|
||||
// Возвращаем соединение в пул
|
||||
self.return_redis_connection(redis).await;
|
||||
}
|
||||
|
||||
/// Получает путь из ключа (имени файла) в Redis с таймаутом.
|
||||
pub async fn get_path(&self, filename: &str) -> Result<Option<String>, actix_web::Error> {
|
||||
let Some(mut redis) = self.redis.clone() else {
|
||||
warn!("⚠️ Redis not available, returning None for path lookup");
|
||||
return Ok(None);
|
||||
let mut redis = match self.get_redis_connection().await {
|
||||
Ok(conn) => conn,
|
||||
Err(_) => {
|
||||
warn!("⚠️ Redis pool not available, returning None for path lookup");
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
|
||||
let new_path: Option<String> =
|
||||
let result: Option<String> =
|
||||
tokio::time::timeout(self.request_timeout, redis.hget(PATH_MAPPING_KEY, filename))
|
||||
.await
|
||||
.map_err(|_| ErrorInternalServerError("Redis operation timeout"))?
|
||||
.map_err(|_| ErrorInternalServerError("Failed to get path mapping from Redis"))?;
|
||||
|
||||
Ok(new_path)
|
||||
// Возвращаем соединение в пул
|
||||
self.return_redis_connection(redis).await;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn set_path(&self, filename: &str, filepath: &str) {
|
||||
let Some(mut redis) = self.redis.clone() else {
|
||||
warn!(
|
||||
"⚠️ Redis not available, skipping path caching for {}",
|
||||
filename
|
||||
);
|
||||
return;
|
||||
let mut redis = match self.get_redis_connection().await {
|
||||
Ok(conn) => conn,
|
||||
Err(_) => {
|
||||
warn!(
|
||||
"⚠️ Redis pool not available, skipping path caching for {}",
|
||||
filename
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = tokio::time::timeout(
|
||||
@@ -284,16 +433,22 @@ impl AppState {
|
||||
{
|
||||
warn!("⚠️ Redis operation failed for {}: {}", filename, e);
|
||||
}
|
||||
|
||||
// Возвращаем соединение в пул
|
||||
self.return_redis_connection(redis).await;
|
||||
}
|
||||
|
||||
/// создает или получает текущее значение квоты пользователя с таймаутом
|
||||
pub async fn get_or_create_quota(&self, user_id: &str) -> Result<u64, actix_web::Error> {
|
||||
let Some(mut redis) = self.redis.clone() else {
|
||||
warn!(
|
||||
"⚠️ Redis not available, returning default quota for user {}",
|
||||
user_id
|
||||
);
|
||||
return Ok(0); // Возвращаем 0 как fallback
|
||||
let mut redis = match self.get_redis_connection().await {
|
||||
Ok(conn) => conn,
|
||||
Err(_) => {
|
||||
warn!(
|
||||
"⚠️ Redis pool not available, returning default quota for user {}",
|
||||
user_id
|
||||
);
|
||||
return Ok(0); // Возвращаем 0 как fallback
|
||||
}
|
||||
};
|
||||
let quota_key = format!("quota:{}", user_id);
|
||||
|
||||
@@ -313,8 +468,12 @@ impl AppState {
|
||||
.map_err(|_| ErrorInternalServerError("Redis timeout setting user quota"))?
|
||||
.map_err(|_| ErrorInternalServerError("Failed to set initial user quota in Redis"))?;
|
||||
|
||||
// Возвращаем соединение в пул
|
||||
self.return_redis_connection(redis).await;
|
||||
Ok(0) // Возвращаем 0 как начальную квоту
|
||||
} else {
|
||||
// Возвращаем соединение в пул
|
||||
self.return_redis_connection(redis).await;
|
||||
Ok(quota)
|
||||
}
|
||||
}
|
||||
@@ -325,12 +484,15 @@ impl AppState {
|
||||
user_id: &str,
|
||||
bytes: u64,
|
||||
) -> Result<u64, actix_web::Error> {
|
||||
let Some(mut redis) = self.redis.clone() else {
|
||||
warn!(
|
||||
"⚠️ Redis not available, skipping quota increment for user {}",
|
||||
user_id
|
||||
);
|
||||
return Ok(0); // Возвращаем 0 как fallback
|
||||
let mut redis = match self.get_redis_connection().await {
|
||||
Ok(conn) => conn,
|
||||
Err(_) => {
|
||||
warn!(
|
||||
"⚠️ Redis pool not available, skipping quota increment for user {}",
|
||||
user_id
|
||||
);
|
||||
return Ok(0); // Возвращаем 0 как fallback
|
||||
}
|
||||
};
|
||||
let quota_key = format!("quota:{}", user_id);
|
||||
|
||||
@@ -354,6 +516,9 @@ impl AppState {
|
||||
.await
|
||||
.map_err(|_| ErrorInternalServerError("Redis timeout setting initial user quota"))?
|
||||
.map_err(|_| ErrorInternalServerError("Failed to set initial user quota in Redis"))?;
|
||||
|
||||
// Возвращаем соединение в пул
|
||||
self.return_redis_connection(redis).await;
|
||||
return Ok(bytes);
|
||||
}
|
||||
|
||||
@@ -366,6 +531,8 @@ impl AppState {
|
||||
.map_err(|_| ErrorInternalServerError("Redis timeout incrementing user quota"))?
|
||||
.map_err(|_| ErrorInternalServerError("Failed to increment user quota in Redis"))?;
|
||||
|
||||
// Возвращаем соединение в пул
|
||||
self.return_redis_connection(redis).await;
|
||||
Ok(new_quota)
|
||||
}
|
||||
|
||||
|
||||
40
src/auth.rs
40
src/auth.rs
@@ -242,6 +242,46 @@ pub async fn authenticate_request(
|
||||
})
|
||||
}
|
||||
|
||||
/// Универсальная функция аутентификации с использованием connection pool
|
||||
/// Автоматически получает и возвращает соединения из пула
|
||||
pub async fn authenticate_request_with_pool(
|
||||
req: &actix_web::HttpRequest,
|
||||
app_state: &crate::app_state::AppState,
|
||||
) -> Result<Author, actix_web::Error> {
|
||||
// Извлекаем токен из запроса
|
||||
let token = extract_token_from_request(req).ok_or_else(|| {
|
||||
warn!("No authorization token provided");
|
||||
actix_web::error::ErrorUnauthorized("Authorization token required")
|
||||
})?;
|
||||
|
||||
// Получаем соединение из пула
|
||||
match app_state.get_redis_connection().await {
|
||||
Ok(mut conn) => {
|
||||
// Валидируем токен с Redis соединением
|
||||
let result =
|
||||
secure_token_validation(&token, Some(&mut conn), app_state.request_timeout).await;
|
||||
|
||||
// Возвращаем соединение в пул
|
||||
app_state.return_redis_connection(conn).await;
|
||||
|
||||
result.map_err(|e| {
|
||||
warn!("Token validation failed: {}", e);
|
||||
actix_web::error::ErrorUnauthorized("Invalid or expired token")
|
||||
})
|
||||
}
|
||||
Err(_) => {
|
||||
// Fallback на JWT-only валидацию если Redis недоступен
|
||||
warn!("Redis pool unavailable, falling back to JWT-only validation");
|
||||
secure_token_validation(&token, None, app_state.request_timeout)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Token validation failed: {}", e);
|
||||
actix_web::error::ErrorUnauthorized("Invalid or expired token")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Сохраняет имя файла в Redis для пользователя
|
||||
pub async fn user_added_file(
|
||||
redis: Option<&mut MultiplexedConnection>,
|
||||
|
||||
@@ -3,7 +3,7 @@ use actix_web::{HttpRequest, HttpResponse, Result, web};
|
||||
use log::{error, info, warn};
|
||||
|
||||
use crate::app_state::AppState;
|
||||
use crate::auth::{authenticate_request, user_added_file};
|
||||
use crate::auth::{authenticate_request_with_pool, user_added_file};
|
||||
use crate::handlers::MAX_USER_QUOTA_BYTES;
|
||||
use crate::lookup::store_file_info;
|
||||
use crate::s3_utils::{self, generate_key_with_extension, upload_to_s3};
|
||||
@@ -20,11 +20,8 @@ pub async fn upload_handler(
|
||||
mut payload: Multipart,
|
||||
state: web::Data<AppState>,
|
||||
) -> Result<HttpResponse, actix_web::Error> {
|
||||
// Получаем Redis соединение для проверки сессий
|
||||
let mut redis_conn = state.redis.clone();
|
||||
|
||||
// Универсальная аутентификация с извлечением токена и валидацией
|
||||
let author = authenticate_request(&req, redis_conn.as_mut(), state.request_timeout).await?;
|
||||
// Универсальная аутентификация с connection pool
|
||||
let author = authenticate_request_with_pool(&req, &state).await?;
|
||||
|
||||
let user_id = &author.user_id;
|
||||
info!(
|
||||
@@ -174,16 +171,26 @@ pub async fn upload_handler(
|
||||
));
|
||||
}
|
||||
|
||||
// Сохраняем информацию о файле в Redis
|
||||
if let Err(e) = store_file_info(redis_conn.as_mut(), &filename, &content_type).await
|
||||
{
|
||||
error!("Failed to store file info in Redis: {}", e);
|
||||
// Не прерываем процесс, файл уже загружен в S3
|
||||
}
|
||||
// Получаем соединение из пула для Redis операций
|
||||
if let Ok(mut redis_conn) = state.get_redis_connection().await {
|
||||
// Сохраняем информацию о файле в Redis
|
||||
if let Err(e) =
|
||||
store_file_info(Some(&mut redis_conn), &filename, &content_type).await
|
||||
{
|
||||
error!("Failed to store file info in Redis: {}", e);
|
||||
// Не прерываем процесс, файл уже загружен в S3
|
||||
}
|
||||
|
||||
if let Err(e) = user_added_file(redis_conn.as_mut(), user_id, &filename).await {
|
||||
error!("Failed to record user file association: {}", e);
|
||||
// Не прерываем процесс
|
||||
if let Err(e) = user_added_file(Some(&mut redis_conn), user_id, &filename).await
|
||||
{
|
||||
error!("Failed to record user file association: {}", e);
|
||||
// Не прерываем процесс
|
||||
}
|
||||
|
||||
// Возвращаем соединение в пул
|
||||
state.return_redis_connection(redis_conn).await;
|
||||
} else {
|
||||
warn!("Redis pool unavailable, skipping file metadata storage");
|
||||
}
|
||||
|
||||
// Сохраняем маппинг пути
|
||||
|
||||
@@ -3,7 +3,7 @@ use log::{error, info, warn};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::app_state::AppState;
|
||||
use crate::auth::{Author, authenticate_request};
|
||||
use crate::auth::{Author, authenticate_request_with_pool};
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct UserWithQuotaResponse {
|
||||
@@ -26,11 +26,8 @@ pub async fn get_current_user_handler(
|
||||
) -> Result<HttpResponse, actix_web::Error> {
|
||||
info!("Getting user info for valid token");
|
||||
|
||||
// Получаем Redis соединение для проверки сессий
|
||||
let mut redis_conn = state.redis.clone();
|
||||
|
||||
// Универсальная аутентификация с извлечением токена и валидацией
|
||||
let user = match authenticate_request(&req, redis_conn.as_mut(), state.request_timeout).await {
|
||||
// Универсальная аутентификация с connection pool
|
||||
let user = match authenticate_request_with_pool(&req, &state).await {
|
||||
Ok(user) => {
|
||||
info!(
|
||||
"Successfully retrieved user info: user_id={}, username={:?}",
|
||||
|
||||
@@ -10,5 +10,8 @@ pub mod security;
|
||||
pub mod thumbnail;
|
||||
|
||||
// Реэкспортируем основные типы для удобства
|
||||
pub use app_state::AppState;
|
||||
pub use auth::{Author, authenticate_request, extract_token_from_request, secure_token_validation};
|
||||
pub use app_state::{AppState, RedisConnectionPool};
|
||||
pub use auth::{
|
||||
Author, authenticate_request, authenticate_request_with_pool, extract_token_from_request,
|
||||
secure_token_validation,
|
||||
};
|
||||
|
||||
218
tests/redis_pool_test.rs
Normal file
218
tests/redis_pool_test.rs
Normal file
@@ -0,0 +1,218 @@
|
||||
use quoter::{AppState, RedisConnectionPool, authenticate_request_with_pool};
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
|
||||
/// Тест создания Redis connection pool
|
||||
#[tokio::test]
|
||||
async fn test_redis_connection_pool_creation() {
|
||||
// Используем невалидный URL для тестирования обработки ошибок
|
||||
let invalid_url = "redis://invalid-host:6379";
|
||||
|
||||
let result = RedisConnectionPool::new(invalid_url.to_string(), 5, Duration::from_secs(1)).await;
|
||||
|
||||
// Должен вернуть ошибку для невалидного URL
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
/// Тест статистики connection pool
|
||||
#[tokio::test]
|
||||
async fn test_redis_pool_stats() {
|
||||
// Создаем мок пула (в реальном тесте нужен валидный Redis)
|
||||
// Этот тест демонстрирует API
|
||||
|
||||
// В реальном окружении с Redis:
|
||||
// let pool = RedisConnectionPool::new(
|
||||
// "redis://localhost:6379".to_string(),
|
||||
// 10,
|
||||
// Duration::from_secs(5)
|
||||
// ).await.unwrap();
|
||||
//
|
||||
// let (available, max) = pool.get_stats().await;
|
||||
// assert_eq!(max, 10);
|
||||
// assert!(available <= max);
|
||||
|
||||
// Для CI/CD без Redis просто проверяем, что код компилируется
|
||||
assert!(true);
|
||||
}
|
||||
|
||||
/// Тест health check connection pool
|
||||
#[tokio::test]
|
||||
async fn test_redis_pool_health_check() {
|
||||
// Тест с невалидным URL должен вернуть false
|
||||
if let Ok(pool) = RedisConnectionPool::new(
|
||||
"redis://invalid-host:6379".to_string(),
|
||||
5,
|
||||
Duration::from_millis(100), // Короткий таймаут
|
||||
)
|
||||
.await
|
||||
{
|
||||
let health = pool.health_check().await;
|
||||
assert!(!health); // Должен быть false для невалидного хоста
|
||||
}
|
||||
}
|
||||
|
||||
/// Тест получения соединения из пула
|
||||
#[tokio::test]
|
||||
async fn test_redis_pool_get_connection() {
|
||||
// Тест с невалидным URL для проверки обработки ошибок
|
||||
if let Ok(pool) = RedisConnectionPool::new(
|
||||
"redis://invalid-host:6379".to_string(),
|
||||
5,
|
||||
Duration::from_millis(100),
|
||||
)
|
||||
.await
|
||||
{
|
||||
let result = pool.get_connection().await;
|
||||
// Должен вернуть ошибку для невалидного хоста
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
/// Тест возврата соединения в пул
|
||||
#[tokio::test]
|
||||
async fn test_redis_pool_return_connection() {
|
||||
// Демонстрирует API для возврата соединений
|
||||
// В реальном тесте с валидным Redis:
|
||||
//
|
||||
// let pool = RedisConnectionPool::new(...).await.unwrap();
|
||||
// let conn = pool.get_connection().await.unwrap();
|
||||
// pool.return_connection(conn).await;
|
||||
//
|
||||
// let (available_after, _) = pool.get_stats().await;
|
||||
// assert_eq!(available_after, available_before + 1);
|
||||
|
||||
assert!(true); // Проверяем компиляцию
|
||||
}
|
||||
|
||||
/// Тест производительности connection pool
|
||||
#[tokio::test]
|
||||
async fn test_redis_pool_performance() {
|
||||
use std::time::Instant;
|
||||
|
||||
// Тест создания пула (без реального Redis)
|
||||
let start = Instant::now();
|
||||
|
||||
for _ in 0..100 {
|
||||
let _result = RedisConnectionPool::new(
|
||||
"redis://invalid-host:6379".to_string(),
|
||||
5,
|
||||
Duration::from_millis(1), // Очень короткий таймаут
|
||||
)
|
||||
.await;
|
||||
// Игнорируем результат, так как Redis недоступен
|
||||
}
|
||||
|
||||
let duration = start.elapsed();
|
||||
println!("100 pool creation attempts took: {:?}", duration);
|
||||
|
||||
// Проверяем, что операции выполняются быстро
|
||||
assert!(duration < Duration::from_secs(10));
|
||||
}
|
||||
|
||||
/// Тест concurrent доступа к пулу
|
||||
#[tokio::test]
|
||||
async fn test_redis_pool_concurrent_access() {
|
||||
// Демонстрирует concurrent использование пула
|
||||
let tasks = (0..10).map(|i| {
|
||||
tokio::spawn(async move {
|
||||
// В реальном тесте здесь был бы доступ к пулу
|
||||
sleep(Duration::from_millis(i * 10)).await;
|
||||
format!("Task {} completed", i)
|
||||
})
|
||||
});
|
||||
|
||||
let results: Vec<_> = futures::future::join_all(tasks).await;
|
||||
|
||||
// Проверяем, что все задачи завершились успешно
|
||||
for (i, result) in results.iter().enumerate() {
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.as_ref().unwrap(), &format!("Task {} completed", i));
|
||||
}
|
||||
}
|
||||
|
||||
/// Тест AppState с Redis connection pool
|
||||
#[tokio::test]
|
||||
async fn test_app_state_redis_pool_methods() {
|
||||
// Тестируем методы AppState для работы с пулом
|
||||
// В реальном окружении нужен валидный Redis
|
||||
|
||||
// Создаем AppState без Redis (для тестирования fallback)
|
||||
use quoter::security::SecurityConfig;
|
||||
|
||||
// Этот тест проверяет, что методы существуют и компилируются
|
||||
// В реальном тесте с Redis:
|
||||
// let app_state = AppState::new().await;
|
||||
// let health = app_state.redis_health_check().await;
|
||||
// let stats = app_state.redis_pool_stats().await;
|
||||
|
||||
assert!(true); // Проверяем компиляцию
|
||||
}
|
||||
|
||||
/// Тест authenticate_request_with_pool
|
||||
#[tokio::test]
|
||||
async fn test_authenticate_request_with_pool() {
|
||||
use actix_web::test;
|
||||
use quoter::security::SecurityConfig;
|
||||
|
||||
// Создаем тестовый запрос
|
||||
let req = test::TestRequest::default()
|
||||
.insert_header(("authorization", "Bearer invalid-token"))
|
||||
.to_http_request();
|
||||
|
||||
// В реальном тесте здесь был бы валидный AppState с Redis
|
||||
// let app_state = AppState::new().await;
|
||||
// let result = authenticate_request_with_pool(&req, &app_state).await;
|
||||
// assert!(result.is_err()); // Невалидный токен должен быть отклонен
|
||||
|
||||
// Для CI/CD проверяем, что функция существует
|
||||
assert!(true);
|
||||
}
|
||||
|
||||
/// Тест graceful fallback при недоступности Redis
|
||||
#[tokio::test]
|
||||
async fn test_redis_fallback_behavior() {
|
||||
// Тестируем поведение при недоступности Redis
|
||||
// Система должна работать в JWT-only режиме
|
||||
|
||||
// В реальном тесте:
|
||||
// 1. Создаем AppState с недоступным Redis
|
||||
// 2. Проверяем, что аутентификация работает через JWT
|
||||
// 3. Проверяем, что операции с квотами возвращают fallback значения
|
||||
|
||||
assert!(true); // Проверяем компиляцию
|
||||
}
|
||||
|
||||
/// Интеграционный тест Redis connection pool
|
||||
#[tokio::test]
|
||||
async fn test_redis_pool_integration() {
|
||||
// Полный интеграционный тест (требует реального Redis)
|
||||
|
||||
// Проверяем переменную окружения для интеграционных тестов
|
||||
if std::env::var("REDIS_INTEGRATION_TEST").is_ok() {
|
||||
// Запускаем интеграционные тесты только если установлена переменная
|
||||
let redis_url =
|
||||
std::env::var("REDIS_URL").unwrap_or_else(|_| "redis://localhost:6379".to_string());
|
||||
|
||||
if let Ok(pool) = RedisConnectionPool::new(redis_url, 10, Duration::from_secs(5)).await {
|
||||
// Тестируем получение соединения
|
||||
let conn_result = pool.get_connection().await;
|
||||
assert!(conn_result.is_ok());
|
||||
|
||||
if let Ok(conn) = conn_result {
|
||||
// Возвращаем соединение в пул
|
||||
pool.return_connection(conn).await;
|
||||
}
|
||||
|
||||
// Тестируем health check
|
||||
let health = pool.health_check().await;
|
||||
assert!(health);
|
||||
|
||||
// Тестируем статистику
|
||||
let (available, max) = pool.get_stats().await;
|
||||
assert_eq!(max, 10);
|
||||
assert!(available <= max);
|
||||
}
|
||||
} else {
|
||||
println!("Skipping Redis integration test (set REDIS_INTEGRATION_TEST=1 to enable)");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user