diff --git a/.config/e2e-full-selection.txt b/.config/e2e-full-selection.txt index dfad0f0bd..2c7b6882e 100644 --- a/.config/e2e-full-selection.txt +++ b/.config/e2e-full-selection.txt @@ -1,2 +1,2 @@ -sha256-darwin=9f767b37ed8b1c82da62ea441462d75487785c8086e56f08fb6f6cd89c6e2e52 -sha256-linux=fbdaf42b220958d4b1e8880e0f8b5a7992d38e21051bb60596dd4538424757d6 +sha256-darwin=03bdfb6a9d6e25d744c385f1461e651f05e4da78e5b0ead2adb3e8b2463e3834 +sha256-linux=78c46adad135231017fb8679fb91877b5ae3ec6ce1f75d3d045d2c1076c12a49 diff --git a/crates/e2e_test/src/reliant/conditional_writes.rs b/crates/e2e_test/src/reliant/conditional_writes.rs index 166ce66f5..6bc37980c 100644 --- a/crates/e2e_test/src/reliant/conditional_writes.rs +++ b/crates/e2e_test/src/reliant/conditional_writes.rs @@ -1,52 +1,26 @@ #![cfg(test)] -use aws_config::meta::region::RegionProviderChain; +use crate::common::{RustFSTestEnvironment, TEST_BUCKET, init_logging}; use aws_sdk_s3::Client; -use aws_sdk_s3::config::{Credentials, Region}; -use aws_sdk_s3::error::SdkError; +use aws_sdk_s3::error::{ProvideErrorMetadata, SdkError}; use aws_sdk_s3::types::{CompletedMultipartUpload, CompletedPart}; use bytes::Bytes; use std::error::Error; +use std::fmt::Debug; -const ENDPOINT: &str = "http://localhost:9000"; -const ACCESS_KEY: &str = "rustfsadmin"; -const SECRET_KEY: &str = "rustfsadmin"; -const BUCKET: &str = "api-test"; +type TestResult = Result<(), Box>; -async fn create_aws_s3_client() -> Result> { - let region_provider = RegionProviderChain::default_provider().or_else(Region::new("us-east-1")); - let shared_config = aws_config::defaults(aws_config::BehaviorVersion::latest()) - .region(region_provider) - .credentials_provider(Credentials::new(ACCESS_KEY, SECRET_KEY, None, None, "static")) - .endpoint_url(ENDPOINT) - .load() - .await; - - let client = Client::from_conf( - aws_sdk_s3::Config::from(&shared_config) - .to_builder() - .force_path_style(true) - .build(), +fn assert_s3_error_code(result: Result>, expected: &str) +where + T: Debug, + E: ProvideErrorMetadata + Debug, +{ + let error = result.expect_err("conditional request must fail"); + assert_eq!( + error.as_service_error().and_then(ProvideErrorMetadata::code), + Some(expected), + "unexpected conditional request error: {error:?}" ); - Ok(client) -} - -/// Setup test bucket, creating it if it doesn't exist -async fn setup_test_bucket(client: &Client) -> Result<(), Box> { - match client.create_bucket().bucket(BUCKET).send().await { - Ok(_) => {} - Err(SdkError::ServiceError(e)) => { - let e = e.into_err(); - let error_code = e.meta().code().unwrap_or(""); - if !error_code.eq("BucketAlreadyExists") { - return Err(e.into()); - } - } - Err(e) => { - return Err(e.into()); - } - } - Ok(()) } /// Generate test data of specified size @@ -60,7 +34,12 @@ fn generate_test_data(size: usize) -> Vec { } /// Upload an object and return its ETag -async fn upload_object_with_metadata(client: &Client, bucket: &str, key: &str, data: &[u8]) -> Result> { +async fn upload_object_with_metadata( + client: &Client, + bucket: &str, + key: &str, + data: &[u8], +) -> Result> { let response = client .put_object() .bucket(bucket) @@ -69,188 +48,164 @@ async fn upload_object_with_metadata(client: &Client, bucket: &str, key: &str, d .send() .await?; - let etag = response.e_tag().unwrap_or("").to_string(); - Ok(etag) + response + .e_tag() + .map(str::to_owned) + .ok_or_else(|| std::io::Error::other("put object response did not include an ETag").into()) } -/// Cleanup test objects from bucket -async fn cleanup_objects(client: &Client, bucket: &str, keys: &[&str]) { - for key in keys { - let _ = client.delete_object().bucket(bucket).key(*key).send().await; - } -} - -/// Generate unique test object key -fn generate_test_key(prefix: &str) -> String { - use std::time::{SystemTime, UNIX_EPOCH}; - let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos(); - format!("{prefix}-{timestamp}") +async fn object_body(client: &Client, key: &str) -> Result> { + let response = client.get_object().bucket(TEST_BUCKET).key(key).send().await?; + Ok(response.body.collect().await?.into_bytes()) } #[tokio::test] -#[ignore = "requires running RustFS server at localhost:9000"] -async fn test_conditional_put_okay() -> Result<(), Box> { - let client = create_aws_s3_client().await?; - setup_test_bucket(&client).await?; +async fn test_conditional_put_okay() -> TestResult { + init_logging(); + let mut env = RustFSTestEnvironment::new().await?; + env.start_rustfs_server(vec![]).await?; + env.create_test_bucket(TEST_BUCKET).await?; + let client = env.create_s3_client(); - let test_key = generate_test_key("conditional-put-ok"); + let test_key = "conditional-put-ok"; let initial_data = generate_test_data(1024); // 1KB test data - let updated_data = generate_test_data(2048); // 2KB updated data + let matching_data = generate_test_data(2048); // 2KB updated data + let non_matching_data = generate_test_data(3072); // 3KB updated data // Upload initial object and get its ETag - let initial_etag = upload_object_with_metadata(&client, BUCKET, &test_key, &initial_data).await?; + let initial_etag = upload_object_with_metadata(&client, TEST_BUCKET, test_key, &initial_data).await?; // Test 1: PUT with matching If-Match condition (should succeed) - let response1 = client + client .put_object() - .bucket(BUCKET) - .key(&test_key) - .body(Bytes::from(updated_data.clone()).into()) + .bucket(TEST_BUCKET) + .key(test_key) + .body(Bytes::from(matching_data.clone()).into()) .if_match(&initial_etag) .send() - .await; - assert!(response1.is_ok(), "PUT with matching If-Match should succeed"); + .await?; + assert_eq!(object_body(&client, test_key).await?.as_ref(), matching_data); // Test 2: PUT with non-matching If-None-Match condition (should succeed) let fake_etag = "\"fake-etag-12345\""; - let response2 = client + client .put_object() - .bucket(BUCKET) - .key(&test_key) - .body(Bytes::from(updated_data.clone()).into()) + .bucket(TEST_BUCKET) + .key(test_key) + .body(Bytes::from(non_matching_data.clone()).into()) .if_none_match(fake_etag) .send() - .await; - assert!(response2.is_ok(), "PUT with non-matching If-None-Match should succeed"); - - // Cleanup - cleanup_objects(&client, BUCKET, &[&test_key]).await; + .await?; + assert_eq!(object_body(&client, test_key).await?.as_ref(), non_matching_data); Ok(()) } #[tokio::test] -#[ignore = "requires running RustFS server at localhost:9000"] -async fn test_conditional_put_failed() -> Result<(), Box> { - let client = create_aws_s3_client().await?; - setup_test_bucket(&client).await?; +async fn test_conditional_put_failed() -> TestResult { + init_logging(); + let mut env = RustFSTestEnvironment::new().await?; + env.start_rustfs_server(vec![]).await?; + env.create_test_bucket(TEST_BUCKET).await?; + let client = env.create_s3_client(); - let test_key = generate_test_key("conditional-put-failed"); + let test_key = "conditional-put-failed"; let initial_data = generate_test_data(1024); let updated_data = generate_test_data(2048); // Upload initial object and get its ETag - let initial_etag = upload_object_with_metadata(&client, BUCKET, &test_key, &initial_data).await?; + let initial_etag = upload_object_with_metadata(&client, TEST_BUCKET, test_key, &initial_data).await?; // Test 1: PUT with non-matching If-Match condition (should fail with 412) let fake_etag = "\"fake-etag-should-not-match\""; let response1 = client .put_object() - .bucket(BUCKET) - .key(&test_key) + .bucket(TEST_BUCKET) + .key(test_key) .body(Bytes::from(updated_data.clone()).into()) .if_match(fake_etag) .send() .await; - assert!(response1.is_err(), "PUT with non-matching If-Match should fail"); - if let Err(e) = response1 { - if let SdkError::ServiceError(e) = e { - let e = e.into_err(); - let error_code = e.meta().code().unwrap_or(""); - assert_eq!("PreconditionFailed", error_code); - } else { - panic!("Unexpected error: {e:?}"); - } - } + assert_s3_error_code(response1, "PreconditionFailed"); + assert_eq!(object_body(&client, test_key).await?.as_ref(), initial_data); // Test 2: PUT with matching If-None-Match condition (should fail with 412) let response2 = client .put_object() - .bucket(BUCKET) - .key(&test_key) + .bucket(TEST_BUCKET) + .key(test_key) .body(Bytes::from(updated_data.clone()).into()) .if_none_match(&initial_etag) .send() .await; - assert!(response2.is_err(), "PUT with matching If-None-Match should fail"); - if let Err(e) = response2 { - if let SdkError::ServiceError(e) = e { - let e = e.into_err(); - let error_code = e.meta().code().unwrap_or(""); - assert_eq!("PreconditionFailed", error_code); - } else { - panic!("Unexpected error: {e:?}"); - } - } - - // Cleanup - only need to clean up the initial object since failed PUTs shouldn't create objects - cleanup_objects(&client, BUCKET, &[&test_key]).await; + assert_s3_error_code(response2, "PreconditionFailed"); + assert_eq!(object_body(&client, test_key).await?.as_ref(), initial_data); Ok(()) } #[tokio::test] -#[ignore = "requires running RustFS server at localhost:9000"] -async fn test_conditional_put_when_object_does_not_exist() -> Result<(), Box> { - let client = create_aws_s3_client().await?; - setup_test_bucket(&client).await?; +async fn test_conditional_put_when_object_does_not_exist() -> TestResult { + init_logging(); + let mut env = RustFSTestEnvironment::new().await?; + env.start_rustfs_server(vec![]).await?; + env.create_test_bucket(TEST_BUCKET).await?; + let client = env.create_s3_client(); - let key = "some_key"; - cleanup_objects(&client, BUCKET, &[key]).await; + let key = "conditional-put-missing"; // When the object does not exist, the If-Match condition should always fail let response1 = client .put_object() - .bucket(BUCKET) + .bucket(TEST_BUCKET) .key(key) .body(Bytes::from(generate_test_data(1024)).into()) .if_match("*") .send() .await; - assert!(response1.is_err()); - if let Err(e) = response1 { - if let SdkError::ServiceError(e) = e { - let e = e.into_err(); - let error_code = e.meta().code().unwrap_or(""); - assert_eq!("NoSuchKey", error_code); - } else { - panic!("Unexpected error: {e:?}"); - } - } + assert_s3_error_code(response1, "NoSuchKey"); // When the object does not exist, the If-None-Match condition should be able to succeed - let response2 = client + let created_data = generate_test_data(1024); + client .put_object() - .bucket(BUCKET) + .bucket(TEST_BUCKET) .key(key) - .body(Bytes::from(generate_test_data(1024)).into()) + .body(Bytes::from(created_data.clone()).into()) .if_none_match("*") .send() - .await; - assert!(response2.is_ok()); + .await?; + assert_eq!(object_body(&client, key).await?.as_ref(), created_data); - cleanup_objects(&client, BUCKET, &[key]).await; Ok(()) } #[tokio::test] -#[ignore = "requires running RustFS server at localhost:9000"] -async fn test_conditional_multi_part_upload() -> Result<(), Box> { - let client = create_aws_s3_client().await?; - setup_test_bucket(&client).await?; +async fn test_conditional_multi_part_upload() -> TestResult { + init_logging(); + let mut env = RustFSTestEnvironment::new().await?; + env.start_rustfs_server(vec![]).await?; + env.create_test_bucket(TEST_BUCKET).await?; + let client = env.create_s3_client(); - let test_key = generate_test_key("multipart-upload-ok"); + let test_key = "conditional-multipart-upload"; let test_data = generate_test_data(1024); - let initial_etag = upload_object_with_metadata(&client, BUCKET, &test_key, &test_data).await?; + let initial_etag = upload_object_with_metadata(&client, TEST_BUCKET, test_key, &test_data).await?; let part_size = 5 * 1024 * 1024; // 5MB per part (minimum for multipart) let num_parts = 3; let mut parts = Vec::new(); + let mut expected_data = Vec::with_capacity(part_size * usize::try_from(num_parts)?); // Initiate multipart upload - let initiate_response = client.create_multipart_upload().bucket(BUCKET).key(&test_key).send().await?; + let initiate_response = client + .create_multipart_upload() + .bucket(TEST_BUCKET) + .key(test_key) + .send() + .await?; let upload_id = initiate_response .upload_id() @@ -258,12 +213,13 @@ async fn test_conditional_multi_part_upload() -> Result<(), Box Result<(), Box