mod common;
use bucketwarden_auth::OperatorRole;
use bucketwarden_lock::ObjectLock;
use bucketwarden_s3::{ObjectMetadata, PutObjectRequest, ReplicationRule, ServerSideEncryption};
use common::*;
use std::collections::BTreeMap;
#[test]
fn replication_status_report_exposes_completed_skipped_and_missing_destination_entries() {
let mut runtime = runtime();
runtime
.create_bucket("alice", "archive-001")
.expect("source");
runtime
.create_bucket("alice", "archive-002")
.expect("destination");
runtime.create_local_user("ops");
runtime
.assign_operator_role("alice", "ops", OperatorRole::BucketAdmin, "archive-001")
.expect("bucket admin");
runtime
.put_bucket_replication(
"alice",
"archive-001",
Some("arn:aws:iam::123456789012:role/bucketwarden-replication".to_string()),
vec![
ReplicationRule {
id: "records".to_string(),
status: "Enabled".to_string(),
destination_bucket: "arn:aws:s3:::archive-002".to_string(),
prefix: Some("records/".to_string()),
tag_filter: BTreeMap::new(),
delete_marker_replication: false,
existing_object_replication: false,
replicate_encrypted_objects: false,
},
ReplicationRule {
id: "lost".to_string(),
status: "Enabled".to_string(),
destination_bucket: "arn:aws:s3:::missing-destination".to_string(),
prefix: Some("lost/".to_string()),
tag_filter: BTreeMap::new(),
delete_marker_replication: false,
existing_object_replication: false,
replicate_encrypted_objects: true,
},
],
)
.expect("replication config");
runtime
.put_object(
"alice",
PutObjectRequest {
bucket: "archive-001".to_string(),
key: "records/a.txt".to_string(),
body: b"complete".to_vec(),
metadata: ObjectMetadata::default(),
},
ObjectLock::none(),
)
.expect("completed candidate");
runtime
.put_object(
"alice",
PutObjectRequest {
bucket: "archive-001".to_string(),
key: "records/secret.txt".to_string(),
body: b"skip".to_vec(),
metadata: ObjectMetadata {
content_type: "text/plain".to_string(),
content_encoding: None,
user_metadata: BTreeMap::new(),
encryption: Some(ServerSideEncryption {
algorithm: "aws:kms".to_string(),
kms_key_id: Some("local-dev".to_string()),
}),
},
},
ObjectLock::none(),
)
.expect("skipped encrypted");
runtime
.put_object(
"alice",
PutObjectRequest {
bucket: "archive-001".to_string(),
key: "lost/a.txt".to_string(),
body: b"missing".to_vec(),
metadata: ObjectMetadata::default(),
},
ObjectLock::none(),
)
.expect("missing destination candidate");
runtime
.run_bucket_replication("alice", "archive-001")
.expect("replicate");
let report = runtime
.replication_status_report("ops", "archive-001")
.expect("status report");
assert_eq!(report.entry_count, 3);
assert_eq!(report.completed_count, 1);
assert_eq!(report.pending_count, 0);
assert_eq!(report.skipped_encrypted_count, 1);
assert_eq!(report.missing_destination_count, 1);
assert!(report.last_replication_sequence.is_some());
assert!(report.entries.iter().any(|entry| {
entry.key == "records/a.txt"
&& entry.status == "COMPLETED"
&& entry.destination_present
&& entry.eligible
}));
assert!(report.entries.iter().any(|entry| {
entry.key == "records/secret.txt"
&& entry.status == "SKIPPED_ENCRYPTED"
&& !entry.destination_present
&& !entry.eligible
}));
assert!(report.entries.iter().any(|entry| {
entry.key == "lost/a.txt"
&& entry.status == "MISSING_DESTINATION"
&& !entry.destination_present
&& !entry.eligible
}));
assert!(runtime.audit_events().iter().any(|event| {
event.action == "ops:GetReplicationStatus" && event.resource == "archive-001"
}));
}
#[test]
fn replication_status_report_orders_same_key_versions_numerically() {
let mut runtime = runtime();
runtime
.create_bucket("alice", "archive-001")
.expect("source");
runtime
.create_bucket("alice", "archive-002")
.expect("destination");
runtime.create_local_user("ops");
runtime
.assign_operator_role("alice", "ops", OperatorRole::BucketAdmin, "archive-001")
.expect("bucket admin");
runtime
.put_bucket_replication(
"alice",
"archive-001",
Some("arn:aws:iam::123456789012:role/bucketwarden-replication".to_string()),
vec![ReplicationRule {
id: "records".to_string(),
status: "Enabled".to_string(),
destination_bucket: "arn:aws:s3:::archive-002".to_string(),
prefix: Some("records/".to_string()),
tag_filter: BTreeMap::new(),
delete_marker_replication: false,
existing_object_replication: false,
replicate_encrypted_objects: true,
}],
)
.expect("replication config");
for version in 1..=11 {
runtime
.put_object(
"alice",
PutObjectRequest {
bucket: "archive-001".to_string(),
key: "records/a.txt".to_string(),
body: format!("version-{version}").into_bytes(),
metadata: ObjectMetadata::default(),
},
ObjectLock::none(),
)
.expect("put version");
}
runtime
.run_bucket_replication("alice", "archive-001")
.expect("replicate");
let report = runtime
.replication_status_report("ops", "archive-001")
.expect("status report");
let ordered_versions = report
.entries
.iter()
.filter(|entry| entry.key == "records/a.txt")
.map(|entry| entry.version_id.as_str())
.collect::<Vec<_>>();
assert_eq!(
ordered_versions,
vec!["v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8", "v9", "v10", "v11"]
);
}