use axum::{
Json,
extract::{Path, Query, State},
http::StatusCode,
};
use serde::{Deserialize, Serialize};
use super::server::AppState;
#[cfg(feature = "ban-manager")]
use crate::ban::{BanFilter, BanTarget};
#[derive(Serialize)]
pub struct ApiResponse<T: Serialize> {
pub success: bool,
pub message: String,
pub data: Option<T>,
}
impl<T: Serialize> ApiResponse<T> {
pub fn ok(data: T) -> Self {
Self {
success: true,
message: "OK".to_string(),
data: Some(data),
}
}
pub fn error(message: impl Into<String>) -> Self {
Self {
success: false,
message: message.into(),
data: None,
}
}
}
#[derive(Serialize)]
pub struct SystemStatus {
pub total_requests: u64,
pub blocked_requests: u64,
pub success_rate: f64,
#[cfg(feature = "ban-manager")]
pub active_bans: usize,
#[cfg(feature = "circuit-breaker")]
pub circuit_breaker: String,
}
pub async fn get_status(State(state): State<AppState>) -> Json<ApiResponse<SystemStatus>> {
let stats = state.governor.stats().await;
let blocked = stats.rejected_requests + stats.banned_requests;
let total = stats.total_requests;
let success_rate = if total > 0 {
(total - blocked) as f64 / total as f64
} else {
1.0
};
#[cfg(feature = "ban-manager")]
let active_bans: usize = if let Some(ref bm) = state.ban_manager {
bm.list_bans(BanFilter {
active_only: true,
..Default::default()
})
.await
.map(|v| v.len())
.unwrap_or(0)
} else {
0
};
#[cfg(feature = "circuit-breaker")]
let cb_state = if let Some(ref cb) = state.circuit_breaker {
cb.get_state().await.to_string()
} else {
"disabled".to_string()
};
Json(ApiResponse::ok(SystemStatus {
total_requests: total,
blocked_requests: blocked,
success_rate,
#[cfg(feature = "ban-manager")]
active_bans,
#[cfg(feature = "circuit-breaker")]
circuit_breaker: cb_state,
}))
}
#[derive(Serialize)]
pub struct LimiterStatus {
pub key: String,
pub limit: u64,
pub remaining: u64,
pub reset_at: u64,
}
pub async fn get_limiter_status(
State(_state): State<AppState>,
Path(key): Path<String>,
) -> (StatusCode, Json<ApiResponse<LimiterStatus>>) {
let _ = key;
(
StatusCode::NOT_IMPLEMENTED,
Json(ApiResponse::error("limiter status query not implemented")),
)
}
#[derive(Deserialize)]
pub struct UnbanRequest {
pub reason: Option<String>,
pub operator: Option<String>,
}
#[derive(Deserialize, Default)]
pub struct BanTargetQuery {
#[serde(rename = "type")]
pub target_type: Option<String>,
}
#[cfg(feature = "ban-manager")]
pub async fn delete_ban(
State(state): State<AppState>,
Path(target): Path<String>,
Query(query): Query<BanTargetQuery>,
Json(req): Json<UnbanRequest>,
) -> (StatusCode, Json<ApiResponse<()>>) {
let Some(ref ban_manager) = state.ban_manager else {
return (
StatusCode::SERVICE_UNAVAILABLE,
Json(ApiResponse::error("Ban manager not configured")),
);
};
let ban_target = match query.target_type.as_deref() {
Some("ip") => BanTarget::Ip(target),
Some("user") => BanTarget::UserId(target),
Some("mac") => BanTarget::Mac(target),
Some("geo") => BanTarget::Geo {
country_code: target,
},
Some(other) => {
return (
StatusCode::BAD_REQUEST,
Json(ApiResponse::error(format!(
"unsupported target type: {}",
other
))),
);
}
None => {
if target.parse::<std::net::IpAddr>().is_ok() {
BanTarget::Ip(target)
} else {
BanTarget::UserId(target)
}
}
};
let operator = req.operator.unwrap_or_else(|| "admin-api".to_string());
match ban_manager.delete_ban(&ban_target, operator).await {
Ok(true) => (
StatusCode::OK,
Json(ApiResponse {
success: true,
message: req.reason.unwrap_or_else(|| "Ban removed".to_string()),
data: Some(()),
}),
),
Ok(false) => (
StatusCode::NOT_FOUND,
Json(ApiResponse::error("Ban not found")),
),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiResponse::error(format!("Failed to remove ban: {}", e))),
),
}
}
#[cfg(not(feature = "ban-manager"))]
pub async fn delete_ban(
Path(_target): Path<String>,
Query(_query): Query<BanTargetQuery>,
) -> (StatusCode, Json<ApiResponse<()>>) {
(
StatusCode::SERVICE_UNAVAILABLE,
Json(ApiResponse::error("Ban manager not configured")),
)
}
#[derive(Deserialize)]
#[cfg(feature = "ban-manager")]
pub struct CreateBanRequest {
pub target: crate::storage::BanTarget,
pub reason: String,
#[serde(default)]
pub operator: Option<String>,
#[serde(default)]
pub duration_secs: Option<u64>,
}
#[derive(Serialize)]
#[cfg(feature = "ban-manager")]
pub struct BanResponse {
pub id: String,
pub ban_times: u32,
pub expires_at: i64,
pub is_manual: bool,
}
#[cfg(feature = "ban-manager")]
pub async fn create_ban(
State(state): State<AppState>,
Json(req): Json<CreateBanRequest>,
) -> (StatusCode, Json<ApiResponse<BanResponse>>) {
use crate::ban::BanSource;
use std::time::Duration;
let Some(ref ban_manager) = state.ban_manager else {
return (
StatusCode::SERVICE_UNAVAILABLE,
Json(ApiResponse::error("Ban manager not configured")),
);
};
let operator = req.operator.unwrap_or_else(|| "admin-api".to_string());
let source = BanSource::Manual { operator };
let duration = req.duration_secs.map(Duration::from_secs);
match ban_manager
.create_ban(
req.target,
req.reason,
source,
serde_json::json!({"source": "http-api"}),
duration,
)
.await
{
Ok(detail) => (
StatusCode::CREATED,
Json(ApiResponse::ok(BanResponse {
id: detail.id,
ban_times: detail.ban_times,
expires_at: detail.expires_at.timestamp(),
is_manual: detail.is_manual,
})),
),
Err(e) => {
let status = match &e {
crate::error::LimiteronError::ValidationError(_) => StatusCode::BAD_REQUEST,
crate::error::LimiteronError::AuthorizationError(_) => StatusCode::FORBIDDEN,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
(status, Json(ApiResponse::error(format!("{}", e))))
}
}
}
#[cfg(not(feature = "ban-manager"))]
pub async fn create_ban() -> (StatusCode, Json<ApiResponse<()>>) {
(
StatusCode::SERVICE_UNAVAILABLE,
Json(ApiResponse::error("Ban manager not configured")),
)
}
#[derive(Deserialize)]
pub struct UpdateQuotaRequest {
pub resource: String,
pub new_limit: u64,
#[serde(default)]
pub duration_secs: Option<u64>,
}
#[derive(Serialize)]
pub struct UpdateQuotaResponse {
pub success: bool,
pub expires_at: Option<u64>,
}
#[cfg(feature = "quota-control")]
pub async fn update_quota(
State(state): State<AppState>,
Path(tenant_id): Path<String>,
Json(req): Json<UpdateQuotaRequest>,
) -> (StatusCode, Json<ApiResponse<UpdateQuotaResponse>>) {
let Some(ref quota_controller) = state.quota_controller else {
return (
StatusCode::SERVICE_UNAVAILABLE,
Json(ApiResponse::error("Quota controller not configured")),
);
};
if req.new_limit == 0 {
match quota_controller
.reset_quota(&tenant_id, &req.resource)
.await
{
Ok(_) => (
StatusCode::OK,
Json(ApiResponse::ok(UpdateQuotaResponse {
success: true,
expires_at: req.duration_secs.map(|d| {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|t| t.as_secs() + d)
.unwrap_or(0)
}),
})),
),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiResponse::error(format!("Failed to reset quota: {}", e))),
),
}
} else {
(
StatusCode::BAD_REQUEST,
Json(ApiResponse::error(
"Per-tenant quota limit update is not supported; use global QuotaConfig update_config instead",
)),
)
}
}
#[cfg(not(feature = "quota-control"))]
pub async fn update_quota(
Path(_tenant_id): Path<String>,
) -> (StatusCode, Json<ApiResponse<UpdateQuotaResponse>>) {
(
StatusCode::SERVICE_UNAVAILABLE,
Json(ApiResponse::error("Quota controller not configured")),
)
}
#[derive(Serialize)]
pub struct CircuitBreakerStatus {
pub state: String,
pub failure_rate: f64,
pub slow_call_rate: f64,
}
#[cfg(feature = "circuit-breaker")]
pub async fn get_circuit_breaker_status(
State(state): State<AppState>,
) -> (StatusCode, Json<ApiResponse<CircuitBreakerStatus>>) {
if let Some(ref cb) = state.circuit_breaker {
let stats = cb.get_stats().await;
let total = stats.total_calls as f64;
let failure_rate = if total > 0.0 {
stats.failure_count as f64 / total
} else {
0.0
};
(
StatusCode::OK,
Json(ApiResponse::ok(CircuitBreakerStatus {
state: stats.state.to_string(),
failure_rate,
slow_call_rate: 0.0,
})),
)
} else {
(
StatusCode::SERVICE_UNAVAILABLE,
Json(ApiResponse::error("Circuit breaker not configured")),
)
}
}
#[cfg(not(feature = "circuit-breaker"))]
pub async fn get_circuit_breaker_status() -> (StatusCode, Json<ApiResponse<()>>) {
(
StatusCode::SERVICE_UNAVAILABLE,
Json(ApiResponse::error("Circuit breaker not configured")),
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::admin::AppState;
use crate::admin::{make_governor, make_state};
use std::sync::Arc;
#[test]
fn test_api_response_ok_contains_data() {
let resp: ApiResponse<i32> = ApiResponse::ok(42);
assert!(resp.success);
assert_eq!(resp.message, "OK");
assert_eq!(resp.data, Some(42));
}
#[test]
fn test_api_response_error_has_no_data() {
let resp: ApiResponse<i32> = ApiResponse::error("something failed");
assert!(!resp.success);
assert_eq!(resp.message, "something failed");
assert_eq!(resp.data, None);
}
#[test]
fn test_api_response_error_accepts_string_and_str() {
let owned = String::from("owned error");
let resp1: ApiResponse<i32> = ApiResponse::error(owned);
assert_eq!(resp1.message, "owned error");
let resp2: ApiResponse<i32> = ApiResponse::error("borrowed error");
assert_eq!(resp2.message, "borrowed error");
}
#[tokio::test]
async fn test_get_status_empty_governor() {
let state = make_state().await;
let resp = get_status(State(state)).await;
assert!(resp.0.success);
let data = resp.0.data.unwrap();
assert_eq!(data.total_requests, 0);
assert_eq!(data.blocked_requests, 0);
assert_eq!(data.success_rate, 1.0);
}
#[tokio::test]
async fn test_get_limiter_status_returns_key() {
let state = make_state().await;
let (status, resp) = get_limiter_status(State(state), Path("my-key".to_string())).await;
assert_eq!(status, StatusCode::NOT_IMPLEMENTED);
assert!(!resp.0.success);
}
#[cfg(feature = "ban-manager")]
#[tokio::test]
async fn test_delete_ban_no_manager() {
let state = make_state().await;
let req = UnbanRequest {
reason: None,
operator: None,
};
let resp = delete_ban(
State(state),
Path("192.168.1.1".to_string()),
Query(Default::default()),
Json(req),
)
.await;
assert!(!resp.1.0.success);
assert_eq!(resp.1.0.message, "Ban manager not configured");
}
#[cfg(feature = "ban-manager")]
#[tokio::test]
async fn test_delete_ban_with_manager_ip_target() {
use crate::BanManager;
let ban_manager = Arc::new(BanManager::new().await.unwrap());
let governor = Arc::new(make_governor().await);
let state = AppState {
governor,
ban_manager: Some(ban_manager),
#[cfg(feature = "quota-control")]
quota_controller: None,
#[cfg(feature = "circuit-breaker")]
circuit_breaker: None,
};
let req = UnbanRequest {
reason: Some("test".to_string()),
operator: Some("tester".to_string()),
};
let resp = delete_ban(
State(state),
Path("192.168.1.1".to_string()),
Query(Default::default()),
Json(req),
)
.await;
assert!(!resp.1.0.success);
assert_eq!(resp.1.0.message, "Ban not found");
}
#[cfg(feature = "ban-manager")]
#[tokio::test]
async fn test_delete_ban_with_manager_userid_target() {
use crate::BanManager;
let ban_manager = Arc::new(BanManager::new().await.unwrap());
let governor = Arc::new(make_governor().await);
let state = AppState {
governor,
ban_manager: Some(ban_manager),
#[cfg(feature = "quota-control")]
quota_controller: None,
#[cfg(feature = "circuit-breaker")]
circuit_breaker: None,
};
let req = UnbanRequest {
reason: None,
operator: None,
};
let resp = delete_ban(
State(state),
Path("user-123".to_string()),
Query(Default::default()),
Json(req),
)
.await;
assert!(!resp.1.0.success);
assert_eq!(resp.1.0.message, "Ban not found");
}
#[cfg(feature = "quota-control")]
#[tokio::test]
async fn test_update_quota_no_controller() {
let state = make_state().await;
let req = UpdateQuotaRequest {
resource: "api".to_string(),
new_limit: 0,
duration_secs: None,
};
let resp = update_quota(State(state), Path("tenant-1".to_string()), Json(req)).await;
assert!(!resp.1.0.success);
assert_eq!(resp.1.0.message, "Quota controller not configured");
}
#[cfg(feature = "quota-control")]
#[tokio::test]
async fn test_update_quota_unsupported_limit() {
use crate::QuotaController;
use crate::cache::CacheQuotaStorage;
use crate::quota::QuotaConfig;
use crate::storage::QuotaStorage;
use oxcache::backend::memory::DashMapMemoryBackend;
let storage: Arc<dyn QuotaStorage> = Arc::new(CacheQuotaStorage::new(Arc::new(
DashMapMemoryBackend::new(),
)));
let quota_controller = Arc::new(QuotaController::with_dependencies(
storage,
QuotaConfig::default(),
));
let governor = Arc::new(make_governor().await);
let state = AppState {
governor,
#[cfg(feature = "ban-manager")]
ban_manager: None,
quota_controller: Some(quota_controller),
#[cfg(feature = "circuit-breaker")]
circuit_breaker: None,
};
let req = UpdateQuotaRequest {
resource: "api".to_string(),
new_limit: 100,
duration_secs: None,
};
let resp = update_quota(State(state), Path("tenant-1".to_string()), Json(req)).await;
assert!(!resp.1.0.success);
assert!(resp.1.0.message.contains("not supported"));
}
#[cfg(feature = "circuit-breaker")]
#[tokio::test]
async fn test_get_circuit_breaker_status_no_cb() {
let state = make_state().await;
let resp = get_circuit_breaker_status(State(state)).await;
assert!(!resp.1.0.success);
assert_eq!(resp.1.0.message, "Circuit breaker not configured");
}
#[cfg(feature = "circuit-breaker")]
#[tokio::test]
async fn test_get_circuit_breaker_status_with_cb() {
use crate::circuit::{CircuitBreaker, CircuitBreakerConfig};
let cb = Arc::new(CircuitBreaker::with_dependencies(
CircuitBreakerConfig::default(),
));
let governor = Arc::new(make_governor().await);
let state = AppState {
governor,
#[cfg(feature = "ban-manager")]
ban_manager: None,
#[cfg(feature = "quota-control")]
quota_controller: None,
circuit_breaker: Some(cb),
};
let resp = get_circuit_breaker_status(State(state)).await;
assert!(resp.1.0.success);
let data = resp.1.0.data.unwrap();
assert_eq!(data.failure_rate, 0.0);
assert_eq!(data.slow_call_rate, 0.0);
}
#[cfg(feature = "ban-manager")]
#[tokio::test]
async fn test_get_status_with_ban_manager_and_cb() {
use crate::BanManager;
use crate::circuit::{CircuitBreaker, CircuitBreakerConfig};
let ban_manager = Arc::new(BanManager::new().await.unwrap());
let cb = Arc::new(CircuitBreaker::with_dependencies(
CircuitBreakerConfig::default(),
));
let governor = Arc::new(make_governor().await);
let state = AppState {
governor,
ban_manager: Some(ban_manager),
#[cfg(feature = "quota-control")]
quota_controller: None,
circuit_breaker: Some(cb),
};
let resp = get_status(State(state)).await;
assert!(resp.0.success);
let data = resp.0.data.unwrap();
assert_eq!(data.active_bans, 0);
assert!(!data.circuit_breaker.is_empty());
}
#[cfg(feature = "ban-manager")]
#[tokio::test]
async fn test_delete_ban_success() {
use crate::BanManager;
use crate::ban::BanTarget;
use crate::storage::BanRecord;
let ban_manager = Arc::new(BanManager::new().await.unwrap());
let target = BanTarget::Ip("10.0.0.1".to_string());
let now = chrono::Utc::now();
ban_manager
.add_ban(BanRecord {
target: target.clone(),
ban_times: 1,
duration: std::time::Duration::from_secs(3600),
banned_at: now,
expires_at: now + chrono::Duration::seconds(3600),
is_manual: true,
reason: "test ban".to_string(),
})
.await
.unwrap();
let governor = Arc::new(make_governor().await);
let state = AppState {
governor,
ban_manager: Some(ban_manager),
#[cfg(feature = "quota-control")]
quota_controller: None,
#[cfg(feature = "circuit-breaker")]
circuit_breaker: None,
};
let req = UnbanRequest {
reason: Some("manual unban".to_string()),
operator: Some("admin".to_string()),
};
let resp = delete_ban(
State(state),
Path("10.0.0.1".to_string()),
Query(Default::default()),
Json(req),
)
.await;
assert!(resp.1.0.success);
assert_eq!(resp.1.0.message, "manual unban");
}
#[cfg(feature = "ban-manager")]
#[tokio::test]
async fn test_delete_ban_mac_with_explicit_type() {
use crate::BanManager;
use crate::ban::BanTarget;
use crate::storage::BanRecord;
let ban_manager = Arc::new(BanManager::new().await.unwrap());
let target = BanTarget::Mac("00:1a:2b:3c:4d:5e".to_string());
let now = chrono::Utc::now();
ban_manager
.add_ban(BanRecord {
target: target.clone(),
ban_times: 1,
duration: std::time::Duration::from_secs(3600),
banned_at: now,
expires_at: now + chrono::Duration::seconds(3600),
is_manual: true,
reason: "mac ban".to_string(),
})
.await
.unwrap();
let governor = Arc::new(make_governor().await);
let state = AppState {
governor,
ban_manager: Some(ban_manager),
#[cfg(feature = "quota-control")]
quota_controller: None,
#[cfg(feature = "circuit-breaker")]
circuit_breaker: None,
};
let req = UnbanRequest {
reason: Some("mac unban".to_string()),
operator: Some("admin".to_string()),
};
let query = Query(BanTargetQuery {
target_type: Some("mac".to_string()),
});
let resp = delete_ban(
State(state),
Path("00:1a:2b:3c:4d:5e".to_string()),
query,
Json(req),
)
.await;
assert!(resp.1.0.success);
assert_eq!(resp.1.0.message, "mac unban");
}
#[cfg(feature = "ban-manager")]
#[tokio::test]
async fn test_delete_ban_geo_with_explicit_type() {
use crate::BanManager;
use crate::ban::BanTarget;
use crate::storage::BanRecord;
let ban_manager = Arc::new(BanManager::new().await.unwrap());
let target = BanTarget::Geo {
country_code: "CN".to_string(),
};
let now = chrono::Utc::now();
ban_manager
.add_ban(BanRecord {
target: target.clone(),
ban_times: 1,
duration: std::time::Duration::from_secs(3600),
banned_at: now,
expires_at: now + chrono::Duration::seconds(3600),
is_manual: true,
reason: "geo ban".to_string(),
})
.await
.unwrap();
let governor = Arc::new(make_governor().await);
let state = AppState {
governor,
ban_manager: Some(ban_manager),
#[cfg(feature = "quota-control")]
quota_controller: None,
#[cfg(feature = "circuit-breaker")]
circuit_breaker: None,
};
let req = UnbanRequest {
reason: Some("geo unban".to_string()),
operator: Some("admin".to_string()),
};
let query = Query(BanTargetQuery {
target_type: Some("geo".to_string()),
});
let resp = delete_ban(State(state), Path("CN".to_string()), query, Json(req)).await;
assert!(resp.1.0.success);
assert_eq!(resp.1.0.message, "geo unban");
}
#[cfg(feature = "ban-manager")]
#[tokio::test]
async fn test_delete_ban_unsupported_target_type_returns_400() {
use crate::BanManager;
let ban_manager = Arc::new(BanManager::new().await.unwrap());
let governor = Arc::new(make_governor().await);
let state = AppState {
governor,
ban_manager: Some(ban_manager),
#[cfg(feature = "quota-control")]
quota_controller: None,
#[cfg(feature = "circuit-breaker")]
circuit_breaker: None,
};
let req = UnbanRequest {
reason: None,
operator: None,
};
let query = Query(BanTargetQuery {
target_type: Some("foo".to_string()),
});
let (status, resp) =
delete_ban(State(state), Path("1.2.3.4".to_string()), query, Json(req)).await;
assert_eq!(status, StatusCode::BAD_REQUEST);
assert!(!resp.0.success);
assert!(resp.0.message.contains("unsupported target type: foo"));
}
#[cfg(feature = "ban-manager")]
#[tokio::test]
async fn test_delete_ban_mac_without_type_falls_back_to_userid() {
use crate::BanManager;
use crate::ban::BanTarget;
use crate::storage::BanRecord;
let ban_manager = Arc::new(BanManager::new().await.unwrap());
let target = BanTarget::Mac("aa:bb:cc:dd:ee:ff".to_string());
let now = chrono::Utc::now();
ban_manager
.add_ban(BanRecord {
target: target.clone(),
ban_times: 1,
duration: std::time::Duration::from_secs(3600),
banned_at: now,
expires_at: now + chrono::Duration::seconds(3600),
is_manual: true,
reason: "mac ban".to_string(),
})
.await
.unwrap();
let governor = Arc::new(make_governor().await);
let state = AppState {
governor,
ban_manager: Some(ban_manager),
#[cfg(feature = "quota-control")]
quota_controller: None,
#[cfg(feature = "circuit-breaker")]
circuit_breaker: None,
};
let req = UnbanRequest {
reason: None,
operator: None,
};
let (status, resp) = delete_ban(
State(state),
Path("aa:bb:cc:dd:ee:ff".to_string()),
Query(Default::default()),
Json(req),
)
.await;
assert_eq!(status, StatusCode::NOT_FOUND);
assert_eq!(resp.0.message, "Ban not found");
}
#[cfg(feature = "quota-control")]
#[tokio::test]
async fn test_update_quota_reset_success() {
use crate::QuotaController;
use crate::cache::CacheQuotaStorage;
use crate::quota::QuotaConfig;
use crate::storage::QuotaStorage;
use oxcache::backend::memory::DashMapMemoryBackend;
let storage: Arc<dyn QuotaStorage> = Arc::new(CacheQuotaStorage::new(Arc::new(
DashMapMemoryBackend::new(),
)));
let quota_controller = Arc::new(QuotaController::with_dependencies(
storage,
QuotaConfig::default(),
));
let governor = Arc::new(make_governor().await);
let state = AppState {
governor,
#[cfg(feature = "ban-manager")]
ban_manager: None,
quota_controller: Some(quota_controller),
#[cfg(feature = "circuit-breaker")]
circuit_breaker: None,
};
let req = UpdateQuotaRequest {
resource: "api".to_string(),
new_limit: 0,
duration_secs: Some(3600),
};
let resp = update_quota(State(state), Path("tenant-1".to_string()), Json(req)).await;
assert!(resp.1.0.success);
let data = resp.1.0.data.unwrap();
assert!(data.expires_at.is_some());
}
#[cfg(feature = "circuit-breaker")]
#[tokio::test]
async fn test_get_circuit_breaker_status_with_failures() {
use crate::circuit::{CircuitBreaker, CircuitBreakerConfig};
use crate::error::{LimiteronError, StorageError};
let cb = Arc::new(CircuitBreaker::with_dependencies(
CircuitBreakerConfig::default(),
));
let _ = cb
.execute(|| async {
Err::<(), LimiteronError>(LimiteronError::StorageError(
StorageError::ConnectionError("test".to_string()),
))
})
.await;
let governor = Arc::new(make_governor().await);
let state = AppState {
governor,
#[cfg(feature = "ban-manager")]
ban_manager: None,
#[cfg(feature = "quota-control")]
quota_controller: None,
circuit_breaker: Some(cb),
};
let resp = get_circuit_breaker_status(State(state)).await;
assert!(resp.1.0.success);
let data = resp.1.0.data.unwrap();
assert!(data.failure_rate > 0.0);
}
}