bucketwarden-server 0.1.0

BucketWarden storage server runtime.
Documentation
mod common;

use bucketwarden_lock::ObjectLock;
use bucketwarden_s3::{
    ListObjectVersionsRequest, ObjectMetadata, PutObjectRequest, ReplicationRule,
};
use bucketwarden_server::RuntimeError;
use common::*;
use std::collections::BTreeMap;

#[test]
fn replication_preserves_version_order_delete_markers_and_source_completion_status() {
    let mut runtime = runtime();
    runtime
        .create_bucket("alice", "archive-001")
        .expect("source");
    runtime
        .create_bucket("alice", "archive-002")
        .expect("destination");
    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");

    let v1 = runtime
        .put_object(
            "alice",
            PutObjectRequest {
                bucket: "archive-001".to_string(),
                key: "records/a.json".to_string(),
                body: br#"{"version":1}"#.to_vec(),
                metadata: ObjectMetadata::default(),
            },
            ObjectLock::none(),
        )
        .expect("v1")
        .version_id;
    let v2 = runtime
        .put_object(
            "alice",
            PutObjectRequest {
                bucket: "archive-001".to_string(),
                key: "records/a.json".to_string(),
                body: br#"{"version":2}"#.to_vec(),
                metadata: ObjectMetadata::default(),
            },
            ObjectLock::none(),
        )
        .expect("v2")
        .version_id;

    let first_run = runtime
        .run_bucket_replication("alice", "archive-001")
        .expect("replicate");
    assert_eq!(first_run.replicated_object_versions, 2);
    assert_eq!(
        runtime
            .get_object_version("alice", "archive-001", "records/a.json", &v1)
            .expect("source v1")
            .replication_status
            .as_deref(),
        Some("COMPLETED")
    );
    assert_eq!(
        runtime
            .get_object_version("alice", "archive-001", "records/a.json", &v2)
            .expect("source v2")
            .replication_status
            .as_deref(),
        Some("COMPLETED")
    );

    let _delete_marker = runtime
        .delete_object("alice", "archive-001", "records/a.json", false)
        .expect("delete marker")
        .delete_marker_version_id;
    let second_run = runtime
        .run_bucket_replication("alice", "archive-001")
        .expect("replicate delete marker");
    assert_eq!(second_run.replicated_delete_markers, 0);

    let destination_versions = runtime
        .list_object_versions(
            "alice",
            ListObjectVersionsRequest {
                bucket: "archive-002".to_string(),
                prefix: Some("records/".to_string()),
                ..ListObjectVersionsRequest::default()
            },
        )
        .expect("list versions");
    assert_eq!(destination_versions.versions.len(), 2);
    assert!(destination_versions.delete_markers.is_empty());

    runtime
        .delete_object_version("alice", "archive-001", "records/a.json", &v2, false)
        .expect("delete source version");
    let deletion_run = runtime
        .run_bucket_replication("alice", "archive-001")
        .expect("replicate deleted version");
    assert_eq!(deletion_run.replicated_deleted_versions, 1);
    assert!(matches!(
        runtime.get_object_version("alice", "archive-002", "records/a.json", &v2),
        Err(RuntimeError::NoSuchVersion { .. })
    ));
}

#[test]
fn replication_respects_existing_object_replication_setting() {
    let mut runtime = runtime();
    runtime.set_clock_epoch_seconds(10);
    runtime
        .create_bucket("alice", "archive-001")
        .expect("source");
    runtime
        .create_bucket("alice", "archive-002")
        .expect("destination");
    let existing = runtime
        .put_object(
            "alice",
            PutObjectRequest {
                bucket: "archive-001".to_string(),
                key: "records/existing.json".to_string(),
                body: br#"{"existing":true}"#.to_vec(),
                metadata: ObjectMetadata::default(),
            },
            ObjectLock::none(),
        )
        .expect("existing source");

    runtime.set_clock_epoch_seconds(20);
    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");

    runtime.set_clock_epoch_seconds(30);
    let current = runtime
        .put_object(
            "alice",
            PutObjectRequest {
                bucket: "archive-001".to_string(),
                key: "records/current.json".to_string(),
                body: br#"{"current":true}"#.to_vec(),
                metadata: ObjectMetadata::default(),
            },
            ObjectLock::none(),
        )
        .expect("current source");

    let run = runtime
        .run_bucket_replication("alice", "archive-001")
        .expect("replicate");

    assert_eq!(run.replicated_object_versions, 1);
    assert!(matches!(
        runtime.get_object_version(
            "alice",
            "archive-002",
            "records/existing.json",
            &existing.version_id
        ),
        Err(RuntimeError::NoSuchKey(_)) | Err(RuntimeError::NoSuchVersion { .. })
    ));
    assert_eq!(
        runtime
            .get_object_version(
                "alice",
                "archive-002",
                "records/current.json",
                &current.version_id
            )
            .expect("replicated current")
            .body,
        br#"{"current":true}"#
    );
}