diff --git a/.vscode/launch.json b/.vscode/launch.json index 2eb7256..cd66e91 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -18,7 +18,7 @@ "args": [], "cwd": "${workspaceFolder}", "env": { - "RUST_LOG": "debug", + "RUST_LOG": "info", "DATABASE_URL": "postgres://gbuser:gbpassword@localhost:5432/generalbots", "REDIS_URL": "redis://localhost:6379" } @@ -42,7 +42,9 @@ "args": [ "--test-threads=1" ], - "cwd": "${workspaceFolder}" + "cwd": "${workspaceFolder}", "env": { + "RUST_LOG": "info" + } }, { "type": "lldb", @@ -61,7 +63,9 @@ } }, "args": [], - "cwd": "${workspaceFolder}" + "cwd": "${workspaceFolder}", "env": { + "RUST_LOG": "info" + } }, ], "compounds": [ diff --git a/.vscode/settings.json b/.vscode/settings.json index 9e8b448..4ca308b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,6 @@ "lldb.executable": "/usr/bin/lldb", "lldb.showDisassembly": "never", "lldb.dereferencePointers": true, - "lldb.consoleMode": "commands" + "lldb.consoleMode": "commands", + "rust-test Explorer.cargoTestExtraArgs": ["--", "--nocapture"] } \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index f470116..5994d8a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2334,6 +2334,19 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + [[package]] name = "env_logger" version = "0.11.6" @@ -2761,6 +2774,7 @@ dependencies = [ "thiserror 1.0.69", "tokio", "tokio-openssl", + "tokio-stream", "tokio-test", "tower 0.4.13", "tower-http", @@ -2872,10 +2886,12 @@ dependencies = [ "actix-multipart", "actix-web", "async-trait", + "env_logger 0.10.2", "futures 0.3.31", "gb-core", "jsonwebtoken", "lettre", + "log", "minio", "rstest", "sanitize-filename", @@ -2884,6 +2900,7 @@ dependencies = [ "tempfile", "thiserror 1.0.69", "tokio", + "tokio-stream", "tokio-test", "tracing", "uuid", @@ -3080,6 +3097,7 @@ dependencies = [ "sqlx", "tempfile", "tokio", + "tokio-stream", "tokio-tungstenite 0.24.0", "tracing", "tungstenite 0.20.1", @@ -4614,7 +4632,7 @@ dependencies = [ "crc", "dashmap 6.1.0", "derivative", - "env_logger", + "env_logger 0.11.6", "futures-util", "hex", "hmac", @@ -4724,12 +4742,6 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" -[[package]] -name = "multimap" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1a5d38b9b352dbd913288736af36af41c48d61b1a8cd34bcecd727561b7d511" - [[package]] name = "multimap" version = "0.10.0" @@ -6064,7 +6076,7 @@ dependencies = [ "heck 0.5.0", "itertools 0.13.0", "log", - "multimap 0.9.1", + "multimap 0.10.0", "once_cell", "petgraph 0.6.5", "prettyplease 0.2.30", diff --git a/Cargo.toml b/Cargo.toml index 98392d0..a7fd4c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,8 @@ futures = "0.3" futures-util = "0.3" # Add futures-util here parking_lot = "0.12" bytes = "1.0" +log = "0.4" +env_logger = "0.10" # Web framework and servers axum = { version = "0.7.9", features = ["ws", "multipart"] } diff --git a/gb-auth/Cargo.toml b/gb-auth/Cargo.toml index d07625b..dda7052 100644 --- a/gb-auth/Cargo.toml +++ b/gb-auth/Cargo.toml @@ -47,6 +47,7 @@ axum-extra = { version = "0.7" } # Add headers feature tower = "0.4" tower-http = { version = "0.5", features = ["auth", "cors", "trace"] } headers = "0.3" +tokio-stream = { workspace = true } [dev-dependencies] rstest = "0.18" diff --git a/gb-core/src/models.rs b/gb-core/src/models.rs index b7e9d76..7ce43dc 100644 --- a/gb-core/src/models.rs +++ b/gb-core/src/models.rs @@ -19,18 +19,16 @@ pub struct CoreError(pub String); pub enum CustomerStatus { Active, Inactive, - Suspended + Suspended, } #[derive(Debug, Clone, Serialize, Deserialize)] pub enum SubscriptionTier { Free, Pro, - Enterprise + Enterprise, } - - #[derive(Debug, Serialize, Deserialize)] pub struct Instance { pub id: Uuid, @@ -118,7 +116,7 @@ impl FromStr for UserStatus { "active" => Ok(UserStatus::Active), "inactive" => Ok(UserStatus::Inactive), "suspended" => Ok(UserStatus::Suspended), - _ => Ok(UserStatus::Inactive) + _ => Ok(UserStatus::Inactive), } } } @@ -146,8 +144,6 @@ pub struct User { pub created_at: DateTime, } - - // Update the Customer struct to include these fields #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Customer { @@ -155,16 +151,16 @@ pub struct Customer { pub name: String, pub max_instances: u32, pub email: String, - pub status: CustomerStatus, // Add this field - pub subscription_tier: SubscriptionTier, // Add this field + pub status: CustomerStatus, // Add this field + pub subscription_tier: SubscriptionTier, // Add this field pub created_at: DateTime, pub updated_at: DateTime, } impl Customer { pub fn new( - name: String, - email: String, + name: String, + email: String, subscription_tier: SubscriptionTier, max_instances: u32, ) -> Self { @@ -174,7 +170,7 @@ impl Customer { email, max_instances, subscription_tier, - status: CustomerStatus::Active, // Default to Active + status: CustomerStatus::Active, // Default to Active created_at: Utc::now(), updated_at: Utc::now(), } @@ -238,15 +234,17 @@ pub struct FileInfo { pub created_at: DateTime, } +// App state shared across all handlers + // App state shared across all handlers pub struct AppState { - pub config: AppConfig, - pub db_pool: PgPool, - pub redis_pool: RedisConnectionManager, - pub kafka_producer: FutureProducer, - // pub zitadel_client: AuthServiceClient, - pub minio_client: MinioClient, + pub minio_client: Option, + pub config: Option, + pub db_pool: Option, + pub redis_pool: Option, + pub kafka_producer: Option, + //pub zitadel_client: Option, } // File models @@ -288,7 +286,7 @@ pub struct Conversation { pub struct ConversationMember { pub conversation_id: Uuid, pub user_id: Uuid, - pub joined_at: DateTime + pub joined_at: DateTime, } // Calendar models @@ -348,36 +346,33 @@ pub struct ApiResponse { pub enum AppError { #[error("Database error: {0}")] Database(#[from] sqlx::Error), - + #[error("Redis error: {0}")] Redis(#[from] redis::RedisError), - + #[error("Kafka error: {0}")] Kafka(String), - + #[error("Zitadel error: {0}")] Zitadel(#[from] tonic::Status), - + #[error("Minio error: {0}")] Minio(String), - + #[error("Validation error: {0}")] Validation(String), - + #[error("Not found: {0}")] NotFound(String), - + #[error("Unauthorized: {0}")] Unauthorized(String), - + #[error("Forbidden: {0}")] Forbidden(String), - + #[error("Internal server error: {0}")] Internal(String), - - - } impl actix_web::ResponseError for AppError { @@ -385,7 +380,9 @@ impl actix_web::ResponseError for AppError { let (status, error_message) = match self { AppError::Validation(_) => (actix_web::http::StatusCode::BAD_REQUEST, self.to_string()), AppError::NotFound(_) => (actix_web::http::StatusCode::NOT_FOUND, self.to_string()), - AppError::Unauthorized(_) => (actix_web::http::StatusCode::UNAUTHORIZED, self.to_string()), + AppError::Unauthorized(_) => { + (actix_web::http::StatusCode::UNAUTHORIZED, self.to_string()) + } AppError::Forbidden(_) => (actix_web::http::StatusCode::FORBIDDEN, self.to_string()), _ => ( actix_web::http::StatusCode::INTERNAL_SERVER_ERROR, diff --git a/gb-file/Cargo.toml b/gb-file/Cargo.toml index d26e6bc..3d0b223 100644 --- a/gb-file/Cargo.toml +++ b/gb-file/Cargo.toml @@ -22,6 +22,9 @@ actix-web ={ workspace = true } actix-multipart ={ workspace = true } sanitize-filename = { workspace = true } tempfile = { workspace = true } +log = { workspace = true } +env_logger = { workspace = true } +tokio-stream = { workspace = true } [dev-dependencies] rstest= { workspace = true } diff --git a/gb-file/src/handlers.rs b/gb-file/src/handlers.rs index 6431753..98c2a01 100644 --- a/gb-file/src/handlers.rs +++ b/gb-file/src/handlers.rs @@ -1,13 +1,15 @@ use actix_multipart::Multipart; -use futures::TryStreamExt; -use gb_core::models::AppState; -use std::io::Write; -use gb_core::models::AppError; -use gb_core::utils::{create_response, extract_user_id}; use actix_web::{post, web, HttpRequest, HttpResponse}; -use tempfile::NamedTempFile; +use gb_core::models::AppError; +use gb_core::models::AppState; +use gb_core::utils::{create_response, extract_user_id}; use minio::s3::builders::ObjectContent; use minio::s3::Client; +use std::io::Write; +use tempfile::NamedTempFile; +use minio::s3::types::ToStream; +use tokio_stream::StreamExt; + #[post("/files/upload/{folder_path}")] pub async fn upload_file( @@ -17,24 +19,29 @@ pub async fn upload_file( ) -> Result { let folder_path = folder_path.into_inner(); - // Create a temporary file to store the uploaded file + // Create a temporary file to store the uploaded file. + let mut temp_file = NamedTempFile::new().map_err(|e| { actix_web::error::ErrorInternalServerError(format!("Failed to create temp file: {}", e)) })?; let mut file_name = None; - // Iterate over the multipart stream + // Iterate over the multipart stream. + while let Some(mut field) = payload.try_next().await? { let content_disposition = field.content_disposition(); file_name = content_disposition .get_filename() .map(|name| name.to_string()); - // Write the file content to the temporary file + // Write the file content to the temporary file. while let Some(chunk) = field.try_next().await? { temp_file.write_all(&chunk).map_err(|e| { - actix_web::error::ErrorInternalServerError(format!("Failed to write to temp file: {}", e)) + actix_web::error::ErrorInternalServerError(format!( + "Failed to write to temp file: {}", + e + )) })?; } } @@ -46,7 +53,7 @@ pub async fn upload_file( let object_name = format!("{}/{}", folder_path, file_name); // Upload the file to the MinIO bucket - let client: Client = state.minio_client.clone(); + let client: Client = state.minio_client.clone().unwrap(); let bucket_name = "file-upload-rust-bucket"; let content = ObjectContent::from(temp_file.path()); @@ -79,10 +86,46 @@ pub async fn delete_file( _file_path: web::Json, ) -> Result { let _user_id = extract_user_id(&req)?; - - + Ok(create_response( true, Some("File deleted successfully".to_string()), )) } +#[post("/files/list/{folder_path}")] +pub async fn list_file( + folder_path: web::Path, + state: web::Data, +) -> Result { + let folder_path = folder_path.into_inner(); + + let client: Client = state.minio_client.clone().unwrap(); + let bucket_name = "file-upload-rust-bucket"; + + // Create the stream using the to_stream() method + let mut objects_stream = client + .list_objects(bucket_name) + .prefix(Some(folder_path)) + .to_stream() + .await; + + let mut file_list = Vec::new(); + + // Use StreamExt::next() to iterate through the stream + while let Some(items) = objects_stream.next().await { + match items { + Ok(result) => { + for item in result.contents { + file_list.push(item.name); + } + }, + Err(e) => { + return Err(actix_web::error::ErrorInternalServerError( + format!("Failed to list files in MinIO: {}", e) + )); + } + } + } + + Ok(HttpResponse::Ok().json(file_list)) +} \ No newline at end of file diff --git a/gb-server/src/main.rs b/gb-server/src/main.rs index 6795da4..e31ebff 100644 --- a/gb-server/src/main.rs +++ b/gb-server/src/main.rs @@ -26,11 +26,11 @@ async fn main() -> std::io::Result<()> { let minio_client = init_minio(&config).await.expect("Failed to initialize Minio"); let app_state = web::Data::new(models::AppState { - config: config.clone(), - db_pool, - redis_pool, - kafka_producer, - minio_client, + config: Some(config.clone()), + db_pool: Some(db_pool), + redis_pool: Some(redis_pool), + kafka_producer: Some(kafka_producer), + minio_client: Some(minio_client), }); // Start HTTP server diff --git a/gb-testing/Cargo.toml b/gb-testing/Cargo.toml index 43945bb..21a4a79 100644 --- a/gb-testing/Cargo.toml +++ b/gb-testing/Cargo.toml @@ -27,6 +27,7 @@ criterion = { workspace = true, features = ["async_futures"] } # Async Runtime tokio = { workspace = true } +tokio-stream= { workspace = true } async-trait = { workspace = true } # HTTP Client diff --git a/gb-testing/src/chaos/mod.rs b/gb-testing/src/chaos/mod.rs index 31a9384..2becfeb 100644 --- a/gb-testing/src/chaos/mod.rs +++ b/gb-testing/src/chaos/mod.rs @@ -1,12 +1,11 @@ pub struct ChaosTest { - namespace: String, } impl ChaosTest { pub async fn new(namespace: String) -> anyhow::Result { // Initialize the ChaosTest struct - Ok(ChaosTest { namespace }) + Ok(ChaosTest { }) } pub async fn network_partition(&self) -> anyhow::Result<()> { diff --git a/gb-testing/tests/file_upload_test.rs b/gb-testing/tests/file_upload_test.rs index e096ea2..1cfe4a7 100644 --- a/gb-testing/tests/file_upload_test.rs +++ b/gb-testing/tests/file_upload_test.rs @@ -1,14 +1,10 @@ use actix_web::{test, web, App}; use anyhow::Result; -use async_trait::async_trait; use bytes::Bytes; use gb_core::models::AppState; use gb_file::handlers::upload_file; -use gb_testing::integration::{IntegrationTest, IntegrationTestCase}; -use minio::s3::args::{ - BucketExistsArgs, GetObjectArgs, MakeBucketArgs, RemoveObjectArgs, StatObjectArgs, -}; -use minio::s3::client::{Client as MinioClient, ClientBuilder as MinioClientBuilder}; +use minio::s3::args::{BucketExistsArgs, GetObjectArgs, MakeBucketArgs, StatObjectArgs}; +use minio::s3::client::ClientBuilder as MinioClientBuilder; use minio::s3::creds::StaticProvider; use minio::s3::http::BaseUrl; use std::fs::File; @@ -17,13 +13,10 @@ use std::io::Write; use std::str::FromStr; use tempfile::NamedTempFile; - #[tokio::test] - async fn test_successful_file_upload() -> Result<()> { - // Setup test environment and MinIO client - let base_url = format!("https://{}", "localhost:9000"); + let base_url = format!("http://{}", "localhost:9000"); let base_url = BaseUrl::from_str(&base_url)?; let credentials = StaticProvider::new(&"minioadmin", &"minioadmin", None); @@ -45,14 +38,16 @@ async fn test_successful_file_upload() -> Result<()> { } let app_state = web::Data::new(AppState { - minio_client, - config: todo!(), - db_pool: todo!(), - redis_pool: todo!(), - kafka_producer: todo!(), + minio_client: Some(minio_client.clone()), + config: None, + db_pool: None, + kafka_producer: None, + redis_pool: None, }); - let app = test::init_service(App::new().app_data(app_state.clone()).service(upload_file)).await; + let app = + test::init_service(App::new().app_data(app_state.clone()) + .service(upload_file)).await; // Create a test file with content let mut temp_file = NamedTempFile::new()?; @@ -91,17 +86,13 @@ async fn test_successful_file_upload() -> Result<()> { // Using object-based API for stat_object let stat_object_args = StatObjectArgs::new(bucket_name, object_name)?; - let object_exists = - minio_client - .stat_object(&stat_object_args) - .await - .is_ok(); + let object_exists = minio_client.clone().stat_object(&stat_object_args).await.is_ok(); assert!(object_exists, "Uploaded file should exist in MinIO"); // Verify file content using object-based API - let get_object_args = GetObjectArgs::new(bucket_name, object_name)?; - let get_object_result = minio_client.get_object(bucket_name, object_name); + // let get_object_args = GetObjectArgs::new(bucket_name, object_name)?; + // let get_object_result = minio_client.get_object(bucket_name, object_name); // let mut object_content = Vec::new(); // get_object_result.read_to_end(&mut object_content)?; diff --git a/gb-testing/tests/int_file_list_test.rs b/gb-testing/tests/int_file_list_test.rs new file mode 100644 index 0000000..c389bf8 --- /dev/null +++ b/gb-testing/tests/int_file_list_test.rs @@ -0,0 +1,110 @@ +use actix_web::{test, web, App}; +use anyhow::Result; +use bytes::Bytes; +use gb_core::models::AppState; +use gb_file::handlers::list_file; +use minio::s3::args::{BucketExistsArgs, MakeBucketArgs}; +use minio::s3::builders::SegmentedBytes; +use minio::s3::client::ClientBuilder as MinioClientBuilder; +use minio::s3::creds::StaticProvider; +use minio::s3::http::BaseUrl; +use minio::s3::types::ToStream; +use std::fs::File; +use std::io::Read; +use std::io::Write; +use std::str::FromStr; +use tempfile::NamedTempFile; +use tokio_stream::StreamExt; + + +#[tokio::test] + +async fn test_successful_file_listing() -> Result<(), Box> { + // Setup test environment and MinIO client + let base_url = format!("http://{}", "localhost:9000"); + let base_url = BaseUrl::from_str(&base_url)?; + let credentials = StaticProvider::new("minioadmin", "minioadmin", None); + + let minio_client = MinioClientBuilder::new(base_url.clone()) + .provider(Some(Box::new(credentials))) + .build()?; + + // Create test bucket if it doesn't exist + let bucket_name = "file-upload-rust-bucket"; + + // Using object-based API for bucket_exists + let bucket_exists_args = BucketExistsArgs::new(bucket_name)?; + let bucket_exists = minio_client.bucket_exists(&bucket_exists_args).await?; + + if !bucket_exists { + // Using object-based API for make_bucket + let make_bucket_args = MakeBucketArgs::new(bucket_name)?; + minio_client.make_bucket(&make_bucket_args).await?; + } + + // Put a single file in the bucket + let folder_path = "test-folder"; + let file_name = "test.txt"; + let object_name = format!("{}/{}", folder_path, file_name); + + // Create a temporary file with some content + let mut temp_file = NamedTempFile::new()?; + writeln!(temp_file, "This is a test file.")?; + + // Upload the file to the bucket + let mut file = File::open(temp_file.path())?; + let mut buffer = Vec::new(); + file.read_to_end(&mut buffer)?; + let content = SegmentedBytes::from(Bytes::from(buffer)); + minio_client.put_object(bucket_name, &object_name, content); + + let app_state = web::Data::new(AppState { + minio_client: Some(minio_client.clone()), + config: None, + db_pool: None, + kafka_producer: None, + redis_pool: None, + }); + + let app = test::init_service(App::new().app_data(app_state.clone()).service(list_file)).await; + + // Execute request to list files in the folder + let req = test::TestRequest::post() + .uri(&format!("/files/list/{}", folder_path)) + .to_request(); + + let resp = test::call_service(&app, req).await; + + // Verify response + assert_eq!(resp.status(), 200); + + // Parse the response body as JSON + let body = test::read_body(resp).await; + let file_list: Vec = serde_json::from_slice(&body)?; + + // Verify the uploaded file is in the list + assert!( + file_list.contains(&object_name), + "Uploaded file should be listed" + ); + + // List all objects in a directory. + let mut list_objects = minio_client + .list_objects("my-bucket") + .use_api_v1(true) + .recursive(true) + .to_stream() + .await; + while let Some(result) = list_objects.next().await { + match result { + Ok(resp) => { + for item in resp.contents { + println!("{:?}", item); + } + } + Err(e) => println!("Error: {:?}", e), + } + } + + Ok(()) +} diff --git a/gb-testing/tests/load_auth_test.rs b/gb-testing/tests/load_auth_test.rs index a6ba23f..0ce4615 100644 --- a/gb-testing/tests/load_auth_test.rs +++ b/gb-testing/tests/load_auth_test.rs @@ -1,9 +1,9 @@ -use gb_testing::load::{LoadTest, LoadTestConfig}; +use gb_testing::load::LoadTestConfig; use std::time::Duration; #[tokio::test] async fn test_auth_load() -> anyhow::Result<()> { - let config = LoadTestConfig { + let _config = LoadTestConfig { users: 100, duration: Duration::from_secs(300), ramp_up: Duration::from_secs(60),