use apcore::acl::{ACLRule, AuditEntry, ACL};
use apcore::acl_handlers::{register_condition, ACLConditionHandler};
use apcore::context::{Context, Identity};
use async_trait::async_trait;
use serde_json::{json, Value};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
struct PanickingHandler;
#[async_trait]
impl ACLConditionHandler for PanickingHandler {
async fn evaluate(&self, _value: &Value, _ctx: &Context<Value>) -> bool {
panic!("simulated handler panic");
}
}
fn make_context() -> Context<Value> {
let identity = Identity::new(
"test-user".to_string(),
"user".to_string(),
vec![],
HashMap::new(),
);
Context::new(identity)
}
fn make_acl_capturing(captured: Arc<Mutex<Vec<AuditEntry>>>) -> ACL {
ACL::init_builtin_handlers();
register_condition("_panicking_rs", Arc::new(PanickingHandler));
let mut conditions = serde_json::Map::new();
conditions.insert("_panicking_rs".to_string(), json!(true));
let rule = ACLRule {
callers: vec!["*".to_string()],
targets: vec!["*".to_string()],
effect: "allow".to_string(),
description: None,
conditions: Some(Value::Object(conditions)),
};
let mut acl = ACL::new(vec![rule], "deny", None);
acl.set_audit_logger(move |entry: &AuditEntry| {
captured.lock().unwrap().push(entry.clone());
});
acl
}
fn silence_panic_hook<T>(f: impl FnOnce() -> T) -> T {
let prev = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let out = f();
std::panic::set_hook(prev);
out
}
#[test]
fn sync_check_denies_and_records_on_handler_panic() {
let captured = Arc::new(Mutex::new(Vec::new()));
let acl = make_acl_capturing(Arc::clone(&captured));
let ctx = make_context();
let decision = silence_panic_hook(|| acl.check(Some("caller.mod"), "target.mod", Some(&ctx)));
assert!(
!decision,
"a panicking condition handler must fail closed (deny)"
);
let entries = captured.lock().unwrap();
let entry = entries.last().expect("an audit entry must be emitted");
let handler_error = entry
.handler_error
.as_deref()
.expect("audit entry must record handler_error on panic");
assert!(
handler_error.contains("_panicking_rs") && handler_error.contains("panicked"),
"handler_error must identify the panicking condition: {handler_error}"
);
assert!(
handler_error.contains("simulated handler panic"),
"handler_error must carry the panic message: {handler_error}"
);
}
#[tokio::test]
async fn async_check_denies_and_records_on_handler_panic() {
let captured = Arc::new(Mutex::new(Vec::new()));
let acl = make_acl_capturing(Arc::clone(&captured));
let ctx = make_context();
let prev = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let decision = acl
.async_check(Some("caller.mod"), "target.mod", Some(&ctx))
.await;
std::panic::set_hook(prev);
assert!(
!decision,
"a panicking condition handler must fail closed (deny) on the async path"
);
let entries = captured.lock().unwrap();
let entry = entries.last().expect("an audit entry must be emitted");
let handler_error = entry
.handler_error
.as_deref()
.expect("audit entry must record handler_error on panic (async)");
assert!(
handler_error.contains("_panicking_rs") && handler_error.contains("panicked"),
"handler_error must identify the panicking condition: {handler_error}"
);
}