125 lines
3.6 KiB
Rust
125 lines
3.6 KiB
Rust
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<T> {
|
|
code: u32,
|
|
message: String,
|
|
data: T,
|
|
trace_id: Option<String>,
|
|
}
|
|
|
|
async fn start_mock_iam(
|
|
call_count: Arc<AtomicUsize>,
|
|
fail: Arc<AtomicBool>,
|
|
) -> (String, tokio::task::JoinHandle<()>) {
|
|
let handler = move |Json(_body): Json<Value>| {
|
|
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();
|
|
}
|