mod common;
use std::collections::BTreeMap;
use bucketwarden_lock::ObjectLock;
use bucketwarden_s3::{
BucketObjectLockConfiguration, CopyObjectRequest, MetadataDirective, ObjectLegalHoldRequest,
ObjectMetadata, ObjectRetentionRequest, ObjectTaggingRequest, PutObjectRequest, S3HttpRequest,
};
use bucketwarden_server::{
BulkObjectCopyRequest, BulkObjectDeleteRequest, BulkObjectDeleteTarget,
BulkObjectLegalHoldRequest, BulkObjectRestoreRequest, BulkObjectRestoreTarget,
BulkObjectRetentionRequest, BulkObjectTaggingRequest,
};
use common::runtime;
#[test]
fn bulk_object_admin_operations_apply_updates_and_collect_per_entry_errors() {
let mut runtime = runtime();
runtime
.handle_s3_http(S3HttpRequest::new("alice", "PUT", "/archive-001"))
.expect("bucket");
runtime
.put_bucket_object_lock_configuration("alice", "archive-001", None)
.expect("object lock");
let alpha_version = runtime
.put_object(
"alice",
PutObjectRequest {
bucket: "archive-001".to_string(),
key: "records/alpha.txt".to_string(),
body: b"alpha".to_vec(),
metadata: ObjectMetadata::default(),
},
ObjectLock::none(),
)
.expect("put alpha")
.version_id;
let beta_version = runtime
.put_object(
"alice",
PutObjectRequest {
bucket: "archive-001".to_string(),
key: "records/beta.txt".to_string(),
body: b"beta".to_vec(),
metadata: ObjectMetadata::default(),
},
ObjectLock::none(),
)
.expect("put beta")
.version_id;
let mut alpha_tags = BTreeMap::new();
alpha_tags.insert("class".to_string(), "gold".to_string());
let mut beta_tags = BTreeMap::new();
beta_tags.insert("class".to_string(), "silver".to_string());
let bulk_tagging = runtime.bulk_put_object_tagging(
"alice",
BulkObjectTaggingRequest {
entries: vec![
ObjectTaggingRequest {
bucket: "archive-001".to_string(),
key: "records/alpha.txt".to_string(),
version_id: Some(alpha_version.clone()),
tags: alpha_tags.clone(),
},
ObjectTaggingRequest {
bucket: "archive-001".to_string(),
key: "records/missing.txt".to_string(),
version_id: None,
tags: beta_tags.clone(),
},
ObjectTaggingRequest {
bucket: "archive-001".to_string(),
key: "records/beta.txt".to_string(),
version_id: Some(beta_version.clone()),
tags: beta_tags.clone(),
},
],
},
);
assert_eq!(bulk_tagging.updated.len(), 2);
assert_eq!(bulk_tagging.errors.len(), 1);
assert_eq!(bulk_tagging.errors[0].code, "NoSuchKey");
assert_eq!(
runtime
.get_object_tagging(
"alice",
"archive-001",
"records/alpha.txt",
Some(&alpha_version)
)
.expect("alpha tags")
.tags,
alpha_tags
);
assert_eq!(
runtime
.get_object_tagging(
"alice",
"archive-001",
"records/beta.txt",
Some(&beta_version)
)
.expect("beta tags")
.tags,
beta_tags
);
let bulk_holds = runtime.bulk_put_object_legal_hold(
"alice",
BulkObjectLegalHoldRequest {
entries: vec![
ObjectLegalHoldRequest {
bucket: "archive-001".to_string(),
key: "records/alpha.txt".to_string(),
version_id: Some(alpha_version.clone()),
enabled: true,
},
ObjectLegalHoldRequest {
bucket: "archive-001".to_string(),
key: "records/missing.txt".to_string(),
version_id: None,
enabled: true,
},
],
},
);
assert_eq!(bulk_holds.updated.len(), 1);
assert_eq!(bulk_holds.errors.len(), 1);
assert_eq!(bulk_holds.errors[0].code, "NoSuchKey");
assert!(
runtime
.get_object_legal_hold(
"alice",
"archive-001",
"records/alpha.txt",
Some(&alpha_version)
)
.expect("alpha hold")
.enabled
);
let bulk_retention = runtime.bulk_put_object_retention(
"alice",
BulkObjectRetentionRequest {
entries: vec![
ObjectRetentionRequest {
bucket: "archive-001".to_string(),
key: "records/alpha.txt".to_string(),
version_id: Some(alpha_version.clone()),
mode: "GOVERNANCE".to_string(),
retain_until_epoch_seconds: 86_400,
bypass_governance: false,
},
ObjectRetentionRequest {
bucket: "archive-001".to_string(),
key: "records/missing.txt".to_string(),
version_id: None,
mode: "GOVERNANCE".to_string(),
retain_until_epoch_seconds: 86_400,
bypass_governance: false,
},
],
},
);
assert_eq!(bulk_retention.updated.len(), 1);
assert_eq!(bulk_retention.errors.len(), 1);
assert_eq!(bulk_retention.errors[0].code, "NoSuchKey");
let retention = runtime
.get_object_retention(
"alice",
"archive-001",
"records/alpha.txt",
Some(&alpha_version),
)
.expect("alpha retention");
assert_eq!(retention.mode.as_deref(), Some("GOVERNANCE"));
assert_eq!(retention.retain_until_epoch_seconds, Some(86_400));
let mut copied_metadata = ObjectMetadata {
content_type: "text/csv".to_string(),
..ObjectMetadata::default()
};
copied_metadata
.user_metadata
.insert("archive-class".to_string(), "cold".to_string());
let bulk_copy = runtime.bulk_copy_objects(
"alice",
BulkObjectCopyRequest {
entries: vec![
CopyObjectRequest {
source_bucket: "archive-001".to_string(),
source_key: "records/alpha.txt".to_string(),
source_version_id: None,
destination_bucket: "archive-001".to_string(),
destination_key: "records/copied/alpha.csv".to_string(),
metadata_directive: MetadataDirective::Replace(copied_metadata.clone()),
destination_encryption: None,
},
CopyObjectRequest {
source_bucket: "archive-001".to_string(),
source_key: "records/missing.txt".to_string(),
source_version_id: None,
destination_bucket: "archive-001".to_string(),
destination_key: "records/copied/missing.txt".to_string(),
metadata_directive: MetadataDirective::Copy,
destination_encryption: None,
},
CopyObjectRequest {
source_bucket: "archive-001".to_string(),
source_key: "records/beta.txt".to_string(),
source_version_id: None,
destination_bucket: "archive-001".to_string(),
destination_key: "records/copied/beta.txt".to_string(),
metadata_directive: MetadataDirective::Copy,
destination_encryption: None,
},
],
},
);
assert_eq!(bulk_copy.copied.len(), 2);
assert_eq!(bulk_copy.errors.len(), 1);
assert_eq!(bulk_copy.errors[0].code, "NoSuchKey");
assert_eq!(bulk_copy.errors[0].source_key, "records/missing.txt");
assert_eq!(
runtime
.get_object("alice", "archive-001", "records/copied/alpha.csv")
.expect("copied alpha")
.metadata,
copied_metadata
);
let copied_beta = runtime
.get_object("alice", "archive-001", "records/copied/beta.txt")
.expect("copied beta");
assert_eq!(copied_beta.body, b"beta");
assert_eq!(copied_beta.metadata, ObjectMetadata::default());
let bulk_restore = runtime.bulk_restore_objects(
"alice",
BulkObjectRestoreRequest {
entries: vec![
BulkObjectRestoreTarget {
bucket: "archive-001".to_string(),
key: "records/beta.txt".to_string(),
version_id: Some(beta_version.clone()),
},
BulkObjectRestoreTarget {
bucket: "archive-001".to_string(),
key: "records/copied/beta.txt".to_string(),
version_id: None,
},
BulkObjectRestoreTarget {
bucket: "archive-001".to_string(),
key: "records/missing.txt".to_string(),
version_id: None,
},
],
},
);
assert_eq!(bulk_restore.restored.len(), 2);
assert_eq!(bulk_restore.errors.len(), 1);
assert_eq!(bulk_restore.errors[0].code, "NoSuchKey");
assert_eq!(bulk_restore.restored[0].version_id, beta_version);
assert_eq!(
bulk_restore.restored[0].restore_header,
"ongoing-request=\"false\""
);
assert_eq!(bulk_restore.restored[0].bucket, "archive-001");
assert_eq!(bulk_restore.restored[0].key, "records/beta.txt");
assert_eq!(bulk_restore.restored[1].bucket, "archive-001");
assert_eq!(bulk_restore.restored[1].key, "records/copied/beta.txt");
let bulk_delete = runtime.bulk_delete_objects(
"alice",
BulkObjectDeleteRequest {
entries: vec![
BulkObjectDeleteTarget {
bucket: "archive-001".to_string(),
key: "records/copied/beta.txt".to_string(),
version_id: None,
bypass_governance: false,
},
BulkObjectDeleteTarget {
bucket: "archive-001".to_string(),
key: "records/beta.txt".to_string(),
version_id: Some(beta_version.clone()),
bypass_governance: false,
},
BulkObjectDeleteTarget {
bucket: "archive-001".to_string(),
key: "records/missing.txt".to_string(),
version_id: None,
bypass_governance: false,
},
],
},
);
assert_eq!(bulk_delete.deleted.len(), 2);
assert_eq!(bulk_delete.errors.len(), 1);
assert_eq!(bulk_delete.errors[0].code, "NoSuchKey");
assert_eq!(bulk_delete.errors[0].bucket, "archive-001");
assert_eq!(bulk_delete.deleted[0].key, "records/copied/beta.txt");
assert!(bulk_delete.deleted[0].delete_marker);
assert!(bulk_delete.deleted[0].delete_marker_version_id.is_some());
assert_eq!(bulk_delete.deleted[1].key, "records/beta.txt");
assert_eq!(
bulk_delete.deleted[1].version_id.as_deref(),
Some(beta_version.as_str())
);
assert!(!bulk_delete.deleted[1].delete_marker);
assert!(bulk_delete.deleted[1].delete_marker_version_id.is_none());
assert!(matches!(
runtime.get_object("alice", "archive-001", "records/beta.txt"),
Err(_)
));
let lock_config = runtime
.get_bucket_object_lock_configuration("alice", "archive-001")
.expect("lock config");
assert_eq!(
lock_config,
BucketObjectLockConfiguration {
bucket: "archive-001".to_string(),
enabled: true,
default_retention: None,
}
);
}