bucketwarden-server 0.1.0

BucketWarden storage server runtime.
Documentation
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,
        }
    );
}