bucketwarden-server 0.1.0

BucketWarden storage server runtime.
Documentation
use super::*;

#[test]
fn runtime_put_get_delete_emits_audit_and_replication() {
    let mut runtime = runtime();
    runtime
        .create_bucket("alice", "archive-001")
        .expect("bucket");
    let put = runtime
        .put_object(
            "alice",
            PutObjectRequest {
                bucket: "archive-001".to_string(),
                key: "records/a.txt".to_string(),
                body: b"payload".to_vec(),
                metadata: ObjectMetadata::default(),
            },
            ObjectLock::none(),
        )
        .expect("put");
    let got = runtime
        .get_object("alice", "archive-001", "records/a.txt")
        .expect("get");
    let deleted = runtime
        .delete_object("alice", "archive-001", "records/a.txt", false)
        .expect("delete");
    assert_eq!(put.version_id, "v1");
    assert_eq!(got.body, b"payload");
    assert_eq!(deleted.delete_marker_version_id, "v2");
    assert_eq!(runtime.replication_records().len(), 2);
    assert_eq!(runtime.audit_events().len(), 6);
    let actions = runtime
        .audit_events()
        .iter()
        .map(|event| event.action.as_str())
        .collect::<Vec<_>>();
    assert!(actions.contains(&"kms:Encrypt"));
    assert!(actions.contains(&"kms:Decrypt"));
}
#[test]
fn explicit_deny_blocks_allowed_delete() {
    let mut runtime = runtime();
    runtime.deny("alice", "s3:DeleteObject", "archive-001/private/*");
    runtime
        .create_bucket("alice", "archive-001")
        .expect("bucket");
    runtime
        .put_object(
            "alice",
            PutObjectRequest {
                bucket: "archive-001".to_string(),
                key: "private/a.txt".to_string(),
                body: b"payload".to_vec(),
                metadata: ObjectMetadata::default(),
            },
            ObjectLock::none(),
        )
        .expect("put");
    let error = runtime
        .delete_object("alice", "archive-001", "private/a.txt", false)
        .expect_err("delete denied");
    assert!(matches!(
        error,
        RuntimeError::AccessDenied {
            explicit_deny: true,
            ..
        }
    ));
    assert_eq!(
        runtime
            .get_object("alice", "archive-001", "private/a.txt")
            .expect("still readable")
            .body,
        b"payload"
    );
}
#[test]
fn legal_hold_and_retention_prevent_delete() {
    let mut runtime = runtime();
    runtime
        .create_bucket("alice", "archive-001")
        .expect("bucket");
    runtime
        .put_object(
            "alice",
            PutObjectRequest {
                bucket: "archive-001".to_string(),
                key: "locked.txt".to_string(),
                body: b"payload".to_vec(),
                metadata: ObjectMetadata::default(),
            },
            ObjectLock::none(),
        )
        .expect("put");
    runtime
        .set_legal_hold("alice", "archive-001", "locked.txt", true)
        .expect("legal hold");
    let held = runtime
        .delete_object("alice", "archive-001", "locked.txt", true)
        .expect_err("held");
    assert!(matches!(
        held,
        RuntimeError::ObjectLocked(LockError::LegalHold)
    ));
    runtime
        .set_legal_hold("alice", "archive-001", "locked.txt", false)
        .expect("release legal hold");
    runtime
        .set_retention(
            "alice",
            "archive-001",
            "locked.txt",
            RetentionMode::Governance,
            100,
            false,
        )
        .expect("retention");
    let retained = runtime
        .delete_object("alice", "archive-001", "locked.txt", false)
        .expect_err("retained");
    assert!(matches!(
        retained,
        RuntimeError::ObjectLocked(LockError::GovernanceRetentionActive { .. })
    ));
    runtime
        .delete_object("alice", "archive-001", "locked.txt", true)
        .expect("governance bypass");
}

