// Copyright 2024 RustFS Team // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! End-to-end tests for S3 dummy-compat bucket APIs. #[cfg(test)] mod tests { use crate::common::{RustFSTestEnvironment, init_logging, signed_s3_request}; use aws_sdk_s3::error::ProvideErrorMetadata; use aws_sdk_s3::types::{ AccelerateConfiguration, BucketAccelerateStatus, BucketLoggingStatus, IndexDocument, LoggingEnabled, Payer, RequestPaymentConfiguration, WebsiteConfiguration, }; use http::Method; use http::header::CONTENT_TYPE; use serial_test::serial; use tracing::info; #[tokio::test] #[serial] async fn test_dummy_bucket_compatibility_endpoints() { init_logging(); info!("Starting test: dummy-compat bucket APIs should match S3-compatible behavior"); let mut env = RustFSTestEnvironment::new().await.expect("Failed to create test environment"); env.start_rustfs_server(vec![]).await.expect("Failed to start RustFS"); let client = env.create_s3_client(); let bucket = "test-get-bucket-logging"; client .create_bucket() .bucket(bucket) .send() .await .expect("Failed to create bucket"); let result = client.get_bucket_logging().bucket(bucket).send().await; assert!( result.is_ok(), "GetBucketLogging should return success for existing bucket, got: {:?}", result.err() ); let output = result.unwrap(); assert!( output.logging_enabled().is_none(), "Default GetBucketLogging should return empty logging configuration" ); let put_logging = client .put_bucket_logging() .bucket(bucket) .bucket_logging_status( BucketLoggingStatus::builder() .logging_enabled( LoggingEnabled::builder() .target_bucket(bucket) .target_prefix("logs/") .build() .expect("failed to build LoggingEnabled"), ) .build(), ) .send() .await; assert!( put_logging.is_ok(), "PutBucketLogging should return success for existing bucket, got: {:?}", put_logging.err() ); let output_after_put = client .get_bucket_logging() .bucket(bucket) .send() .await .expect("GetBucketLogging should succeed after PutBucketLogging"); let logging_after_put = output_after_put .logging_enabled() .expect("GetBucketLogging should return persisted logging_enabled"); assert_eq!( logging_after_put.target_bucket(), bucket, "GetBucketLogging should preserve target bucket" ); assert_eq!( logging_after_put.target_prefix(), "logs/", "GetBucketLogging should preserve target prefix" ); let accelerate = client .get_bucket_accelerate_configuration() .bucket(bucket) .send() .await .expect("GetBucketAccelerateConfiguration should succeed"); assert!( accelerate.status().is_none(), "Default GetBucketAccelerateConfiguration should return empty status" ); let payment = client .get_bucket_request_payment() .bucket(bucket) .send() .await .expect("GetBucketRequestPayment should succeed"); assert_eq!( payment.payer().map(|p| p.as_str()), Some("BucketOwner"), "GetBucketRequestPayment should return BucketOwner by default" ); let put_accelerate = client .put_bucket_accelerate_configuration() .bucket(bucket) .accelerate_configuration( AccelerateConfiguration::builder() .status(BucketAccelerateStatus::Suspended) .build(), ) .send() .await; assert!( put_accelerate.is_ok(), "PutBucketAccelerateConfiguration should return success for existing bucket, got: {:?}", put_accelerate.err() ); let put_request_payment = client .put_bucket_request_payment() .bucket(bucket) .request_payment_configuration( RequestPaymentConfiguration::builder() .payer(Payer::Requester) .build() .expect("failed to build RequestPaymentConfiguration"), ) .send() .await; assert!( put_request_payment.is_ok(), "PutBucketRequestPayment should return success for existing bucket, got: {:?}", put_request_payment.err() ); let accelerate_after_put = client .get_bucket_accelerate_configuration() .bucket(bucket) .send() .await .expect("GetBucketAccelerateConfiguration should succeed after put"); assert_eq!( accelerate_after_put.status().map(|s| s.as_str()), Some("Suspended"), "GetBucketAccelerateConfiguration should preserve put status" ); let payment_after_put = client .get_bucket_request_payment() .bucket(bucket) .send() .await .expect("GetBucketRequestPayment should succeed after put"); assert_eq!( payment_after_put.payer().map(|p| p.as_str()), Some("Requester"), "GetBucketRequestPayment should preserve put payer" ); let put_website = client .put_bucket_website() .bucket(bucket) .website_configuration( WebsiteConfiguration::builder() .index_document( IndexDocument::builder() .suffix("index.html") .build() .expect("failed to build IndexDocument"), ) .build(), ) .send() .await; assert!( put_website.is_ok(), "PutBucketWebsite should return success for existing bucket, got: {:?}", put_website.err() ); let website = client.get_bucket_website().bucket(bucket).send().await; assert!(website.is_ok(), "GetBucketWebsite should return persisted website configuration"); let website_output = website.unwrap(); assert_eq!( website_output.index_document().map(|doc| doc.suffix()), Some("index.html"), "GetBucketWebsite should preserve index document suffix" ); client .delete_bucket_website() .bucket(bucket) .send() .await .expect("DeleteBucketWebsite should return success"); let website_after_delete = client.get_bucket_website().bucket(bucket).send().await; assert!( website_after_delete.is_err(), "GetBucketWebsite should return NoSuchWebsiteConfiguration after deletion" ); let website_err = website_after_delete.err().unwrap(); let website_code = website_err.as_service_error().and_then(|e| e.code()); assert!( matches!(website_code, Some("NoSuchWebsiteConfiguration")), "Unexpected GetBucketWebsite error code: {:?}, err: {:?}", website_code, website_err ); env.stop_server(); } #[tokio::test] #[serial] async fn test_dummy_bucket_compatibility_endpoints_no_such_bucket() { init_logging(); info!("Starting test: dummy-compat bucket APIs should return NoSuchBucket for missing bucket"); let mut env = RustFSTestEnvironment::new().await.expect("Failed to create test environment"); env.start_rustfs_server(vec![]).await.expect("Failed to start RustFS"); let client = env.create_s3_client(); let missing_bucket = "test-dummy-bucket-missing"; let get_logging = client.get_bucket_logging().bucket(missing_bucket).send().await; assert!(get_logging.is_err(), "GetBucketLogging should fail for missing bucket"); let get_logging_err = get_logging.err().unwrap(); let get_logging_code = get_logging_err.as_service_error().and_then(|e| e.code()); assert!( matches!(get_logging_code, Some("NoSuchBucket")), "Unexpected GetBucketLogging error code: {:?}, err: {:?}", get_logging_code, get_logging_err ); let put_logging = client .put_bucket_logging() .bucket(missing_bucket) .bucket_logging_status(BucketLoggingStatus::builder().build()) .send() .await; assert!(put_logging.is_err(), "PutBucketLogging should fail for missing bucket"); let put_logging_err = put_logging.err().unwrap(); let put_logging_code = put_logging_err.as_service_error().and_then(|e| e.code()); assert!( matches!(put_logging_code, Some("NoSuchBucket")), "Unexpected PutBucketLogging error code: {:?}, err: {:?}", put_logging_code, put_logging_err ); let get_accelerate = client .get_bucket_accelerate_configuration() .bucket(missing_bucket) .send() .await; assert!(get_accelerate.is_err(), "GetBucketAccelerateConfiguration should fail for missing bucket"); let get_accelerate_err = get_accelerate.err().unwrap(); let get_accelerate_code = get_accelerate_err.as_service_error().and_then(|e| e.code()); assert!( matches!(get_accelerate_code, Some("NoSuchBucket")), "Unexpected GetBucketAccelerateConfiguration error code: {:?}, err: {:?}", get_accelerate_code, get_accelerate_err ); let get_request_payment = client.get_bucket_request_payment().bucket(missing_bucket).send().await; assert!(get_request_payment.is_err(), "GetBucketRequestPayment should fail for missing bucket"); let get_request_payment_err = get_request_payment.err().unwrap(); let get_request_payment_code = get_request_payment_err.as_service_error().and_then(|e| e.code()); assert!( matches!(get_request_payment_code, Some("NoSuchBucket")), "Unexpected GetBucketRequestPayment error code: {:?}, err: {:?}", get_request_payment_code, get_request_payment_err ); let put_accelerate = client .put_bucket_accelerate_configuration() .bucket(missing_bucket) .accelerate_configuration( AccelerateConfiguration::builder() .status(BucketAccelerateStatus::Suspended) .build(), ) .send() .await; assert!(put_accelerate.is_err(), "PutBucketAccelerateConfiguration should fail for missing bucket"); let put_accelerate_err = put_accelerate.err().unwrap(); let put_accelerate_code = put_accelerate_err.as_service_error().and_then(|e| e.code()); assert!( matches!(put_accelerate_code, Some("NoSuchBucket")), "Unexpected PutBucketAccelerateConfiguration error code: {:?}, err: {:?}", put_accelerate_code, put_accelerate_err ); let put_request_payment = client .put_bucket_request_payment() .bucket(missing_bucket) .request_payment_configuration( RequestPaymentConfiguration::builder() .payer(Payer::BucketOwner) .build() .expect("failed to build RequestPaymentConfiguration"), ) .send() .await; assert!(put_request_payment.is_err(), "PutBucketRequestPayment should fail for missing bucket"); let put_request_payment_err = put_request_payment.err().unwrap(); let put_request_payment_code = put_request_payment_err.as_service_error().and_then(|e| e.code()); assert!( matches!(put_request_payment_code, Some("NoSuchBucket")), "Unexpected PutBucketRequestPayment error code: {:?}, err: {:?}", put_request_payment_code, put_request_payment_err ); let put_website = client .put_bucket_website() .bucket(missing_bucket) .website_configuration( WebsiteConfiguration::builder() .index_document( IndexDocument::builder() .suffix("index.html") .build() .expect("failed to build IndexDocument"), ) .build(), ) .send() .await; assert!(put_website.is_err(), "PutBucketWebsite should fail for missing bucket"); let put_website_err = put_website.err().unwrap(); let put_website_code = put_website_err.as_service_error().and_then(|e| e.code()); assert!( matches!(put_website_code, Some("NoSuchBucket")), "Unexpected PutBucketWebsite error code: {:?}, err: {:?}", put_website_code, put_website_err ); let get_website = client.get_bucket_website().bucket(missing_bucket).send().await; assert!(get_website.is_err(), "GetBucketWebsite should fail for missing bucket"); let get_website_err = get_website.err().unwrap(); let get_website_code = get_website_err.as_service_error().and_then(|e| e.code()); assert!( matches!(get_website_code, Some("NoSuchBucket")), "Unexpected GetBucketWebsite error code: {:?}, err: {:?}", get_website_code, get_website_err ); let delete_website = client.delete_bucket_website().bucket(missing_bucket).send().await; assert!(delete_website.is_err(), "DeleteBucketWebsite should fail for missing bucket"); let delete_website_err = delete_website.err().unwrap(); let delete_website_code = delete_website_err.as_service_error().and_then(|e| e.code()); assert!( matches!(delete_website_code, Some("NoSuchBucket")), "Unexpected DeleteBucketWebsite error code: {:?}, err: {:?}", delete_website_code, delete_website_err ); env.stop_server(); } #[tokio::test] #[serial] async fn test_dummy_bucket_endpoints_http_contracts() { init_logging(); info!("Starting test: dummy-compat bucket API HTTP contracts"); let mut env = RustFSTestEnvironment::new().await.expect("Failed to create test environment"); env.start_rustfs_server(vec![]).await.expect("Failed to start RustFS"); let client = env.create_s3_client(); let bucket = "test-dummy-bucket-http-contracts"; client .create_bucket() .bucket(bucket) .send() .await .expect("Failed to create bucket"); let logging_response = signed_s3_request( Method::GET, &format!("{}/{bucket}?logging=", env.url), None, None, &env.access_key, &env.secret_key, ) .await .expect("GetBucketLogging HTTP request failed"); assert_eq!(logging_response.status(), 200, "GetBucketLogging should return 200"); let logging_body = logging_response .text() .await .expect("Failed to read GetBucketLogging response body"); assert!( logging_body.contains("BucketOwner"), "GetBucketRequestPayment should return BucketOwner payer, got: {payment_body}" ); let website_response = signed_s3_request( Method::GET, &format!("{}/{bucket}?website=", env.url), None, None, &env.access_key, &env.secret_key, ) .await .expect("GetBucketWebsite HTTP request failed"); assert_eq!( website_response.status(), 404, "GetBucketWebsite should return 404 when website config is absent" ); let website_content_type = website_response .headers() .get(CONTENT_TYPE) .expect("GetBucketWebsite response should include Content-Type") .to_str() .expect("GetBucketWebsite Content-Type should be valid ASCII") .to_ascii_lowercase(); assert!( website_content_type.contains("xml"), "GetBucketWebsite error response should be XML, got content-type: {website_content_type}" ); let website_body = website_response .text() .await .expect("Failed to read GetBucketWebsite response body"); assert!( website_body.contains("NoSuchWebsiteConfiguration"), "GetBucketWebsite should return NoSuchWebsiteConfiguration code, got: {website_body}" ); let delete_response = signed_s3_request( Method::DELETE, &format!("{}/{bucket}?website=", env.url), None, None, &env.access_key, &env.secret_key, ) .await .expect("DeleteBucketWebsite HTTP request failed"); assert_eq!(delete_response.status(), 204, "DeleteBucketWebsite should return 204"); env.stop_server(); } }