pub const FIRST_BAN_DURATION_SECS: u64 = 60;
pub const SECOND_BAN_DURATION_SECS: u64 = 300;
pub const THIRD_BAN_DURATION_SECS: u64 = 1800;
pub const FOURTH_BAN_DURATION_SECS: u64 = 7200;
pub const MAX_BAN_DURATION_SECS: u64 = 86400;
pub const AUTO_UNBAN_INTERVAL_SECS: u64 = 60;
pub const DEFAULT_PAGINATION_LIMIT: u64 = 100;
pub const MAX_PAGINATION_LIMIT: u64 = 1000;
pub const MAX_BAN_REASON_LENGTH: usize = 500;
pub const MAX_USER_ID_LENGTH: usize = 100;
pub const MAX_MAC_ADDRESS_LENGTH: usize = 17;
use crate::error::FlowGuardError;
use crate::storage::{BanRecord, BanStorage, BanTarget};
use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use std::time::Duration as StdDuration;
use tokio::sync::RwLock;
use tracing::{debug, error, info, instrument};
#[cfg(feature = "ban-manager")]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum BanSource {
Auto,
Manual { operator: String },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[cfg(feature = "ban-manager")]
pub enum BanPriority {
Ip = 1,
UserId = 2,
Mac = 3,
DeviceId = 4,
ApiKey = 5,
}
impl BanPriority {
pub fn from_target(target: &BanTarget) -> Self {
match target {
BanTarget::Ip(_) => BanPriority::Ip,
BanTarget::UserId(_) => BanPriority::UserId,
BanTarget::Mac(_) => BanPriority::Mac,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg(feature = "ban-manager")]
pub struct BanDetail {
pub id: String,
pub target: BanTarget,
pub ban_times: u32,
pub duration: StdDuration,
pub banned_at: DateTime<Utc>,
pub expires_at: DateTime<Utc>,
pub is_manual: bool,
pub reason: String,
pub source: BanSource,
pub metadata: serde_json::Value,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub unbanned_at: Option<DateTime<Utc>>,
pub unbanned_by: Option<String>,
}
impl From<BanRecord> for BanDetail {
fn from(record: BanRecord) -> Self {
Self {
id: uuid::Uuid::new_v4().to_string(),
target: record.target,
ban_times: record.ban_times,
duration: record.duration,
banned_at: record.banned_at,
expires_at: record.expires_at,
is_manual: record.is_manual,
reason: record.reason,
source: if record.is_manual {
BanSource::Manual {
operator: "unknown".to_string(),
}
} else {
BanSource::Auto
},
metadata: serde_json::json!({}),
created_at: record.banned_at,
updated_at: record.banned_at,
unbanned_at: None,
unbanned_by: None,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg(feature = "ban-manager")]
pub struct BanFilter {
pub target_type: Option<String>,
pub target_value: Option<String>,
pub active_only: bool,
pub manual_only: bool,
pub start_time: Option<DateTime<Utc>>,
pub end_time: Option<DateTime<Utc>>,
pub offset: Option<u64>,
pub limit: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg(feature = "ban-manager")]
pub struct BackoffConfig {
pub first_duration: u64,
pub second_duration: u64,
pub third_duration: u64,
pub fourth_duration: u64,
pub max_duration: u64,
}
impl Default for BackoffConfig {
fn default() -> Self {
Self {
first_duration: FIRST_BAN_DURATION_SECS,
second_duration: SECOND_BAN_DURATION_SECS,
third_duration: THIRD_BAN_DURATION_SECS,
fourth_duration: FOURTH_BAN_DURATION_SECS,
max_duration: MAX_BAN_DURATION_SECS,
}
}
}
#[derive(Debug, Clone)]
#[cfg(feature = "ban-manager")]
pub struct BanManagerConfig {
pub backoff: BackoffConfig,
pub enable_auto_unban: bool,
pub auto_unban_interval: u64,
}
impl Default for BanManagerConfig {
fn default() -> Self {
Self {
backoff: BackoffConfig::default(),
enable_auto_unban: true,
auto_unban_interval: AUTO_UNBAN_INTERVAL_SECS,
}
}
}
#[derive(Clone)]
#[cfg(feature = "ban-manager")]
pub struct BanManager {
storage: Arc<dyn BanStorage>,
config: Arc<RwLock<BanManagerConfig>>,
auto_unban_handle: Arc<RwLock<Option<tokio::task::JoinHandle<()>>>>,
}
fn validate_ip_address(ip: &str) -> Result<(), FlowGuardError> {
if ip.is_empty() {
return Err(FlowGuardError::ValidationError(
"IP地址不能为空".to_string(),
));
}
if ip.len() > 45 {
return Err(FlowGuardError::ValidationError("IP地址过长".to_string()));
}
if ip.parse::<std::net::IpAddr>().is_err() {
return Err(FlowGuardError::ValidationError(format!(
"无效的IP地址格式: {}",
ip
)));
}
Ok(())
}
fn validate_user_id(user_id: &str) -> Result<(), FlowGuardError> {
if user_id.is_empty() {
return Err(FlowGuardError::ValidationError(
"用户ID不能为空".to_string(),
));
}
if user_id.len() > MAX_USER_ID_LENGTH {
return Err(FlowGuardError::ValidationError(format!(
"用户ID过长,最大长度为 {} 字符",
MAX_USER_ID_LENGTH
)));
}
if user_id.contains(|c: char| c.is_control()) {
return Err(FlowGuardError::ValidationError(
"用户ID包含非法字符".to_string(),
));
}
Ok(())
}
fn validate_mac_address(mac: &str) -> Result<(), FlowGuardError> {
if mac.is_empty() {
return Err(FlowGuardError::ValidationError(
"MAC地址不能为空".to_string(),
));
}
if mac.len() > MAX_MAC_ADDRESS_LENGTH {
return Err(FlowGuardError::ValidationError("MAC地址过长".to_string()));
}
let parts: Vec<&str> = mac.split(':').collect();
if parts.len() != 6 {
return Err(FlowGuardError::ValidationError(
"无效的MAC地址格式".to_string(),
));
}
for part in parts {
if part.len() != 2 {
return Err(FlowGuardError::ValidationError(
"无效的MAC地址格式".to_string(),
));
}
if !part.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(FlowGuardError::ValidationError(
"MAC地址包含非法字符".to_string(),
));
}
}
Ok(())
}
fn validate_ban_target(target: &BanTarget) -> Result<(), FlowGuardError> {
match target {
BanTarget::Ip(ip) => validate_ip_address(ip)?,
BanTarget::UserId(user_id) => validate_user_id(user_id)?,
BanTarget::Mac(mac) => validate_mac_address(mac)?,
}
Ok(())
}
fn validate_ban_reason(reason: &str) -> Result<(), FlowGuardError> {
if reason.is_empty() {
return Err(FlowGuardError::ValidationError(
"封禁原因不能为空".to_string(),
));
}
if reason.len() > MAX_BAN_REASON_LENGTH {
return Err(FlowGuardError::ValidationError(format!(
"封禁原因过长,最大长度为 {} 字符",
MAX_BAN_REASON_LENGTH
)));
}
if reason.contains(|c: char| c.is_control()) {
return Err(FlowGuardError::ValidationError(
"封禁原因包含非法字符".to_string(),
));
}
Ok(())
}
impl BanManager {
pub async fn new(
storage: Arc<dyn BanStorage>,
config: Option<BanManagerConfig>,
) -> Result<Self, FlowGuardError> {
let config = config.unwrap_or_default();
let config = Arc::new(RwLock::new(config));
let ban_manager = Self {
storage,
config,
auto_unban_handle: Arc::new(RwLock::new(None)),
};
ban_manager.start_auto_unban_task().await;
info!("BanManager initialized successfully");
Ok(ban_manager)
}
async fn start_auto_unban_task(&self) {
let config = self.config.read().await;
if !config.enable_auto_unban {
return;
}
let storage = self.storage.clone();
let interval_secs = config.auto_unban_interval;
drop(config);
let handle = tokio::spawn(async move {
let mut interval = tokio::time::interval(StdDuration::from_secs(interval_secs));
loop {
interval.tick().await;
debug!("Running auto-unban task");
#[cfg(feature = "postgres")]
if let Some(storage) = storage
.clone()
.as_any()
.downcast_ref::<crate::postgres_storage::PostgresStorage>()
{
#[cfg(feature = "postgres")]
if let Err(e) = storage.cleanup_expired_bans().await {
error!("Auto-unban task failed: {}", e);
}
}
}
});
*self.auto_unban_handle.write().await = Some(handle);
info!("Auto-unban task started (interval: {}s)", interval_secs);
}
pub async fn stop_auto_unban_task(&self) {
let mut handle_guard = self.auto_unban_handle.write().await;
if let Some(handle) = handle_guard.take() {
handle.abort();
info!("Auto-unban task stopped");
}
}
#[instrument(skip(self))]
pub async fn calculate_ban_duration(&self, ban_times: u32) -> StdDuration {
let config = self.config.read().await;
let duration_secs = match ban_times {
1 => config.backoff.first_duration,
2 => config.backoff.second_duration,
3 => config.backoff.third_duration,
_ => config.backoff.fourth_duration,
};
let duration_secs = duration_secs.min(config.backoff.max_duration);
debug!(
"Calculated ban duration: ban_times={}, duration={}s",
ban_times, duration_secs
);
StdDuration::from_secs(duration_secs)
}
#[instrument(skip(self, metadata))]
pub async fn create_ban(
&self,
target: BanTarget,
reason: String,
source: BanSource,
metadata: serde_json::Value,
duration: Option<StdDuration>,
) -> Result<BanDetail, FlowGuardError> {
validate_ban_target(&target)?;
validate_ban_reason(&reason)?;
info!(
"Creating ban: target={:?}, reason={}, source={:?}",
target, reason, source
);
let history = self.storage.get_history(&target).await?;
let ban_times = history.as_ref().map(|h| h.ban_times + 1).unwrap_or(1);
let duration = match duration {
Some(d) => d,
None => {
let config = self.config.read().await;
let duration_secs = match ban_times {
1 => config.backoff.first_duration,
2 => config.backoff.second_duration,
3 => config.backoff.third_duration,
_ => config.backoff.fourth_duration,
};
let duration_secs = duration_secs.min(config.backoff.max_duration);
StdDuration::from_secs(duration_secs)
}
};
let now = Utc::now();
let expires_at = now + Duration::from_std(duration).unwrap();
let is_manual = matches!(source, BanSource::Manual { .. });
let record = BanRecord {
target: target.clone(),
ban_times,
duration,
banned_at: now,
expires_at,
is_manual,
reason: reason.clone(),
};
self.storage.save(&record).await?;
let detail = BanDetail {
id: uuid::Uuid::new_v4().to_string(),
target,
ban_times,
duration,
banned_at: now,
expires_at,
is_manual,
reason,
source,
metadata,
created_at: now,
updated_at: now,
unbanned_at: None,
unbanned_by: None,
};
info!(
"Ban created successfully: id={}, ban_times={}",
detail.id, ban_times
);
Ok(detail)
}
#[instrument(skip(self))]
pub async fn read_ban(&self, target: &BanTarget) -> Result<Option<BanDetail>, FlowGuardError> {
debug!("Reading ban: target={:?}", target);
let record = self.storage.is_banned(target).await?;
Ok(record.map(BanDetail::from))
}
#[instrument(skip(self))]
pub async fn update_ban(
&self,
target: &BanTarget,
reason: Option<String>,
duration: Option<StdDuration>,
metadata: Option<serde_json::Value>,
) -> Result<Option<BanDetail>, FlowGuardError> {
debug!("Updating ban: target={:?}", target);
let current_record = self.storage.is_banned(target).await?;
if current_record.is_none() {
return Ok(None);
}
let mut record = current_record.unwrap();
let now = Utc::now();
if let Some(new_reason) = reason {
record.reason = new_reason;
}
if let Some(new_duration) = duration {
record.duration = new_duration;
record.expires_at = now + Duration::from_std(new_duration).unwrap();
}
self.storage.save(&record).await?;
let mut detail = BanDetail::from(record);
detail.updated_at = now;
if let Some(metadata) = metadata {
detail.metadata = metadata;
}
info!("Ban updated successfully: id={}", detail.id);
Ok(Some(detail))
}
#[instrument(skip(self))]
pub async fn delete_ban(
&self,
target: &BanTarget,
unbanned_by: String,
) -> Result<bool, FlowGuardError> {
info!(
"Deleting ban: target={:?}, unbanned_by={}",
target, unbanned_by
);
let record = self.storage.is_banned(target).await?;
if record.is_none() {
debug!("No active ban found for target: {:?}", target);
return Ok(false);
}
#[cfg(feature = "postgres")]
if let Some(storage) = self
.storage
.as_any()
.downcast_ref::<crate::postgres_storage::PostgresStorage>()
{
let (target_type, target_value) = match target {
BanTarget::Ip(ip) => ("ip", ip.as_str()),
BanTarget::UserId(user_id) => ("user", user_id.as_str()),
BanTarget::Mac(mac) => ("mac", mac.as_str()),
};
sqlx::query(
r#"
UPDATE ban_records
SET unbanned_at = now(),
unbanned_by = $1
WHERE target_type = $2
AND target_value = $3
AND expires_at > now()
AND unbanned_at IS NULL
"#,
)
.bind(&unbanned_by)
.bind(target_type)
.bind(target_value)
.execute(storage.pool())
.await
.map_err(|e| {
FlowGuardError::StorageError(crate::error::StorageError::QueryError(e.to_string()))
})?;
}
self.storage.remove_ban(target).await?;
info!("Ban deleted successfully: target={:?}", target);
Ok(true)
}
#[cfg(any(feature = "telemetry", feature = "monitoring"))]
#[instrument(skip(self))]
pub async fn list_bans(&self, filter: BanFilter) -> Result<Vec<BanDetail>, FlowGuardError> {
debug!("Listing bans with filter: {:?}", filter);
#[cfg(feature = "postgres")]
if let Some(storage) = self
.storage
.as_any()
.downcast_ref::<crate::postgres_storage::PostgresStorage>()
{
let mut conditions = Vec::new();
let mut params: Vec<String> = Vec::new();
if let Some(target_type) = &filter.target_type {
if !["ip", "user", "mac"].contains(&target_type.to_lowercase().as_str()) {
return Err(FlowGuardError::ConfigError("无效的目标类型".to_string()));
}
conditions.push("target_type = $1".to_string());
params.push(target_type.to_lowercase());
}
if let Some(target_value) = &filter.target_value {
if target_value.len() > 255 {
return Err(FlowGuardError::ConfigError(
"目标值长度超过限制".to_string(),
));
}
let param_index = conditions.len() + 1;
conditions.push(format!("target_value LIKE ${}", param_index));
let escaped_value = target_value.replace('%', "\\%").replace('_', "\\_");
params.push(format!("%{}%", escaped_value));
}
if filter.active_only {
conditions.push("expires_at > now() AND unbanned_at IS NULL".to_string());
}
if filter.manual_only {
conditions.push("is_manual = true".to_string());
}
let mut query = String::from(
"SELECT id, target_type, target_value, reason, ban_times, duration_secs, ",
);
query.push_str("banned_at, expires_at, is_manual, unbanned_at, unbanned_by ");
query.push_str("FROM ban_records");
if !conditions.is_empty() {
query.push_str(" WHERE ");
query.push_str(&conditions.join(" AND "));
}
query.push_str(" ORDER BY banned_at DESC LIMIT $1 OFFSET $2");
let limit = filter
.limit
.unwrap_or(DEFAULT_PAGINATION_LIMIT)
.min(MAX_PAGINATION_LIMIT);
let offset = filter.offset.unwrap_or(0);
let mut query_builder = sqlx::query_as::<
_,
(
uuid::Uuid,
String,
String,
String,
i32,
i64,
chrono::DateTime<chrono::Utc>,
chrono::DateTime<chrono::Utc>,
bool,
Option<chrono::DateTime<chrono::Utc>>,
Option<String>,
),
>(&query);
query_builder = query_builder.bind(limit as i64).bind(offset as i64);
for param in ¶ms {
query_builder = query_builder.bind(param);
}
let results = query_builder.fetch_all(storage.pool()).await.map_err(|e| {
error!("查询封禁记录失败: {}", e);
FlowGuardError::StorageError(crate::error::StorageError::QueryError(e.to_string()))
})?;
let bans: Vec<BanDetail> = results
.into_iter()
.map(
|(
id,
target_type,
target_value,
reason,
ban_times,
duration_secs,
banned_at,
expires_at,
is_manual,
unbanned_at,
unbanned_by,
)| {
let target = match target_type.as_str() {
"ip" => BanTarget::Ip(target_value),
"user" => BanTarget::UserId(target_value),
"mac" => BanTarget::Mac(target_value),
_ => BanTarget::UserId(target_value), };
let unbanned_by_clone = unbanned_by.clone();
BanDetail {
id: id.to_string(),
target,
ban_times: ban_times as u32,
duration: std::time::Duration::from_secs(duration_secs as u64),
banned_at,
expires_at,
is_manual,
reason,
source: if is_manual {
BanSource::Manual {
operator: unbanned_by_clone
.unwrap_or_else(|| "unknown".to_string()),
}
} else {
BanSource::Auto
},
metadata: serde_json::json!({}),
created_at: banned_at,
updated_at: banned_at,
unbanned_at,
unbanned_by,
}
},
)
.collect();
debug!("Found {} bans", bans.len());
Ok(bans)
} else {
Ok(Vec::new())
}
}
#[instrument(skip(self, targets))]
pub async fn check_ban_priority(
&self,
targets: &[BanTarget],
) -> Result<Option<BanDetail>, FlowGuardError> {
debug!(
"Checking ban priority for {} targets (parallel with early exit)",
targets.len()
);
if targets.is_empty() {
return Ok(None);
}
if let Some(ip_target) = targets.iter().find(|t| matches!(t, BanTarget::Ip(_))) {
debug!("Checking IP ban first for early exit");
let storage = self.storage.clone();
if let Some(record) = storage.is_banned(ip_target).await? {
debug!("Found IP ban (highest priority): target={:?}", ip_target);
return Ok(Some(BanDetail::from(record)));
}
}
let storage = self.storage.clone();
let check_futures: Vec<_> = targets
.iter()
.filter(|t| !matches!(t, BanTarget::Ip(_))) .map(|target| {
let target = target.clone();
let storage = storage.clone();
Box::pin(async move {
let record = storage.is_banned(&target).await.ok()?;
record.map(|r| (BanPriority::from_target(&target), BanDetail::from(r)))
})
})
.collect();
if check_futures.is_empty() {
return Ok(None);
}
#[cfg(feature = "parallel-checker")]
match futures::future::select_all(check_futures).await {
(Some((priority, detail)), _, _) => {
debug!(
"Found ban with priority {:?}: target={:?}",
priority, detail.target
);
Ok(Some(detail))
}
_ => Ok(None),
}
#[cfg(not(feature = "parallel-checker"))]
{
for future in check_futures {
if let Some((priority, detail)) = future.await {
debug!(
"Found ban with priority {:?}: target={:?}",
priority, detail.target
);
return Ok(Some(detail));
}
}
Ok(None)
}
}
pub async fn get_config(&self) -> BanManagerConfig {
self.config.read().await.clone()
}
pub async fn update_config(&self, new_config: BanManagerConfig) -> Result<(), FlowGuardError> {
info!("Updating BanManager configuration");
*self.config.write().await = new_config;
self.stop_auto_unban_task().await;
self.start_auto_unban_task().await;
info!("BanManager configuration updated successfully");
Ok(())
}
pub async fn add_ban(&self, record: BanRecord) -> Result<(), FlowGuardError> {
let detail = self
.create_ban(
record.target.clone(),
record.reason.clone(),
if record.is_manual {
BanSource::Manual {
operator: "system".to_string(),
}
} else {
BanSource::Auto
},
serde_json::json!({}),
Some(record.duration),
)
.await?;
info!("Ban added: {:?}", detail);
Ok(())
}
pub async fn get_ban(&self, target: &BanTarget) -> Result<Option<BanRecord>, FlowGuardError> {
let detail = self.read_ban(target).await?;
if let Some(detail) = detail {
Ok(Some(BanRecord {
target: detail.target,
ban_times: detail.ban_times,
duration: detail.duration,
banned_at: detail.banned_at,
expires_at: detail.expires_at,
is_manual: detail.is_manual,
reason: detail.reason,
}))
} else {
Ok(None)
}
}
pub async fn is_banned(&self, target: &BanTarget) -> Result<Option<BanRecord>, FlowGuardError> {
self.get_ban(target).await
}
pub async fn get_history(
&self,
target: &BanTarget,
) -> Result<Option<crate::storage::BanHistory>, FlowGuardError> {
self.storage
.get_history(target)
.await
.map_err(FlowGuardError::StorageError)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::MockBanStorage;
#[allow(dead_code)]
fn create_test_ban_manager() -> BanManager {
tokio::runtime::Runtime::new().unwrap().block_on(async {
let storage = Arc::new(MockBanStorage);
BanManager::new(storage, None).await.unwrap()
})
}
#[test]
fn test_ban_priority_ordering() {
assert!(BanPriority::Ip < BanPriority::UserId);
assert!(BanPriority::UserId < BanPriority::Mac);
assert!(BanPriority::Mac < BanPriority::DeviceId);
assert!(BanPriority::DeviceId < BanPriority::ApiKey);
}
#[test]
fn test_ban_priority_from_target() {
let ip_target = BanTarget::Ip("192.168.1.1".to_string());
assert_eq!(BanPriority::from_target(&ip_target), BanPriority::Ip);
let user_target = BanTarget::UserId("user123".to_string());
assert_eq!(BanPriority::from_target(&user_target), BanPriority::UserId);
let mac_target = BanTarget::Mac("00:11:22:33:44:55".to_string());
assert_eq!(BanPriority::from_target(&mac_target), BanPriority::Mac);
}
#[test]
fn test_backoff_config_default() {
let config = BackoffConfig::default();
assert_eq!(config.first_duration, 60);
assert_eq!(config.second_duration, 300);
assert_eq!(config.third_duration, 1800);
assert_eq!(config.fourth_duration, 7200);
assert_eq!(config.max_duration, 86400);
}
#[test]
fn test_ban_manager_config_default() {
let config = BanManagerConfig::default();
assert!(config.enable_auto_unban);
assert_eq!(config.auto_unban_interval, 60);
}
#[tokio::test]
async fn test_calculate_ban_duration() {
let storage = Arc::new(MockBanStorage);
let ban_manager = BanManager::new(storage, None).await.unwrap();
let duration = ban_manager.calculate_ban_duration(1).await;
assert_eq!(duration, StdDuration::from_secs(60));
let duration = ban_manager.calculate_ban_duration(2).await;
assert_eq!(duration, StdDuration::from_secs(300));
let duration = ban_manager.calculate_ban_duration(3).await;
assert_eq!(duration, StdDuration::from_secs(1800));
let duration = ban_manager.calculate_ban_duration(4).await;
assert_eq!(duration, StdDuration::from_secs(7200));
let duration = ban_manager.calculate_ban_duration(5).await;
assert_eq!(duration, StdDuration::from_secs(7200));
}
#[tokio::test]
async fn test_create_ban_auto() {
let storage = Arc::new(MockBanStorage);
let ban_manager = BanManager::new(storage, None).await.unwrap();
let target = BanTarget::UserId("user123".to_string());
let reason = "Excessive requests".to_string();
let source = BanSource::Auto;
let metadata = serde_json::json!({"requests": 1000});
let result = ban_manager
.create_ban(target.clone(), reason.clone(), source, metadata, None)
.await;
assert!(result.is_ok());
let detail = result.unwrap();
assert_eq!(detail.target, target);
assert_eq!(detail.reason, reason);
assert!(!detail.is_manual);
assert_eq!(detail.ban_times, 1);
}
#[tokio::test]
async fn test_create_ban_manual() {
let storage = Arc::new(MockBanStorage);
let ban_manager = BanManager::new(storage, None).await.unwrap();
let target = BanTarget::Ip("192.168.1.1".to_string());
let reason = "Manual ban".to_string();
let source = BanSource::Manual {
operator: "admin".to_string(),
};
let metadata = serde_json::json!({});
let duration = StdDuration::from_secs(3600);
let result = ban_manager
.create_ban(
target.clone(),
reason.clone(),
source,
metadata,
Some(duration),
)
.await;
assert!(result.is_ok());
let detail = result.unwrap();
assert_eq!(detail.target, target);
assert_eq!(detail.reason, reason);
assert!(detail.is_manual);
assert_eq!(detail.duration, duration);
}
#[tokio::test]
async fn test_read_ban_not_found() {
let storage = Arc::new(MockBanStorage);
let ban_manager = BanManager::new(storage, None).await.unwrap();
let target = BanTarget::UserId("nonexistent".to_string());
let result = ban_manager.read_ban(&target).await;
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
#[tokio::test]
async fn test_update_ban_not_found() {
let storage = Arc::new(MockBanStorage);
let ban_manager = BanManager::new(storage, None).await.unwrap();
let target = BanTarget::UserId("nonexistent".to_string());
let result = ban_manager
.update_ban(&target, Some("New reason".to_string()), None, None)
.await;
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
#[tokio::test]
async fn test_delete_ban_not_found() {
let storage = Arc::new(MockBanStorage);
let ban_manager = BanManager::new(storage, None).await.unwrap();
let target = BanTarget::UserId("nonexistent".to_string());
let result = ban_manager.delete_ban(&target, "admin".to_string()).await;
assert!(result.is_ok());
assert!(!result.unwrap());
}
#[tokio::test]
async fn test_list_bans_empty() {
let storage = Arc::new(MockBanStorage);
let ban_manager = BanManager::new(storage, None).await.unwrap();
let filter = BanFilter::default();
let result = ban_manager.list_bans(filter).await;
assert!(result.is_ok());
assert!(result.unwrap().is_empty());
}
#[tokio::test]
async fn test_check_ban_priority_empty() {
let storage = Arc::new(MockBanStorage);
let ban_manager = BanManager::new(storage, None).await.unwrap();
let targets = vec![
BanTarget::Ip("192.168.1.1".to_string()),
BanTarget::UserId("user123".to_string()),
];
let result = ban_manager.check_ban_priority(&targets).await;
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
#[tokio::test]
async fn test_get_config() {
let storage = Arc::new(MockBanStorage);
let ban_manager = BanManager::new(storage, None).await.unwrap();
let config = ban_manager.get_config().await;
assert!(config.enable_auto_unban);
assert_eq!(config.auto_unban_interval, 60);
}
#[tokio::test]
async fn test_update_config() {
let storage = Arc::new(MockBanStorage);
let ban_manager = BanManager::new(storage, None).await.unwrap();
let new_config = BanManagerConfig {
backoff: BackoffConfig::default(),
enable_auto_unban: false,
auto_unban_interval: 120,
};
let result = ban_manager.update_config(new_config.clone()).await;
assert!(result.is_ok());
let updated_config = ban_manager.get_config().await;
assert!(!updated_config.enable_auto_unban);
assert_eq!(updated_config.auto_unban_interval, 120);
}
#[tokio::test]
async fn test_stop_auto_unban_task() {
let storage = Arc::new(MockBanStorage);
let ban_manager = BanManager::new(storage, None).await.unwrap();
ban_manager.stop_auto_unban_task().await;
}
#[tokio::test]
async fn test_ban_filter_default() {
let filter = BanFilter::default();
assert!(filter.target_type.is_none());
assert!(filter.target_value.is_none());
assert!(!filter.active_only);
assert!(!filter.manual_only);
assert!(filter.start_time.is_none());
assert!(filter.end_time.is_none());
assert!(filter.offset.is_none());
assert!(filter.limit.is_none());
}
#[test]
fn test_ban_source_equality() {
let source1 = BanSource::Auto;
let source2 = BanSource::Auto;
assert_eq!(source1, source2);
let source3 = BanSource::Manual {
operator: "admin".to_string(),
};
let source4 = BanSource::Manual {
operator: "admin".to_string(),
};
assert_eq!(source3, source4);
}
}