diff --git a/crates/cli/src/commands/rm.rs b/crates/cli/src/commands/rm.rs index 5f1f76f..af1d701 100644 --- a/crates/cli/src/commands/rm.rs +++ b/crates/cli/src/commands/rm.rs @@ -436,4 +436,21 @@ mod tests { let options = delete_request_options(&args); assert!(options.force_delete); } + + #[test] + fn test_delete_request_options_keep_force_delete_disabled_without_purge() { + let args = RmArgs { + paths: vec!["test/bucket/object.txt".to_string()], + recursive: false, + force: true, + dry_run: false, + incomplete: false, + versions: false, + bypass: false, + purge: false, + }; + + let options = delete_request_options(&args); + assert!(!options.force_delete); + } } diff --git a/crates/cli/tests/integration.rs b/crates/cli/tests/integration.rs index 5511f9c..0aa6ea2 100644 --- a/crates/cli/tests/integration.rs +++ b/crates/cli/tests/integration.rs @@ -2408,6 +2408,7 @@ mod version_operations { std::fs::write(temp_file.path(), "versioned delete content").expect("Failed to write"); let normal_key = "normal-delete.txt"; + let force_key = "force-delete.txt"; let purge_key = "purge-delete.txt"; let upload_output = run_rc( @@ -2475,6 +2476,72 @@ mod version_operations { "Expected latest version to be a delete marker after normal rm" ); + let force_upload_output = run_rc( + &[ + "cp", + temp_file + .path() + .to_str() + .expect("Temp file path should be UTF-8"), + &format!("test/{}/{}", bucket_name, force_key), + ], + config_dir.path(), + ); + assert!( + force_upload_output.status.success(), + "Failed to upload force delete object: {}", + String::from_utf8_lossy(&force_upload_output.stderr) + ); + + let force_delete_output = run_rc( + &[ + "rm", + &format!("test/{}/{}", bucket_name, force_key), + "--force", + "--json", + ], + config_dir.path(), + ); + assert!( + force_delete_output.status.success(), + "Failed to force delete versioned object: {}", + String::from_utf8_lossy(&force_delete_output.stderr) + ); + + let force_versions_output = run_rc( + &[ + "version", + "list", + &format!("test/{}/{}", bucket_name, force_key), + "--json", + ], + config_dir.path(), + ); + assert!( + force_versions_output.status.success(), + "Failed to list versions after force rm: {}", + String::from_utf8_lossy(&force_versions_output.stderr) + ); + + let force_stdout = String::from_utf8_lossy(&force_versions_output.stdout); + let force_versions: serde_json::Value = + serde_json::from_str(&force_stdout).expect("Invalid JSON version list"); + let force_versions = force_versions + .as_array() + .expect("Version list should be a JSON array"); + assert_eq!( + force_versions.len(), + 2, + "Expected --force rm to keep the object version and create a delete marker" + ); + assert!( + force_versions.iter().any(|entry| { + entry["is_delete_marker"].as_bool() == Some(true) + && entry["is_latest"].as_bool() == Some(true) + }), + "Expected latest version to be a delete marker after force rm" + ); + let purge_upload_output = run_rc( &[ "cp", @@ -2548,6 +2615,21 @@ mod version_operations { String::from_utf8_lossy(&normal_cleanup_output.stderr) ); + let force_cleanup_output = run_rc( + &[ + "rm", + &format!("test/{}/{}", bucket_name, force_key), + "--purge", + "--json", + ], + config_dir.path(), + ); + assert!( + force_cleanup_output.status.success(), + "Failed to purge force cleanup object: {}", + String::from_utf8_lossy(&force_cleanup_output.stderr) + ); + cleanup_bucket(config_dir.path(), &bucket_name); } } diff --git a/crates/s3/src/client.rs b/crates/s3/src/client.rs index 75f0db5..32649b7 100644 --- a/crates/s3/src/client.rs +++ b/crates/s3/src/client.rs @@ -3195,6 +3195,42 @@ mod tests { assert_eq!(request.headers().get("x-rustfs-force-delete"), Some("true")); } + #[tokio::test] + async fn delete_objects_without_force_delete_omits_rustfs_header() { + let response = http::Response::builder() + .status(200) + .body(SdkBody::from( + r#" +"#, + )) + .expect("build delete objects response"); + let (client, request_receiver) = test_s3_client(Some(response)); + + let _ = client + .delete_objects_with_options( + "bucket", + vec!["key.txt".to_string()], + DeleteRequestOptions::default(), + ) + .await; + + let request = request_receiver.expect_request(); + assert!(request.headers().get("x-rustfs-force-delete").is_none()); + } + + #[tokio::test] + async fn delete_objects_with_empty_keys_skips_http_request() { + let (client, request_receiver) = test_s3_client(None); + + let deleted = client + .delete_objects_with_options("bucket", Vec::new(), DeleteRequestOptions::default()) + .await + .expect("empty delete should succeed"); + + assert!(deleted.is_empty()); + request_receiver.expect_no_request(); + } + #[tokio::test] async fn read_next_part_fills_buffer_until_eof() { use tokio::io::AsyncWriteExt;