From 84791e48776c48e0ac7aa0316aca1f79274a78af Mon Sep 17 00:00:00 2001 From: overtrue Date: Wed, 28 May 2025 00:12:41 +0800 Subject: [PATCH] feat: add comprehensive tests for rustfs storage module --- rustfs/src/storage/error.rs | 425 ++++++++++++++++++++++++++++++++++ rustfs/src/storage/options.rs | 425 ++++++++++++++++++++++++++++++++++ 2 files changed, 850 insertions(+) diff --git a/rustfs/src/storage/error.rs b/rustfs/src/storage/error.rs index ec5a5838b..fa2da7f06 100644 --- a/rustfs/src/storage/error.rs +++ b/rustfs/src/storage/error.rs @@ -87,3 +87,428 @@ pub fn to_s3_error(err: Error) -> S3Error { S3Error::with_message(S3ErrorCode::InternalError, format!(" ec err {}", err)) } + +#[cfg(test)] +mod tests { + use super::*; + use s3s::S3ErrorCode; + + #[test] + fn test_to_s3_error_not_implemented() { + let storage_err = StorageError::NotImplemented; + let err = Error::new(storage_err); + let s3_err = to_s3_error(err); + + assert_eq!(*s3_err.code(), S3ErrorCode::NotImplemented); + } + + #[test] + fn test_to_s3_error_invalid_argument() { + let storage_err = StorageError::InvalidArgument( + "test-bucket".to_string(), + "test-object".to_string(), + "test-version".to_string(), + ); + let err = Error::new(storage_err); + let s3_err = to_s3_error(err); + + assert_eq!(*s3_err.code(), S3ErrorCode::InvalidArgument); + assert!(s3_err.message().unwrap().contains("Invalid arguments provided")); + assert!(s3_err.message().unwrap().contains("test-bucket")); + assert!(s3_err.message().unwrap().contains("test-object")); + assert!(s3_err.message().unwrap().contains("test-version")); + } + + #[test] + fn test_to_s3_error_method_not_allowed() { + let storage_err = StorageError::MethodNotAllowed; + let err = Error::new(storage_err); + let s3_err = to_s3_error(err); + + assert_eq!(*s3_err.code(), S3ErrorCode::MethodNotAllowed); + } + + #[test] + fn test_to_s3_error_bucket_not_found() { + let storage_err = StorageError::BucketNotFound("test-bucket".to_string()); + let err = Error::new(storage_err); + let s3_err = to_s3_error(err); + + assert_eq!(*s3_err.code(), S3ErrorCode::NoSuchBucket); + assert!(s3_err.message().unwrap().contains("bucket not found")); + assert!(s3_err.message().unwrap().contains("test-bucket")); + } + + #[test] + fn test_to_s3_error_bucket_not_empty() { + let storage_err = StorageError::BucketNotEmpty("test-bucket".to_string()); + let err = Error::new(storage_err); + let s3_err = to_s3_error(err); + + assert_eq!(*s3_err.code(), S3ErrorCode::BucketNotEmpty); + assert!(s3_err.message().unwrap().contains("bucket not empty")); + assert!(s3_err.message().unwrap().contains("test-bucket")); + } + + #[test] + fn test_to_s3_error_bucket_name_invalid() { + let storage_err = StorageError::BucketNameInvalid("invalid-bucket-name".to_string()); + let err = Error::new(storage_err); + let s3_err = to_s3_error(err); + + assert_eq!(*s3_err.code(), S3ErrorCode::InvalidBucketName); + assert!(s3_err.message().unwrap().contains("invalid bucket name")); + assert!(s3_err.message().unwrap().contains("invalid-bucket-name")); + } + + #[test] + fn test_to_s3_error_object_name_invalid() { + let storage_err = StorageError::ObjectNameInvalid( + "test-bucket".to_string(), + "invalid-object".to_string(), + ); + let err = Error::new(storage_err); + let s3_err = to_s3_error(err); + + assert_eq!(*s3_err.code(), S3ErrorCode::InvalidArgument); + assert!(s3_err.message().unwrap().contains("invalid object name")); + assert!(s3_err.message().unwrap().contains("test-bucket")); + assert!(s3_err.message().unwrap().contains("invalid-object")); + } + + #[test] + fn test_to_s3_error_bucket_exists() { + let storage_err = StorageError::BucketExists("existing-bucket".to_string()); + let err = Error::new(storage_err); + let s3_err = to_s3_error(err); + + assert_eq!(*s3_err.code(), S3ErrorCode::BucketAlreadyExists); + assert!(s3_err.message().unwrap().contains("existing-bucket")); + } + + #[test] + fn test_to_s3_error_storage_full() { + let storage_err = StorageError::StorageFull; + let err = Error::new(storage_err); + let s3_err = to_s3_error(err); + + assert_eq!(*s3_err.code(), S3ErrorCode::ServiceUnavailable); + assert!(s3_err.message().unwrap().contains("Storage reached its minimum free drive threshold")); + } + + #[test] + fn test_to_s3_error_slow_down() { + let storage_err = StorageError::SlowDown; + let err = Error::new(storage_err); + let s3_err = to_s3_error(err); + + assert_eq!(*s3_err.code(), S3ErrorCode::SlowDown); + assert!(s3_err.message().unwrap().contains("Please reduce your request rate")); + } + + #[test] + fn test_to_s3_error_prefix_access_denied() { + let storage_err = StorageError::PrefixAccessDenied( + "test-bucket".to_string(), + "test-prefix".to_string(), + ); + let err = Error::new(storage_err); + let s3_err = to_s3_error(err); + + assert_eq!(*s3_err.code(), S3ErrorCode::AccessDenied); + assert!(s3_err.message().unwrap().contains("PrefixAccessDenied")); + assert!(s3_err.message().unwrap().contains("test-bucket")); + assert!(s3_err.message().unwrap().contains("test-prefix")); + } + + #[test] + fn test_to_s3_error_invalid_upload_id_key_combination() { + let storage_err = StorageError::InvalidUploadIDKeyCombination( + "test-bucket".to_string(), + "test-object".to_string(), + ); + let err = Error::new(storage_err); + let s3_err = to_s3_error(err); + + assert_eq!(*s3_err.code(), S3ErrorCode::InvalidArgument); + assert!(s3_err.message().unwrap().contains("Invalid UploadID KeyCombination")); + assert!(s3_err.message().unwrap().contains("test-bucket")); + assert!(s3_err.message().unwrap().contains("test-object")); + } + + #[test] + fn test_to_s3_error_malformed_upload_id() { + let storage_err = StorageError::MalformedUploadID("malformed-id".to_string()); + let err = Error::new(storage_err); + let s3_err = to_s3_error(err); + + assert_eq!(*s3_err.code(), S3ErrorCode::InvalidArgument); + assert!(s3_err.message().unwrap().contains("Malformed UploadID")); + assert!(s3_err.message().unwrap().contains("malformed-id")); + } + + #[test] + fn test_to_s3_error_object_name_too_long() { + let storage_err = StorageError::ObjectNameTooLong( + "test-bucket".to_string(), + "very-long-object-name".to_string(), + ); + let err = Error::new(storage_err); + let s3_err = to_s3_error(err); + + assert_eq!(*s3_err.code(), S3ErrorCode::InvalidArgument); + assert!(s3_err.message().unwrap().contains("Object name too long")); + assert!(s3_err.message().unwrap().contains("test-bucket")); + assert!(s3_err.message().unwrap().contains("very-long-object-name")); + } + + #[test] + fn test_to_s3_error_object_name_prefix_as_slash() { + let storage_err = StorageError::ObjectNamePrefixAsSlash( + "test-bucket".to_string(), + "/invalid-object".to_string(), + ); + let err = Error::new(storage_err); + let s3_err = to_s3_error(err); + + assert_eq!(*s3_err.code(), S3ErrorCode::InvalidArgument); + assert!(s3_err.message().unwrap().contains("Object name contains forward slash as prefix")); + assert!(s3_err.message().unwrap().contains("test-bucket")); + assert!(s3_err.message().unwrap().contains("/invalid-object")); + } + + #[test] + fn test_to_s3_error_object_not_found() { + let storage_err = StorageError::ObjectNotFound( + "test-bucket".to_string(), + "missing-object".to_string(), + ); + let err = Error::new(storage_err); + let s3_err = to_s3_error(err); + + assert_eq!(*s3_err.code(), S3ErrorCode::NoSuchKey); + assert!(s3_err.message().unwrap().contains("test-bucket")); + assert!(s3_err.message().unwrap().contains("missing-object")); + } + + #[test] + fn test_to_s3_error_version_not_found() { + let storage_err = StorageError::VersionNotFound( + "test-bucket".to_string(), + "test-object".to_string(), + "missing-version".to_string(), + ); + let err = Error::new(storage_err); + let s3_err = to_s3_error(err); + + assert_eq!(*s3_err.code(), S3ErrorCode::NoSuchVersion); + assert!(s3_err.message().unwrap().contains("test-bucket")); + assert!(s3_err.message().unwrap().contains("test-object")); + assert!(s3_err.message().unwrap().contains("missing-version")); + } + + #[test] + fn test_to_s3_error_invalid_upload_id() { + let storage_err = StorageError::InvalidUploadID( + "test-bucket".to_string(), + "test-object".to_string(), + "invalid-upload-id".to_string(), + ); + let err = Error::new(storage_err); + let s3_err = to_s3_error(err); + + assert_eq!(*s3_err.code(), S3ErrorCode::InvalidPart); + assert!(s3_err.message().unwrap().contains("Invalid upload id")); + assert!(s3_err.message().unwrap().contains("test-bucket")); + assert!(s3_err.message().unwrap().contains("test-object")); + assert!(s3_err.message().unwrap().contains("invalid-upload-id")); + } + + #[test] + fn test_to_s3_error_invalid_version_id() { + let storage_err = StorageError::InvalidVersionID( + "test-bucket".to_string(), + "test-object".to_string(), + "invalid-version-id".to_string(), + ); + let err = Error::new(storage_err); + let s3_err = to_s3_error(err); + + assert_eq!(*s3_err.code(), S3ErrorCode::InvalidArgument); + assert!(s3_err.message().unwrap().contains("Invalid version id")); + assert!(s3_err.message().unwrap().contains("test-bucket")); + assert!(s3_err.message().unwrap().contains("test-object")); + assert!(s3_err.message().unwrap().contains("invalid-version-id")); + } + + #[test] + fn test_to_s3_error_data_movement_overwrite_err() { + let storage_err = StorageError::DataMovementOverwriteErr( + "test-bucket".to_string(), + "test-object".to_string(), + "test-version".to_string(), + ); + let err = Error::new(storage_err); + let s3_err = to_s3_error(err); + + assert_eq!(*s3_err.code(), S3ErrorCode::InvalidArgument); + assert!(s3_err.message().unwrap().contains("invalid data movement operation")); + assert!(s3_err.message().unwrap().contains("source and destination pool are the same")); + assert!(s3_err.message().unwrap().contains("test-bucket")); + assert!(s3_err.message().unwrap().contains("test-object")); + assert!(s3_err.message().unwrap().contains("test-version")); + } + + #[test] + fn test_to_s3_error_object_exists_as_directory() { + let storage_err = StorageError::ObjectExistsAsDirectory( + "test-bucket".to_string(), + "directory-object".to_string(), + ); + let err = Error::new(storage_err); + let s3_err = to_s3_error(err); + + assert_eq!(*s3_err.code(), S3ErrorCode::InvalidArgument); + assert!(s3_err.message().unwrap().contains("Object exists on")); + assert!(s3_err.message().unwrap().contains("as directory")); + assert!(s3_err.message().unwrap().contains("test-bucket")); + assert!(s3_err.message().unwrap().contains("directory-object")); + } + + #[test] + fn test_to_s3_error_insufficient_read_quorum() { + let storage_err = StorageError::InsufficientReadQuorum; + let err = Error::new(storage_err); + let s3_err = to_s3_error(err); + + assert_eq!(*s3_err.code(), S3ErrorCode::SlowDown); + assert!(s3_err.message().unwrap().contains("Storage resources are insufficient for the read operation")); + } + + #[test] + fn test_to_s3_error_insufficient_write_quorum() { + let storage_err = StorageError::InsufficientWriteQuorum; + let err = Error::new(storage_err); + let s3_err = to_s3_error(err); + + assert_eq!(*s3_err.code(), S3ErrorCode::SlowDown); + assert!(s3_err.message().unwrap().contains("Storage resources are insufficient for the write operation")); + } + + #[test] + fn test_to_s3_error_decommission_not_started() { + let storage_err = StorageError::DecommissionNotStarted; + let err = Error::new(storage_err); + let s3_err = to_s3_error(err); + + assert_eq!(*s3_err.code(), S3ErrorCode::InvalidArgument); + assert!(s3_err.message().unwrap().contains("Decommission Not Started")); + } + + #[test] + fn test_to_s3_error_decommission_already_running() { + let storage_err = StorageError::DecommissionAlreadyRunning; + let err = Error::new(storage_err); + let s3_err = to_s3_error(err); + + assert_eq!(*s3_err.code(), S3ErrorCode::InternalError); + assert!(s3_err.message().unwrap().contains("Decommission already running")); + } + + #[test] + fn test_to_s3_error_volume_not_found() { + let storage_err = StorageError::VolumeNotFound("test-volume".to_string()); + let err = Error::new(storage_err); + let s3_err = to_s3_error(err); + + assert_eq!(*s3_err.code(), S3ErrorCode::NoSuchBucket); + assert!(s3_err.message().unwrap().contains("bucket not found")); + assert!(s3_err.message().unwrap().contains("test-volume")); + } + + #[test] + fn test_to_s3_error_invalid_part() { + let storage_err = StorageError::InvalidPart( + 1, + "expected-part".to_string(), + "got-part".to_string(), + ); + let err = Error::new(storage_err); + let s3_err = to_s3_error(err); + + assert_eq!(*s3_err.code(), S3ErrorCode::InvalidPart); + assert!(s3_err.message().unwrap().contains("Specified part could not be found")); + assert!(s3_err.message().unwrap().contains("PartNumber")); + assert!(s3_err.message().unwrap().contains("expected-part")); + assert!(s3_err.message().unwrap().contains("got-part")); + } + + #[test] + fn test_to_s3_error_done_for_now() { + let storage_err = StorageError::DoneForNow; + let err = Error::new(storage_err); + let s3_err = to_s3_error(err); + + assert_eq!(*s3_err.code(), S3ErrorCode::InternalError); + assert!(s3_err.message().unwrap().contains("DoneForNow")); + } + + #[test] + fn test_to_s3_error_non_storage_error() { + // Test with a non-StorageError + let err = Error::from_string("Generic error message".to_string()); + let s3_err = to_s3_error(err); + + assert_eq!(*s3_err.code(), S3ErrorCode::InternalError); + assert!(s3_err.message().unwrap().contains("ec err")); + assert!(s3_err.message().unwrap().contains("Generic error message")); + } + + #[test] + fn test_to_s3_error_with_unicode_strings() { + let storage_err = StorageError::BucketNotFound("测试桶".to_string()); + let err = Error::new(storage_err); + let s3_err = to_s3_error(err); + + assert_eq!(*s3_err.code(), S3ErrorCode::NoSuchBucket); + assert!(s3_err.message().unwrap().contains("bucket not found")); + assert!(s3_err.message().unwrap().contains("测试桶")); + } + + #[test] + fn test_to_s3_error_with_special_characters() { + let storage_err = StorageError::ObjectNameInvalid( + "bucket-with-@#$%".to_string(), + "object-with-!@#$%^&*()".to_string(), + ); + let err = Error::new(storage_err); + let s3_err = to_s3_error(err); + + assert_eq!(*s3_err.code(), S3ErrorCode::InvalidArgument); + assert!(s3_err.message().unwrap().contains("invalid object name")); + assert!(s3_err.message().unwrap().contains("bucket-with-@#$%")); + assert!(s3_err.message().unwrap().contains("object-with-!@#$%^&*()")); + } + + #[test] + fn test_to_s3_error_with_empty_strings() { + let storage_err = StorageError::BucketNotFound("".to_string()); + let err = Error::new(storage_err); + let s3_err = to_s3_error(err); + + assert_eq!(*s3_err.code(), S3ErrorCode::NoSuchBucket); + assert!(s3_err.message().unwrap().contains("bucket not found")); + } + + #[test] + fn test_to_s3_error_with_very_long_strings() { + let long_bucket_name = "a".repeat(1000); + let storage_err = StorageError::BucketNotFound(long_bucket_name.clone()); + let err = Error::new(storage_err); + let s3_err = to_s3_error(err); + + assert_eq!(*s3_err.code(), S3ErrorCode::NoSuchBucket); + assert!(s3_err.message().unwrap().contains("bucket not found")); + assert!(s3_err.message().unwrap().contains(&long_bucket_name)); + } +} diff --git a/rustfs/src/storage/options.rs b/rustfs/src/storage/options.rs index ae0df5391..64a2f2c3e 100644 --- a/rustfs/src/storage/options.rs +++ b/rustfs/src/storage/options.rs @@ -241,3 +241,428 @@ lazy_static! { "x-amz-replication-status" ]; } + +#[cfg(test)] +mod tests { + use super::*; + use http::{HeaderMap, HeaderValue}; + use std::collections::HashMap; + use uuid::Uuid; + + fn create_test_headers() -> HeaderMap { + let mut headers = HeaderMap::new(); + headers.insert("content-type", HeaderValue::from_static("application/json")); + headers.insert("x-amz-meta-custom", HeaderValue::from_static("custom-value")); + headers.insert("x-rustfs-meta-internal", HeaderValue::from_static("internal-value")); + headers.insert("cache-control", HeaderValue::from_static("no-cache")); + headers + } + + fn create_test_metadata() -> HashMap { + let mut metadata = HashMap::new(); + metadata.insert("key1".to_string(), "value1".to_string()); + metadata.insert("key2".to_string(), "value2".to_string()); + metadata + } + + #[tokio::test] + async fn test_del_opts_basic() { + let headers = create_test_headers(); + let metadata = Some(create_test_metadata()); + + let result = del_opts("test-bucket", "test-object", None, &headers, metadata).await; + + assert!(result.is_ok()); + let opts = result.unwrap(); + assert!(opts.user_defined.is_some()); + assert_eq!(opts.version_id, None); + } + + #[tokio::test] + async fn test_del_opts_with_directory_object() { + let headers = create_test_headers(); + + let result = del_opts("test-bucket", "test-dir/", None, &headers, None).await; + + assert!(result.is_ok()); + let opts = result.unwrap(); + assert_eq!(opts.version_id, Some(Uuid::nil().to_string())); + } + + #[tokio::test] + async fn test_del_opts_with_valid_version_id() { + let headers = create_test_headers(); + let valid_uuid = Uuid::new_v4().to_string(); + + let result = del_opts("test-bucket", "test-object", Some(valid_uuid.clone()), &headers, None).await; + + // This test may fail if versioning is not enabled for the bucket + // In a real test environment, you would mock BucketVersioningSys + match result { + Ok(opts) => { + assert_eq!(opts.version_id, Some(valid_uuid)); + } + Err(_) => { + // Expected if versioning is not enabled + } + } + } + + #[tokio::test] + async fn test_del_opts_with_invalid_version_id() { + let headers = create_test_headers(); + let invalid_uuid = "invalid-uuid".to_string(); + + let result = del_opts("test-bucket", "test-object", Some(invalid_uuid), &headers, None).await; + + assert!(result.is_err()); + if let Err(err) = result { + if let Some(storage_err) = err.downcast_ref::() { + match storage_err { + StorageError::InvalidVersionID(bucket, object, version) => { + assert_eq!(bucket, "test-bucket"); + assert_eq!(object, "test-object"); + assert_eq!(version, "invalid-uuid"); + } + _ => panic!("Expected InvalidVersionID error"), + } + } + } + } + + #[tokio::test] + async fn test_get_opts_basic() { + let headers = create_test_headers(); + + let result = get_opts("test-bucket", "test-object", None, None, &headers).await; + + assert!(result.is_ok()); + let opts = result.unwrap(); + assert_eq!(opts.part_number, None); + assert_eq!(opts.version_id, None); + } + + #[tokio::test] + async fn test_get_opts_with_part_number() { + let headers = create_test_headers(); + + let result = get_opts("test-bucket", "test-object", None, Some(5), &headers).await; + + assert!(result.is_ok()); + let opts = result.unwrap(); + assert_eq!(opts.part_number, Some(5)); + } + + #[tokio::test] + async fn test_get_opts_with_directory_object() { + let headers = create_test_headers(); + + let result = get_opts("test-bucket", "test-dir/", None, None, &headers).await; + + assert!(result.is_ok()); + let opts = result.unwrap(); + assert_eq!(opts.version_id, Some(Uuid::nil().to_string())); + } + + #[tokio::test] + async fn test_get_opts_with_invalid_version_id() { + let headers = create_test_headers(); + let invalid_uuid = "invalid-uuid".to_string(); + + let result = get_opts("test-bucket", "test-object", Some(invalid_uuid), None, &headers).await; + + assert!(result.is_err()); + if let Err(err) = result { + if let Some(storage_err) = err.downcast_ref::() { + match storage_err { + StorageError::InvalidVersionID(bucket, object, version) => { + assert_eq!(bucket, "test-bucket"); + assert_eq!(object, "test-object"); + assert_eq!(version, "invalid-uuid"); + } + _ => panic!("Expected InvalidVersionID error"), + } + } + } + } + + #[tokio::test] + async fn test_put_opts_basic() { + let headers = create_test_headers(); + let metadata = Some(create_test_metadata()); + + let result = put_opts("test-bucket", "test-object", None, &headers, metadata).await; + + assert!(result.is_ok()); + let opts = result.unwrap(); + assert!(opts.user_defined.is_some()); + assert_eq!(opts.version_id, None); + } + + #[tokio::test] + async fn test_put_opts_with_directory_object() { + let headers = create_test_headers(); + + let result = put_opts("test-bucket", "test-dir/", None, &headers, None).await; + + assert!(result.is_ok()); + let opts = result.unwrap(); + assert_eq!(opts.version_id, Some(Uuid::nil().to_string())); + } + + #[tokio::test] + async fn test_put_opts_with_invalid_version_id() { + let headers = create_test_headers(); + let invalid_uuid = "invalid-uuid".to_string(); + + let result = put_opts("test-bucket", "test-object", Some(invalid_uuid), &headers, None).await; + + assert!(result.is_err()); + if let Err(err) = result { + if let Some(storage_err) = err.downcast_ref::() { + match storage_err { + StorageError::InvalidVersionID(bucket, object, version) => { + assert_eq!(bucket, "test-bucket"); + assert_eq!(object, "test-object"); + assert_eq!(version, "invalid-uuid"); + } + _ => panic!("Expected InvalidVersionID error"), + } + } + } + } + + #[tokio::test] + async fn test_copy_dst_opts() { + let headers = create_test_headers(); + let metadata = Some(create_test_metadata()); + + let result = copy_dst_opts("test-bucket", "test-object", None, &headers, metadata).await; + + assert!(result.is_ok()); + let opts = result.unwrap(); + assert!(opts.user_defined.is_some()); + } + + #[test] + fn test_copy_src_opts() { + let headers = create_test_headers(); + + let result = copy_src_opts("test-bucket", "test-object", &headers); + + assert!(result.is_ok()); + let opts = result.unwrap(); + assert!(opts.user_defined.is_none()); + } + + #[test] + fn test_put_opts_from_headers() { + let headers = create_test_headers(); + let metadata = Some(create_test_metadata()); + + let result = put_opts_from_headers(&headers, metadata); + + assert!(result.is_ok()); + let opts = result.unwrap(); + assert!(opts.user_defined.is_some()); + let user_defined = opts.user_defined.unwrap(); + assert_eq!(user_defined.get("key1"), Some(&"value1".to_string())); + assert_eq!(user_defined.get("key2"), Some(&"value2".to_string())); + } + + #[test] + fn test_get_default_opts_with_metadata() { + let headers = create_test_headers(); + let metadata = Some(create_test_metadata()); + + let result = get_default_opts(&headers, metadata, false); + + assert!(result.is_ok()); + let opts = result.unwrap(); + assert!(opts.user_defined.is_some()); + let user_defined = opts.user_defined.unwrap(); + assert_eq!(user_defined.get("key1"), Some(&"value1".to_string())); + assert_eq!(user_defined.get("key2"), Some(&"value2".to_string())); + } + + #[test] + fn test_get_default_opts_without_metadata() { + let headers = create_test_headers(); + + let result = get_default_opts(&headers, None, false); + + assert!(result.is_ok()); + let opts = result.unwrap(); + assert!(opts.user_defined.is_none()); + } + + #[test] + fn test_extract_metadata_basic() { + let headers = create_test_headers(); + + let metadata = extract_metadata(&headers); + + assert!(metadata.contains_key("content-type")); + assert_eq!(metadata.get("content-type"), Some(&"application/json".to_string())); + assert!(metadata.contains_key("cache-control")); + assert_eq!(metadata.get("cache-control"), Some(&"no-cache".to_string())); + assert!(metadata.contains_key("custom")); + assert_eq!(metadata.get("custom"), Some(&"custom-value".to_string())); + assert!(metadata.contains_key("internal")); + assert_eq!(metadata.get("internal"), Some(&"internal-value".to_string())); + } + + #[test] + fn test_extract_metadata_from_mime_amz_meta() { + let mut headers = HeaderMap::new(); + headers.insert("x-amz-meta-user-id", HeaderValue::from_static("12345")); + headers.insert("x-amz-meta-project", HeaderValue::from_static("test-project")); + headers.insert("x-amz-meta-", HeaderValue::from_static("empty-key")); // Should be ignored + + let mut metadata = HashMap::new(); + extract_metadata_from_mime(&headers, &mut metadata); + + assert_eq!(metadata.get("user-id"), Some(&"12345".to_string())); + assert_eq!(metadata.get("project"), Some(&"test-project".to_string())); + assert!(!metadata.contains_key("")); + } + + #[test] + fn test_extract_metadata_from_mime_rustfs_meta() { + let mut headers = HeaderMap::new(); + headers.insert("x-rustfs-meta-internal-id", HeaderValue::from_static("67890")); + headers.insert("x-rustfs-meta-category", HeaderValue::from_static("documents")); + + let mut metadata = HashMap::new(); + extract_metadata_from_mime(&headers, &mut metadata); + + assert_eq!(metadata.get("internal-id"), Some(&"67890".to_string())); + assert_eq!(metadata.get("category"), Some(&"documents".to_string())); + } + + #[test] + fn test_extract_metadata_from_mime_supported_headers() { + let mut headers = HeaderMap::new(); + headers.insert("content-type", HeaderValue::from_static("text/plain")); + headers.insert("cache-control", HeaderValue::from_static("max-age=3600")); + headers.insert("content-language", HeaderValue::from_static("en-US")); + headers.insert("content-encoding", HeaderValue::from_static("gzip")); + headers.insert("content-disposition", HeaderValue::from_static("attachment")); + headers.insert("x-amz-storage-class", HeaderValue::from_static("STANDARD")); + headers.insert("x-amz-tagging", HeaderValue::from_static("key1=value1&key2=value2")); + headers.insert("expires", HeaderValue::from_static("Wed, 21 Oct 2015 07:28:00 GMT")); + headers.insert("x-amz-replication-status", HeaderValue::from_static("COMPLETED")); + + let mut metadata = HashMap::new(); + extract_metadata_from_mime(&headers, &mut metadata); + + assert_eq!(metadata.get("content-type"), Some(&"text/plain".to_string())); + assert_eq!(metadata.get("cache-control"), Some(&"max-age=3600".to_string())); + assert_eq!(metadata.get("content-language"), Some(&"en-US".to_string())); + assert_eq!(metadata.get("content-encoding"), Some(&"gzip".to_string())); + assert_eq!(metadata.get("content-disposition"), Some(&"attachment".to_string())); + assert_eq!(metadata.get("x-amz-storage-class"), Some(&"STANDARD".to_string())); + assert_eq!(metadata.get("x-amz-tagging"), Some(&"key1=value1&key2=value2".to_string())); + assert_eq!(metadata.get("expires"), Some(&"Wed, 21 Oct 2015 07:28:00 GMT".to_string())); + assert_eq!(metadata.get("x-amz-replication-status"), Some(&"COMPLETED".to_string())); + } + + #[test] + fn test_extract_metadata_from_mime_default_content_type() { + let headers = HeaderMap::new(); + + let mut metadata = HashMap::new(); + extract_metadata_from_mime(&headers, &mut metadata); + + assert_eq!(metadata.get("content-type"), Some(&"binary/octet-stream".to_string())); + } + + #[test] + fn test_extract_metadata_from_mime_existing_content_type() { + let mut headers = HeaderMap::new(); + headers.insert("content-type", HeaderValue::from_static("application/json")); + + let mut metadata = HashMap::new(); + extract_metadata_from_mime(&headers, &mut metadata); + + assert_eq!(metadata.get("content-type"), Some(&"application/json".to_string())); + } + + #[test] + fn test_extract_metadata_from_mime_unicode_values() { + let mut headers = HeaderMap::new(); + headers.insert("x-amz-meta-chinese", HeaderValue::from_bytes("测试值".as_bytes()).unwrap()); + headers.insert("x-rustfs-meta-emoji", HeaderValue::from_bytes("🚀".as_bytes()).unwrap()); + + let mut metadata = HashMap::new(); + extract_metadata_from_mime(&headers, &mut metadata); + + assert_eq!(metadata.get("chinese"), Some(&"测试值".to_string())); + assert_eq!(metadata.get("emoji"), Some(&"🚀".to_string())); + } + + #[test] + fn test_extract_metadata_from_mime_unsupported_headers() { + let mut headers = HeaderMap::new(); + headers.insert("authorization", HeaderValue::from_static("Bearer token")); + headers.insert("host", HeaderValue::from_static("example.com")); + headers.insert("user-agent", HeaderValue::from_static("test-agent")); + + let mut metadata = HashMap::new(); + extract_metadata_from_mime(&headers, &mut metadata); + + // These headers should not be included in metadata + assert!(!metadata.contains_key("authorization")); + assert!(!metadata.contains_key("host")); + assert!(!metadata.contains_key("user-agent")); + // But default content-type should be added + assert_eq!(metadata.get("content-type"), Some(&"binary/octet-stream".to_string())); + } + + #[test] + fn test_supported_headers_constant() { + let expected_headers = vec![ + "content-type", + "cache-control", + "content-language", + "content-encoding", + "content-disposition", + "x-amz-storage-class", + "x-amz-tagging", + "expires", + "x-amz-replication-status" + ]; + + assert_eq!(*SUPPORTED_HEADERS, expected_headers); + assert_eq!(SUPPORTED_HEADERS.len(), 9); + } + + #[test] + fn test_extract_metadata_empty_headers() { + let headers = HeaderMap::new(); + + let metadata = extract_metadata(&headers); + + // Should only contain default content-type + assert_eq!(metadata.len(), 1); + assert_eq!(metadata.get("content-type"), Some(&"binary/octet-stream".to_string())); + } + + #[test] + fn test_extract_metadata_mixed_headers() { + let mut headers = HeaderMap::new(); + headers.insert("content-type", HeaderValue::from_static("application/xml")); + headers.insert("x-amz-meta-version", HeaderValue::from_static("1.0")); + headers.insert("x-rustfs-meta-source", HeaderValue::from_static("upload")); + headers.insert("cache-control", HeaderValue::from_static("public")); + headers.insert("authorization", HeaderValue::from_static("Bearer xyz")); // Should be ignored + + let metadata = extract_metadata(&headers); + + assert_eq!(metadata.get("content-type"), Some(&"application/xml".to_string())); + assert_eq!(metadata.get("version"), Some(&"1.0".to_string())); + assert_eq!(metadata.get("source"), Some(&"upload".to_string())); + assert_eq!(metadata.get("cache-control"), Some(&"public".to_string())); + assert!(!metadata.contains_key("authorization")); + } +}