//! Tests for el-aop. #[cfg(test)] mod tests { use std::collections::HashMap; use std::sync::Arc; use crate::{ aspects::*, chain::AspectChain, registry::AspectRegistry, AopError, Aspect, InvocationContext, InvocationResult, }; fn succeed_proceed(value: impl Into + Clone) -> crate::ProceedFn { let v = value.into(); Box::new(move |_ctx| Ok(InvocationResult::new(v.clone()))) } fn fail_proceed(msg: impl Into + Clone) -> crate::ProceedFn { let m = msg.into(); Box::new(move |_ctx| Err(AopError::Aspect(m.clone()))) } fn ctx(target: &str, method: &str) -> InvocationContext { InvocationContext::new(target, method) } fn authed_ctx(target: &str, method: &str) -> InvocationContext { ctx(target, method).with_meta("user_id", "user-123") } fn admin_ctx(target: &str, method: &str) -> InvocationContext { ctx(target, method) .with_meta("user_id", "admin-1") .with_meta("roles", "admin,user") } // ── Test 1: AuthenticateAspect rejects unauthenticated calls ───────────── #[test] fn test_authenticate_rejects_unauthenticated() { let aspect = AuthenticateAspect; let mut ctx = ctx("AdminDashboard", "load"); let result = aspect.before(&mut ctx); assert!(result.is_err()); assert!(matches!(result, Err(AopError::Unauthenticated))); } // ── Test 2: AuthenticateAspect allows authenticated calls ───────────────── #[test] fn test_authenticate_allows_authenticated() { let aspect = AuthenticateAspect; let mut ctx = authed_ctx("AdminDashboard", "load"); let result = aspect.before(&mut ctx); assert!(result.is_ok()); } // ── Test 3: AuthorizeAspect rejects wrong role ──────────────────────────── #[test] fn test_authorize_rejects_wrong_role() { let aspect = AuthorizeAspect::new("admin"); let mut ctx = authed_ctx("Dashboard", "delete").with_meta("roles", "user"); let result = aspect.before(&mut ctx); assert!(matches!(result, Err(AopError::Forbidden { .. }))); } // ── Test 4: AuthorizeAspect allows correct role ─────────────────────────── #[test] fn test_authorize_allows_correct_role() { let aspect = AuthorizeAspect::new("admin"); let mut ctx = admin_ctx("Dashboard", "delete"); let result = aspect.before(&mut ctx); assert!(result.is_ok()); } // ── Test 5: CacheAspect returns cached result on second call ────────────── #[test] fn test_cache_returns_cached_result() { let aspect = CacheAspect::new(300); let ctx = authed_ctx("OrderService", "get_orders"); let call_count = Arc::new(std::sync::atomic::AtomicU32::new(0)); let cc = call_count.clone(); let proceed: crate::ProceedFn = Box::new(move |_ctx| { let n = cc.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1; Ok(InvocationResult::new(format!("result-{}", n))) }); // First call — executes proceed let r1 = aspect.around(ctx.clone(), &proceed).unwrap(); // Second call — should return cached (proceed not called again) let proceed2: crate::ProceedFn = Box::new(|_ctx| { panic!("proceed should not be called on cache hit"); }); let r2 = aspect.around(ctx.clone(), &proceed2).unwrap(); assert_eq!(r1.value, r2.value, "cached value should be returned"); } // ── Test 6: RateLimitAspect blocks after limit exceeded ─────────────────── #[test] fn test_rate_limit_blocks_after_limit() { let aspect = RateLimitAspect::new(2, 60); let mut ctx = authed_ctx("OrderService", "create_order"); // First two calls succeed assert!(aspect.before(&mut ctx).is_ok()); assert!(aspect.before(&mut ctx).is_ok()); // Third call should be blocked let result = aspect.before(&mut ctx); assert!(matches!(result, Err(AopError::RateLimited { .. }))); } // ── Test 7: LogAspect passes through to proceed ─────────────────────────── #[test] fn test_log_aspect_passthrough() { let aspect = LogAspect::new("info"); let ctx = authed_ctx("UserService", "get_user"); let result = aspect.around(ctx, &succeed_proceed("user-data")).unwrap(); assert_eq!(result.value, "user-data"); } // ── Test 8: ValidateAspect rejects required field missing ──────────────── #[test] fn test_validate_required_field() { let aspect = ValidateAspect::new(); aspect.add_rule("UserService", "create_user", "name", "required"); let mut ctx = authed_ctx("UserService", "create_user"); // No "name" arg let result = aspect.before(&mut ctx); assert!(matches!(result, Err(AopError::ValidationFailed(_)))); } // ── Test 9: ValidateAspect passes when field is present ────────────────── #[test] fn test_validate_required_field_present() { let aspect = ValidateAspect::new(); aspect.add_rule("UserService", "create_user", "email", "email"); let mut ctx = authed_ctx("UserService", "create_user") .with_arg("email", "alice@example.com"); assert!(aspect.before(&mut ctx).is_ok()); } // ── Test 10: ValidateAspect rejects invalid email ───────────────────────── #[test] fn test_validate_email_rule() { let aspect = ValidateAspect::new(); aspect.add_rule("UserService", "create_user", "email", "email"); let mut ctx = authed_ctx("UserService", "create_user") .with_arg("email", "not-an-email"); let result = aspect.before(&mut ctx); assert!(matches!(result, Err(AopError::ValidationFailed(_)))); } // ── Test 11: RetryAspect retries on failure ─────────────────────────────── #[test] fn test_retry_succeeds_on_third_attempt() { let aspect = RetryAspect::new(3); let ctx = authed_ctx("OrderService", "place_order"); let attempt = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0)); let attempt_clone = attempt.clone(); let proceed: crate::ProceedFn = Box::new(move |_ctx| { let n = attempt_clone.fetch_add(1, std::sync::atomic::Ordering::Relaxed); if n < 2 { Err(AopError::Aspect("transient error".into())) } else { Ok(InvocationResult::new("success")) } }); let result = aspect.around(ctx, &proceed).unwrap(); assert_eq!(result.value, "success"); } // ── Test 12: RetryAspect exhausts all attempts ─────────────────────────── #[test] fn test_retry_exhausted() { let aspect = RetryAspect::new(3); let ctx = authed_ctx("OrderService", "place_order"); let result = aspect.around(ctx, &fail_proceed("always fails")); assert!(matches!(result, Err(AopError::RetriesExhausted { attempts: 3, .. }))); } // ── Test 13: TraceAspect injects trace/span IDs ─────────────────────────── #[test] fn test_trace_aspect_injects_ids() { let aspect = TraceAspect::new("el-ui"); let ctx = authed_ctx("UserService", "get_user"); let result = aspect.around(ctx, &succeed_proceed("data")).unwrap(); assert!( result.metadata.contains_key("trace_id"), "should inject trace_id" ); assert!( result.metadata.contains_key("span_id"), "should inject span_id" ); } // ── Test 14: AspectChain executes aspects in order ──────────────────────── #[test] fn test_aspect_chain_ordering() { let order = Arc::new(std::sync::Mutex::new(Vec::new())); struct OrderTracker { name: &'static str, order: Arc>>, } impl Aspect for OrderTracker { fn name(&self) -> &'static str { self.name } fn around(&self, ctx: InvocationContext, proceed: &crate::ProceedFn) -> crate::AopResult { self.order.lock().unwrap().push(self.name); proceed(ctx) } } let chain = AspectChain::new() .add(Arc::new(OrderTracker { name: "first", order: order.clone() })) .add(Arc::new(OrderTracker { name: "second", order: order.clone() })) .add(Arc::new(OrderTracker { name: "third", order: order.clone() })); let ctx = authed_ctx("MyService", "my_method"); chain.execute(ctx, succeed_proceed("ok")).unwrap(); let recorded = order.lock().unwrap(); assert_eq!(*recorded, vec!["first", "second", "third"]); } // ── Test 15: AspectChain with auth + authorize rejects unauthenticated ──── #[test] fn test_aspect_chain_auth_flow() { let chain = AspectChain::new() .add(Arc::new(AuthenticateAspect)) .add(Arc::new(AuthorizeAspect::new("admin"))); // Unauthenticated — should fail at authenticate let ctx = ctx("AdminDashboard", "load"); let result = chain.execute(ctx, succeed_proceed("ok")); assert!(matches!(result, Err(AopError::Unauthenticated))); // Authenticated but wrong role — should fail at authorize let ctx = authed_ctx("AdminDashboard", "load").with_meta("roles", "user"); let result = chain.execute(ctx, succeed_proceed("ok")); assert!(matches!(result, Err(AopError::Forbidden { .. }))); // Admin — should succeed let ctx = admin_ctx("AdminDashboard", "load"); let result = chain.execute(ctx, succeed_proceed("ok")); assert!(result.is_ok()); } // ── Test 16: AspectRegistry registers all builtins ──────────────────────── #[test] fn test_registry_has_builtins() { let registry = AspectRegistry::with_builtins(); for name in ["authenticate", "authorize", "cache", "rate_limit", "log", "validate", "retry", "trace"] { assert!(registry.contains(name), "should have built-in: {}", name); } } // ── Test 17: AspectRegistry creates aspects from params ─────────────────── #[test] fn test_registry_creates_aspect() { let registry = AspectRegistry::with_builtins(); let mut params = HashMap::new(); params.insert("role".into(), "admin".into()); let aspect = registry.create("authorize", ¶ms); assert!(aspect.is_some(), "should create authorize aspect"); assert_eq!(aspect.unwrap().name(), "authorize"); } // ── Test 18: AspectRegistry::create returns None for unknown aspect ─────── #[test] fn test_registry_unknown_aspect() { let registry = AspectRegistry::with_builtins(); let result = registry.create("unknown_aspect", &HashMap::new()); assert!(result.is_none()); } // ── Test 19: CacheAspect with zero TTL doesn't serve stale data ────────── #[test] fn test_cache_zero_ttl() { let aspect = CacheAspect::new(0); // Immediate expiry let ctx = authed_ctx("Service", "method"); let n = Arc::new(std::sync::atomic::AtomicU32::new(0)); let nc = n.clone(); // Both calls should hit proceed since ttl=0 means instant expiry let p1: crate::ProceedFn = Box::new(move |_ctx| { let v = nc.fetch_add(1, std::sync::atomic::Ordering::Relaxed); Ok(InvocationResult::new(v.to_string())) }); // Zero TTL will expire immediately; we just verify it doesn't panic let _ = aspect.around(ctx.clone(), &p1); // Second call should also execute proceed let p2: crate::ProceedFn = Box::new(|_ctx| Ok(InvocationResult::new("fresh"))); let r = aspect.around(ctx, &p2).unwrap(); assert_eq!(r.value, "fresh"); } // ── Test 20: Empty AspectChain calls proceed directly ───────────────────── #[test] fn test_empty_chain_calls_proceed() { let chain = AspectChain::new(); assert!(chain.is_empty()); let ctx = authed_ctx("Service", "method"); let result = chain.execute(ctx, succeed_proceed("direct")).unwrap(); assert_eq!(result.value, "direct"); } }