From 365e8429d2d26ee7d9cdb5d8ffa7cd10754a435c Mon Sep 17 00:00:00 2001 From: dal Date: Thu, 20 Mar 2025 15:56:33 -0600 Subject: [PATCH] add the update chat rest and handler --- api/src/routes/rest/routes/chats/mod.rs | 3 + .../routes/rest/routes/chats/update_chat.rs | 89 +++++++++++ api/tests/integration/chats/mod.rs | 3 +- .../integration/chats/update_chat_test.rs | 139 ++++++++++++++++++ 4 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 api/src/routes/rest/routes/chats/update_chat.rs create mode 100644 api/tests/integration/chats/update_chat_test.rs diff --git a/api/src/routes/rest/routes/chats/mod.rs b/api/src/routes/rest/routes/chats/mod.rs index 3f4c15abe..ab83131dd 100644 --- a/api/src/routes/rest/routes/chats/mod.rs +++ b/api/src/routes/rest/routes/chats/mod.rs @@ -9,6 +9,7 @@ mod get_chat_raw_llm_messages; mod list_chats; mod post_chat; mod sharing; +mod update_chat; mod update_chats; pub use delete_chats::delete_chats_route; @@ -16,6 +17,7 @@ pub use get_chat::get_chat_route; pub use get_chat_raw_llm_messages::get_chat_raw_llm_messages; pub use list_chats::list_chats_route; pub use post_chat::post_chat_route; +pub use update_chat::update_chat_route; pub use update_chats::update_chats_route; pub fn router() -> Router { @@ -25,6 +27,7 @@ pub fn router() -> Router { .route("/", put(update_chats_route)) .route("/", delete(delete_chats_route)) .route("/:id", get(get_chat_route)) + .route("/:id", put(update_chat_route)) .route("/:id/raw_llm_messages", get(get_chat_raw_llm_messages)) .nest("/:id/sharing", sharing::router()) } diff --git a/api/src/routes/rest/routes/chats/update_chat.rs b/api/src/routes/rest/routes/chats/update_chat.rs new file mode 100644 index 000000000..6d7c30d72 --- /dev/null +++ b/api/src/routes/rest/routes/chats/update_chat.rs @@ -0,0 +1,89 @@ +use anyhow::Result; +use axum::extract::Path; +use axum::http::StatusCode; +use axum::Extension; +use axum::Json; +use handlers::chats::update_chats_handler::{ChatUpdate, ChatUpdateResult}; +use handlers::chats::update_chats_handler; +use middleware::AuthenticatedUser; +use uuid::Uuid; + +use crate::routes::rest::ApiResponse; + +/// Update a single chat by ID +/// +/// PUT /api/v1/chats/:id +pub async fn update_chat_route( + Path(chat_id): Path, + Extension(user): Extension, + Json(update): Json, +) -> Result, (StatusCode, &'static str)> { + let chat_update = ChatUpdate { + id: chat_id, + title: update.title, + }; + + match update_chats_handler(vec![chat_update], &user.id).await { + Ok(results) => { + if results.is_empty() { + Err((StatusCode::INTERNAL_SERVER_ERROR, "Failed to update chat")) + } else if !results[0].success { + match &results[0].error { + Some(error) if error.contains("not found") => { + Err((StatusCode::NOT_FOUND, "Chat not found")) + } + Some(error) if error.contains("permission") => { + Err((StatusCode::FORBIDDEN, "You don't have permission to update this chat")) + } + _ => Err((StatusCode::INTERNAL_SERVER_ERROR, "Failed to update chat")), + } + } else { + let result = ChatUpdateResult { + id: results[0].id, + success: results[0].success, + error: results[0].error.clone(), + }; + Ok(ApiResponse::JsonData(result)) + } + } + Err(e) => { + tracing::error!("Error updating chat {}: {}", chat_id, e); + Err((StatusCode::INTERNAL_SERVER_ERROR, "Failed to update chat")) + } + } +} + +#[derive(Debug, serde::Deserialize)] +pub struct ChatUpdateRequest { + pub title: String, +} + +#[cfg(test)] +mod tests { + use super::*; + use axum::{ + body::Body, + http::{Request, StatusCode}, + routing::put, + Router, + }; + use std::str::FromStr; + use tower::ServiceExt; + + #[tokio::test] + async fn test_update_chat_route_success() { + // This test is a simplified unit test example + // More comprehensive integration tests are in the tests directory + let chat_id = Uuid::from_str("00000000-0000-0000-0000-000000000001").unwrap(); + let user_id = Uuid::from_str("00000000-0000-0000-0000-000000000002").unwrap(); + + // Since we can't mock the handler function easily in this context, + // this test would normally use a test database in integration tests + // Here we're just checking the basic structure + let update_request = ChatUpdateRequest { + title: "Updated Title".to_string(), + }; + + // In a real test implementation, we would set up a proper testing environment + } +} \ No newline at end of file diff --git a/api/tests/integration/chats/mod.rs b/api/tests/integration/chats/mod.rs index 690d5862b..edcede243 100644 --- a/api/tests/integration/chats/mod.rs +++ b/api/tests/integration/chats/mod.rs @@ -1,2 +1,3 @@ pub mod sharing; -pub mod get_chat_test; \ No newline at end of file +pub mod get_chat_test; +pub mod update_chat_test; \ No newline at end of file diff --git a/api/tests/integration/chats/update_chat_test.rs b/api/tests/integration/chats/update_chat_test.rs new file mode 100644 index 000000000..fdfa5b2c7 --- /dev/null +++ b/api/tests/integration/chats/update_chat_test.rs @@ -0,0 +1,139 @@ +use anyhow::Result; +use chrono::Utc; +use database::models::Chat; +use diesel::prelude::*; +use std::str::FromStr; +use uuid::Uuid; + +use crate::common::{ + db::run_test_with_db, + fixtures::{builder::TestFixtureBuilder, chats::CreateChatParams}, + http::test_app::{TestApp, TestAppBuilder}, + http::client::ApiClient, +}; + +#[tokio::test] +async fn test_update_chat() -> Result<()> { + run_test_with_db(|db| async move { + // Create test user + let user_id = Uuid::new_v4(); + let mut fixture_builder = TestFixtureBuilder::new(db.clone()); + let user = fixture_builder.create_user(user_id).await?; + + // Create test chat + let chat_id = Uuid::new_v4(); + let chat = fixture_builder + .create_chat(CreateChatParams { + id: Some(chat_id), + title: Some("Original Title".to_string()), + created_by: Some(user_id), + ..Default::default() + }) + .await?; + + // Setup test app + let app = TestAppBuilder::new().with_db(db.clone()).build().await?; + let client = ApiClient::new(app.client, user.clone()); + + // Test updating the chat + let response = client + .put(&format!("/api/v1/chats/{}", chat_id)) + .json(&serde_json::json!({ + "title": "Updated Title" + })) + .send() + .await?; + + // Verify response + assert_eq!(response.status(), 200); + let json = response.json::().await?; + assert_eq!(json["id"], chat_id.to_string()); + assert_eq!(json["success"], true); + assert_eq!(json["error"], serde_json::Value::Null); + + // Verify database update + let updated_chat = db.with_conn(|conn| async move { + use database::schema::chats; + + chats::table + .find(chat_id) + .first::(conn) + .await + .map_err(anyhow::Error::from) + }).await?; + + assert_eq!(updated_chat.title, "Updated Title"); + + Ok(()) + }).await +} + +#[tokio::test] +async fn test_update_chat_not_found() -> Result<()> { + run_test_with_db(|db| async move { + // Create test user + let user_id = Uuid::new_v4(); + let mut fixture_builder = TestFixtureBuilder::new(db.clone()); + let user = fixture_builder.create_user(user_id).await?; + + // Setup test app + let app = TestAppBuilder::new().with_db(db.clone()).build().await?; + let client = ApiClient::new(app.client, user.clone()); + + // Test updating a non-existent chat + let non_existent_id = Uuid::new_v4(); + let response = client + .put(&format!("/api/v1/chats/{}", non_existent_id)) + .json(&serde_json::json!({ + "title": "Updated Title" + })) + .send() + .await?; + + // Verify response + assert_eq!(response.status(), 404); + + Ok(()) + }).await +} + +#[tokio::test] +async fn test_update_chat_unauthorized() -> Result<()> { + run_test_with_db(|db| async move { + // Create two test users + let user1_id = Uuid::new_v4(); + let user2_id = Uuid::new_v4(); + let mut fixture_builder = TestFixtureBuilder::new(db.clone()); + let user1 = fixture_builder.create_user(user1_id).await?; + let user2 = fixture_builder.create_user(user2_id).await?; + + // Create test chat owned by user1 + let chat_id = Uuid::new_v4(); + let chat = fixture_builder + .create_chat(CreateChatParams { + id: Some(chat_id), + title: Some("Original Title".to_string()), + created_by: Some(user1_id), + ..Default::default() + }) + .await?; + + // Setup test app with user2 (who doesn't own the chat) + let app = TestAppBuilder::new().with_db(db.clone()).build().await?; + let client = ApiClient::new(app.client, user2.clone()); + + // Test user2 trying to update user1's chat + let response = client + .put(&format!("/api/v1/chats/{}", chat_id)) + .json(&serde_json::json!({ + "title": "Updated Title" + })) + .send() + .await?; + + // Verify response (should be forbidden) + assert_eq!(response.status(), 403); + + Ok(()) + }).await +} \ No newline at end of file