bucketwarden-server 0.1.0

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