diff --git a/Cargo.lock b/Cargo.lock index c01256e..f552235 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2677,6 +2677,7 @@ dependencies = [ "serde", "serde_json", "tokio", + "url", "uuid", ] diff --git a/Cargo.toml b/Cargo.toml index 283f92a..e2a9f4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ aws-sdk-s3 = { version = "1.104.0", default-features = false, features = ["rt-to image = { version = "0.25.6", default-features = false, features = ["jpeg", "png", "webp", "tiff"] } mime_guess = "2.0.5" md5 = "0.7.0" +url = "2.5.4" aws-config = { version = "1.8.6", default-features = false, features = ["rt-tokio", "rustls"] } actix-multipart = "0.7.2" log = "0.4.22" diff --git a/src/app_state.rs b/src/app_state.rs index 024e935..df1e6a6 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -23,21 +23,14 @@ impl AppState { /// Инициализация нового состояния приложения. pub async fn new() -> Self { let security_config = SecurityConfig::default(); - Self::new_with_config(security_config).await.unwrap_or_else(|e| { - log::error!("❌ Failed to initialize AppState: {}", e); - std::process::exit(1); - }) + Self::new_with_config(security_config).await } /// Инициализация с кастомной конфигурацией безопасности. - pub async fn new_with_config(security_config: SecurityConfig) -> Result> { + pub async fn new_with_config(security_config: SecurityConfig) -> Self { // Получаем конфигурацию для Redis с таймаутом let redis_url = env::var("REDIS_URL").expect("REDIS_URL must be set"); - log::info!("🔗 Attempting Redis connection to: {}", redis_url.replace(&redis_url.split('@').nth(0).unwrap_or(""), "***")); - let redis_client = RedisClient::open(redis_url.clone()).map_err(|e| { - log::error!("❌ Failed to parse Redis URL: {}", e); - e - })?; + let redis_client = RedisClient::open(redis_url).expect("Invalid Redis URL"); // Устанавливаем таймаут для Redis операций с graceful fallback let redis_connection = match tokio::time::timeout( @@ -134,7 +127,7 @@ impl AppState { // Кэшируем список файлов из AWS при старте приложения app_state.cache_filelist().await; - Ok(app_state) + app_state } /// Кэширует список файлов из Storj S3 в Redis. diff --git a/tests/redis_connection_test.rs b/tests/redis_connection_test.rs new file mode 100644 index 0000000..b1c0e55 --- /dev/null +++ b/tests/redis_connection_test.rs @@ -0,0 +1,165 @@ +use redis::{Client, AsyncCommands}; +use std::env; + +#[tokio::test] +async fn test_redis_url_parsing() { + // Тестируем различные форматы Redis URL + + let test_urls = vec![ + // Простой URL без пароля + "redis://localhost:6379", + // URL с паролем (должен использовать дефолтное имя пользователя 'redis') + "redis://:password@localhost:6379", + // URL с пользователем и паролем + "redis://user:password@localhost:6379", + // URL с длинным hex паролем (как в Dokku) - должен использовать дефолтное имя 'redis' + "redis://:dbc5f9f9007c555e209964454c9d6abecae5f1db72e490acd5c94354dc012282@dokku-redis-discoursio-redis:6379", + // URL с IP адресом + "redis://172.17.0.3:6379", + // URL с exposed портом + "redis://localhost:12389", + // Тест с явным указанием дефолтного пользователя 'redis' + "redis://redis:password@localhost:6379", + // Тест с пустым пользователем и паролем (должен работать как без аутентификации) + "redis://:@localhost:6379", + ]; + + for url in test_urls { + // Парсим URL для детального вывода + if let Ok(parsed) = url::Url::parse(url) { + println!("Testing Redis URL: {}", url.replace(&url.split('@').nth(0).unwrap_or(""), "***")); + println!(" Host: {}", parsed.host_str().unwrap_or("none")); + println!(" Port: {}", parsed.port().unwrap_or(0)); + println!(" Username: '{}'", parsed.username()); + println!(" Password: '{}'", if parsed.password().is_some() { "***" } else { "none" }); + } else { + println!("Testing Redis URL: {}", url); + } + + match Client::open(url) { + Ok(client) => { + println!("✅ URL parsed successfully"); + + // Попробуем подключиться (с коротким таймаутом) + match tokio::time::timeout( + std::time::Duration::from_secs(2), + client.get_multiplexed_async_connection() + ).await { + Ok(Ok(_)) => println!("✅ Connection successful"), + Ok(Err(e)) => println!("❌ Connection failed: {}", e), + Err(_) => println!("⏰ Connection timeout"), + } + } + Err(e) => { + println!("❌ URL parsing failed: {}", e); + } + } + println!("---"); + } +} + +#[tokio::test] +async fn test_redis_connection_with_env() { + // Тестируем с реальным REDIS_URL из окружения + if let Ok(redis_url) = env::var("REDIS_URL") { + println!("Testing with real REDIS_URL: {}", + redis_url.replace(&redis_url.split('@').nth(0).unwrap_or(""), "***")); + + // Парсим реальный URL для детального вывода + if let Ok(parsed) = url::Url::parse(&redis_url) { + println!(" Host: {}", parsed.host_str().unwrap_or("none")); + println!(" Port: {}", parsed.port().unwrap_or(0)); + println!(" Username: '{}'", parsed.username()); + println!(" Password: '{}'", if parsed.password().is_some() { "***" } else { "none" }); + } + + match Client::open(redis_url) { + Ok(client) => { + println!("✅ Real URL parsed successfully"); + + match tokio::time::timeout( + std::time::Duration::from_secs(5), + client.get_multiplexed_async_connection() + ).await { + Ok(Ok(mut conn)) => { + println!("✅ Real connection successful"); + + // Попробуем выполнить простую команду + match tokio::time::timeout( + std::time::Duration::from_secs(2), + conn.ping::() + ).await { + Ok(Ok(result)) => println!("✅ PING successful: {}", result), + Ok(Err(e)) => println!("❌ PING failed: {}", e), + Err(_) => println!("⏰ PING timeout"), + } + } + Ok(Err(e)) => println!("❌ Real connection failed: {}", e), + Err(_) => println!("⏰ Real connection timeout"), + } + } + Err(e) => { + println!("❌ Real URL parsing failed: {}", e); + } + } + } else { + println!("⚠️ REDIS_URL not set, skipping real connection test"); + } +} + +#[test] +fn test_redis_url_components() { + // Тестируем парсинг компонентов URL + let test_url = "redis://:dbc5f9f9007c555e209964454c9d6abecae5f1db72e490acd5c94354dc012282@dokku-redis-discoursio-redis:6379"; + + if let Ok(parsed) = url::Url::parse(test_url) { + println!("✅ URL parsed successfully"); + println!(" Scheme: {}", parsed.scheme()); + println!(" Host: {}", parsed.host_str().unwrap_or("none")); + println!(" Port: {}", parsed.port().unwrap_or(0)); + println!(" Username: '{}'", parsed.username()); + println!(" Password: {}", if parsed.password().is_some() { "***" } else { "none" }); + println!(" Path: {}", parsed.path()); + + // Проверяем, что пустое имя пользователя означает дефолтное 'redis' + if parsed.username().is_empty() && parsed.password().is_some() { + println!(" ⚠️ Empty username with password - Redis client should use default 'redis' user"); + } + } else { + println!("❌ URL parsing failed"); + } +} + +#[tokio::test] +async fn test_redis_default_username_behavior() { + // Тестируем поведение Redis client с пустым именем пользователя + println!("Testing Redis default username behavior..."); + + let test_cases = vec![ + ("redis://:password@localhost:6379", "Empty username with password"), + ("redis://redis:password@localhost:6379", "Explicit redis username"), + ("redis://:@localhost:6379", "Empty username and password"), + ("redis://localhost:6379", "No authentication"), + ]; + + for (url, description) in test_cases { + println!("\n--- {} ---", description); + println!("URL: {}", url.replace(&url.split('@').nth(0).unwrap_or(""), "***")); + + if let Ok(parsed) = url::Url::parse(url) { + println!(" Parsed Username: '{}'", parsed.username()); + println!(" Parsed Password: {}", if parsed.password().is_some() { "***" } else { "none" }); + + // Redis client поведение: + // - Если username пустой, но есть password -> использует дефолтное имя 'redis' + // - Если username и password пустые -> без аутентификации + if parsed.username().is_empty() && parsed.password().is_some() { + println!(" Expected Redis behavior: Use default username 'redis'"); + } else if parsed.username().is_empty() && parsed.password().is_none() { + println!(" Expected Redis behavior: No authentication"); + } else { + println!(" Expected Redis behavior: Use explicit credentials"); + } + } + } +}