From b387f6c58aa5a9048a305f90f8839760383c7edc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=89=E6=AD=A3=E8=B6=85?= Date: Sun, 12 Apr 2026 08:49:51 +0800 Subject: [PATCH] refactor(storage): inline object lock config entrypoints (#2494) --- rustfs/src/app/object_usecase.rs | 289 +----------------- .../src/app/object_usecase/zero_copy_tests.rs | 3 +- rustfs/src/storage/ecfs.rs | 181 ++++++++++- rustfs/src/storage/ecfs_test.rs | 118 ++++++- 4 files changed, 293 insertions(+), 298 deletions(-) diff --git a/rustfs/src/app/object_usecase.rs b/rustfs/src/app/object_usecase.rs index 3408cb7e1..0dcc2079b 100644 --- a/rustfs/src/app/object_usecase.rs +++ b/rustfs/src/app/object_usecase.rs @@ -54,7 +54,6 @@ use rustfs_ecstore::bucket::{ bucket_lifecycle_ops::{RestoreRequestOps, enqueue_transition_immediate, post_restore_opts}, lifecycle::{self, Lifecycle, TransitionOptions}, }, - metadata::{BUCKET_VERSIONING_CONFIG, OBJECT_LOCK_CONFIG}, metadata_sys, object_lock::{ objectlock::{get_object_legalhold_meta, get_object_retention_meta}, @@ -66,7 +65,6 @@ use rustfs_ecstore::bucket::{ schedule_replication_delete, }, tagging::decode_tags, - utils::serialize, versioning::VersioningApi, versioning_sys::BucketVersioningSys, }; @@ -77,8 +75,7 @@ use rustfs_ecstore::error::{StorageError, is_err_bucket_not_found, is_err_object use rustfs_ecstore::new_object_layer_fn; use rustfs_ecstore::set_disk::is_valid_storage_class; use rustfs_ecstore::store_api::{ - BucketOperations, BucketOptions, ChunkNativePutData, HTTPRangeSpec, ObjectIO, ObjectInfo, ObjectOperations, ObjectOptions, - ObjectToDelete, + ChunkNativePutData, HTTPRangeSpec, ObjectIO, ObjectInfo, ObjectOperations, ObjectOptions, ObjectToDelete, }; use rustfs_filemeta::{ REPLICATE_INCOMING_DELETE, ReplicationStatusType, ReplicationType, RestoreStatusOps, VersionPurgeStatusType, @@ -513,83 +510,6 @@ pub(crate) async fn build_put_like_object_lock_metadata( Ok(Some(eval_metadata)) } -const MAXIMUM_RETENTION_DAYS: i32 = 36_500; -const MAXIMUM_RETENTION_YEARS: i32 = 100; - -fn invalid_object_lock_configuration(message: impl Into) -> S3Error { - S3Error::with_message(S3ErrorCode::MalformedXML, message.into()) -} - -fn invalid_retention_period(message: impl Into) -> S3Error { - let mut err = S3Error::with_message(S3ErrorCode::Custom("InvalidRetentionPeriod".into()), message.into()); - err.set_status_code(StatusCode::BAD_REQUEST); - err -} - -fn validate_default_retention_configuration(default_retention: &DefaultRetention) -> S3Result<()> { - let Some(mode) = default_retention.mode.as_ref() else { - return Err(invalid_object_lock_configuration("retention mode must be specified")); - }; - - match mode.as_str() { - ObjectLockRetentionMode::COMPLIANCE | ObjectLockRetentionMode::GOVERNANCE => {} - _ => { - return Err(invalid_object_lock_configuration(format!("unknown retention mode {}", mode.as_str()))); - } - } - - match (default_retention.days, default_retention.years) { - (Some(days), None) => { - if days <= 0 { - return Err(invalid_retention_period( - "Default retention period must be a positive integer value for 'Days'", - )); - } - if days > MAXIMUM_RETENTION_DAYS { - return Err(invalid_retention_period(format!("Default retention period too large for 'Days' {days}",))); - } - } - (None, Some(years)) => { - if years <= 0 { - return Err(invalid_retention_period( - "Default retention period must be a positive integer value for 'Years'", - )); - } - if years > MAXIMUM_RETENTION_YEARS { - return Err(invalid_retention_period(format!( - "Default retention period too large for 'Years' {years}", - ))); - } - } - (Some(_), Some(_)) => { - return Err(invalid_object_lock_configuration("either Days or Years must be specified, not both")); - } - (None, None) => { - return Err(invalid_object_lock_configuration("either Days or Years must be specified")); - } - } - - Ok(()) -} - -fn validate_object_lock_configuration_input(input_cfg: &ObjectLockConfiguration) -> S3Result<()> { - let enabled = input_cfg.object_lock_enabled.as_ref().map(ObjectLockEnabled::as_str); - if enabled != Some(ObjectLockEnabled::ENABLED) { - return Err(invalid_object_lock_configuration( - "only 'Enabled' value is allowed to ObjectLockEnabled element", - )); - } - - if let Some(rule) = input_cfg.rule.as_ref() { - let Some(default_retention) = rule.default_retention.as_ref() else { - return Err(invalid_object_lock_configuration("Rule must include DefaultRetention")); - }; - validate_default_retention_configuration(default_retention)?; - } - - Ok(()) -} - pub(crate) fn validate_existing_object_lock_for_write(existing_obj_info: &ObjectInfo) -> S3Result<()> { let legal_hold = get_object_legalhold_meta(&existing_obj_info.user_defined); if legal_hold @@ -790,76 +710,6 @@ impl DefaultObjectUsecase { result } - #[instrument(level = "debug", skip(self))] - pub async fn execute_put_object_lock_configuration( - &self, - req: S3Request, - ) -> S3Result> { - let PutObjectLockConfigurationInput { - bucket, - object_lock_configuration, - .. - } = req.input; - - let Some(input_cfg) = object_lock_configuration else { return Err(s3_error!(InvalidArgument)) }; - - let Some(store) = new_object_layer_fn() else { - return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string())); - }; - - store - .get_bucket_info(&bucket, &BucketOptions::default()) - .await - .map_err(ApiError::from)?; - - validate_object_lock_configuration_input(&input_cfg)?; - - match metadata_sys::get_object_lock_config(&bucket).await { - Ok(_) => {} - Err(err) => { - if err == StorageError::ConfigNotFound { - // AWS S3 allows enabling Object Lock on existing buckets if versioning - // is already enabled. Reject only when versioning is not enabled. - if !BucketVersioningSys::enabled(&bucket).await { - return Err(S3Error::with_message( - S3ErrorCode::InvalidBucketState, - "Object Lock configuration cannot be enabled on existing buckets".to_string(), - )); - } - } else { - warn!("get_object_lock_config err {:?}", err); - return Err(S3Error::with_message( - S3ErrorCode::InternalError, - "Failed to get bucket ObjectLockConfiguration".to_string(), - )); - } - } - }; - - let data = serialize(&input_cfg).map_err(|err| S3Error::with_message(S3ErrorCode::InternalError, format!("{}", err)))?; - - metadata_sys::update(&bucket, OBJECT_LOCK_CONFIG, data) - .await - .map_err(ApiError::from)?; - - // When Object Lock is enabled, automatically enable versioning if not already enabled. - // This matches S3-compatible behavior. - let versioning_config = BucketVersioningSys::get(&bucket).await.map_err(ApiError::from)?; - if !versioning_config.enabled() { - let enable_versioning_config = VersioningConfiguration { - status: Some(BucketVersioningStatus::from_static(BucketVersioningStatus::ENABLED)), - ..Default::default() - }; - let versioning_data = serialize(&enable_versioning_config) - .map_err(|err| S3Error::with_message(S3ErrorCode::InternalError, format!("{}", err)))?; - metadata_sys::update(&bucket, BUCKET_VERSIONING_CONFIG, versioning_data) - .await - .map_err(ApiError::from)?; - } - - Ok(S3Response::new(PutObjectLockConfigurationOutput::default())) - } - #[instrument( level = "debug", skip(self, req), @@ -1176,35 +1026,6 @@ impl DefaultObjectUsecase { result } - #[instrument(level = "debug", skip(self))] - pub async fn execute_get_object_lock_configuration( - &self, - req: S3Request, - ) -> S3Result> { - let GetObjectLockConfigurationInput { bucket, .. } = req.input; - - let object_lock_configuration = match metadata_sys::get_object_lock_config(&bucket).await { - Ok((cfg, _created)) => Some(cfg), - Err(err) => { - if err == StorageError::ConfigNotFound { - return Err(S3Error::with_message( - S3ErrorCode::ObjectLockConfigurationNotFoundError, - "Object Lock configuration does not exist for this bucket".to_string(), - )); - } - warn!("get_object_lock_config err {:?}", err); - return Err(S3Error::with_message( - S3ErrorCode::InternalError, - "Failed to load Object Lock configuration".to_string(), - )); - } - }; - - Ok(S3Response::new(GetObjectLockConfigurationOutput { - object_lock_configuration, - })) - } - #[instrument(level = "debug", skip(self, req))] pub async fn execute_copy_object(&self, req: S3Request) -> S3Result> { let mut helper = OperationHelper::new(&req, EventName::ObjectCreatedCopy, S3Operation::CopyObject); @@ -2860,114 +2681,6 @@ mod tests { assert_eq!(err.code(), &S3ErrorCode::InternalError); } - #[tokio::test] - async fn execute_put_object_lock_configuration_returns_internal_error_when_store_uninitialized() { - let input = PutObjectLockConfigurationInput::builder() - .bucket("test-bucket".to_string()) - .object_lock_configuration(Some(ObjectLockConfiguration { - object_lock_enabled: Some(ObjectLockEnabled::from_static(ObjectLockEnabled::ENABLED)), - rule: None, - })) - .build() - .unwrap(); - - let req = build_request(input, Method::PUT); - let usecase = DefaultObjectUsecase::without_context(); - - let err = usecase.execute_put_object_lock_configuration(req).await.unwrap_err(); - assert_eq!(err.code(), &S3ErrorCode::InternalError); - } - - #[test] - fn validate_object_lock_configuration_rejects_disabled_status() { - let cfg = ObjectLockConfiguration { - object_lock_enabled: Some(ObjectLockEnabled::from("Disabled".to_string())), - rule: None, - }; - - let err = validate_object_lock_configuration_input(&cfg).unwrap_err(); - assert_eq!(err.code(), &S3ErrorCode::MalformedXML); - } - - #[test] - fn validate_object_lock_configuration_rejects_invalid_default_retention_mode() { - let cfg = ObjectLockConfiguration { - object_lock_enabled: Some(ObjectLockEnabled::from_static(ObjectLockEnabled::ENABLED)), - rule: Some(ObjectLockRule { - default_retention: Some(DefaultRetention { - mode: Some(ObjectLockRetentionMode::from("abc".to_string())), - days: Some(1), - years: None, - }), - }), - }; - - let err = validate_object_lock_configuration_input(&cfg).unwrap_err(); - assert_eq!(err.code(), &S3ErrorCode::MalformedXML); - } - - #[test] - fn validate_object_lock_configuration_rejects_days_and_years_together() { - let cfg = ObjectLockConfiguration { - object_lock_enabled: Some(ObjectLockEnabled::from_static(ObjectLockEnabled::ENABLED)), - rule: Some(ObjectLockRule { - default_retention: Some(DefaultRetention { - mode: Some(ObjectLockRetentionMode::from_static(ObjectLockRetentionMode::GOVERNANCE)), - days: Some(1), - years: Some(1), - }), - }), - }; - - let err = validate_object_lock_configuration_input(&cfg).unwrap_err(); - assert_eq!(err.code(), &S3ErrorCode::MalformedXML); - } - - #[test] - fn validate_object_lock_configuration_rejects_missing_default_retention() { - let cfg = ObjectLockConfiguration { - object_lock_enabled: Some(ObjectLockEnabled::from_static(ObjectLockEnabled::ENABLED)), - rule: Some(ObjectLockRule { default_retention: None }), - }; - - let err = validate_object_lock_configuration_input(&cfg).unwrap_err(); - assert_eq!(err.code(), &S3ErrorCode::MalformedXML); - } - - #[test] - fn validate_object_lock_configuration_rejects_zero_days() { - let cfg = ObjectLockConfiguration { - object_lock_enabled: Some(ObjectLockEnabled::from_static(ObjectLockEnabled::ENABLED)), - rule: Some(ObjectLockRule { - default_retention: Some(DefaultRetention { - mode: Some(ObjectLockRetentionMode::from_static(ObjectLockRetentionMode::GOVERNANCE)), - days: Some(0), - years: None, - }), - }), - }; - - let err = validate_object_lock_configuration_input(&cfg).unwrap_err(); - assert_eq!(err.code(), &S3ErrorCode::Custom("InvalidRetentionPeriod".into())); - } - - #[test] - fn validate_object_lock_configuration_rejects_too_many_years() { - let cfg = ObjectLockConfiguration { - object_lock_enabled: Some(ObjectLockEnabled::from_static(ObjectLockEnabled::ENABLED)), - rule: Some(ObjectLockRule { - default_retention: Some(DefaultRetention { - mode: Some(ObjectLockRetentionMode::from_static(ObjectLockRetentionMode::COMPLIANCE)), - days: None, - years: Some(MAXIMUM_RETENTION_YEARS + 1), - }), - }), - }; - - let err = validate_object_lock_configuration_input(&cfg).unwrap_err(); - assert_eq!(err.code(), &S3ErrorCode::Custom("InvalidRetentionPeriod".into())); - } - #[tokio::test] async fn execute_head_object_rejects_range_with_part_number() { let input = HeadObjectInput::builder() diff --git a/rustfs/src/app/object_usecase/zero_copy_tests.rs b/rustfs/src/app/object_usecase/zero_copy_tests.rs index 65b262e3b..72089f631 100644 --- a/rustfs/src/app/object_usecase/zero_copy_tests.rs +++ b/rustfs/src/app/object_usecase/zero_copy_tests.rs @@ -23,7 +23,8 @@ use rustfs_ecstore::{ endpoints::{EndpointServerPools, Endpoints, PoolEndpoints}, store::ECStore, store_api::{ - BucketOperations, ChunkNativePutData, CompletePart, MakeBucketOptions, MultipartOperations, ObjectIO, ObjectOptions, + BucketOperations, BucketOptions, ChunkNativePutData, CompletePart, MakeBucketOptions, MultipartOperations, ObjectIO, + ObjectOptions, }, }; use rustfs_object_io::get::{GetObjectBodySource, GetObjectReadSetup}; diff --git a/rustfs/src/storage/ecfs.rs b/rustfs/src/storage/ecfs.rs index f953d25f1..9b7a7875e 100644 --- a/rustfs/src/storage/ecfs.rs +++ b/rustfs/src/storage/ecfs.rs @@ -21,14 +21,20 @@ use crate::storage::helper::OperationHelper; use crate::storage::options::get_opts; use crate::storage::s3_api::acl; use crate::storage::{parse_object_lock_legal_hold, parse_object_lock_retention, validate_bucket_object_lock_enabled}; +use http::StatusCode; use metrics::{counter, histogram}; use rustfs_ecstore::{ bucket::{ - metadata::{BUCKET_ACCELERATE_CONFIG, BUCKET_LOGGING_CONFIG, BUCKET_REQUEST_PAYMENT_CONFIG, BUCKET_WEBSITE_CONFIG}, + metadata::{ + BUCKET_ACCELERATE_CONFIG, BUCKET_LOGGING_CONFIG, BUCKET_REQUEST_PAYMENT_CONFIG, BUCKET_VERSIONING_CONFIG, + BUCKET_WEBSITE_CONFIG, OBJECT_LOCK_CONFIG, + }, metadata_sys, object_lock::objectlock_sys::check_retention_for_modification, tagging::{decode_tags, decode_tags_to_map, encode_tags}, utils::serialize, + versioning::VersioningApi, + versioning_sys::BucketVersioningSys, }, error::{StorageError, is_err_bucket_not_found, is_err_object_not_found, is_err_version_not_found}, new_object_layer_fn, @@ -128,6 +134,83 @@ pub(crate) fn parse_object_version_id(version_id: Option) -> S3Result) -> S3Error { + S3Error::with_message(S3ErrorCode::MalformedXML, message.into()) +} + +fn invalid_retention_period(message: impl Into) -> S3Error { + let mut err = S3Error::with_message(S3ErrorCode::Custom("InvalidRetentionPeriod".into()), message.into()); + err.set_status_code(StatusCode::BAD_REQUEST); + err +} + +fn validate_default_retention_configuration(default_retention: &DefaultRetention) -> S3Result<()> { + let Some(mode) = default_retention.mode.as_ref() else { + return Err(invalid_object_lock_configuration("retention mode must be specified")); + }; + + match mode.as_str() { + ObjectLockRetentionMode::COMPLIANCE | ObjectLockRetentionMode::GOVERNANCE => {} + _ => { + return Err(invalid_object_lock_configuration(format!("unknown retention mode {}", mode.as_str()))); + } + } + + match (default_retention.days, default_retention.years) { + (Some(days), None) => { + if days <= 0 { + return Err(invalid_retention_period( + "Default retention period must be a positive integer value for 'Days'", + )); + } + if days > MAXIMUM_RETENTION_DAYS { + return Err(invalid_retention_period(format!("Default retention period too large for 'Days' {days}",))); + } + } + (None, Some(years)) => { + if years <= 0 { + return Err(invalid_retention_period( + "Default retention period must be a positive integer value for 'Years'", + )); + } + if years > MAXIMUM_RETENTION_YEARS { + return Err(invalid_retention_period(format!( + "Default retention period too large for 'Years' {years}", + ))); + } + } + (Some(_), Some(_)) => { + return Err(invalid_object_lock_configuration("either Days or Years must be specified, not both")); + } + (None, None) => { + return Err(invalid_object_lock_configuration("either Days or Years must be specified")); + } + } + + Ok(()) +} + +pub(crate) fn validate_object_lock_configuration_input(input_cfg: &ObjectLockConfiguration) -> S3Result<()> { + let enabled = input_cfg.object_lock_enabled.as_ref().map(ObjectLockEnabled::as_str); + if enabled != Some(ObjectLockEnabled::ENABLED) { + return Err(invalid_object_lock_configuration( + "only 'Enabled' value is allowed to ObjectLockEnabled element", + )); + } + + if let Some(rule) = input_cfg.rule.as_ref() { + let Some(default_retention) = rule.default_retention.as_ref() else { + return Err(invalid_object_lock_configuration("Rule must include DefaultRetention")); + }; + validate_default_retention_configuration(default_retention)?; + } + + Ok(()) +} + #[async_trait::async_trait] impl S3 for FS { #[instrument(level = "debug", skip(self))] @@ -608,8 +691,37 @@ impl S3 for FS { req: S3Request, ) -> S3Result> { record_s3_op(S3Operation::GetObjectLockConfiguration, &req.input.bucket); - let usecase = DefaultObjectUsecase::from_global(); - usecase.execute_get_object_lock_configuration(req).await + let GetObjectLockConfigurationInput { bucket, .. } = req.input; + + let Some(store) = new_object_layer_fn() else { + return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string())); + }; + + store + .get_bucket_info(&bucket, &BucketOptions::default()) + .await + .map_err(ApiError::from)?; + + let object_lock_configuration = match metadata_sys::get_object_lock_config(&bucket).await { + Ok((cfg, _created)) => Some(cfg), + Err(err) => { + if err == StorageError::ConfigNotFound { + return Err(S3Error::with_message( + S3ErrorCode::ObjectLockConfigurationNotFoundError, + "Object Lock configuration does not exist for this bucket".to_string(), + )); + } + warn!("get_object_lock_config err {:?}", err); + return Err(S3Error::with_message( + S3ErrorCode::InternalError, + "Failed to load Object Lock configuration".to_string(), + )); + } + }; + + Ok(S3Response::new(GetObjectLockConfigurationOutput { + object_lock_configuration, + })) } async fn get_object_retention( @@ -1026,8 +1138,67 @@ impl S3 for FS { &self, req: S3Request, ) -> S3Result> { - let usecase = DefaultObjectUsecase::from_global(); - usecase.execute_put_object_lock_configuration(req).await + let PutObjectLockConfigurationInput { + bucket, + object_lock_configuration, + .. + } = req.input; + + let Some(input_cfg) = object_lock_configuration else { return Err(s3_error!(InvalidArgument)) }; + + let Some(store) = new_object_layer_fn() else { + return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string())); + }; + + store + .get_bucket_info(&bucket, &BucketOptions::default()) + .await + .map_err(ApiError::from)?; + + validate_object_lock_configuration_input(&input_cfg)?; + + match metadata_sys::get_object_lock_config(&bucket).await { + Ok(_) => {} + Err(err) => { + if err == StorageError::ConfigNotFound { + if !BucketVersioningSys::enabled(&bucket).await { + return Err(S3Error::with_message( + S3ErrorCode::InvalidBucketState, + "Object Lock configuration cannot be enabled on existing buckets".to_string(), + )); + } + } else { + warn!("get_object_lock_config err {:?}", err); + return Err(S3Error::with_message( + S3ErrorCode::InternalError, + "Failed to get bucket ObjectLockConfiguration".to_string(), + )); + } + } + }; + + let data = serialize(&input_cfg).map_err(|err| S3Error::with_message(S3ErrorCode::InternalError, format!("{err}")))?; + + metadata_sys::update(&bucket, OBJECT_LOCK_CONFIG, data) + .await + .map_err(ApiError::from)?; + + // When Object Lock is enabled, automatically enable versioning if not already enabled. + // This matches S3-compatible behavior. + let versioning_config = BucketVersioningSys::get(&bucket).await.map_err(ApiError::from)?; + if !versioning_config.enabled() { + let enable_versioning_config = VersioningConfiguration { + status: Some(BucketVersioningStatus::from_static(BucketVersioningStatus::ENABLED)), + ..Default::default() + }; + let versioning_data = serialize(&enable_versioning_config) + .map_err(|err| S3Error::with_message(S3ErrorCode::InternalError, format!("{err}")))?; + metadata_sys::update(&bucket, BUCKET_VERSIONING_CONFIG, versioning_data) + .await + .map_err(ApiError::from)?; + } + + Ok(S3Response::new(PutObjectLockConfigurationOutput::default())) } async fn put_object_retention( diff --git a/rustfs/src/storage/ecfs_test.rs b/rustfs/src/storage/ecfs_test.rs index 71572e357..64245b56a 100644 --- a/rustfs/src/storage/ecfs_test.rs +++ b/rustfs/src/storage/ecfs_test.rs @@ -16,7 +16,7 @@ mod tests { use crate::config::WorkloadProfile; use crate::server::cors; - use crate::storage::ecfs::FS; + use crate::storage::ecfs::{FS, validate_object_lock_configuration_input}; use crate::storage::s3_api::common::{rustfs_initiator, rustfs_owner}; use crate::storage::{ apply_cors_headers, check_preconditions, get_adaptive_buffer_size_with_profile, get_buffer_size_opt_in, is_etag_equal, @@ -35,9 +35,10 @@ mod tests { }; use rustfs_zip::CompressionFormat; use s3s::dto::{ - CORSConfiguration, CORSRule, DeleteObjectTaggingInput, Delimiter, GetObjectAclInput, GetObjectLegalHoldInput, - GetObjectRetentionInput, GetObjectTaggingInput, LambdaFunctionConfiguration, ObjectLockLegalHold, - ObjectLockLegalHoldStatus, ObjectLockRetention, ObjectLockRetentionMode, PutObjectLegalHoldInput, + CORSConfiguration, CORSRule, DefaultRetention, DeleteObjectTaggingInput, Delimiter, GetObjectAclInput, + GetObjectLegalHoldInput, GetObjectRetentionInput, GetObjectTaggingInput, LambdaFunctionConfiguration, + ObjectLockConfiguration, ObjectLockEnabled, ObjectLockLegalHold, ObjectLockLegalHoldStatus, ObjectLockRetention, + ObjectLockRetentionMode, ObjectLockRule, PutObjectLegalHoldInput, PutObjectLockConfigurationInput, PutObjectRetentionInput, PutObjectTaggingInput, QueueConfiguration, Tag, Tagging, TopicConfiguration, }; use s3s::{S3, S3Error, S3ErrorCode, S3Request, s3_error}; @@ -251,6 +252,115 @@ mod tests { assert_eq!(err.code(), &S3ErrorCode::InternalError); } + #[tokio::test] + async fn test_put_object_lock_configuration_returns_internal_error_when_store_uninitialized() { + let input = PutObjectLockConfigurationInput::builder() + .bucket("test-bucket".to_string()) + .object_lock_configuration(Some(ObjectLockConfiguration { + object_lock_enabled: Some(ObjectLockEnabled::from_static(ObjectLockEnabled::ENABLED)), + rule: None, + })) + .build() + .unwrap(); + + let fs = FS::new(); + let err = fs + .put_object_lock_configuration(build_request(input, Method::PUT)) + .await + .unwrap_err(); + assert_eq!(err.code(), &S3ErrorCode::InternalError); + } + + #[test] + fn test_validate_object_lock_configuration_rejects_disabled_status() { + let cfg = ObjectLockConfiguration { + object_lock_enabled: Some(ObjectLockEnabled::from("Disabled".to_string())), + rule: None, + }; + + let err = validate_object_lock_configuration_input(&cfg).unwrap_err(); + assert_eq!(err.code(), &S3ErrorCode::MalformedXML); + } + + #[test] + fn test_validate_object_lock_configuration_rejects_invalid_default_retention_mode() { + let cfg = ObjectLockConfiguration { + object_lock_enabled: Some(ObjectLockEnabled::from_static(ObjectLockEnabled::ENABLED)), + rule: Some(ObjectLockRule { + default_retention: Some(DefaultRetention { + mode: Some(ObjectLockRetentionMode::from("abc".to_string())), + days: Some(1), + years: None, + }), + }), + }; + + let err = validate_object_lock_configuration_input(&cfg).unwrap_err(); + assert_eq!(err.code(), &S3ErrorCode::MalformedXML); + } + + #[test] + fn test_validate_object_lock_configuration_rejects_days_and_years_together() { + let cfg = ObjectLockConfiguration { + object_lock_enabled: Some(ObjectLockEnabled::from_static(ObjectLockEnabled::ENABLED)), + rule: Some(ObjectLockRule { + default_retention: Some(DefaultRetention { + mode: Some(ObjectLockRetentionMode::from_static(ObjectLockRetentionMode::GOVERNANCE)), + days: Some(1), + years: Some(1), + }), + }), + }; + + let err = validate_object_lock_configuration_input(&cfg).unwrap_err(); + assert_eq!(err.code(), &S3ErrorCode::MalformedXML); + } + + #[test] + fn test_validate_object_lock_configuration_rejects_missing_default_retention() { + let cfg = ObjectLockConfiguration { + object_lock_enabled: Some(ObjectLockEnabled::from_static(ObjectLockEnabled::ENABLED)), + rule: Some(ObjectLockRule { default_retention: None }), + }; + + let err = validate_object_lock_configuration_input(&cfg).unwrap_err(); + assert_eq!(err.code(), &S3ErrorCode::MalformedXML); + } + + #[test] + fn test_validate_object_lock_configuration_rejects_zero_days() { + let cfg = ObjectLockConfiguration { + object_lock_enabled: Some(ObjectLockEnabled::from_static(ObjectLockEnabled::ENABLED)), + rule: Some(ObjectLockRule { + default_retention: Some(DefaultRetention { + mode: Some(ObjectLockRetentionMode::from_static(ObjectLockRetentionMode::GOVERNANCE)), + days: Some(0), + years: None, + }), + }), + }; + + let err = validate_object_lock_configuration_input(&cfg).unwrap_err(); + assert_eq!(err.code(), &S3ErrorCode::Custom("InvalidRetentionPeriod".into())); + } + + #[test] + fn test_validate_object_lock_configuration_rejects_too_many_years() { + let cfg = ObjectLockConfiguration { + object_lock_enabled: Some(ObjectLockEnabled::from_static(ObjectLockEnabled::ENABLED)), + rule: Some(ObjectLockRule { + default_retention: Some(DefaultRetention { + mode: Some(ObjectLockRetentionMode::from_static(ObjectLockRetentionMode::COMPLIANCE)), + days: None, + years: Some(101), + }), + }), + }; + + let err = validate_object_lock_configuration_input(&cfg).unwrap_err(); + assert_eq!(err.code(), &S3ErrorCode::Custom("InvalidRetentionPeriod".into())); + } + #[tokio::test] async fn test_get_object_tagging_returns_internal_error_when_store_uninitialized() { let input = GetObjectTaggingInput::builder()