mod common;
use bucketwarden_auth::OperatorRole;
use bucketwarden_lock::{ObjectLock, RetentionMode};
use bucketwarden_s3::{ObjectMetadata, PutObjectRequest};
use bucketwarden_server::{
ConsoleApiAuditQuery, ConsoleApiListQuery, ConsoleApiLoginRequest, ConsoleApiPreferences,
ConsoleApiRequest, RuntimeConfig, RuntimeError,
};
use common::*;
use serde_json::json;
use std::collections::BTreeMap;
fn seeded_console_runtime() -> (bucketwarden_server::BucketWarden, String) {
let mut runtime = runtime();
runtime
.create_custom_identity("root", "shared-secret")
.expect("custom identity");
runtime
.assign_operator_role("root", "root", OperatorRole::ClusterAdmin, "*")
.expect("cluster admin");
runtime
.create_bucket("alice", "archive-001")
.expect("bucket");
runtime
.put_bucket_object_lock_configuration("alice", "archive-001", None)
.expect("object lock");
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 session = runtime
.console_api_login(ConsoleApiLoginRequest {
principal_id: "root".to_string(),
shared_secret: "shared-secret".to_string(),
})
.expect("login");
(runtime, session.access_key_id)
}
#[test]
fn console_api_authenticates_sessions_and_expires_or_rejects_invalid_inputs() {
let (mut runtime, access_key_id) = seeded_console_runtime();
let current = runtime
.console_api_current_user(&access_key_id)
.expect("current user");
assert_eq!(current.principal_id, "root");
assert_eq!(current.scope, "*");
let invalid = runtime.console_api_login(ConsoleApiLoginRequest {
principal_id: "root".to_string(),
shared_secret: "wrong-secret".to_string(),
});
assert!(matches!(invalid, Err(RuntimeError::Auth(_))));
runtime.set_clock_epoch_seconds(current.expires_at_epoch_seconds + 1);
assert!(matches!(
runtime.console_api_current_user(&access_key_id),
Err(RuntimeError::Auth(_))
));
}
#[test]
fn console_api_reports_buckets_objects_versions_governance_and_audit() {
let (mut runtime, access_key_id) = seeded_console_runtime();
let overview = runtime
.console_api_overview(&access_key_id)
.expect("overview");
assert_eq!(overview.session.principal_id, "root");
assert_eq!(overview.metrics.bucket_count, 1);
assert_eq!(overview.metrics.version_count, 2);
let health = runtime
.console_api_health_report(&access_key_id)
.expect("health");
assert_eq!(health.bucket_count, 1);
let config = runtime
.console_api_configuration_report(&access_key_id)
.expect("config");
assert_eq!(
config.bucket_regions.get("archive-001").map(String::as_str),
Some("us-east-1")
);
let buckets = runtime
.console_api_bucket_list(
&access_key_id,
ConsoleApiListQuery {
q: Some("archive".to_string()),
..ConsoleApiListQuery::default()
},
)
.expect("bucket list");
assert_eq!(buckets.page.total, 1);
assert_eq!(buckets.buckets[0].name, "archive-001");
let detail = runtime
.console_api_bucket_detail(&access_key_id, "archive-001")
.expect("bucket detail");
assert!(detail.bucket.has_object_lock);
let objects = runtime
.console_api_object_list(
&access_key_id,
"archive-001",
ConsoleApiListQuery::default(),
)
.expect("object list");
assert_eq!(objects.objects[0].key, "records/a.txt");
assert_eq!(objects.objects[0].version_count, 2);
let versions = runtime
.console_api_object_version_history(
&access_key_id,
"archive-001",
"records/a.txt",
ConsoleApiListQuery::default(),
)
.expect("versions");
assert_eq!(versions.page.total, 2);
assert!(versions.versions[0].is_latest);
let governance = runtime
.console_api_object_governance_summary(&access_key_id, "archive-001", "records/a.txt", None)
.expect("governance");
assert!(governance.legal_hold);
assert_eq!(governance.retention_mode.as_deref(), Some("Governance"));
let audit = runtime
.console_api_audit_events(
&access_key_id,
ConsoleApiAuditQuery {
outcome: Some("Allowed".to_string()),
..ConsoleApiAuditQuery::default()
},
)
.expect("audit");
assert!(audit.page.total > 0);
assert!(audit
.events
.iter()
.all(|event| matches!(event.outcome, bucketwarden_audit::AuditOutcome::Allowed)));
}
#[test]
fn console_api_evidence_preferences_and_errors_are_runtime_backed() {
let (mut runtime, access_key_id) = seeded_console_runtime();
let evidence = runtime
.console_api_evidence_list(&access_key_id, ConsoleApiListQuery::default())
.expect("evidence list");
assert!(evidence
.evidence
.iter()
.any(|item| item.name == "snapshot_json" && item.bytes > 0));
let export = runtime
.console_api_evidence_export(&access_key_id)
.expect("evidence export");
assert_eq!(export.content_type, "application/json");
assert!(export.report.snapshot_json.contains("\"buckets\""));
let mut values = BTreeMap::new();
values.insert("bucketwarden.ui.density".to_string(), "compact".to_string());
let saved = runtime
.console_api_preferences_write(&access_key_id, ConsoleApiPreferences { values })
.expect("write preferences");
assert_eq!(
saved
.values
.get("bucketwarden.ui.density")
.map(String::as_str),
Some("compact")
);
let restored = bucketwarden_server::BucketWarden::restore(
RuntimeConfig::development(),
runtime.snapshot(),
)
.expect("restore");
let mut restored = restored;
let current = restored
.console_api_preferences_read(&access_key_id)
.expect("read preferences");
assert_eq!(
current
.values
.get("bucketwarden.ui.density")
.map(String::as_str),
Some("compact")
);
let error = restored
.console_api_object_list(&access_key_id, "missing", ConsoleApiListQuery::default())
.expect_err("missing bucket");
let envelope = restored.console_api_error_envelope(&error, Some("req-1"));
assert_eq!(envelope.status, 404);
assert_eq!(envelope.code, "NoSuchBucket");
assert!(!envelope.retryable);
}
#[test]
fn console_api_route_dispatch_covers_protected_and_unprotected_endpoints() {
let (mut runtime, access_key_id) = seeded_console_runtime();
let missing_session = runtime
.handle_console_api(ConsoleApiRequest {
method: "GET".to_string(),
path: "/ui/api/overview".to_string(),
..ConsoleApiRequest::default()
})
.expect("error response");
assert_eq!(missing_session.status, 401);
assert_eq!(missing_session.body["code"], "AuthenticationFailed");
let login = runtime
.handle_console_api(ConsoleApiRequest {
method: "POST".to_string(),
path: "/ui/api/login".to_string(),
body: Some(json!({
"principal_id": "root",
"shared_secret": "shared-secret"
})),
..ConsoleApiRequest::default()
})
.expect("login route");
assert_eq!(login.status, 200);
assert_eq!(login.body["principal_id"], "root");
let mut query = BTreeMap::new();
query.insert("bucket".to_string(), "archive-001".to_string());
query.insert("key".to_string(), "records/a.txt".to_string());
let versions = runtime
.handle_console_api(ConsoleApiRequest {
method: "GET".to_string(),
path: "/ui/api/object-versions".to_string(),
access_key_id: Some(access_key_id.clone()),
query,
body: None,
})
.expect("versions route");
assert_eq!(versions.status, 200);
assert_eq!(versions.body["page"]["total"], 2);
runtime
.handle_console_api(ConsoleApiRequest {
method: "POST".to_string(),
path: "/ui/api/logout".to_string(),
access_key_id: Some(access_key_id.clone()),
..ConsoleApiRequest::default()
})
.expect("logout route");
let after_logout = runtime
.handle_console_api(ConsoleApiRequest {
method: "GET".to_string(),
path: "/ui/api/current-user".to_string(),
access_key_id: Some(access_key_id),
..ConsoleApiRequest::default()
})
.expect("revoked route");
assert_eq!(after_logout.status, 401);
}