Merge branch 'evals' of https://github.com/buster-so/buster into evals

This commit is contained in:
Nate Kelley 2025-04-02 10:42:57 -06:00
commit b77848f150
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
3 changed files with 207 additions and 52 deletions

View File

@ -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(())
}

View File

@ -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<Uuid>,
}
/// 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<AuthenticatedUser>,
Json(request): Json<DuplicateChatRequest>,
) -> Result<ApiResponse<DuplicateChatResponse>, (StatusCode, String)> {
) -> Result<ApiResponse<ChatWithMessages>, (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<Uuid>) {
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.
}
}

119
api/tests/example_test.rs Normal file
View File

@ -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::<diesel::sql_types::Uuid, _>(Uuid::new_v4())
.bind::<diesel::sql_types::Text, _>("Test item")
.bind::<diesel::sql_types::Text, _>(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::<diesel::sql_types::Text, _>(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(())
}