#[cfg(test)]
mod tests {
use std::time::{Duration, Instant};
use crate::authz::engine::AuthzEngine;
use crate::authz::model::{ChronixAction, ChronixNamespace, ChronixPrincipal};
fn make_policy(id: usize) -> String {
format!(
r#"permit(
principal in Chronix::Role::"role_{id}",
action == Chronix::Action::"Read",
resource == Chronix::Namespace::"namespace_{id}"
);"#
)
}
#[cfg(debug_assertions)]
const LATENCY_MULTIPLIER: u64 = 500;
#[cfg(not(debug_assertions))]
const LATENCY_MULTIPLIER: u64 = 1;
#[test]
#[ignore = "perf benchmark — run with --ignored"]
fn authz_decision_latency_100_policies() {
let engine = AuthzEngine::new();
for i in 0..100 {
engine
.add_policy(&format!("policy_{i}"), &make_policy(i))
.unwrap();
}
let principal = ChronixPrincipal::new("user1");
let resource = ChronixNamespace::new("namespace_50");
for _ in 0..50 {
let _ = engine.authorize_namespace(&principal, ChronixAction::Read, &resource);
}
let iterations = 5_000;
let mut latencies = Vec::with_capacity(iterations);
for _ in 0..iterations {
let start = Instant::now();
let d = engine.authorize_namespace(&principal, ChronixAction::Read, &resource);
latencies.push(start.elapsed());
assert!(!d.is_allowed());
}
latencies.sort();
let p99 = latencies[(iterations as f64 * 0.99) as usize];
let target = Duration::from_micros(100 * LATENCY_MULTIPLIER);
assert!(
p99 < target,
"100-policy p99 authz latency {p99:?} exceeds {target:?} target"
);
}
#[test]
#[ignore = "perf benchmark — run with --ignored"]
fn authz_decision_latency_1000_policies() {
let engine = AuthzEngine::new();
for i in 0..1000 {
engine
.add_policy(&format!("policy_{i}"), &make_policy(i))
.unwrap();
}
let principal = ChronixPrincipal::new("user1");
let resource = ChronixNamespace::new("namespace_500");
for _ in 0..20 {
let _ = engine.authorize_namespace(&principal, ChronixAction::Read, &resource);
}
let iterations = 2_000;
let mut latencies = Vec::with_capacity(iterations);
for _ in 0..iterations {
let start = Instant::now();
let _ = engine.authorize_namespace(&principal, ChronixAction::Read, &resource);
latencies.push(start.elapsed());
}
latencies.sort();
let p99 = latencies[(iterations as f64 * 0.99) as usize];
let target = Duration::from_micros(100 * LATENCY_MULTIPLIER);
assert!(
p99 < target,
"1000-policy p99 authz latency {p99:?} exceeds {target:?} target"
);
}
#[test]
#[ignore = "perf benchmark — run with --ignored"]
fn authz_allow_decision_latency() {
let engine = AuthzEngine::new();
for i in 0..100 {
engine
.add_policy(&format!("policy_{i}"), &make_policy(i))
.unwrap();
}
let principal = ChronixPrincipal::new("alice").with_role("role_50");
let resource = ChronixNamespace::new("namespace_50");
let decision = engine.authorize_namespace(&principal, ChronixAction::Read, &resource);
assert!(
decision.is_allowed(),
"Expected allow for principal with matching role"
);
let iterations = 5_000;
let mut latencies = Vec::with_capacity(iterations);
for _ in 0..iterations {
let start = Instant::now();
let _ = engine.authorize_namespace(&principal, ChronixAction::Read, &resource);
latencies.push(start.elapsed());
}
latencies.sort();
let avg = latencies.iter().sum::<Duration>() / iterations as u32;
let p99 = latencies[(iterations as f64 * 0.99) as usize];
let target = Duration::from_micros(100 * LATENCY_MULTIPLIER);
assert!(p99 < target, "Allow-path p99 {p99:?} exceeds {target:?}");
assert!(
avg < Duration::from_micros(50 * LATENCY_MULTIPLIER),
"Allow-path average {avg:?} too high"
);
}
#[test]
#[ignore = "perf benchmark — run with --ignored"]
fn authz_deny_decision_fast() {
let engine = AuthzEngine::new();
for i in 0..100 {
engine
.add_policy(&format!("policy_{i}"), &make_policy(i))
.unwrap();
}
let principal = ChronixPrincipal::new("nobody");
let resource = ChronixNamespace::new("nonexistent");
let iterations = 5_000;
let mut latencies = Vec::with_capacity(iterations);
for _ in 0..iterations {
let start = Instant::now();
let decision = engine.authorize_namespace(&principal, ChronixAction::Write, &resource);
latencies.push(start.elapsed());
assert!(!decision.is_allowed());
}
latencies.sort();
let p99 = latencies[(iterations as f64 * 0.99) as usize];
let target = Duration::from_micros(100 * LATENCY_MULTIPLIER);
assert!(p99 < target, "Deny decision p99 {p99:?} exceeds {target:?}");
}
#[test]
#[ignore = "perf benchmark — run with --ignored"]
fn policy_hot_reload_under_100ms_1000_policies() {
let engine = AuthzEngine::new();
let mut policy_src = String::new();
for i in 0..1000 {
policy_src.push_str(&make_policy(i));
policy_src.push('\n');
}
let start = Instant::now();
let count = engine.load_policies(&policy_src).unwrap();
let elapsed = start.elapsed();
assert_eq!(count, 1000);
let target = Duration::from_millis(100 * LATENCY_MULTIPLIER);
assert!(
elapsed < target,
"Reload of 1000 policies took {elapsed:?}, exceeds {target:?}"
);
}
#[test]
#[ignore = "perf benchmark — run with --ignored"]
fn authz_overhead_small_relative_to_query() {
let engine = AuthzEngine::new();
for i in 0..100 {
engine
.add_policy(&format!("policy_{i}"), &make_policy(i))
.unwrap();
}
let principal = ChronixPrincipal::new("alice").with_role("role_50");
let resource = ChronixNamespace::new("namespace_50");
assert!(
engine
.authorize_namespace(&principal, ChronixAction::Read, &resource)
.is_allowed()
);
let iterations = 5_000;
let start = Instant::now();
for _ in 0..iterations {
let _ = engine.authorize_namespace(&principal, ChronixAction::Read, &resource);
}
let authz_total = start.elapsed();
let authz_per_op = authz_total / iterations as u32;
let simulated_query_cost = Duration::from_millis(10);
let overhead_pct =
authz_per_op.as_nanos() as f64 / simulated_query_cost.as_nanos() as f64 * 100.0;
assert!(
overhead_pct < 50.0,
"Authorization overhead {overhead_pct:.2}% of 10ms query is too high (per-op: {authz_per_op:?})"
);
}
#[test]
#[ignore = "perf benchmark — run with --ignored"]
fn authz_scales_sublinearly_with_policy_count() {
let principal = ChronixPrincipal::new("user1");
let resource = ChronixNamespace::new("m_test");
let mut results = Vec::new();
for count in [10, 100, 500] {
let engine = AuthzEngine::new();
for i in 0..count {
engine
.add_policy(&format!("p_{i}"), &make_policy(i))
.unwrap();
}
for _ in 0..20 {
let _ = engine.authorize_namespace(&principal, ChronixAction::Read, &resource);
}
let iterations = 1_000;
let start = Instant::now();
for _ in 0..iterations {
let _ = engine.authorize_namespace(&principal, ChronixAction::Read, &resource);
}
let avg = start.elapsed() / iterations as u32;
results.push((count, avg));
}
let (_, lat_10) = results[0];
let (_, lat_500) = results[2];
let ratio = lat_500.as_nanos() as f64 / lat_10.as_nanos().max(1) as f64;
assert!(
ratio < 100.0,
"500-policy/10-policy latency ratio {ratio:.1}x is too high \
({lat_10:?} → {lat_500:?})"
);
}
}