merge main

This commit is contained in:
houseme
2025-05-28 17:26:31 +08:00
100 changed files with 10881 additions and 2464 deletions
+4 -4
View File
@@ -174,9 +174,9 @@ async fn license_handler() -> impl IntoResponse {
.unwrap()
}
fn _is_private_ip(ip: std::net::IpAddr) -> bool {
fn _is_private_ip(ip: IpAddr) -> bool {
match ip {
std::net::IpAddr::V4(ip) => {
IpAddr::V4(ip) => {
let octets = ip.octets();
// 10.0.0.0/8
octets[0] == 10 ||
@@ -185,7 +185,7 @@ fn _is_private_ip(ip: std::net::IpAddr) -> bool {
// 192.168.0.0/16
(octets[0] == 192 && octets[1] == 168)
}
std::net::IpAddr::V6(_) => false,
IpAddr::V6(_) => false,
}
}
@@ -281,7 +281,7 @@ async fn start_server(server_addr: SocketAddr, tls_path: Option<String>, app: Ro
.handle(handle.clone())
.serve(app.into_make_service())
.await
.map_err(|e| io::Error::other(e))?;
.map_err(io::Error::other)?;
info!("HTTPS server running on https://{}", server_addr);
+1
View File
@@ -2420,6 +2420,7 @@ impl Node for NodeService {
}
#[cfg(test)]
#[allow(unused_imports)]
mod tests {
use super::*;
use protos::proto_gen::node_service::{
+3 -3
View File
@@ -34,7 +34,7 @@ pub fn get_license() -> Option<Token> {
#[allow(unreachable_code)]
pub fn license_check() -> Result<()> {
return Ok(());
let inval_license = LICENSE.get().map(|token| {
let invalid_license = LICENSE.get().map(|token| {
if token.expired < SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() {
error!("License expired");
return Err(Error::from_string("Incorrect license, please contact RustFS.".to_string()));
@@ -43,7 +43,7 @@ pub fn license_check() -> Result<()> {
Ok(())
});
// let inval_license = config::get_config().license.as_ref().map(|license| {
// let invalid_license = config::get_config().license.as_ref().map(|license| {
// if license.is_empty() {
// error!("License is empty");
// return Err(Error::from_string("Incorrect license, please contact RustFS.".to_string()));
@@ -58,7 +58,7 @@ pub fn license_check() -> Result<()> {
// Ok(())
// });
if inval_license.is_none() || inval_license.is_some_and(|v| v.is_err()) {
if invalid_license.is_none() || invalid_license.is_some_and(|v| v.is_err()) {
return Err(Error::from_string("Incorrect license, please contact RustFS.".to_string()));
}
+28 -2
View File
@@ -72,6 +72,7 @@ const MI_B: usize = 1024 * 1024;
#[global_allocator]
static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
#[allow(clippy::result_large_err)]
fn check_auth(req: Request<()>) -> Result<Request<()>, Status> {
let token: MetadataValue<_> = "rustfs rpc".parse().unwrap();
@@ -80,6 +81,7 @@ fn check_auth(req: Request<()>) -> Result<Request<()>, Status> {
_ => Err(Status::unauthenticated("No valid auth token")),
}
}
#[instrument]
fn print_server_info() {
let cfg = CONSOLE_CONFIG.get().unwrap();
@@ -452,7 +454,8 @@ async fn run(opt: config::Opt) -> Result<()> {
let conn = http_server_clone.serve_connection(TokioIo::new(tls_socket), value_clone);
let conn = graceful_clone.watch(conn);
if let Err(err) = conn.await {
error!("Https Connection error: {}", err);
// Handle hyper::Error and low-level IO errors at a more granular level
handle_connection_error(&*err);
}
});
});
@@ -467,7 +470,8 @@ async fn run(opt: config::Opt) -> Result<()> {
let conn = http_server_clone.serve_connection(TokioIo::new(socket), value_clone);
let conn = graceful_clone.watch(conn);
if let Err(err) = conn.await {
error!("Http Connection error: {}", err);
// Handle hyper::Error and low-level IO errors at a more granular level
handle_connection_error(&*err);
}
});
debug!("Http handshake success");
@@ -591,3 +595,25 @@ async fn run(opt: config::Opt) -> Result<()> {
info!("server is stopped state: {:?}", state_manager.current_state());
Ok(())
}
fn handle_connection_error(err: &(dyn std::error::Error + 'static)) {
if let Some(hyper_err) = err.downcast_ref::<hyper::Error>() {
if hyper_err.is_incomplete_message() {
warn!("The HTTP connection is closed prematurely and the message is not completed:{}", hyper_err);
} else if hyper_err.is_closed() {
warn!("The HTTP connection is closed:{}", hyper_err);
} else if hyper_err.is_parse() {
error!("HTTP message parsing failed:{}", hyper_err);
} else if hyper_err.is_user() {
error!("HTTP user-custom error:{}", hyper_err);
} else if hyper_err.is_canceled() {
warn!("The HTTP connection is canceled:{}", hyper_err);
} else {
error!("Unknown hyper error:{:?}", hyper_err);
}
} else if let Some(io_err) = err.downcast_ref::<std::io::Error>() {
error!("Unknown connection IO error:{}", io_err);
} else {
error!("Unknown connection error type:{:?}", err);
}
}
+400
View File
@@ -87,3 +87,403 @@ 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));
}
}
+425
View File
@@ -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<HeaderValue> {
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<String, String> {
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::<StorageError>() {
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::<StorageError>() {
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::<StorageError>() {
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"));
}
}