use std::sync::{ Arc, atomic::{AtomicBool, AtomicUsize, Ordering}, }; use std::time::Duration; use axum::response::IntoResponse; use axum::{Json, Router, routing::post}; use cms_service::infrastructure::iam_client::{IamClient, IamClientConfig}; use serde::Serialize; use serde_json::Value; #[derive(Debug, Serialize)] struct AuthorizationCheckResponse { allowed: bool, } #[derive(Debug, Serialize)] struct ApiSuccessResponse { code: u32, message: String, data: T, trace_id: Option, } async fn start_mock_iam( call_count: Arc, fail: Arc, ) -> (String, tokio::task::JoinHandle<()>) { let handler = move |Json(_body): Json| { let call_count = call_count.clone(); let fail = fail.clone(); async move { call_count.fetch_add(1, Ordering::SeqCst); if fail.load(Ordering::SeqCst) { return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, "fail").into_response(); } let resp = ApiSuccessResponse { code: 0, message: "ok".to_string(), data: AuthorizationCheckResponse { allowed: true }, trace_id: None, }; (axum::http::StatusCode::OK, Json(resp)).into_response() } }; let app = Router::new() .route("/authorize/check-expr", post(handler.clone())) .route("/api/v1/authorize/check-expr", post(handler)); let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let base_url = format!("http://{}", addr); let handle = tokio::spawn(async move { axum::serve(listener, app).await.unwrap(); }); (base_url, handle) } #[tokio::test] async fn iam_client_caches_decisions() { let call_count = Arc::new(AtomicUsize::new(0)); let fail = Arc::new(AtomicBool::new(false)); let (base_url, handle) = start_mock_iam(call_count.clone(), fail.clone()).await; let client = IamClient::new(IamClientConfig { base_url, timeout: Duration::from_millis(500), cache_ttl: Duration::from_secs(5), cache_stale_if_error: Duration::from_secs(30), cache_max_entries: 1000, }); let tenant_id = uuid::Uuid::new_v4(); let user_id = uuid::Uuid::new_v4(); client .require_permission(tenant_id, user_id, "cms:article:edit", "token") .await .unwrap(); client .require_permission(tenant_id, user_id, "cms:article:edit", "token") .await .unwrap(); assert_eq!(call_count.load(Ordering::SeqCst), 1); handle.abort(); } #[tokio::test] async fn iam_client_uses_stale_cache_on_error() { let call_count = Arc::new(AtomicUsize::new(0)); let fail = Arc::new(AtomicBool::new(false)); let (base_url, handle) = start_mock_iam(call_count.clone(), fail.clone()).await; let client = IamClient::new(IamClientConfig { base_url, timeout: Duration::from_millis(500), cache_ttl: Duration::from_millis(50), cache_stale_if_error: Duration::from_secs(30), cache_max_entries: 1000, }); let tenant_id = uuid::Uuid::new_v4(); let user_id = uuid::Uuid::new_v4(); client .require_permission(tenant_id, user_id, "cms:article:edit", "token") .await .unwrap(); tokio::time::sleep(Duration::from_millis(70)).await; fail.store(true, Ordering::SeqCst); client .require_permission(tenant_id, user_id, "cms:article:edit", "token") .await .unwrap(); assert!(call_count.load(Ordering::SeqCst) >= 1); handle.abort(); }