mod common;
use bucketwarden_auth::OperatorRole;
use bucketwarden_lock::{ObjectLock, RetentionMode};
use bucketwarden_s3::{
BucketPolicyRequest, BucketQuotaConfiguration, LifecycleRule, ObjectMetadata, PutObjectRequest,
};
use bucketwarden_server::RuntimeError;
use common::*;
#[test]
fn ops_console_report_covers_all_console_feature_surfaces() {
let mut runtime = runtime();
runtime.create_local_user("root");
runtime
.assign_operator_role("root", "root", OperatorRole::ClusterAdmin, "*")
.expect("cluster admin");
runtime.create_tenant("tenant-a");
runtime
.create_local_user_in_tenant("alice", "tenant-a")
.expect("tenant user");
runtime
.create_service_account_in_tenant("svc-a", "tenant-a")
.expect("service account");
runtime.allow("alice", "s3:*", "*");
runtime
.create_bucket("alice", "archive-001")
.expect("bucket");
runtime
.put_bucket_policy(
"alice",
BucketPolicyRequest {
bucket: "archive-001".to_string(),
policy_json:
r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":"*","Action":"s3:GetObject","Resource":"archive-001/public/*"}]}"#
.to_string(),
},
)
.expect("policy");
runtime
.put_bucket_lifecycle(
"alice",
"archive-001",
vec![LifecycleRule {
id: "expire".to_string(),
prefix: Some("records/".to_string()),
status: "Enabled".to_string(),
expiration_days: Some(90),
..LifecycleRule::default()
}],
)
.expect("lifecycle");
runtime
.put_bucket_object_lock_configuration("alice", "archive-001", None)
.expect("object lock");
runtime
.put_bucket_quota(
"root",
"archive-001",
BucketQuotaConfiguration {
max_objects: None,
max_requests: Some(1),
},
)
.expect("bucket quota");
runtime
.put_object(
"alice",
PutObjectRequest {
bucket: "archive-001".to_string(),
key: "records/a.txt".to_string(),
body: b"payload-v1".to_vec(),
metadata: ObjectMetadata::default(),
},
ObjectLock::none(),
)
.expect("put v1");
runtime
.put_object(
"alice",
PutObjectRequest {
bucket: "archive-001".to_string(),
key: "records/a.txt".to_string(),
body: b"payload-v2".to_vec(),
metadata: ObjectMetadata::default(),
},
ObjectLock {
legal_hold: true,
retention_mode: Some(RetentionMode::Governance),
retain_until_epoch_seconds: Some(100),
},
)
.expect("put v2");
let report = runtime
.ops_console_report("root", None)
.expect("console report");
assert_eq!(report.scope, "runtime");
assert_eq!(report.buckets.len(), 1);
assert_eq!(report.buckets[0].name, "archive-001");
assert!(report.buckets[0].has_policy);
assert!(report.buckets[0].has_lifecycle);
assert!(report.buckets[0].has_object_lock);
assert_eq!(report.objects.len(), 1);
assert_eq!(report.objects[0].version_count, 2);
assert!(report.objects[0].legal_hold);
assert_eq!(
report.objects[0].retention_mode.as_deref(),
Some("Governance")
);
assert!(report
.users
.iter()
.any(|user| user.principal_id == "alice" && user.kind == "LocalUser"));
assert!(report
.users
.iter()
.any(|user| user.principal_id == "svc-a" && user.kind == "ServiceAccount"));
assert_eq!(report.policies.len(), 1);
assert_eq!(report.policies[0].statement_count, 1);
assert_eq!(report.metrics.bucket_count, 1);
assert_eq!(report.metrics.object_count, 1);
assert_eq!(report.metrics.version_count, 2);
assert!(report.audit.audit_event_count >= 4);
assert_eq!(report.retention_bucket_count, 1);
assert_eq!(report.retained_version_count, 1);
assert_eq!(report.lifecycle_bucket_count, 1);
assert_eq!(report.lifecycle_rule_count, 1);
assert_eq!(report.native_support.len(), 16);
assert_eq!(report.semantic_parity.len(), 16);
assert!(report
.native_support
.iter()
.all(|feature| feature.native_support_state == "implemented"));
assert!(report
.observability_evidence
.contains(&"ops:GetConsoleReport".to_string()));
assert!(report
.validation_tests
.contains(&"crates/bucketwarden-server/tests/ops_console.rs".to_string()));
assert!(runtime
.audit_events()
.iter()
.any(|event| event.action == "ops:GetConsoleReport"));
}
#[test]
fn ops_console_report_supports_bucket_scope_and_fail_closed_authorization() {
let mut runtime = runtime();
runtime
.create_bucket("alice", "archive-001")
.expect("bucket");
runtime.create_bucket("alice", "logs-001").expect("bucket");
runtime.create_local_user("ops");
runtime
.assign_operator_role(
"alice",
"ops",
OperatorRole::ReadOnlyOperator,
"archive-001",
)
.expect("read only");
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 archive");
runtime
.put_object(
"alice",
PutObjectRequest {
bucket: "logs-001".to_string(),
key: "logs/a.txt".to_string(),
body: b"payload".to_vec(),
metadata: ObjectMetadata::default(),
},
ObjectLock::none(),
)
.expect("put logs");
let report = runtime
.ops_console_report("ops", Some("archive-001"))
.expect("bucket console report");
assert_eq!(report.scope, "bucket");
assert_eq!(report.target, "archive-001");
assert_eq!(report.buckets.len(), 1);
assert_eq!(report.buckets[0].name, "archive-001");
assert_eq!(report.objects.len(), 1);
assert_eq!(report.objects[0].bucket, "archive-001");
assert!(report.users.is_empty());
assert!(runtime.ops_console_report("ops", Some("logs-001")).is_err());
}
#[test]
fn ops_console_report_rejects_invalid_principals_permissions_and_bucket_inputs() {
let mut runtime = runtime();
runtime
.create_bucket("alice", "archive-001")
.expect("bucket");
runtime.create_local_user("diag-only");
runtime
.assign_operator_role(
"alice",
"diag-only",
OperatorRole::BucketAdmin,
"archive-001",
)
.expect("bucket admin");
runtime.create_local_user("root");
runtime
.assign_operator_role("root", "root", OperatorRole::ClusterAdmin, "*")
.expect("cluster admin");
assert!(matches!(
runtime.ops_console_report("missing-principal", Some("archive-001")),
Err(RuntimeError::Auth(_))
));
assert!(matches!(
runtime.ops_console_report("diag-only", Some("archive-001")),
Err(RuntimeError::OperatorActionDenied { principal, .. }) if principal == "diag-only"
));
assert!(matches!(
runtime.ops_console_report("root", Some("missing-bucket")),
Err(RuntimeError::NoSuchBucket(bucket)) if bucket == "missing-bucket"
));
}
#[test]
fn ops_console_report_handles_empty_valid_runtime_without_panics() {
let mut runtime = runtime();
runtime.create_local_user("root");
runtime
.assign_operator_role("root", "root", OperatorRole::ClusterAdmin, "*")
.expect("cluster admin");
let report = runtime
.ops_console_report("root", None)
.expect("empty runtime console report");
assert_eq!(report.scope, "runtime");
assert!(report.buckets.is_empty());
assert!(report.objects.is_empty());
assert_eq!(report.metrics.bucket_count, 0);
assert_eq!(report.metrics.object_count, 0);
assert_eq!(report.metrics.version_count, 0);
assert!(report
.failure_modes
.contains(&"runtime has no buckets".to_string()));
assert_eq!(report.native_support.len(), 16);
assert_eq!(report.semantic_parity.len(), 16);
}
#[test]
fn ops_console_report_aggregates_worst_case_versions_delete_markers_and_findings() {
let mut runtime = runtime();
runtime.create_local_user("root");
runtime
.assign_operator_role("root", "root", OperatorRole::ClusterAdmin, "*")
.expect("cluster admin");
for bucket_index in 0..3 {
let bucket = format!("archive-{bucket_index:03}");
runtime.create_bucket("alice", &bucket).expect("bucket");
if bucket_index == 0 {
runtime
.put_bucket_policy(
"alice",
BucketPolicyRequest {
bucket: bucket.clone(),
policy_json:
r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":"*","Action":"s3:GetObject","Resource":"archive-000/*"},{"Effect":"Deny","Principal":"*","Action":"s3:DeleteObject","Resource":"archive-000/protected/*"}]}"#
.to_string(),
},
)
.expect("policy");
}
if bucket_index == 1 {
runtime
.put_bucket_lifecycle(
"alice",
&bucket,
vec![LifecycleRule {
id: "expire-noncurrent".to_string(),
prefix: Some("records/".to_string()),
status: "Enabled".to_string(),
noncurrent_expiration_days: Some(30),
..LifecycleRule::default()
}],
)
.expect("lifecycle");
}
if bucket_index == 2 {
runtime
.put_bucket_object_lock_configuration("alice", &bucket, None)
.expect("object lock");
}
for key_index in 0..4 {
let key = format!("records/{key_index:03}.txt");
for version_index in 0..3 {
runtime
.put_object(
"alice",
PutObjectRequest {
bucket: bucket.clone(),
key: key.clone(),
body: format!("{bucket}:{key}:{version_index}").into_bytes(),
metadata: ObjectMetadata::default(),
},
ObjectLock::none(),
)
.expect("put object version");
}
if key_index % 2 == 0 {
runtime
.delete_object("alice", &bucket, &key, false)
.expect("delete marker");
}
}
}
let report = runtime
.ops_console_report("root", None)
.expect("worst-case console report");
assert_eq!(report.buckets.len(), 3);
assert_eq!(report.objects.len(), 12);
assert_eq!(report.metrics.bucket_count, 3);
assert_eq!(report.metrics.version_count, 42);
assert_eq!(report.metrics.delete_marker_count, 6);
assert_eq!(
report
.buckets
.iter()
.map(|bucket| bucket.delete_marker_count)
.sum::<usize>(),
6
);
assert_eq!(report.policies.len(), 1);
assert_eq!(report.policies[0].statement_count, 2);
assert_eq!(report.lifecycle_bucket_count, 1);
assert_eq!(report.lifecycle_rule_count, 1);
assert_eq!(report.retention_bucket_count, 1);
assert!(report.security_governance_findings.iter().any(|finding| {
finding == "bucket:archive-001:policy:not-configured"
|| finding == "bucket:archive-002:policy:not-configured"
}));
assert!(!report.product_caveats.is_empty());
assert!(report
.product_caveats
.iter()
.any(|caveat| caveat.contains("object")));
}