From ba613b3b88443e2fba11e8046aaebc2eee2c2a8d Mon Sep 17 00:00:00 2001 From: overtrue Date: Thu, 10 Sep 2026 03:13:47 +0800 Subject: [PATCH] test: align transition fixtures and KMS context expectations --- crates/ecstore/src/set_disk/ops/object.rs | 105 ++++++++++++++++++++-- crates/ecstore/src/store/init.rs | 31 ++++--- rustfs/src/storage/sse.rs | 7 +- 3 files changed, 121 insertions(+), 22 deletions(-) diff --git a/crates/ecstore/src/set_disk/ops/object.rs b/crates/ecstore/src/set_disk/ops/object.rs index 675883978..9cfcd4e4d 100644 --- a/crates/ecstore/src/set_disk/ops/object.rs +++ b/crates/ecstore/src/set_disk/ops/object.rs @@ -13807,7 +13807,17 @@ mod transition_commit_failure_tests { #[tokio::test] #[serial_test::serial] async fn restore_failure_after_snapshot_cleans_exact_generation_and_returns_primary_error() { - let (_temp_dirs, disk_stores, set_disks) = hermetic_set_disks(4).await; + assert_restore_failure_cleanup_boundary(true).await; + } + + #[tokio::test] + #[serial_test::serial] + async fn restore_failure_after_snapshot_preserves_corrupt_known_transition_metadata() { + assert_restore_failure_cleanup_boundary(false).await; + } + + async fn assert_restore_failure_cleanup_boundary(legacy_unknown: bool) { + let (temp_dirs, disk_stores, set_disks) = hermetic_set_disks(4).await; let bucket = "restore-post-snapshot-cleanup-bucket"; let object = "object.bin"; for disk in &disk_stores { @@ -13816,7 +13826,15 @@ mod transition_commit_failure_tests { let mut reader = PutObjReader::from_vec(b"post-snapshot cleanup source".repeat(1024)); let original = set_disks - .put_object(bucket, object, &mut reader, &ObjectOptions::default()) + .put_object( + bucket, + object, + &mut reader, + &ObjectOptions { + write_completion: WriteCompletion::TailDrained, + ..Default::default() + }, + ) .await .expect("source object should be written"); let tier_name = format!("COLDTIER{}", &Uuid::new_v4().simple().to_string()[..8]).to_uppercase(); @@ -13857,16 +13875,70 @@ mod transition_commit_failure_tests { .await .expect("transitioned metadata should be readable") .into_owned(); + let known_state = source_fi.transition_version_state; + assert_ne!(known_state, rustfs_filemeta::TransitionVersionState::Unknown); source_fi.metadata.extend(restore_metadata(operation_id, true)); - rustfs_utils::http::insert_str( - &mut source_fi.metadata, - rustfs_utils::http::SUFFIX_TRANSITION_TIER_DESTINATION_ID, - "invalid".to_string(), - ); set_disks .update_object_meta(bucket, object, source_fi, &online_disks) .await - .expect("invalid backend identity fixture should be persisted"); + .expect("restore markers should be persisted"); + + // Normal writes reject damage to a reconciled binding. Model on-disk + // corruption directly, with and without the legacy missing-state field. + let mut corrupted_metadata = Vec::new(); + for temp_dir in &temp_dirs { + let metadata_path = temp_dir.path().join(bucket).join(object).join(STORAGE_FORMAT_FILE); + let encoded = tokio::fs::read(&metadata_path) + .await + .expect("transition metadata should be readable"); + let mut metadata = FileMeta::load(&encoded).expect("transition metadata should decode"); + let (version_index, mut version) = metadata + .find_version(original.version_id) + .expect("transitioned version should exist"); + let object_meta = version.object.as_mut().expect("transitioned version should be an object"); + rustfs_utils::http::insert_bytes( + &mut object_meta.meta_sys, + rustfs_utils::http::SUFFIX_TRANSITION_TIER_DESTINATION_ID, + b"invalid".to_vec(), + ); + if legacy_unknown { + rustfs_utils::http::remove_bytes( + &mut object_meta.meta_sys, + rustfs_utils::http::SUFFIX_TRANSITIONED_VERSION_STATE, + ); + } + metadata.versions[version_index] = + rustfs_filemeta::FileMetaShallowVersion::try_from(version).expect("corrupt fixture should re-encode"); + tokio::fs::write(&metadata_path, metadata.marshal_msg().expect("corrupt fixture should encode")) + .await + .expect("corrupt fixture should be written"); + let persisted = tokio::fs::read(&metadata_path) + .await + .expect("corrupt fixture should be readable"); + let fixture = FileMeta::load(&persisted) + .expect("corrupt fixture should decode") + .find_version(original.version_id) + .expect("corrupt version should exist") + .1 + .into_fileinfo(bucket, object, true) + .expect("corrupt version should decode"); + assert_eq!( + fixture.transition_version_state, + if legacy_unknown { + rustfs_filemeta::TransitionVersionState::Unknown + } else { + known_state + } + ); + assert_eq!( + rustfs_utils::http::get_str(&fixture.metadata, rustfs_utils::http::SUFFIX_TRANSITION_TIER_DESTINATION_ID), + Some("invalid".to_string()) + ); + for (key, value) in restore_metadata(operation_id, true) { + assert_eq!(fixture.metadata.get(&key), Some(&value), "fixture must retain restore marker {key}"); + } + corrupted_metadata.push((metadata_path, persisted)); + } set_disks.invalidate_get_object_metadata_cache(bucket, object).await; let mut opts = ObjectOptions::default(); @@ -13889,6 +13961,23 @@ mod transition_commit_failure_tests { .await .expect("cleanup should leave the transitioned object readable"); assert_eq!(cleaned.transitioned_object.status, TRANSITION_COMPLETE); + if !legacy_unknown { + // Known bindings with corrupt identities must be repaired before + // cleanup; rejection must preserve both the binding and markers. + for (key, value) in restore_metadata(operation_id, true) { + assert_eq!(cleaned.user_defined.get(&key), Some(&value), "cleanup must preserve restore marker {key}"); + } + for (metadata_path, before) in corrupted_metadata { + assert_eq!( + tokio::fs::read(metadata_path) + .await + .expect("rejected cleanup metadata should remain readable"), + before, + "rejected cleanup must leave corrupt known metadata unchanged" + ); + } + return; + } assert!(!cleaned.user_defined.contains_key(s3s::header::X_AMZ_RESTORE.as_str())); assert!( rustfs_utils::http::get_str(cleaned.user_defined.as_ref(), rustfs_utils::http::SUFFIX_RESTORE_OPERATION_ID,) diff --git a/crates/ecstore/src/store/init.rs b/crates/ecstore/src/store/init.rs index b431bc4a8..1c17a9b38 100644 --- a/crates/ecstore/src/store/init.rs +++ b/crates/ecstore/src/store/init.rs @@ -19057,27 +19057,34 @@ mod tests { .await .expect("transition metadata should be readable"); let mut metadata = FileMeta::load(&encoded).expect("transition metadata should decode"); - let mut transitioned = metadata - .get_all_file_info_versions(bucket, object, true) - .expect("transitioned versions should decode") - .versions - .into_iter() - .find(|version| version.version_id == history.version_id) + let (version_index, mut transitioned) = metadata + .find_version(history.version_id) .expect("transitioned history should exist"); - transitioned.transition_version_state = rustfs_filemeta::TransitionVersionState::Unknown; - rustfs_utils::http::metadata_compat::remove_str( - &mut transitioned.metadata, + // Rewrite the serialized record to model legacy metadata; + // ordinary writes preserve an already reconciled state. + rustfs_utils::http::metadata_compat::remove_bytes( + &mut transitioned.object.as_mut().expect("history should be an object").meta_sys, rustfs_utils::http::metadata_compat::SUFFIX_TRANSITIONED_VERSION_STATE, ); - metadata - .add_version(transitioned) - .expect("unknown state should replace the transitioned version"); + metadata.versions[version_index] = rustfs_filemeta::FileMetaShallowVersion::try_from(transitioned) + .expect("legacy history should re-encode"); tokio::fs::write( &metadata_path, metadata.marshal_msg().expect("unknown transition metadata should encode"), ) .await .expect("unknown transition metadata should be written"); + let encoded = tokio::fs::read(&metadata_path) + .await + .expect("legacy transition metadata should be readable"); + let legacy = FileMeta::load(&encoded) + .expect("legacy transition metadata should decode") + .find_version(history.version_id) + .expect("legacy history should exist") + .1 + .into_fileinfo(bucket, object, true) + .expect("legacy history should decode"); + assert_eq!(legacy.transition_version_state, rustfs_filemeta::TransitionVersionState::Unknown); } let lifecycle_event = crate::bucket::lifecycle::lifecycle::Event { action: rustfs_scanner_metrics::metrics::IlmAction::DeleteAllVersionsAction, diff --git a/rustfs/src/storage/sse.rs b/rustfs/src/storage/sse.rs index 5280391ad..c47e267f1 100644 --- a/rustfs/src/storage/sse.rs +++ b/rustfs/src/storage/sse.rs @@ -5939,8 +5939,11 @@ mod tests { }) .await .expect_err("mismatched kms context should fail"); - assert_eq!(err.code, S3ErrorCode::InternalError); - assert_eq!(err.message, ApiError::error_code_to_message(&S3ErrorCode::InternalError)); + assert_eq!(err.code, S3ErrorCode::InvalidRequest); + assert_eq!( + err.message, + "Encryption context mismatch: Context mismatch for key 'tenant': expected 'alpha', got 'beta'" + ); assert_eq!(super::kms_data_plane_error_class(&err), "context_mismatch"); manager.stop().await.expect("kms service should stop cleanly");