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"
);
}