#[test]
fn retention_update_semantics_enforce_governance_and_compliance_rules() {
    let mut runtime = runtime();
    runtime
        .create_bucket("alice", "archive-001")
        .expect("bucket");
    runtime
        .put_object(
            "alice",
            PutObjectRequest {
                bucket: "archive-001".to_string(),
                key: "records/a.txt".to_string(),
                body: b"payload".to_vec(),
                metadata: ObjectMetadata::default(),
            },
            ObjectLock::retained(RetentionMode::Governance, 100),
        )
        .expect("put");
    let shorten_governance = runtime
        .set_retention(
            "alice",
            "archive-001",
            "records/a.txt",
            RetentionMode::Governance,
            50,
            false,
        )
        .expect_err("governance shortening without bypass");
    assert!(matches!(
        shorten_governance,
        RuntimeError::ObjectLocked(LockError::GovernanceRetentionBypassRequired { .. })
    ));
    runtime
        .set_retention(
            "alice",
            "archive-001",
            "records/a.txt",
            RetentionMode::Governance,
            50,
            true,
        )
        .expect("governance bypassed shorten");
    runtime
        .set_retention(
            "alice",
            "archive-001",
            "records/a.txt",
            RetentionMode::Compliance,
            150,
            false,
        )
        .expect("tighten to compliance");
    let downgrade = runtime
        .set_retention(
            "alice",
            "archive-001",
            "records/a.txt",
            RetentionMode::Governance,
            200,
            true,
        )
        .expect_err("compliance cannot downgrade");
    assert!(matches!(
        downgrade,
        RuntimeError::ObjectLocked(LockError::ComplianceModeImmutable)
    ));
    let shorten_compliance = runtime
        .set_retention(
            "alice",
            "archive-001",
            "records/a.txt",
            RetentionMode::Compliance,
            120,
            true,
        )
        .expect_err("compliance cannot shorten");
    assert!(matches!(
        shorten_compliance,
        RuntimeError::ObjectLocked(LockError::ComplianceRetentionCannotBeShortened { .. })
    ));
}
#[test]
fn health_reports_runtime_counters() {
    let mut runtime = runtime();
    runtime
        .create_bucket("alice", "archive-001")
        .expect("bucket");
    let health = runtime.health();
    assert_eq!(health.status, "ok");
    assert_eq!(health.bucket_count, 1);
    assert_eq!(health.object_version_count, 0);
    assert_eq!(health.audit_event_count, 1);
}
#[test]
fn delete_marker_hides_current_object_but_version_read_still_works() {
    let mut runtime = runtime();
    runtime
        .create_bucket("alice", "archive-001")
        .expect("bucket");
    let put = runtime
        .put_object(
            "alice",
            PutObjectRequest {
                bucket: "archive-001".to_string(),
                key: "records/a.txt".to_string(),
                body: b"payload".to_vec(),
                metadata: ObjectMetadata::default(),
            },
            ObjectLock::none(),
        )
        .expect("put");
    runtime
        .delete_object("alice", "archive-001", "records/a.txt", false)
        .expect("delete");
    assert!(matches!(
        runtime.get_object("alice", "archive-001", "records/a.txt"),
        Err(RuntimeError::NoSuchKey(_))
    ));
    let version = runtime
        .get_object_version("alice", "archive-001", "records/a.txt", &put.version_id)
        .expect("version read");
    assert_eq!(version.body, b"payload");
}
#[test]
fn list_head_and_copy_use_current_visible_versions() {
    let mut runtime = runtime();
    runtime
        .create_bucket("alice", "archive-001")
        .expect("bucket");
    runtime
        .create_bucket("alice", "archive-002")
        .expect("bucket");
    let mut metadata = ObjectMetadata {
        content_type: "text/plain".to_string(),
        ..ObjectMetadata::default()
    };
    metadata
        .user_metadata
        .insert("tenant".to_string(), "alpha".to_string());
    runtime
        .put_object(
            "alice",
            PutObjectRequest {
                bucket: "archive-001".to_string(),
                key: "records/a.txt".to_string(),
                body: b"payload".to_vec(),
                metadata: metadata.clone(),
            },
            ObjectLock::none(),
        )
        .expect("put");
    runtime
        .put_object(
            "alice",
            PutObjectRequest {
                bucket: "archive-001".to_string(),
                key: "tmp/skip.txt".to_string(),
                body: b"skip".to_vec(),
                metadata: ObjectMetadata::default(),
            },
            ObjectLock::none(),
        )
        .expect("put");
    runtime
        .delete_object("alice", "archive-001", "tmp/skip.txt", false)
        .expect("delete");
    let listing = runtime
        .list_objects(
            "alice",
            ListObjectsRequest {
                bucket: "archive-001".to_string(),
                prefix: Some("records/".to_string()),
                ..ListObjectsRequest::default()
            },
        )
        .expect("list");
    assert_eq!(listing.objects.len(), 1);
    assert_eq!(listing.objects[0].key, "records/a.txt");
    let head = runtime
        .head_object("alice", "archive-001", "records/a.txt")
        .expect("head");
    assert_eq!(head.content_length, 7);
    assert_eq!(head.metadata, metadata);
    let copied = runtime
        .copy_object(
            "alice",
            CopyObjectRequest {
                source_bucket: "archive-001".to_string(),
                source_key: "records/a.txt".to_string(),
                source_version_id: None,
                destination_bucket: "archive-002".to_string(),
                destination_key: "copies/a.txt".to_string(),
                metadata_directive: MetadataDirective::Copy,
                destination_encryption: None,
            },
        )
        .expect("copy");
    assert_eq!(copied.version_id, "v1");
    assert_eq!(
        runtime
            .get_object("alice", "archive-002", "copies/a.txt")
            .expect("copied")
            .body,
        b"payload"
    );
}