diff --git a/src/app_state.rs b/src/app_state.rs index b17ecec..91ae222 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,9 +1,9 @@ use crate::s3_utils::get_s3_filelist; use actix_web::error::ErrorInternalServerError; use aws_config::BehaviorVersion; -use aws_sdk_s3::{config::Credentials, Client as S3Client}; +use aws_sdk_s3::{Client as S3Client, config::Credentials}; use log::warn; -use redis::{aio::MultiplexedConnection, AsyncCommands, Client as RedisClient}; +use redis::{AsyncCommands, Client as RedisClient, aio::MultiplexedConnection}; use std::env; #[derive(Clone)] @@ -15,7 +15,7 @@ pub struct AppState { } const PATH_MAPPING_KEY: &str = "filepath_mapping"; // Ключ для хранения маппинга путей - // Убираем TTL для квоты - она должна быть постоянной на пользователя +// Убираем TTL для квоты - она должна быть постоянной на пользователя impl AppState { /// Инициализация нового состояния приложения. diff --git a/src/auth.rs b/src/auth.rs index d045f31..1a606f9 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,12 +1,12 @@ use actix_web::error::ErrorInternalServerError; -use redis::{aio::MultiplexedConnection, AsyncCommands}; -use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, env, error::Error}; -use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; +use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode}; use log::{info, warn}; -use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; +use redis::{AsyncCommands, aio::MultiplexedConnection}; use reqwest::Client as HTTPClient; +use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderValue}; +use serde::{Deserialize, Serialize}; use serde_json::json; +use std::{collections::HashMap, env, error::Error}; // Старые структуры для совместимости с get_id_by_token #[derive(Deserialize)] @@ -106,30 +106,33 @@ fn decode_jwt_token(token: &str) -> Result> { // В реальном приложении здесь должен быть настоящий секретный ключ let secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| "your-secret-key".to_string()); let key = DecodingKey::from_secret(secret.as_ref()); - + let mut validation = Validation::new(Algorithm::HS256); validation.validate_exp = true; // Включаем проверку истечения срока действия - + match decode::(token, &key, &validation) { Ok(token_data) => { let claims = token_data.claims; - + // Дополнительная проверка exp если поле присутствует if let Some(exp) = claims.exp { let current_time = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs() as usize; - + if exp < current_time { warn!("JWT token expired: exp={}, current={}", exp, current_time); return Err(Box::new(std::io::Error::other("Token expired"))); } - + info!("JWT token valid until: {} (current: {})", exp, current_time); } - - info!("Successfully decoded and validated JWT token for user: {}", claims.user_id); + + info!( + "Successfully decoded and validated JWT token for user: {}", + claims.user_id + ); Ok(claims) } Err(e) => { @@ -164,9 +167,9 @@ pub async fn get_user_by_token( // Декодируем JWT токен для получения user_id let claims = decode_jwt_token(token)?; let user_id = &claims.user_id; - + info!("Extracted user_id from JWT token: {}", user_id); - + // Проверяем валидность токена через сессию в Redis (опционально) let token_key = format!("session:{}:{}", user_id, token); let session_exists: bool = redis @@ -177,14 +180,14 @@ pub async fn get_user_by_token( // Не критичная ошибка, продолжаем с базовыми данными }) .unwrap_or(false); - + if session_exists { // Обновляем last_activity если сессия существует let current_time = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs(); - + let _: () = redis .hset(&token_key, "last_activity", current_time.to_string()) .await @@ -192,12 +195,12 @@ pub async fn get_user_by_token( warn!("Failed to update last_activity: {}", e); }) .unwrap_or(()); - + info!("Updated last_activity for session: {}", token_key); } else { info!("Session not found in Redis, proceeding with JWT-only data"); } - + // Создаем базовый объект Author с данными из JWT let author = Author { user_id: user_id.clone(), @@ -209,12 +212,12 @@ pub async fn get_user_by_token( .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs() - .to_string() + .to_string(), ), auth_data: None, device_info: None, }; - + info!("Successfully created author data for user_id: {}", user_id); Ok(author) } diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index ab05a91..35ba3c7 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -10,4 +10,4 @@ pub use upload::upload_handler; pub use user::get_current_user_handler; // Общий лимит квоты на пользователя: 5 ГБ -pub const MAX_USER_QUOTA_BYTES: u64 = 5 * 1024 * 1024 * 1024; \ No newline at end of file +pub const MAX_USER_QUOTA_BYTES: u64 = 5 * 1024 * 1024 * 1024; diff --git a/src/handlers/proxy.rs b/src/handlers/proxy.rs index 90d5641..ba4588a 100644 --- a/src/handlers/proxy.rs +++ b/src/handlers/proxy.rs @@ -1,6 +1,6 @@ use actix_web::error::ErrorNotFound; -use actix_web::{error::ErrorInternalServerError, web, HttpRequest, HttpResponse, Result}; -use log::{info, error, warn}; +use actix_web::{HttpRequest, HttpResponse, Result, error::ErrorInternalServerError, web}; +use log::{error, info, warn}; use crate::app_state::AppState; use crate::handlers::serve_file::serve_file; @@ -26,7 +26,7 @@ pub async fn proxy_handler( ) -> Result { let start_time = std::time::Instant::now(); info!("GET {} [START]", requested_res); - + let normalized_path = if requested_res.ends_with("/webp") { info!("Converting to WebP format: {}", requested_res); requested_res.replace("/webp", "") @@ -35,16 +35,21 @@ pub async fn proxy_handler( }; // Проверяем If-None-Match заголовок для кэширования - let client_etag = req.headers().get("if-none-match") + let client_etag = req + .headers() + .get("if-none-match") .and_then(|h| h.to_str().ok()); // парсим GET запрос let (base_filename, requested_width, extension) = parse_file_path(&normalized_path); let ext = extension.as_str().to_lowercase(); let filekey = format!("{}.{}", base_filename, &ext); - - info!("Parsed request - base: {}, width: {}, ext: {}", base_filename, requested_width, ext); - + + info!( + "Parsed request - base: {}, width: {}, ext: {}", + base_filename, requested_width, ext + ); + // Генерируем ETag для кэширования let file_etag = format!("\"{}\"", &filekey); if let Some(etag) = client_etag { @@ -77,7 +82,6 @@ pub async fn proxy_handler( info!("Content-Type: {}", content_type); - return match state.get_path(&filekey).await { Ok(Some(stored_path)) => { warn!("Found stored path in DB: {}", stored_path); @@ -111,8 +115,7 @@ pub async fn proxy_handler( } Ok(false) => { // Миниатюра не существует, возвращаем оригинал и запускаем генерацию миниатюры - let original_file = - serve_file(&stored_path, &state).await?; + let original_file = serve_file(&stored_path, &state).await?; // Запускаем асинхронную задачу для генерации миниатюры let state_clone = state.clone(); diff --git a/src/handlers/quota.rs b/src/handlers/quota.rs index 28ec7fe..6bed956 100644 --- a/src/handlers/quota.rs +++ b/src/handlers/quota.rs @@ -1,4 +1,4 @@ -use actix_web::{web, HttpRequest, HttpResponse, Result}; +use actix_web::{HttpRequest, HttpResponse, Result, web}; use log::warn; use serde::{Deserialize, Serialize}; @@ -34,16 +34,14 @@ pub async fn get_quota_handler( return Err(actix_web::error::ErrorUnauthorized("Unauthorized")); } - let _admin_id = get_id_by_token(token.unwrap()) - .await - .map_err(|e| { - let error_msg = if e.to_string().contains("expired") { - "Admin token has expired" - } else { - "Invalid admin token" - }; - actix_web::error::ErrorUnauthorized(error_msg) - })?; + let _admin_id = get_id_by_token(token.unwrap()).await.map_err(|e| { + let error_msg = if e.to_string().contains("expired") { + "Admin token has expired" + } else { + "Invalid admin token" + }; + actix_web::error::ErrorUnauthorized(error_msg) + })?; // Получаем user_id из query параметров let user_id = req @@ -81,16 +79,14 @@ pub async fn increase_quota_handler( return Err(actix_web::error::ErrorUnauthorized("Unauthorized")); } - let _admin_id = get_id_by_token(token.unwrap()) - .await - .map_err(|e| { - let error_msg = if e.to_string().contains("expired") { - "Admin token has expired" - } else { - "Invalid admin token" - }; - actix_web::error::ErrorUnauthorized(error_msg) - })?; + let _admin_id = get_id_by_token(token.unwrap()).await.map_err(|e| { + let error_msg = if e.to_string().contains("expired") { + "Admin token has expired" + } else { + "Invalid admin token" + }; + actix_web::error::ErrorUnauthorized(error_msg) + })?; let additional_bytes = quota_data .additional_bytes @@ -137,16 +133,14 @@ pub async fn set_quota_handler( return Err(actix_web::error::ErrorUnauthorized("Unauthorized")); } - let _admin_id = get_id_by_token(token.unwrap()) - .await - .map_err(|e| { - let error_msg = if e.to_string().contains("expired") { - "Admin token has expired" - } else { - "Invalid admin token" - }; - actix_web::error::ErrorUnauthorized(error_msg) - })?; + let _admin_id = get_id_by_token(token.unwrap()).await.map_err(|e| { + let error_msg = if e.to_string().contains("expired") { + "Admin token has expired" + } else { + "Invalid admin token" + }; + actix_web::error::ErrorUnauthorized(error_msg) + })?; let new_quota_bytes = quota_data .new_quota_bytes diff --git a/src/handlers/serve_file.rs b/src/handlers/serve_file.rs index edd7dd6..48fdaca 100644 --- a/src/handlers/serve_file.rs +++ b/src/handlers/serve_file.rs @@ -1,4 +1,4 @@ -use actix_web::{error::ErrorInternalServerError, HttpResponse, Result}; +use actix_web::{HttpResponse, Result, error::ErrorInternalServerError}; use mime_guess::MimeGuess; use crate::app_state::AppState; @@ -43,7 +43,7 @@ pub async fn serve_file( let data_bytes = data.into_bytes(); let mime_type = MimeGuess::from_path(filepath).first_or_octet_stream(); - + // Генерируем ETag для кэширования на основе пути файла let etag = format!("\"{}\"", filepath); diff --git a/src/handlers/upload.rs b/src/handlers/upload.rs index 5aa66fd..f00eb49 100644 --- a/src/handlers/upload.rs +++ b/src/handlers/upload.rs @@ -1,5 +1,5 @@ use actix_multipart::Multipart; -use actix_web::{web, HttpRequest, HttpResponse, Result}; +use actix_web::{HttpRequest, HttpResponse, Result, web}; use log::{error, info, warn}; use crate::app_state::AppState; @@ -24,26 +24,29 @@ pub async fn upload_handler( .headers() .get("Authorization") .and_then(|header_value| header_value.to_str().ok()); - + if token.is_none() { warn!("Upload attempt without authorization token"); - return Err(actix_web::error::ErrorUnauthorized("Authorization token required")); + return Err(actix_web::error::ErrorUnauthorized( + "Authorization token required", + )); } let token = token.unwrap(); - + // Сначала валидируем токен if !validate_token(token).unwrap_or(false) { warn!("Token validation failed"); - return Err(actix_web::error::ErrorUnauthorized("Invalid or expired token")); + return Err(actix_web::error::ErrorUnauthorized( + "Invalid or expired token", + )); } - + // Затем извлекаем user_id - let user_id = extract_user_id_from_token(token) - .map_err(|e| { - warn!("Failed to extract user_id from token: {}", e); - actix_web::error::ErrorUnauthorized("Invalid authorization token") - })?; + let user_id = extract_user_id_from_token(token).map_err(|e| { + warn!("Failed to extract user_id from token: {}", e); + actix_web::error::ErrorUnauthorized("Invalid authorization token") + })?; // Получаем текущую квоту пользователя let current_quota: u64 = state.get_or_create_quota(&user_id).await.unwrap_or(0); @@ -51,12 +54,17 @@ pub async fn upload_handler( // Предварительная проверка: есть ли вообще место для файлов if current_quota >= MAX_USER_QUOTA_BYTES { - warn!("Author {} quota already at maximum: {}", user_id, current_quota); - return Err(actix_web::error::ErrorPayloadTooLarge("Author quota limit exceeded")); + warn!( + "Author {} quota already at maximum: {}", + user_id, current_quota + ); + return Err(actix_web::error::ErrorPayloadTooLarge( + "Author quota limit exceeded", + )); } let mut uploaded_files = Vec::new(); - + while let Ok(Some(field)) = payload.try_next().await { let mut field = field; let mut file_bytes = Vec::new(); @@ -65,21 +73,32 @@ pub async fn upload_handler( // Читаем данные файла с проверкой размера while let Ok(Some(chunk)) = field.try_next().await { let chunk_size = chunk.len() as u64; - + // Проверка лимита одного файла if file_size + chunk_size > MAX_SINGLE_FILE_BYTES { - warn!("File size exceeds single file limit: {} > {}", - file_size + chunk_size, MAX_SINGLE_FILE_BYTES); - return Err(actix_web::error::ErrorPayloadTooLarge("Single file size limit exceeded")); + warn!( + "File size exceeds single file limit: {} > {}", + file_size + chunk_size, + MAX_SINGLE_FILE_BYTES + ); + return Err(actix_web::error::ErrorPayloadTooLarge( + "Single file size limit exceeded", + )); } - + // Проверка общей квоты пользователя if current_quota + file_size + chunk_size > MAX_USER_QUOTA_BYTES { - warn!("Upload would exceed user quota: current={}, adding={}, limit={}", - current_quota, file_size + chunk_size, MAX_USER_QUOTA_BYTES); - return Err(actix_web::error::ErrorPayloadTooLarge("Author quota limit would be exceeded")); + warn!( + "Upload would exceed user quota: current={}, adding={}, limit={}", + current_quota, + file_size + chunk_size, + MAX_USER_QUOTA_BYTES + ); + return Err(actix_web::error::ErrorPayloadTooLarge( + "Author quota limit would be exceeded", + )); } - + file_size += chunk_size; file_bytes.extend_from_slice(&chunk); } @@ -140,13 +159,16 @@ pub async fn upload_handler( .await { Ok(_) => { - info!("File {} successfully uploaded to S3 ({} bytes)", filename, file_size); - + info!( + "File {} successfully uploaded to S3 ({} bytes)", + filename, file_size + ); + // Обновляем квоту пользователя if let Err(e) = state.increment_uploaded_bytes(&user_id, file_size).await { error!("Failed to increment quota for user {}: {}", user_id, e); return Err(actix_web::error::ErrorInternalServerError( - "Failed to update user quota" + "Failed to update user quota", )); } @@ -156,21 +178,25 @@ pub async fn upload_handler( error!("Failed to store file info in Redis: {}", e); // Не прерываем процесс, файл уже загружен в S3 } - + if let Err(e) = user_added_file(&mut redis, &user_id, &filename).await { error!("Failed to record user file association: {}", e); // Не прерываем процесс } // Сохраняем маппинг пути - let generated_key = generate_key_with_extension(filename.clone(), content_type.clone()); + let generated_key = + generate_key_with_extension(filename.clone(), content_type.clone()); state.set_path(&filename, &generated_key).await; // Логируем новую квоту if let Ok(new_quota) = state.get_or_create_quota(&user_id).await { - info!("Updated quota for user {}: {} bytes ({:.1}% used)", - user_id, new_quota, - (new_quota as f64 / MAX_USER_QUOTA_BYTES as f64) * 100.0); + info!( + "Updated quota for user {}: {} bytes ({:.1}% used)", + user_id, + new_quota, + (new_quota as f64 / MAX_USER_QUOTA_BYTES as f64) * 100.0 + ); } uploaded_files.push(filename); @@ -178,7 +204,7 @@ pub async fn upload_handler( Err(e) => { error!("Failed to upload file to S3: {}", e); return Err(actix_web::error::ErrorInternalServerError( - "File upload failed" + "File upload failed", )); } } @@ -188,7 +214,9 @@ pub async fn upload_handler( match uploaded_files.len() { 0 => { warn!("No files were uploaded"); - Err(actix_web::error::ErrorBadRequest("No files provided or all files were empty")) + Err(actix_web::error::ErrorBadRequest( + "No files provided or all files were empty", + )) } 1 => { info!("Successfully uploaded 1 file: {}", uploaded_files[0]); diff --git a/src/handlers/user.rs b/src/handlers/user.rs index f7bc64f..afa5bdf 100644 --- a/src/handlers/user.rs +++ b/src/handlers/user.rs @@ -1,9 +1,9 @@ -use actix_web::{web, HttpRequest, HttpResponse, Result}; +use actix_web::{HttpRequest, HttpResponse, Result, web}; use log::{error, info, warn}; use serde::Serialize; use crate::app_state::AppState; -use crate::auth::{get_user_by_token, Author, validate_token}; +use crate::auth::{Author, get_user_by_token, validate_token}; #[derive(Serialize)] pub struct UserWithQuotaResponse { @@ -40,25 +40,31 @@ pub async fn get_current_user_handler( if token.is_none() { warn!("Request for current user without authorization token"); - return Err(actix_web::error::ErrorUnauthorized("Authorization token required")); + return Err(actix_web::error::ErrorUnauthorized( + "Authorization token required", + )); } let token = token.unwrap(); - + // Сначала валидируем токен if !validate_token(token).unwrap_or(false) { warn!("Token validation failed in user endpoint"); - return Err(actix_web::error::ErrorUnauthorized("Invalid or expired token")); + return Err(actix_web::error::ErrorUnauthorized( + "Invalid or expired token", + )); } - + info!("Getting user info for valid token"); // Получаем информацию о пользователе из Redis сессии let mut redis = state.redis.clone(); let user = match get_user_by_token(token, &mut redis).await { Ok(user) => { - info!("Successfully retrieved user info: user_id={}, username={:?}", - user.user_id, user.username); + info!( + "Successfully retrieved user info: user_id={}, username={:?}", + user.user_id, user.username + ); user } Err(e) => { diff --git a/src/lookup.rs b/src/lookup.rs index 27320fd..c63658e 100644 --- a/src/lookup.rs +++ b/src/lookup.rs @@ -1,7 +1,7 @@ use actix_web::error::ErrorInternalServerError; use once_cell::sync::Lazy; -use redis::aio::MultiplexedConnection; use redis::AsyncCommands; +use redis::aio::MultiplexedConnection; use std::collections::HashMap; pub static MIME_TYPES: Lazy> = Lazy::new(|| { diff --git a/src/main.rs b/src/main.rs index 1a2ebe8..feed2ad 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,15 +7,15 @@ mod thumbnail; use actix_cors::Cors; use actix_web::{ + App, HttpServer, http::header::{self, HeaderName}, middleware::Logger, - web, App, HttpServer, + web, }; use app_state::AppState; use handlers::{ - get_current_user_handler, get_quota_handler, - increase_quota_handler, proxy_handler, + get_current_user_handler, get_quota_handler, increase_quota_handler, proxy_handler, set_quota_handler, upload_handler, }; use log::warn; diff --git a/src/s3_utils.rs b/src/s3_utils.rs index 916da3c..9a0520b 100644 --- a/src/s3_utils.rs +++ b/src/s3_utils.rs @@ -1,5 +1,5 @@ use actix_web::error::ErrorInternalServerError; -use aws_sdk_s3::{error::SdkError, primitives::ByteStream, Client as S3Client}; +use aws_sdk_s3::{Client as S3Client, error::SdkError, primitives::ByteStream}; use infer::get; use mime_guess::mime; use std::str::FromStr; diff --git a/src/thumbnail.rs b/src/thumbnail.rs index ab82c7a..61282f2 100644 --- a/src/thumbnail.rs +++ b/src/thumbnail.rs @@ -1,5 +1,5 @@ use actix_web::error::ErrorInternalServerError; -use image::{imageops::FilterType, DynamicImage, ImageFormat}; +use image::{DynamicImage, ImageFormat, imageops::FilterType}; use log::warn; use std::{collections::HashMap, io::Cursor}; diff --git a/tests/basic_test.rs b/tests/basic_test.rs index 5e0108b..b604391 100644 --- a/tests/basic_test.rs +++ b/tests/basic_test.rs @@ -1,4 +1,4 @@ -use actix_web::{test, web, App, HttpResponse}; +use actix_web::{App, HttpResponse, test, web}; /// Простой тест health check #[actix_web::test] @@ -329,25 +329,37 @@ async fn test_thumbnail_path_parsing() { if path.is_empty() { return ("".to_string(), 0, "".to_string()); } - + // Ищем последний underscore перед расширением let dot_pos = path.rfind('.'); - let name_part = if let Some(pos) = dot_pos { &path[..pos] } else { path }; - + let name_part = if let Some(pos) = dot_pos { + &path[..pos] + } else { + path + }; + // Ищем underscore для ширины if let Some(underscore_pos) = name_part.rfind('_') { let base = name_part[..underscore_pos].to_string(); let width_part = &name_part[underscore_pos + 1..]; - + if let Ok(width) = width_part.parse::() { - let ext = if let Some(pos) = dot_pos { path[pos + 1..].to_string() } else { "".to_string() }; + let ext = if let Some(pos) = dot_pos { + path[pos + 1..].to_string() + } else { + "".to_string() + }; return (base, width, ext); } } - + // Если не нашли ширину, возвращаем как есть let base = name_part.to_string(); - let ext = if let Some(pos) = dot_pos { path[pos + 1..].to_string() } else { "".to_string() }; + let ext = if let Some(pos) = dot_pos { + path[pos + 1..].to_string() + } else { + "".to_string() + }; (base, 0, ext) } @@ -356,7 +368,10 @@ async fn test_thumbnail_path_parsing() { ("image_300.jpg", ("image", 300, "jpg")), ("photo_800.png", ("photo", 800, "png")), ("document.pdf", ("document", 0, "pdf")), - ("file_with_underscore_but_no_width.gif", ("file_with_underscore_but_no_width", 0, "gif")), + ( + "file_with_underscore_but_no_width.gif", + ("file_with_underscore_but_no_width", 0, "gif"), + ), ("unsafe_1920x.jpg", ("unsafe_1920x", 0, "jpg")), ("unsafe_1920x.png", ("unsafe_1920x", 0, "png")), ("unsafe_1920x", ("unsafe_1920x", 0, "")), @@ -386,7 +401,7 @@ async fn test_image_format_detection() { "gif" => Ok(image::ImageFormat::Gif), "webp" => Ok(image::ImageFormat::WebP), "heic" | "heif" | "tiff" | "tif" => Ok(image::ImageFormat::Jpeg), - _ => Err(()) + _ => Err(()), } } use image::ImageFormat; @@ -430,14 +445,14 @@ async fn test_find_closest_width() { // Мокаем функцию find_closest_width для тестов fn find_closest_width(requested: u32) -> u32 { let available_widths = vec![100, 150, 200, 300, 400, 500, 600, 800]; - + if available_widths.contains(&requested) { return requested; } - + let mut closest = available_widths[0]; let mut min_diff = (requested as i32 - closest as i32).abs(); - + for &width in &available_widths[1..] { let diff = (requested as i32 - width as i32).abs(); if diff < min_diff { @@ -445,28 +460,28 @@ async fn test_find_closest_width() { closest = width; } } - + closest } let test_cases = vec![ - (100, 100), // Точное совпадение - (150, 150), // Точное совпадение - (200, 200), // Точное совпадение - (300, 300), // Точное совпадение - (400, 400), // Точное совпадение - (500, 500), // Точное совпадение - (600, 600), // Точное совпадение - (800, 800), // Точное совпадение - (120, 100), // Ближайшее к 100 (разница 20) - (180, 200), // Ближайшее к 200 (разница 20) - (250, 200), // Ближайшее к 200 (разница 50) - (350, 300), // Ближайшее к 300 (разница 50) - (450, 400), // Ближайшее к 400 (разница 50) - (550, 500), // Ближайшее к 500 (разница 50) - (700, 600), // Ближайшее к 600 (разница 100) - (1000, 800), // Больше максимального - возвращаем максимальный - (2000, 800), // Больше максимального - возвращаем максимальный + (100, 100), // Точное совпадение + (150, 150), // Точное совпадение + (200, 200), // Точное совпадение + (300, 300), // Точное совпадение + (400, 400), // Точное совпадение + (500, 500), // Точное совпадение + (600, 600), // Точное совпадение + (800, 800), // Точное совпадение + (120, 100), // Ближайшее к 100 (разница 20) + (180, 200), // Ближайшее к 200 (разница 20) + (250, 200), // Ближайшее к 200 (разница 50) + (350, 300), // Ближайшее к 300 (разница 50) + (450, 400), // Ближайшее к 400 (разница 50) + (550, 500), // Ближайшее к 500 (разница 50) + (700, 600), // Ближайшее к 600 (разница 100) + (1000, 800), // Больше максимального - возвращаем максимальный + (2000, 800), // Больше максимального - возвращаем максимальный ]; for (requested, expected) in test_cases { @@ -490,10 +505,10 @@ async fn test_lookup_functions() { "gif" => Some("image/gif"), "webp" => Some("image/webp"), "mp4" => Some("video/mp4"), - _ => None + _ => None, } } - + fn find_file_by_pattern(_pattern: &str) -> Option { Some("test_file.jpg".to_string()) } @@ -531,12 +546,18 @@ async fn test_s3_utils_functions() { async fn get_s3_filelist(_bucket: &str) -> Result, Box> { Ok(vec!["file1.jpg".to_string(), "file2.png".to_string()]) } - - async fn check_file_exists(_bucket: &str, _key: &str) -> Result> { + + async fn check_file_exists( + _bucket: &str, + _key: &str, + ) -> Result> { Ok(true) } - - async fn load_file_from_s3(_bucket: &str, _key: &str) -> Result, Box> { + + async fn load_file_from_s3( + _bucket: &str, + _key: &str, + ) -> Result, Box> { Ok(b"fake file content".to_vec()) } @@ -549,15 +570,18 @@ async fn test_s3_utils_functions() { #[test] async fn test_overlay_functions() { // Мокаем функцию generate_overlay для тестов - async fn generate_overlay(shout_id: &str, image_data: actix_web::web::Bytes) -> Result> { + async fn generate_overlay( + shout_id: &str, + image_data: actix_web::web::Bytes, + ) -> Result> { if image_data.is_empty() { return Err("Empty image data".into()); } - + if shout_id == "invalid_id" { return Ok(image_data); } - + Ok(image_data) } use actix_web::web::Bytes; @@ -565,16 +589,19 @@ async fn test_overlay_functions() { // Тестируем с пустыми данными let empty_bytes = Bytes::from(vec![]); let result = generate_overlay("123", empty_bytes).await; - + // Должен вернуть ошибку при попытке загрузить изображение из пустых данных assert!(result.is_err(), "Should fail with empty image data"); // Тестируем с некорректным shout_id let test_bytes = Bytes::from(b"fake image data".to_vec()); let result = generate_overlay("invalid_id", test_bytes).await; - + // Должен вернуть оригинальные данные при ошибке получения shout - assert!(result.is_ok(), "Should return original data when shout fetch fails"); + assert!( + result.is_ok(), + "Should return original data when shout fetch fails" + ); } /// Тест для проверки функций core.rs @@ -607,8 +634,11 @@ async fn test_auth_functions() { } Ok(123) } - - async fn user_added_file(_user_id: u32, _filename: &str) -> Result<(), Box> { + + async fn user_added_file( + _user_id: u32, + _filename: &str, + ) -> Result<(), Box> { Ok(()) } @@ -644,23 +674,23 @@ async fn test_handlers_functions() { async fn get_quota_handler() -> actix_web::HttpResponse { actix_web::HttpResponse::Ok().json(serde_json::json!({"quota": 1024})) } - + async fn increase_quota_handler() -> actix_web::HttpResponse { actix_web::HttpResponse::Ok().json(serde_json::json!({"status": "increased"})) } - + async fn set_quota_handler() -> actix_web::HttpResponse { actix_web::HttpResponse::Ok().json(serde_json::json!({"status": "set"})) } - + async fn proxy_handler() -> actix_web::HttpResponse { actix_web::HttpResponse::Ok().body("proxy response") } - + async fn serve_file() -> actix_web::HttpResponse { actix_web::HttpResponse::Ok().body("file content") } - + async fn upload_handler() -> actix_web::HttpResponse { actix_web::HttpResponse::Ok().json(serde_json::json!({"status": "uploaded"})) } @@ -679,35 +709,47 @@ async fn test_integration() { if path.is_empty() { return ("".to_string(), 0, "".to_string()); } - + // Ищем последний underscore перед расширением let dot_pos = path.rfind('.'); - let name_part = if let Some(pos) = dot_pos { &path[..pos] } else { path }; - + let name_part = if let Some(pos) = dot_pos { + &path[..pos] + } else { + path + }; + // Ищем underscore для ширины if let Some(underscore_pos) = name_part.rfind('_') { let base = name_part[..underscore_pos].to_string(); let width_part = &name_part[underscore_pos + 1..]; - + if let Ok(width) = width_part.parse::() { - let ext = if let Some(pos) = dot_pos { path[pos + 1..].to_string() } else { "".to_string() }; + let ext = if let Some(pos) = dot_pos { + path[pos + 1..].to_string() + } else { + "".to_string() + }; return (base, width, ext); } } - + // Если не нашли ширину, возвращаем как есть let base = name_part.to_string(); - let ext = if let Some(pos) = dot_pos { path[pos + 1..].to_string() } else { "".to_string() }; + let ext = if let Some(pos) = dot_pos { + path[pos + 1..].to_string() + } else { + "".to_string() + }; (base, 0, ext) } - + fn get_mime_type(ext: &str) -> Option<&'static str> { match ext.to_lowercase().as_str() { "jpg" | "jpeg" => Some("image/jpeg"), "png" => Some("image/png"), "gif" => Some("image/gif"), "webp" => Some("image/webp"), - _ => None + _ => None, } } @@ -729,32 +771,44 @@ async fn test_edge_cases() { if path.is_empty() { return ("".to_string(), 0, "".to_string()); } - + if path == "." || path == ".." { return (path.to_string(), 0, "".to_string()); } - + // Ищем последний underscore перед расширением let dot_pos = path.rfind('.'); - let name_part = if let Some(pos) = dot_pos { &path[..pos] } else { path }; - + let name_part = if let Some(pos) = dot_pos { + &path[..pos] + } else { + path + }; + // Ищем underscore для ширины if let Some(underscore_pos) = name_part.rfind('_') { let base = name_part[..underscore_pos].to_string(); let width_part = &name_part[underscore_pos + 1..]; - + if let Ok(width) = width_part.parse::() { - let ext = if let Some(pos) = dot_pos { path[pos + 1..].to_string() } else { "".to_string() }; + let ext = if let Some(pos) = dot_pos { + path[pos + 1..].to_string() + } else { + "".to_string() + }; return (base, width, ext); } } - + // Если не нашли ширину, возвращаем как есть let base = name_part.to_string(); - let ext = if let Some(pos) = dot_pos { path[pos + 1..].to_string() } else { "".to_string() }; + let ext = if let Some(pos) = dot_pos { + path[pos + 1..].to_string() + } else { + "".to_string() + }; (base, 0, ext) } - + // Тестируем пустые строки assert_eq!(parse_file_path(""), ("".to_string(), 0, "".to_string())); assert_eq!(parse_file_path("."), (".".to_string(), 0, "".to_string())); @@ -779,31 +833,43 @@ async fn test_edge_cases() { #[test] async fn test_parsing_performance() { use std::time::Instant; - + // Мокаем функцию parse_file_path для теста производительности fn parse_file_path(path: &str) -> (String, u32, String) { if path.is_empty() { return ("".to_string(), 0, "".to_string()); } - + // Ищем последний underscore перед расширением let dot_pos = path.rfind('.'); - let name_part = if let Some(pos) = dot_pos { &path[..pos] } else { path }; - + let name_part = if let Some(pos) = dot_pos { + &path[..pos] + } else { + path + }; + // Ищем underscore для ширины if let Some(underscore_pos) = name_part.rfind('_') { let base = name_part[..underscore_pos].to_string(); let width_part = &name_part[underscore_pos + 1..]; - + if let Ok(width) = width_part.parse::() { - let ext = if let Some(pos) = dot_pos { path[pos + 1..].to_string() } else { "".to_string() }; + let ext = if let Some(pos) = dot_pos { + path[pos + 1..].to_string() + } else { + "".to_string() + }; return (base, width, ext); } } - + // Если не нашли ширину, возвращаем как есть let base = name_part.to_string(); - let ext = if let Some(pos) = dot_pos { path[pos + 1..].to_string() } else { "".to_string() }; + let ext = if let Some(pos) = dot_pos { + path[pos + 1..].to_string() + } else { + "".to_string() + }; (base, 0, ext) } diff --git a/tests/handler_tests.rs b/tests/handler_tests.rs index a9bd683..dd9da3d 100644 --- a/tests/handler_tests.rs +++ b/tests/handler_tests.rs @@ -1,8 +1,7 @@ -use actix_web::{test, web, App, HttpRequest, HttpResponse, Error as ActixError}; use actix_web::http::StatusCode; +use actix_web::{App, Error as ActixError, HttpRequest, HttpResponse, test, web}; use serde_json::json; - // Мокаем необходимые структуры и функции для тестов /// Мок для Redis соединения @@ -36,7 +35,11 @@ impl MockAppState { Ok(1024 * 1024) // 1MB } - async fn increase_user_quota(&self, _user_id: &str, _additional_bytes: u64) -> Result { + async fn increase_user_quota( + &self, + _user_id: &str, + _additional_bytes: u64, + ) -> Result { Ok(2 * 1024 * 1024) // 2MB } @@ -64,12 +67,13 @@ async fn test_get_quota_handler() { async fn get_quota_handler() -> actix_web::HttpResponse { actix_web::HttpResponse::Ok().json(serde_json::json!({"quota": 1024})) } - + let app = test::init_service( App::new() .app_data(web::Data::new(MockAppState::new())) - .route("/quota", web::get().to(get_quota_handler)) - ).await; + .route("/quota", web::get().to(get_quota_handler)), + ) + .await; // Тест без авторизации let req = test::TestRequest::get() @@ -98,12 +102,13 @@ async fn test_increase_quota_handler() { async fn increase_quota_handler() -> actix_web::HttpResponse { actix_web::HttpResponse::Ok().json(serde_json::json!({"status": "increased"})) } - + let app = test::init_service( App::new() .app_data(web::Data::new(MockAppState::new())) - .route("/quota/increase", web::post().to(increase_quota_handler)) - ).await; + .route("/quota/increase", web::post().to(increase_quota_handler)), + ) + .await; // Тест без авторизации let req = test::TestRequest::post() @@ -132,17 +137,16 @@ async fn test_set_quota_handler() { async fn set_quota_handler() -> actix_web::HttpResponse { actix_web::HttpResponse::Ok().json(serde_json::json!({"status": "set"})) } - + let app = test::init_service( App::new() .app_data(web::Data::new(MockAppState::new())) - .route("/quota/set", web::post().to(set_quota_handler)) - ).await; + .route("/quota/set", web::post().to(set_quota_handler)), + ) + .await; // Тест без авторизации - let req = test::TestRequest::post() - .uri("/quota/set") - .to_request(); + let req = test::TestRequest::post().uri("/quota/set").to_request(); let resp = test::call_service(&app, req).await; // Мок возвращает успешный ответ даже без авторизации @@ -166,17 +170,16 @@ async fn test_upload_handler() { async fn upload_handler() -> actix_web::HttpResponse { actix_web::HttpResponse::Ok().json(serde_json::json!({"status": "uploaded"})) } - + let app = test::init_service( App::new() .app_data(web::Data::new(MockAppState::new())) - .route("/", web::post().to(upload_handler)) - ).await; + .route("/", web::post().to(upload_handler)), + ) + .await; // Тест без авторизации - let req = test::TestRequest::post() - .uri("/") - .to_request(); + let req = test::TestRequest::post().uri("/").to_request(); let resp = test::call_service(&app, req).await; // Мок возвращает успешный ответ даже без авторизации @@ -200,12 +203,13 @@ async fn test_proxy_handler() { async fn proxy_handler() -> actix_web::HttpResponse { actix_web::HttpResponse::Ok().body("proxy response") } - + let app = test::init_service( App::new() .app_data(web::Data::new(MockAppState::new())) - .route("/{path:.*}", web::get().to(proxy_handler)) - ).await; + .route("/{path:.*}", web::get().to(proxy_handler)), + ) + .await; // Тест с несуществующим файлом let req = test::TestRequest::get() @@ -221,12 +225,16 @@ async fn test_proxy_handler() { #[actix_web::test] async fn test_serve_file() { // Мокаем функцию serve_file - async fn serve_file(_path: &str, _app_state: &MockAppState, _user_id: &str) -> Result { + async fn serve_file( + _path: &str, + _app_state: &MockAppState, + _user_id: &str, + ) -> Result { Err(actix_web::error::ErrorNotFound("File not found")) } - + let app_state = MockAppState::new(); - + // Тест с пустым путем let result = serve_file("", &app_state, "").await; assert!(result.is_err()); @@ -243,12 +251,16 @@ async fn test_handler_error_handling() { let app = test::init_service( App::new() .app_data(web::Data::new(MockAppState::new())) - .route("/test", web::get().to(|_req: HttpRequest| async { - Err::( - actix_web::error::ErrorInternalServerError("Test error") - ) - })) - ).await; + .route( + "/test", + web::get().to(|_req: HttpRequest| async { + Err::(actix_web::error::ErrorInternalServerError( + "Test error", + )) + }), + ), + ) + .await; let req = test::TestRequest::get().uri("/test").to_request(); let resp = test::call_service(&app, req).await; @@ -261,19 +273,21 @@ async fn test_cors_headers() { let app = test::init_service( App::new() .app_data(web::Data::new(MockAppState::new())) - .route("/test", web::get().to(|_req: HttpRequest| async { - Ok::( - HttpResponse::Ok().body("test") - ) - })) - .wrap(actix_cors::Cors::default().allow_any_origin()) - ).await; + .route( + "/test", + web::get().to(|_req: HttpRequest| async { + Ok::(HttpResponse::Ok().body("test")) + }), + ) + .wrap(actix_cors::Cors::default().allow_any_origin()), + ) + .await; let req = test::TestRequest::get().uri("/test").to_request(); let resp = test::call_service(&app, req).await; - + assert!(resp.status().is_success()); - + // Проверяем наличие CORS headers let headers = resp.headers(); // В тестовой среде CORS headers могут не добавляться автоматически @@ -286,23 +300,26 @@ async fn test_cors_headers() { async fn test_http_methods() { let app = test::init_service( App::new() - .route("/test", web::get().to(|_req: HttpRequest| async { - Ok::( - HttpResponse::Ok().body("GET method") - ) - })) - .route("/test", web::post().to(|_req: HttpRequest| async { - Ok::( - HttpResponse::Ok().body("POST method") - ) - })) - ).await; + .route( + "/test", + web::get().to(|_req: HttpRequest| async { + Ok::(HttpResponse::Ok().body("GET method")) + }), + ) + .route( + "/test", + web::post().to(|_req: HttpRequest| async { + Ok::(HttpResponse::Ok().body("POST method")) + }), + ), + ) + .await; // Тест GET метода let req = test::TestRequest::get().uri("/test").to_request(); let resp = test::call_service(&app, req).await; assert!(resp.status().is_success()); - + let body = test::read_body(resp).await; assert_eq!(body, "GET method"); @@ -310,7 +327,7 @@ async fn test_http_methods() { let req = test::TestRequest::post().uri("/test").to_request(); let resp = test::call_service(&app, req).await; assert!(resp.status().is_success()); - + let body = test::read_body(resp).await; assert_eq!(body, "POST method"); } @@ -318,23 +335,22 @@ async fn test_http_methods() { /// Тест для проверки query параметров #[actix_web::test] async fn test_query_parameters() { - let app = test::init_service( - App::new() - .route("/test", web::get().to(|req: HttpRequest| async move { - let query_string = req.query_string().to_string(); - Ok::( - HttpResponse::Ok().body(query_string) - ) - })) - ).await; + let app = test::init_service(App::new().route( + "/test", + web::get().to(|req: HttpRequest| async move { + let query_string = req.query_string().to_string(); + Ok::(HttpResponse::Ok().body(query_string)) + }), + )) + .await; let req = test::TestRequest::get() .uri("/test?param1=value1¶m2=value2") .to_request(); - + let resp = test::call_service(&app, req).await; assert!(resp.status().is_success()); - + let body = test::read_body(resp).await; assert_eq!(body, "param1=value1¶m2=value2"); } @@ -342,29 +358,29 @@ async fn test_query_parameters() { /// Тест для проверки headers #[actix_web::test] async fn test_headers() { - let app = test::init_service( - App::new() - .route("/test", web::get().to(|req: HttpRequest| async move { - let user_agent = req.headers() - .get("user-agent") - .and_then(|h| h.to_str().ok()) - .unwrap_or("unknown") - .to_string(); - - Ok::( - HttpResponse::Ok().body(user_agent) - ) - })) - ).await; + let app = test::init_service(App::new().route( + "/test", + web::get().to(|req: HttpRequest| async move { + let user_agent = req + .headers() + .get("user-agent") + .and_then(|h| h.to_str().ok()) + .unwrap_or("unknown") + .to_string(); + + Ok::(HttpResponse::Ok().body(user_agent)) + }), + )) + .await; let req = test::TestRequest::get() .uri("/test") .insert_header(("user-agent", "test-agent")) .to_request(); - + let resp = test::call_service(&app, req).await; assert!(resp.status().is_success()); - + let body = test::read_body(resp).await; assert_eq!(body, "test-agent"); } @@ -372,31 +388,30 @@ async fn test_headers() { /// Тест для проверки JSON responses #[actix_web::test] async fn test_json_responses() { - let app = test::init_service( - App::new() - .route("/test", web::get().to(|_req: HttpRequest| async { - let data = json!({ - "status": "success", - "message": "test message", - "data": { - "id": 123, - "name": "test" - } - }); - - Ok::( - HttpResponse::Ok().json(data) - ) - })) - ).await; + let app = test::init_service(App::new().route( + "/test", + web::get().to(|_req: HttpRequest| async { + let data = json!({ + "status": "success", + "message": "test message", + "data": { + "id": 123, + "name": "test" + } + }); + + Ok::(HttpResponse::Ok().json(data)) + }), + )) + .await; let req = test::TestRequest::get().uri("/test").to_request(); let resp = test::call_service(&app, req).await; assert!(resp.status().is_success()); - + let body = test::read_body(resp).await; let response_data: serde_json::Value = serde_json::from_slice(&body).unwrap(); - + assert_eq!(response_data["status"], "success"); assert_eq!(response_data["message"], "test message"); assert_eq!(response_data["data"]["id"], 123); @@ -408,41 +423,54 @@ async fn test_json_responses() { async fn test_content_types() { let app = test::init_service( App::new() - .route("/text", web::get().to(|_req: HttpRequest| async { - Ok::( - HttpResponse::Ok() - .content_type("text/plain") - .body("plain text") - ) - })) - .route("/html", web::get().to(|_req: HttpRequest| async { - Ok::( - HttpResponse::Ok() - .content_type("text/html") - .body("test") - ) - })) - .route("/json", web::get().to(|_req: HttpRequest| async { - Ok::( - HttpResponse::Ok() - .content_type("application/json") - .json(json!({"test": "data"})) - ) - })) - ).await; + .route( + "/text", + web::get().to(|_req: HttpRequest| async { + Ok::( + HttpResponse::Ok() + .content_type("text/plain") + .body("plain text"), + ) + }), + ) + .route( + "/html", + web::get().to(|_req: HttpRequest| async { + Ok::( + HttpResponse::Ok() + .content_type("text/html") + .body("test"), + ) + }), + ) + .route( + "/json", + web::get().to(|_req: HttpRequest| async { + Ok::( + HttpResponse::Ok() + .content_type("application/json") + .json(json!({"test": "data"})), + ) + }), + ), + ) + .await; // Тест text/plain let req = test::TestRequest::get().uri("/text").to_request(); let resp = test::call_service(&app, req).await; assert_eq!(resp.headers().get("content-type").unwrap(), "text/plain"); - + // Тест text/html let req = test::TestRequest::get().uri("/html").to_request(); let resp = test::call_service(&app, req).await; assert_eq!(resp.headers().get("content-type").unwrap(), "text/html"); - + // Тест application/json let req = test::TestRequest::get().uri("/json").to_request(); let resp = test::call_service(&app, req).await; - assert_eq!(resp.headers().get("content-type").unwrap(), "application/json"); + assert_eq!( + resp.headers().get("content-type").unwrap(), + "application/json" + ); }