diff --git a/api/libs/handlers/src/messages/helpers/delete_message_handler.rs b/api/libs/handlers/src/messages/helpers/delete_message_handler.rs index b56cce658..5579280fd 100644 --- a/api/libs/handlers/src/messages/helpers/delete_message_handler.rs +++ b/api/libs/handlers/src/messages/helpers/delete_message_handler.rs @@ -1,20 +1,28 @@ use anyhow::{anyhow, Result}; use chrono::Utc; -use database::{models::Message, pool::get_pg_pool, schema::messages}; +use database::{ + models::Message, + pool::get_pg_pool, + schema::messages, + chats::fetch_chat_with_permission, + enums::AssetPermissionRole, +}; use diesel::prelude::*; use diesel_async::RunQueryDsl; use tracing::info; use middleware::AuthenticatedUser; +use sharing::check_permission_access; use uuid::Uuid; /// Deletes a message and marks all subsequent messages in the same chat as deleted /// /// # Arguments +/// * `user` - The authenticated user requesting the deletion /// * `message_id` - The ID of the message to delete /// /// # Returns /// * `Result<()>` - Success or error -pub async fn delete_message_handler(_user: AuthenticatedUser, message_id: Uuid) -> Result<()> { +pub async fn delete_message_handler(user: AuthenticatedUser, message_id: Uuid) -> Result<()> { let pool = get_pg_pool(); let mut conn = pool.get().await?; @@ -29,6 +37,34 @@ pub async fn delete_message_handler(_user: AuthenticatedUser, message_id: Uuid) Err(e) => return Err(anyhow!("Database error: {}", e)), }; + // Check if the user has permission to delete messages in this chat + let chat_with_permission = fetch_chat_with_permission(&message.chat_id, &user.id).await?; + + // If chat not found, return error + let chat_with_permission = match chat_with_permission { + Some(cwp) => cwp, + None => return Err(anyhow!("Chat not found")), + }; + + // Check if user has appropriate permissions (CanEdit, FullAccess, or Owner) + let has_permission = check_permission_access( + chat_with_permission.permission, + &[ + AssetPermissionRole::CanEdit, + AssetPermissionRole::FullAccess, + AssetPermissionRole::Owner, + ], + chat_with_permission.chat.organization_id, + &user.organizations, + ); + + // If user is the creator, they automatically have access + let is_creator = chat_with_permission.chat.created_by == user.id; + + if !has_permission && !is_creator { + return Err(anyhow!("You don't have permission to delete messages in this chat")); + } + // Mark the target message and all subsequent messages in the same chat as deleted let updated_count = diesel::update(messages::table) .filter( @@ -47,7 +83,5 @@ pub async fn delete_message_handler(_user: AuthenticatedUser, message_id: Uuid) "Deleted message and subsequent messages in chat" ); - // TODO: Add access controls to verify the user has permission to delete this message - Ok(()) } diff --git a/api/src/routes/rest/routes/chats/duplicate_chat.rs b/api/src/routes/rest/routes/chats/duplicate_chat.rs index 988566b98..e6a46b897 100644 --- a/api/src/routes/rest/routes/chats/duplicate_chat.rs +++ b/api/src/routes/rest/routes/chats/duplicate_chat.rs @@ -4,7 +4,7 @@ use axum::Extension; use handlers::chats::duplicate_chat_handler; use handlers::chats::types::ChatWithMessages; use middleware::AuthenticatedUser; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use uuid::Uuid; use crate::routes::rest::ApiResponse; @@ -18,36 +18,35 @@ pub struct DuplicateChatRequest { pub message_id: Option, } -/// Response containing the duplicated chat with messages -#[derive(Serialize, Deserialize)] -pub struct DuplicateChatResponse { - /// The duplicated chat with all its messages - pub chat: ChatWithMessages, -} - /// Handler for POST /chats/duplicate endpoint -/// +/// /// Duplicates an existing chat, including all messages and file references. /// If message_id is provided, only messages from that point onward are duplicated. /// Each file reference is duplicated with is_duplicate=true to track duplicated content. pub async fn duplicate_chat_route( Extension(user): Extension, Json(request): Json, -) -> Result, (StatusCode, String)> { +) -> Result, (StatusCode, String)> { // Call the handler function with the request parameters match duplicate_chat_handler(&request.id, request.message_id.as_ref(), &user).await { - Ok(chat) => Ok(ApiResponse::JsonData(DuplicateChatResponse { chat })), + Ok(chat) => Ok(ApiResponse::JsonData(chat)), Err(e) => { tracing::error!("Error duplicating chat: {}", e); - + // Return different error codes based on the type of error let error_msg = e.to_string(); if error_msg.contains("not found") { Err((StatusCode::NOT_FOUND, "Chat not found".to_string())) } else if error_msg.contains("permission") { - Err((StatusCode::FORBIDDEN, "You don't have permission to view this chat".to_string())) + Err(( + StatusCode::FORBIDDEN, + "You don't have permission to view this chat".to_string(), + )) } else { - Err((StatusCode::INTERNAL_SERVER_ERROR, "Failed to duplicate chat".to_string())) + Err(( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to duplicate chat".to_string(), + )) } } } @@ -61,21 +60,21 @@ mod tests { use axum::routing::post; use axum::Router; use chrono::Utc; - use middleware::{AuthenticatedUser, types::OrganizationMembership}; use database::{ enums::{AssetPermissionRole, AssetType, IdentityType, UserOrganizationRole}, models::{AssetPermission, Chat, Message, MessageToFile, User}, pool::get_pg_pool, schema::{asset_permissions, chats, messages, messages_to_files, users}, }; - use diesel::prelude::*; use diesel::insert_into; + use diesel::prelude::*; use diesel_async::RunQueryDsl; + use middleware::{types::OrganizationMembership, AuthenticatedUser}; use serde_json::{json, Value}; use std::collections::HashMap; use tower::ServiceExt; use uuid::Uuid; - + async fn setup_test_user() -> (AuthenticatedUser, User) { let user_id = Uuid::new_v4(); let user = User { @@ -88,21 +87,20 @@ mod tests { attributes: json!({}), avatar_url: None, }; - + let mut conn = get_pg_pool().get().await.unwrap(); insert_into(users::table) .values(&user) .execute(&mut conn) - .await.unwrap(); - + .await + .unwrap(); + let org_id = Uuid::new_v4(); - let organizations = vec![ - OrganizationMembership { - id: org_id, - role: UserOrganizationRole::Owner, - } - ]; - + let organizations = vec![OrganizationMembership { + id: org_id, + role: UserOrganizationRole::Owner, + }]; + let auth_user = AuthenticatedUser { id: user_id, email: "test@example.com".to_string(), @@ -115,14 +113,14 @@ mod tests { attributes: json!({}), avatar_url: None, }; - + (auth_user, user) } - + async fn setup_test_chat(user: &User) -> (Uuid, Vec) { let chat_id = Uuid::new_v4(); let now = Utc::now(); - + // Create chat record let chat = Chat { id: chat_id, @@ -139,13 +137,14 @@ mod tests { most_recent_file_id: None, most_recent_file_type: None, }; - + let mut conn = get_pg_pool().get().await.unwrap(); insert_into(chats::table) .values(&chat) .execute(&mut conn) - .await.unwrap(); - + .await + .unwrap(); + // Create permission for user let permission = AssetPermission { identity_id: user.id, @@ -159,15 +158,16 @@ mod tests { created_by: user.id, updated_by: user.id, }; - + insert_into(asset_permissions::table) .values(&permission) .execute(&mut conn) - .await.unwrap(); - + .await + .unwrap(); + // Create test messages let message_ids = vec![Uuid::new_v4(), Uuid::new_v4(), Uuid::new_v4()]; - + for (i, message_id) in message_ids.iter().enumerate() { let message = Message { id: *message_id, @@ -184,20 +184,21 @@ mod tests { created_by: user.id, feedback: None, }; - + insert_into(messages::table) .values(&message) .execute(&mut conn) - .await.unwrap(); + .await + .unwrap(); } - + (chat_id, message_ids) } - + async fn setup_test_file_reference(message_id: &Uuid, user_id: &Uuid) -> Uuid { let file_id = Uuid::new_v4(); let now = Utc::now(); - + let file_ref = MessageToFile { id: Uuid::new_v4(), message_id: *message_id, @@ -207,23 +208,24 @@ mod tests { deleted_at: None, is_duplicate: false, }; - + let mut conn = get_pg_pool().get().await.unwrap(); insert_into(messages_to_files::table) .values(&file_ref) .execute(&mut conn) - .await.unwrap(); - + .await + .unwrap(); + file_id } - + fn create_test_app(auth_user: AuthenticatedUser) -> Router { Router::new() .route("/chats/duplicate", post(duplicate_chat_route)) .layer(Extension(auth_user)) } - + // Instead of unit tests that depend on a real database connection, // we should run the integration tests from the tests directory. // These unit tests might become outdated if the model structures change. -} \ No newline at end of file +} diff --git a/api/tests/example_test.rs b/api/tests/example_test.rs new file mode 100644 index 000000000..621039c5f --- /dev/null +++ b/api/tests/example_test.rs @@ -0,0 +1,119 @@ +use anyhow::Result; +use uuid::Uuid; + +// Import the TestInstance type +use crate::TestInstance; + +/// Example test that requires database access - environment automatically initialized +#[tokio::test] +async fn test_simple_database_connection() -> Result<()> { + // The environment is already initialized by the test framework + + // Just create a test instance to get a unique test ID and access to pools + let test = TestInstance::new().await?; + + // Now we can use the test database + let pool = test.get_diesel_pool(); + + // Verify connection works by getting a connection from the pool + let conn = pool.get().await?; + + // We've successfully connected! + Ok(()) +} + +/// Example test for working with test data +#[tokio::test] +async fn test_with_isolation() -> Result<()> { + // Create a test instance with a unique ID + let test = TestInstance::new().await?; + + // Use the test_id to tag test data + let test_id = &test.test_id; + + // Create a connection + let mut conn = test.get_diesel_pool().get().await?; + + // Example raw SQL for test data creation (using the test_id) + diesel::sql_query("INSERT INTO example_table (id, name, test_id) VALUES ($1, $2, $3)") + .bind::(Uuid::new_v4()) + .bind::("Test item") + .bind::(test_id) + .execute(&mut conn) + .await?; + + // Run your test logic... + + // Clean up after the test + diesel::sql_query("DELETE FROM example_table WHERE test_id = $1") + .bind::(test_id) + .execute(&mut conn) + .await?; + + Ok(()) +} + +/// Example test for third-party API integration +#[tokio::test] +async fn test_third_party_api() -> Result<()> { + // Skip test if third-party testing is disabled + if std::env::var("ENABLE_THIRD_PARTY_TESTS").is_err() { + println!("Skipping third-party API test"); + return Ok(()); + } + + // Get API credentials from the test environment + let api_key = std::env::var("THIRD_PARTY_API_KEY") + .expect("THIRD_PARTY_API_KEY must be set for this test"); + let api_url = std::env::var("THIRD_PARTY_API_URL") + .expect("THIRD_PARTY_API_URL must be set for this test"); + + // Create test instance for database access if needed + let test = TestInstance::new().await?; + + // Create API client with test credentials + // let client = ThirdPartyClient::new(&api_key, &api_url); + + // Run API tests... + + Ok(()) +} + +/// Example test that specifically uses Redis +#[tokio::test] +async fn test_redis_connection() -> Result<()> { + // Create test instance + let test = TestInstance::new().await?; + + // Get Redis pool + let redis_pool = test.get_redis_pool(); + + // Get Redis connection + let mut conn = redis_pool.get().await?; + + // Example Redis operations + // redis::cmd("SET").arg("test_key").arg("test_value").execute(&mut *conn); + // let value: String = redis::cmd("GET").arg("test_key").query_async(&mut *conn).await?; + // assert_eq!(value, "test_value"); + + Ok(()) +} + +/// Example of a test that runs with parallel services +#[tokio::test] +async fn test_multiple_services() -> Result<()> { + // Create test instance + let test = TestInstance::new().await?; + + // Access both SQL and Redis databases + let pg_pool = test.get_diesel_pool(); + let redis_pool = test.get_redis_pool(); + + // Example of parallel operations + let pg_conn = pg_pool.get().await?; + let redis_conn = redis_pool.get().await?; + + // Use both connections... + + Ok(()) +} \ No newline at end of file