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;
use crate::authorization::AuthorizationProvider;
use crate::constants::MAX_BAN_REASON_LENGTH;
use crate::error::LimiteronError;
use crate::storage::BanTarget;
use crate::storage::{BanRecord, BanStorage};
use chrono::{DateTime, Utc};
use log::{debug, error, info};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration as StdDuration;
use tokio::sync::RwLock;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum BanSource {
Auto,
Manual { operator: String },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum BanPriority {
Ip = 1,
UserId = 2,
Mac = 3,
DeviceId = 4,
ApiKey = 5,
Geo = 6,
}
impl BanPriority {
pub fn from_target(target: &BanTarget) -> Self {
match target {
BanTarget::Ip(_) => BanPriority::Ip,
BanTarget::UserId(_) => BanPriority::UserId,
BanTarget::Mac(_) => BanPriority::Mac,
BanTarget::Geo { .. } => BanPriority::Geo,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
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)]
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)]
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)]
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)]
pub struct BanManager {
storage: Arc<dyn BanStorage>,
config: Arc<RwLock<BanManagerConfig>>,
auto_unban_handle: Arc<RwLock<Option<tokio::task::JoinHandle<()>>>>,
authorization_provider: Option<Arc<dyn AuthorizationProvider>>,
#[cfg(feature = "event-system")]
event_emitter: Option<Arc<crate::events::EventEmitter>>,
file_loader: Option<Arc<crate::ban::BanFileLoader>>,
}
#[derive(Default)]
pub struct BanManagerBuilder {
storage: Option<Arc<dyn BanStorage>>,
config: Option<BanManagerConfig>,
authorization_provider: Option<Arc<dyn AuthorizationProvider>>,
#[cfg(feature = "event-system")]
event_emitter: Option<Arc<crate::events::EventEmitter>>,
file_loader_path: Option<PathBuf>,
}
impl BanManagerBuilder {
pub fn new() -> Self {
Self {
storage: None,
config: None,
authorization_provider: None,
#[cfg(feature = "event-system")]
event_emitter: None,
file_loader_path: None,
}
}
pub fn with_storage(mut self, storage: Arc<dyn BanStorage>) -> Self {
self.storage = Some(storage);
self
}
pub fn with_config(mut self, config: BanManagerConfig) -> Self {
self.config = Some(config);
self
}
pub fn with_authorization_provider(mut self, provider: Arc<dyn AuthorizationProvider>) -> Self {
self.authorization_provider = Some(provider);
self
}
#[cfg(feature = "event-system")]
pub fn with_event_emitter(mut self, emitter: Arc<crate::events::EventEmitter>) -> Self {
self.event_emitter = Some(emitter);
self
}
pub fn with_file_loader(mut self, path: impl Into<PathBuf>) -> Self {
self.file_loader_path = Some(path.into());
self
}
pub async fn build(self) -> Result<BanManager, LimiteronError> {
use crate::storage::MemoryBanStorage;
let storage = match self.storage {
Some(s) => s,
None => Arc::new(MemoryBanStorage::new()),
};
let config = self.config.unwrap_or_default();
let mut manager = BanManager::with_dependencies_and_auth(
storage,
config,
self.authorization_provider,
#[cfg(feature = "event-system")]
self.event_emitter,
)
.await?;
if let Some(path) = self.file_loader_path {
let loader = Arc::new(crate::ban::BanFileLoader::new(path));
let result = loader.load_once(&manager).await?;
if result.failure_count > 0 {
log::warn!(
"文件封禁加载存在失败: 成功 {} 条, 失败 {} 条",
result.success_count,
result.failure_count
);
} else {
log::info!("文件封禁加载完成: {} 条", result.success_count);
}
#[cfg(feature = "config-watcher")]
{
loader.start_watching(manager.clone()).await?;
}
manager.file_loader = Some(loader);
}
Ok(manager)
}
}
fn validate_ban_target(target: &BanTarget) -> Result<(), LimiteronError> {
match target {
BanTarget::Ip(ip) => crate::validation::validate_ip_address(ip),
BanTarget::UserId(user_id) => crate::validation::validate_user_id(user_id),
BanTarget::Mac(mac) => crate::validation::validate_mac_address(mac),
BanTarget::Geo { country_code } => {
crate::validation::validate_geo_country_code(country_code)
}
}
}
fn validate_ban_reason(reason: &str) -> Result<(), LimiteronError> {
if reason.is_empty() {
return Err(LimiteronError::ValidationError(
"封禁原因不能为空".to_string(),
));
}
if reason.len() > MAX_BAN_REASON_LENGTH {
return Err(LimiteronError::ValidationError(format!(
"封禁原因过长,最大长度为 {} 字符",
MAX_BAN_REASON_LENGTH
)));
}
if reason.contains(|c: char| c.is_control()) {
return Err(LimiteronError::ValidationError(
"封禁原因包含非法字符".to_string(),
));
}
Ok(())
}
impl BanManager {
pub async fn new() -> Result<Self, LimiteronError> {
use crate::storage::MemoryBanStorage;
let storage: Arc<dyn BanStorage> = Arc::new(MemoryBanStorage::new());
let config = BanManagerConfig::default();
Self::with_dependencies(storage, config).await
}
pub fn builder() -> BanManagerBuilder {
BanManagerBuilder::new()
}
pub async fn with_dependencies(
storage: Arc<dyn BanStorage>,
config: BanManagerConfig,
) -> Result<Self, LimiteronError> {
Self::with_dependencies_and_auth(
storage,
config,
None,
#[cfg(feature = "event-system")]
None,
)
.await
}
pub async fn with_dependencies_and_auth(
storage: Arc<dyn BanStorage>,
config: BanManagerConfig,
authorization_provider: Option<Arc<dyn AuthorizationProvider>>,
#[cfg(feature = "event-system")] event_emitter: Option<Arc<crate::events::EventEmitter>>,
) -> Result<Self, LimiteronError> {
if authorization_provider.is_none() {
log::warn!(
"BanManager 创建时未配置 authorization_provider,\
所有手动封禁操作将跳过细粒度授权检查(仅依赖 admin API key 认证)"
);
}
let config = Arc::new(RwLock::new(config));
let ban_manager = Self {
storage,
config,
auto_unban_handle: Arc::new(RwLock::new(None)),
authorization_provider,
#[cfg(feature = "event-system")]
event_emitter,
file_loader: 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 mut handle_write = self.auto_unban_handle.write().await;
if handle_write.is_some() {
return; }
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");
if let Err(e) = storage.cleanup_expired_bans().await {
error!("Auto-unban task failed: {}", e);
}
}
});
*handle_write = 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");
}
}
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)
}
pub async fn create_ban(
&self,
target: BanTarget,
reason: String,
source: BanSource,
metadata: serde_json::Value,
duration: Option<StdDuration>,
) -> Result<BanDetail, LimiteronError> {
match (&self.authorization_provider, &source) {
(Some(provider), BanSource::Manual { operator }) => {
self.check_authorization(provider, "create_ban", operator, &target)
.await?;
}
(None, BanSource::Manual { operator }) => {
log::warn!(
"手动封禁跳过授权检查(未配置 authorization_provider): \
operator={}, target={:?}",
operator,
target
);
}
_ => {}
}
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 => self.calculate_ban_duration(ban_times).await,
};
let now = Utc::now();
let expires_at = now
+ chrono::Duration::from_std(duration)
.map_err(|e| LimiteronError::TimeError(format!("Invalid duration: {}", e)))?;
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
);
#[cfg(feature = "event-system")]
{
if let Some(ref emitter) = self.event_emitter {
let target_str = match detail.target {
BanTarget::Ip(ref ip) => ip.clone(),
BanTarget::UserId(ref uid) => uid.clone(),
BanTarget::Mac(ref mac) => mac.clone(),
BanTarget::Geo { ref country_code } => country_code.clone(),
};
let event = crate::events::Event::new(crate::events::EventType::BanApplied {
target: target_str,
reason: detail.reason.clone(),
duration: detail.duration.as_secs(),
});
if let Err(e) = emitter.emit(event).await {
error!("Failed to emit ban event: {}", e);
}
}
}
Ok(detail)
}
pub async fn read_ban(&self, target: &BanTarget) -> Result<Option<BanDetail>, LimiteronError> {
debug!("Reading ban: target={:?}", target);
let record = self.storage.is_banned(target).await?;
Ok(record.map(BanDetail::from))
}
pub async fn update_ban(
&self,
target: &BanTarget,
reason: Option<String>,
duration: Option<StdDuration>,
metadata: Option<serde_json::Value>,
) -> Result<Option<BanDetail>, LimiteronError> {
debug!("Updating ban: target={:?}", target);
let current_record = self.storage.is_banned(target).await?;
let current_record = match current_record {
Some(record) => record,
None => return Ok(None),
};
let mut record = current_record;
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
+ chrono::Duration::from_std(new_duration)
.map_err(|e| LimiteronError::TimeError(format!("Invalid duration: {}", e)))?;
}
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))
}
pub async fn delete_ban(
&self,
target: &BanTarget,
unbanned_by: String,
) -> Result<bool, LimiteronError> {
if let Some(provider) = &self.authorization_provider {
self.check_authorization(provider, "remove_ban", &unbanned_by, target)
.await?;
}
info!(
"Deleting ban: target={}, unbanned_by={}",
crate::logging::redact_ban_target(target),
unbanned_by
);
let record = self.storage.is_banned(target).await?;
if record.is_none() {
debug!(
"No active ban found for target: {}",
crate::logging::redact_ban_target(target)
);
return Ok(false);
}
self.storage.remove_ban(target).await?;
info!("Ban deleted successfully: target={:?}", target);
Ok(true)
}
pub async fn list_bans(&self, filter: BanFilter) -> Result<Vec<BanDetail>, LimiteronError> {
debug!("Listing bans with filter: {:?}", filter);
let offset = filter.offset.unwrap_or(0).min(MAX_PAGINATION_LIMIT);
let limit = filter
.limit
.unwrap_or(DEFAULT_PAGINATION_LIMIT)
.min(MAX_PAGINATION_LIMIT);
let active_only =
filter.active_only || filter.start_time.is_some() || filter.end_time.is_some();
let records = self.storage.list_bans(active_only, offset, limit).await?;
let filtered: Vec<_> = records
.into_iter()
.filter(|record| {
if let Some(ref target_type) = filter.target_type {
let target_lower = target_type.to_lowercase();
let matches = match target_lower.as_str() {
"ip" => matches!(&record.target, BanTarget::Ip(_)),
"user" => matches!(&record.target, BanTarget::UserId(_)),
"mac" => matches!(&record.target, BanTarget::Mac(_)),
_ => false,
};
if !matches {
return false;
}
}
if let Some(ref target_value) = filter.target_value {
let value_matches = match &record.target {
BanTarget::Ip(ip) => ip.contains(target_value),
BanTarget::UserId(uid) => uid.contains(target_value),
BanTarget::Mac(mac) => mac.contains(target_value),
BanTarget::Geo { country_code } => country_code.contains(target_value),
};
if !value_matches {
return false;
}
}
if filter.manual_only && !record.is_manual {
return false;
}
if let Some(start) = filter.start_time {
if record.banned_at < start {
return false;
}
}
if let Some(end) = filter.end_time {
if record.banned_at > end {
return false;
}
}
true
})
.collect();
let bans = filtered.into_iter().map(BanDetail::from).collect();
Ok(bans)
}
pub async fn check_ban_priority(
&self,
targets: &[BanTarget],
) -> Result<Option<BanDetail>, LimiteronError> {
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");
if let Some(record) = self.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)), _, _) => {
self.log_ban_found(priority, &detail);
Ok(Some(detail))
}
_ => Ok(None),
}
#[cfg(not(feature = "parallel-checker"))]
{
for future in check_futures {
if let Some((priority, detail)) = future.await {
self.log_ban_found(priority, &detail);
return Ok(Some(detail));
}
}
Ok(None)
}
}
fn log_ban_found(&self, priority: BanPriority, detail: &BanDetail) {
debug!(
"Found ban with priority {:?}: target={:?}",
priority, detail.target
);
}
pub async fn get_config(&self) -> BanManagerConfig {
self.config.read().await.clone()
}
pub async fn update_config(&self, new_config: BanManagerConfig) -> Result<(), LimiteronError> {
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<(), LimiteronError> {
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>, LimiteronError> {
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>, LimiteronError> {
self.get_ban(target).await
}
pub async fn get_history(
&self,
target: &BanTarget,
) -> Result<Option<crate::BanHistory>, LimiteronError> {
self.storage
.get_history(target)
.await
.map_err(LimiteronError::StorageError)
}
async fn check_authorization(
&self,
provider: &Arc<dyn AuthorizationProvider>,
action: &str,
operator: &str,
target: &BanTarget,
) -> Result<(), LimiteronError> {
let target_str = match target {
BanTarget::Ip(ip) => ip.clone(),
BanTarget::UserId(user_id) => user_id.clone(),
BanTarget::Mac(mac) => mac.clone(),
BanTarget::Geo { country_code } => country_code.clone(),
};
provider
.check_authorization(action, operator, &target_str)
.await?;
debug!(
"Authorization passed for {}: operator={}, target={}",
action, operator, target_str
);
Ok(())
}
}
impl Drop for BanManager {
fn drop(&mut self) {
if let Ok(mut handle_guard) = self.auto_unban_handle.try_write() {
if let Some(handle) = handle_guard.take() {
handle.abort();
debug!("BanManager auto-unban task aborted on drop");
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::{BanHistory, BanStorage};
use ahash::AHashMap as StdHashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
#[test]
fn test_ban_priority_geo_value() {
assert_eq!(BanPriority::Geo as u8, 6);
}
#[test]
fn test_ban_priority_geo_is_lowest() {
assert!(BanPriority::Geo > BanPriority::ApiKey);
assert!(BanPriority::Geo > BanPriority::Ip);
assert!(BanPriority::Geo > BanPriority::Mac);
assert!(BanPriority::Geo > BanPriority::UserId);
assert!(BanPriority::Geo > BanPriority::DeviceId);
assert_eq!(BanPriority::Geo, BanPriority::Geo);
}
#[test]
fn test_ban_priority_from_geo_target() {
let target = BanTarget::Geo {
country_code: "CN".to_string(),
};
assert_eq!(BanPriority::from_target(&target), BanPriority::Geo);
}
#[test]
fn test_ban_priority_ordering_complete() {
let priorities = [
BanPriority::Ip,
BanPriority::UserId,
BanPriority::Mac,
BanPriority::DeviceId,
BanPriority::ApiKey,
BanPriority::Geo,
];
for i in 0..priorities.len() - 1 {
assert!(
priorities[i] < priorities[i + 1],
"ordering failed at index {}",
i
);
}
}
#[derive(Clone, Default)]
struct MockBanBehavior {
fail_mode: bool,
force_expired: bool,
max_entries: Option<usize>,
}
struct MockBanStorage {
bans: Arc<RwLock<StdHashMap<BanTarget, BanRecord>>>,
history: Arc<RwLock<StdHashMap<BanTarget, BanHistory>>>,
behavior: Arc<RwLock<MockBanBehavior>>,
}
impl MockBanStorage {
fn new() -> Self {
Self::with_behavior(MockBanBehavior::default())
}
fn with_behavior(behavior: MockBanBehavior) -> Self {
Self {
bans: Arc::new(RwLock::new(StdHashMap::new())),
history: Arc::new(RwLock::new(StdHashMap::new())),
behavior: Arc::new(RwLock::new(behavior)),
}
}
#[allow(dead_code)]
async fn set_behavior(&self, behavior: MockBanBehavior) {
let mut current = self.behavior.write().await;
*current = behavior;
}
#[allow(dead_code)]
async fn clear(&self) {
let mut bans = self.bans.write().await;
let mut history = self.history.write().await;
bans.clear();
history.clear();
}
async fn should_fail(&self) -> bool {
self.behavior.read().await.fail_mode
}
async fn is_force_expired(&self) -> bool {
self.behavior.read().await.force_expired
}
async fn can_insert(&self, current_len: usize) -> Result<(), crate::error::StorageError> {
let behavior = self.behavior.read().await;
if let Some(max_entries) = behavior.max_entries {
if current_len >= max_entries {
return Err(crate::error::StorageError::QueryError(
"超过最大封禁条目限制".to_string(),
));
}
}
Ok(())
}
}
#[async_trait::async_trait]
impl BanStorage for MockBanStorage {
async fn is_banned(
&self,
target: &BanTarget,
) -> Result<Option<BanRecord>, crate::error::StorageError> {
if self.should_fail().await {
return Err(crate::error::StorageError::QueryError(
"MockBanStorage is_banned失败".to_string(),
));
}
if self.is_force_expired().await {
return Ok(None);
}
let mut bans = self.bans.write().await;
let now = chrono::Utc::now();
if let Some(record) = bans.get(target) {
if record.expires_at > now {
return Ok(Some(record.clone()));
}
}
bans.remove(target);
Ok(None)
}
async fn save(&self, record: &BanRecord) -> Result<(), crate::error::StorageError> {
if self.should_fail().await {
return Err(crate::error::StorageError::QueryError(
"MockBanStorage save失败".to_string(),
));
}
let mut bans = self.bans.write().await;
self.can_insert(bans.len()).await?;
bans.insert(record.target.clone(), record.clone());
let mut history = self.history.write().await;
let hist = BanHistory {
ban_times: record.ban_times,
last_banned_at: record.banned_at,
};
history.insert(record.target.clone(), hist);
Ok(())
}
async fn get_history(
&self,
target: &BanTarget,
) -> Result<Option<BanHistory>, crate::error::StorageError> {
if self.should_fail().await {
return Err(crate::error::StorageError::QueryError(
"MockBanStorage get_history失败".to_string(),
));
}
let history = self.history.read().await;
Ok(history.get(target).cloned())
}
async fn increment_ban_times(
&self,
target: &BanTarget,
) -> Result<u64, crate::error::StorageError> {
if self.should_fail().await {
return Err(crate::error::StorageError::QueryError(
"MockBanStorage increment_ban_times失败".to_string(),
));
}
let mut bans = self.bans.write().await;
if let Some(record) = bans.get_mut(target) {
record.ban_times += 1;
Ok(record.ban_times as u64)
} else {
Ok(1)
}
}
async fn get_ban_times(
&self,
target: &BanTarget,
) -> Result<u64, crate::error::StorageError> {
if self.should_fail().await {
return Err(crate::error::StorageError::QueryError(
"MockBanStorage get_ban_times失败".to_string(),
));
}
let bans = self.bans.read().await;
if let Some(record) = bans.get(target) {
Ok(record.ban_times as u64)
} else {
Ok(0)
}
}
async fn remove_ban(&self, target: &BanTarget) -> Result<(), crate::error::StorageError> {
if self.should_fail().await {
return Err(crate::error::StorageError::QueryError(
"MockBanStorage remove_ban失败".to_string(),
));
}
let mut bans = self.bans.write().await;
bans.remove(target);
Ok(())
}
async fn cleanup_expired_bans(&self) -> Result<u64, crate::error::StorageError> {
if self.should_fail().await {
return Err(crate::error::StorageError::QueryError(
"MockBanStorage cleanup_expired_bans失败".to_string(),
));
}
let mut bans = self.bans.write().await;
let now = chrono::Utc::now();
let mut count = 0;
bans.retain(|_, record| {
if record.expires_at <= now {
count += 1;
false
} else {
true
}
});
Ok(count)
}
async fn list_bans(
&self,
active_only: bool,
offset: u64,
limit: u64,
) -> Result<Vec<BanRecord>, crate::error::StorageError> {
if self.should_fail().await {
return Err(crate::error::StorageError::QueryError(
"MockBanStorage list_bans失败".to_string(),
));
}
let bans = self.bans.read().await;
let now = chrono::Utc::now();
#[allow(clippy::map_clone)]
let mut records: Vec<_> = bans.values().map(|r| r.clone()).collect();
if active_only {
records.retain(|r| r.expires_at > now);
}
let total = records.len() as u64;
let start = offset as usize;
let end = (offset.saturating_add(limit)) as usize;
if start >= total as usize {
return Ok(vec![]);
}
Ok(records.into_iter().skip(start).take(end - start).collect())
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
async fn create_test_ban_manager() -> BanManager {
let storage = Arc::new(MockBanStorage::new());
BanManager::with_dependencies(storage, BanManagerConfig::default())
.await
.unwrap()
}
#[allow(dead_code)]
async fn create_test_ban_manager_with_config(config: BanManagerConfig) -> BanManager {
let storage = Arc::new(MockBanStorage::new());
BanManager::with_dependencies(storage, config)
.await
.unwrap()
}
fn create_expired_ban_record(target: BanTarget) -> BanRecord {
let now = chrono::Utc::now();
BanRecord {
target,
ban_times: 1,
duration: StdDuration::from_secs(0),
banned_at: now - chrono::Duration::seconds(10),
expires_at: now - chrono::Duration::seconds(5), is_manual: false,
reason: "expired ban".to_string(),
}
}
#[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 ban_manager = create_test_ban_manager().await;
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 ban_manager = create_test_ban_manager().await;
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 ban_manager = create_test_ban_manager().await;
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 ban_manager = create_test_ban_manager().await;
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 ban_manager = create_test_ban_manager().await;
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 ban_manager = create_test_ban_manager().await;
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 ban_manager = create_test_ban_manager().await;
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 ban_manager = create_test_ban_manager().await;
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 ban_manager = create_test_ban_manager().await;
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 ban_manager = create_test_ban_manager().await;
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 ban_manager = create_test_ban_manager().await;
ban_manager.stop_auto_unban_task().await;
}
#[tokio::test]
async fn test_auto_unban_task_executes_and_handles_cleanup_error() {
let storage = Arc::new(MockBanStorage::with_behavior(MockBanBehavior {
fail_mode: true,
..Default::default()
}));
let config = BanManagerConfig {
enable_auto_unban: true,
auto_unban_interval: 1,
..Default::default()
};
let ban_manager = BanManager::with_dependencies(storage, config)
.await
.expect("BanManager creation should succeed");
tokio::time::sleep(StdDuration::from_millis(200)).await;
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);
}
#[tokio::test]
async fn test_ip_ban_add_check_remove() {
let ban_manager = create_test_ban_manager().await;
let ip = "192.168.1.100";
let target = BanTarget::Ip(ip.to_string());
let detail = ban_manager
.create_ban(
target.clone(),
"IP封禁测试".to_string(),
BanSource::Auto,
serde_json::json!({"ip": ip}),
Some(StdDuration::from_secs(3600)),
)
.await
.unwrap();
assert_eq!(detail.target, target);
assert!(!detail.is_manual);
assert_eq!(detail.ban_times, 1);
let banned = ban_manager.read_ban(&target).await.unwrap();
assert!(banned.is_some());
let ban_record = banned.unwrap();
assert_eq!(ban_record.target, target);
assert_eq!(ban_record.reason, "IP封禁测试");
let removed = ban_manager
.delete_ban(&target, "admin".to_string())
.await
.unwrap();
assert!(removed);
let banned = ban_manager.read_ban(&target).await.unwrap();
assert!(banned.is_none());
}
#[tokio::test]
async fn test_user_id_ban_add_check_remove() {
let ban_manager = create_test_ban_manager().await;
let user_id = "user_test_123";
let target = BanTarget::UserId(user_id.to_string());
let detail = ban_manager
.create_ban(
target.clone(),
"用户违规".to_string(),
BanSource::Manual {
operator: "admin".to_string(),
},
serde_json::json!({"user_id": user_id}),
Some(StdDuration::from_secs(7200)),
)
.await
.unwrap();
assert_eq!(detail.target, target);
assert!(detail.is_manual);
assert_eq!(detail.ban_times, 1);
let banned = ban_manager.read_ban(&target).await.unwrap();
assert!(banned.is_some());
let ban_record = banned.unwrap();
assert_eq!(ban_record.target, target);
assert!(ban_record.is_manual);
let removed = ban_manager
.delete_ban(&target, "admin".to_string())
.await
.unwrap();
assert!(removed);
let banned = ban_manager.read_ban(&target).await.unwrap();
assert!(banned.is_none());
}
#[tokio::test]
async fn test_mac_address_ban_add_check_remove() {
let ban_manager = create_test_ban_manager().await;
let mac = "AA:BB:CC:DD:EE:FF";
let target = BanTarget::Mac(mac.to_string());
let detail = ban_manager
.create_ban(
target.clone(),
"MAC地址封禁".to_string(),
BanSource::Auto,
serde_json::json!({"mac": mac}),
Some(StdDuration::from_secs(1800)),
)
.await
.unwrap();
assert_eq!(detail.target, target);
assert!(!detail.is_manual);
let banned = ban_manager.read_ban(&target).await.unwrap();
assert!(banned.is_some());
let ban_record = banned.unwrap();
assert_eq!(ban_record.target, target);
let removed = ban_manager
.delete_ban(&target, "admin".to_string())
.await
.unwrap();
assert!(removed);
let banned = ban_manager.read_ban(&target).await.unwrap();
assert!(banned.is_none());
}
#[tokio::test]
async fn test_ban_expiry_auto_unban() {
let storage = Arc::new(MockBanStorage::new());
let config = BanManagerConfig {
enable_auto_unban: false, ..Default::default()
};
let ban_manager = BanManager::with_dependencies(storage.clone(), config)
.await
.unwrap();
let target = BanTarget::Ip("10.0.0.1".to_string());
let expired_record = create_expired_ban_record(target.clone());
storage.save(&expired_record).await.unwrap();
let banned = ban_manager.read_ban(&target).await.unwrap();
assert!(banned.is_none(), "过期的封禁记录应该自动解除");
}
#[tokio::test]
async fn test_ban_history_tracking() {
let ban_manager = create_test_ban_manager().await;
let target = BanTarget::UserId("history_user".to_string());
let detail1 = ban_manager
.create_ban(
target.clone(),
"第一次违规".to_string(),
BanSource::Auto,
serde_json::json!({}),
None,
)
.await
.unwrap();
assert_eq!(detail1.ban_times, 1);
ban_manager
.delete_ban(&target, "admin".to_string())
.await
.unwrap();
let detail2 = ban_manager
.create_ban(
target.clone(),
"第二次违规".to_string(),
BanSource::Auto,
serde_json::json!({}),
None,
)
.await
.unwrap();
assert_eq!(detail2.ban_times, 2);
let history = ban_manager.get_history(&target).await.unwrap();
assert!(history.is_some());
let history = history.unwrap();
assert_eq!(history.ban_times, 2);
}
#[tokio::test]
async fn test_incremental_ban_duration() {
let ban_manager = create_test_ban_manager().await;
let target = BanTarget::UserId("incremental_test_user".to_string());
let detail1 = ban_manager
.create_ban(
target.clone(),
"第一次".to_string(),
BanSource::Auto,
serde_json::json!({}),
None,
)
.await
.unwrap();
assert_eq!(detail1.duration, StdDuration::from_secs(60));
ban_manager
.delete_ban(&target, "admin".to_string())
.await
.unwrap();
let detail2 = ban_manager
.create_ban(
target.clone(),
"第二次".to_string(),
BanSource::Auto,
serde_json::json!({}),
None,
)
.await
.unwrap();
assert_eq!(detail2.duration, StdDuration::from_secs(300));
ban_manager
.delete_ban(&target, "admin".to_string())
.await
.unwrap();
let detail3 = ban_manager
.create_ban(
target.clone(),
"第三次".to_string(),
BanSource::Auto,
serde_json::json!({}),
None,
)
.await
.unwrap();
assert_eq!(detail3.duration, StdDuration::from_secs(1800));
ban_manager
.delete_ban(&target, "admin".to_string())
.await
.unwrap();
let detail4 = ban_manager
.create_ban(
target.clone(),
"第四次".to_string(),
BanSource::Auto,
serde_json::json!({}),
None,
)
.await
.unwrap();
assert_eq!(detail4.duration, StdDuration::from_secs(7200));
}
#[tokio::test]
async fn test_manual_vs_auto_ban() {
let ban_manager = create_test_ban_manager().await;
let auto_target = BanTarget::UserId("auto_ban_user".to_string());
let auto_detail = ban_manager
.create_ban(
auto_target.clone(),
"自动封禁".to_string(),
BanSource::Auto,
serde_json::json!({}),
None,
)
.await
.unwrap();
assert!(!auto_detail.is_manual);
assert_eq!(auto_detail.source, BanSource::Auto);
let manual_target = BanTarget::UserId("manual_ban_user".to_string());
let manual_detail = ban_manager
.create_ban(
manual_target.clone(),
"手动封禁".to_string(),
BanSource::Manual {
operator: "admin".to_string(),
},
serde_json::json!({}),
None,
)
.await
.unwrap();
assert!(manual_detail.is_manual);
match manual_detail.source {
BanSource::Manual { operator } => {
assert_eq!(operator, "admin");
}
BanSource::Auto => panic!("期望手动封禁"),
}
}
#[tokio::test]
async 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);
}
#[tokio::test]
async fn test_check_ban_priority_with_ip_ban() {
let storage = Arc::new(MockBanStorage::new());
let ban_manager =
BanManager::with_dependencies(storage.clone(), BanManagerConfig::default())
.await
.unwrap();
let ip_target = BanTarget::Ip("priority.ip.test".to_string());
let record = BanRecord {
target: ip_target.clone(),
ban_times: 1,
duration: StdDuration::from_secs(3600),
banned_at: chrono::Utc::now(),
expires_at: chrono::Utc::now() + chrono::Duration::hours(1),
is_manual: false,
reason: "IP封禁".to_string(),
};
storage.save(&record).await.unwrap();
let targets = vec![
BanTarget::UserId("some_user".to_string()),
BanTarget::Ip("priority.ip.test".to_string()),
BanTarget::Mac("AA:BB:CC:DD:EE:FF".to_string()),
];
let result = ban_manager.check_ban_priority(&targets).await.unwrap();
assert!(result.is_some());
let detail = result.unwrap();
assert_eq!(detail.target, ip_target);
}
#[tokio::test]
async fn test_list_bans_with_filter() {
let ban_manager = create_test_ban_manager().await;
ban_manager
.create_ban(
BanTarget::Ip("192.168.1.1".to_string()),
"IP封禁".to_string(),
BanSource::Auto,
serde_json::json!({}),
None,
)
.await
.unwrap();
ban_manager
.create_ban(
BanTarget::UserId("user1".to_string()),
"用户封禁".to_string(),
BanSource::Manual {
operator: "admin".to_string(),
},
serde_json::json!({}),
None,
)
.await
.unwrap();
let filter = BanFilter {
target_type: Some("ip".to_string()),
..Default::default()
};
let bans = ban_manager.list_bans(filter).await.unwrap();
assert_eq!(bans.len(), 1);
assert!(matches!(bans[0].target, BanTarget::Ip(_)));
let filter = BanFilter {
manual_only: true,
..Default::default()
};
let bans = ban_manager.list_bans(filter).await.unwrap();
assert_eq!(bans.len(), 1);
assert!(bans[0].is_manual);
}
#[tokio::test]
async fn test_update_ban() {
let ban_manager = create_test_ban_manager().await;
let target = BanTarget::UserId("update_test_user".to_string());
ban_manager
.create_ban(
target.clone(),
"原始原因".to_string(),
BanSource::Auto,
serde_json::json!({}),
Some(StdDuration::from_secs(3600)),
)
.await
.unwrap();
let updated = ban_manager
.update_ban(
&target,
Some("更新后的原因".to_string()),
Some(StdDuration::from_secs(7200)),
Some(serde_json::json!({"updated": true})),
)
.await
.unwrap();
assert!(updated.is_some());
let detail = updated.unwrap();
assert_eq!(detail.reason, "更新后的原因");
assert_eq!(detail.duration, StdDuration::from_secs(7200));
}
#[tokio::test]
async fn test_concurrent_ban_operations() {
let storage = Arc::new(MockBanStorage::new());
let ban_manager =
BanManager::with_dependencies(storage.clone(), BanManagerConfig::default())
.await
.unwrap();
let mut handles = vec![];
for i in 0..10 {
let bm = ban_manager.clone();
let handle = tokio::spawn(async move {
let target = BanTarget::UserId(format!("concurrent_user_{}", i));
bm.create_ban(
target,
format!("并发封禁测试 {}", i),
BanSource::Auto,
serde_json::json!({"index": i}),
None,
)
.await
});
handles.push(handle);
}
let results: Vec<_> = futures::future::join_all(handles).await;
for result in results {
assert!(result.unwrap().is_ok());
}
let filter = BanFilter::default();
let bans = ban_manager.list_bans(filter).await.unwrap();
assert_eq!(bans.len(), 10);
}
#[tokio::test]
async fn test_concurrent_unban_operations() {
let storage = Arc::new(MockBanStorage::new());
let ban_manager =
BanManager::with_dependencies(storage.clone(), BanManagerConfig::default())
.await
.unwrap();
for i in 0..5 {
let target = BanTarget::Ip(format!("192.168.1.{}", i));
ban_manager
.create_ban(
target,
"测试封禁".to_string(),
BanSource::Auto,
serde_json::json!({}),
None,
)
.await
.unwrap();
}
let mut handles = vec![];
for i in 0..5 {
let bm = ban_manager.clone();
let handle = tokio::spawn(async move {
let target = BanTarget::Ip(format!("192.168.1.{}", i));
bm.delete_ban(&target, "admin".to_string()).await
});
handles.push(handle);
}
let results: Vec<_> = futures::future::join_all(handles).await;
for result in results {
assert!(result.unwrap().is_ok());
}
let filter = BanFilter::default();
let bans = ban_manager.list_bans(filter).await.unwrap();
assert_eq!(bans.len(), 0);
}
#[tokio::test]
async fn test_concurrent_ban_state_consistency() {
let storage = Arc::new(MockBanStorage::new());
let ban_manager =
BanManager::with_dependencies(storage.clone(), BanManagerConfig::default())
.await
.unwrap();
let target = BanTarget::UserId("consistency_user".to_string());
let mut handles = vec![];
for _ in 0..5 {
let bm = ban_manager.clone();
let t = target.clone();
let handle = tokio::spawn(async move {
bm.create_ban(
t,
"一致性测试".to_string(),
BanSource::Auto,
serde_json::json!({}),
None,
)
.await
});
handles.push(handle);
}
let results: Vec<_> = futures::future::join_all(handles).await;
for result in results {
assert!(result.unwrap().is_ok());
}
let banned = ban_manager.read_ban(&target).await.unwrap();
assert!(banned.is_some());
}
#[tokio::test]
async fn test_concurrent_read_write_operations() {
let storage = Arc::new(MockBanStorage::new());
let ban_manager =
BanManager::with_dependencies(storage.clone(), BanManagerConfig::default())
.await
.unwrap();
let target = BanTarget::UserId("rw_test_user".to_string());
ban_manager
.create_ban(
target.clone(),
"读写测试".to_string(),
BanSource::Auto,
serde_json::json!({}),
None,
)
.await
.unwrap();
let mut handles = vec![];
for _ in 0..5 {
let bm = ban_manager.clone();
let t = target.clone();
let handle = tokio::spawn(async move { bm.read_ban(&t).await });
handles.push(handle);
}
for _ in 0..3 {
let bm = ban_manager.clone();
let t = target.clone();
let handle = tokio::spawn(async move {
bm.update_ban(&t, Some("更新原因".to_string()), None, None)
.await
});
handles.push(handle);
}
let results: Vec<_> = futures::future::join_all(handles).await;
for result in results {
assert!(result.unwrap().is_ok());
}
}
#[tokio::test]
async fn test_create_ban_with_authorization_success() {
use crate::authorization::SimpleAuthorizationProvider;
let storage = Arc::new(MockBanStorage::new());
let auth_provider = Arc::new(SimpleAuthorizationProvider::new(vec!["admin".to_string()]));
let ban_manager = BanManager::builder()
.with_storage(storage)
.with_authorization_provider(auth_provider)
.build()
.await
.unwrap();
let target = BanTarget::Ip("192.168.1.1".to_string());
let source = BanSource::Manual {
operator: "admin".to_string(),
};
let result = ban_manager
.create_ban(
target,
"Test ban".to_string(),
source,
serde_json::json!({}),
None,
)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_create_ban_with_authorization_failure() {
use crate::authorization::SimpleAuthorizationProvider;
let storage = Arc::new(MockBanStorage::new());
let auth_provider = Arc::new(SimpleAuthorizationProvider::new(vec!["admin".to_string()]));
let ban_manager = BanManager::builder()
.with_storage(storage)
.with_authorization_provider(auth_provider)
.build()
.await
.unwrap();
let target = BanTarget::Ip("192.168.1.1".to_string());
let source = BanSource::Manual {
operator: "unauthorized_user".to_string(),
};
let result = ban_manager
.create_ban(
target,
"Test ban".to_string(),
source,
serde_json::json!({}),
None,
)
.await;
assert!(result.is_err());
match result {
Err(LimiteronError::AuthorizationError(msg)) => {
assert!(msg.contains("unauthorized_user"));
}
_ => panic!("期望 AuthorizationError"),
}
}
#[tokio::test]
async fn test_create_ban_auto_bypasses_authorization() {
use crate::authorization::DenyAllAuthorizationProvider;
let storage = Arc::new(MockBanStorage::new());
let auth_provider = Arc::new(DenyAllAuthorizationProvider);
let ban_manager = BanManager::builder()
.with_storage(storage)
.with_authorization_provider(auth_provider)
.build()
.await
.unwrap();
let target = BanTarget::Ip("192.168.1.1".to_string());
let source = BanSource::Auto;
let result = ban_manager
.create_ban(
target,
"Auto ban".to_string(),
source,
serde_json::json!({}),
None,
)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_delete_ban_with_authorization_success() {
use crate::authorization::SimpleAuthorizationProvider;
let storage = Arc::new(MockBanStorage::new());
let auth_provider = Arc::new(SimpleAuthorizationProvider::new(vec!["admin".to_string()]));
let ban_manager = BanManager::builder()
.with_storage(storage)
.with_authorization_provider(auth_provider)
.build()
.await
.unwrap();
let target = BanTarget::Ip("192.168.1.1".to_string());
let result = ban_manager.delete_ban(&target, "admin".to_string()).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_delete_ban_with_authorization_failure() {
use crate::authorization::SimpleAuthorizationProvider;
let storage = Arc::new(MockBanStorage::new());
let auth_provider = Arc::new(SimpleAuthorizationProvider::new(vec!["admin".to_string()]));
let ban_manager = BanManager::builder()
.with_storage(storage)
.with_authorization_provider(auth_provider)
.build()
.await
.unwrap();
let target = BanTarget::Ip("192.168.1.1".to_string());
let result = ban_manager
.delete_ban(&target, "unauthorized_user".to_string())
.await;
assert!(result.is_err());
match result {
Err(LimiteronError::AuthorizationError(msg)) => {
assert!(msg.contains("unauthorized_user"));
}
_ => panic!("期望 AuthorizationError"),
}
}
#[tokio::test]
async fn test_no_authorization_provider_allows_all() {
let ban_manager = create_test_ban_manager().await;
let target = BanTarget::Ip("192.168.1.1".to_string());
let source = BanSource::Manual {
operator: "anyone".to_string(),
};
let result = ban_manager
.create_ban(
target,
"Test ban".to_string(),
source,
serde_json::json!({}),
None,
)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_ban_manager_builder_with_authorization() {
use crate::authorization::SimpleAuthorizationProvider;
let storage = Arc::new(MockBanStorage::new());
let auth_provider = Arc::new(SimpleAuthorizationProvider::new(vec![
"admin".to_string(),
"moderator".to_string(),
]));
let ban_manager = BanManager::builder()
.with_storage(storage)
.with_config(BanManagerConfig::default())
.with_authorization_provider(auth_provider)
.build()
.await
.unwrap();
let target = BanTarget::UserId("user123".to_string());
let result = ban_manager
.delete_ban(&target, "moderator".to_string())
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_validate_ban_reason_empty() {
let ban_manager = create_test_ban_manager().await;
let result = ban_manager
.create_ban(
BanTarget::Ip("192.168.1.1".to_string()),
"".to_string(), BanSource::Auto,
serde_json::json!({}),
None,
)
.await;
assert!(result.is_err());
match result {
Err(LimiteronError::ValidationError(msg)) => {
assert!(msg.contains("不能为空"));
}
_ => panic!("期望 ValidationError"),
}
}
#[tokio::test]
async fn test_validate_ban_reason_too_long() {
let ban_manager = create_test_ban_manager().await;
let long_reason = "x".repeat(MAX_BAN_REASON_LENGTH + 1);
let result = ban_manager
.create_ban(
BanTarget::Ip("192.168.1.1".to_string()),
long_reason,
BanSource::Auto,
serde_json::json!({}),
None,
)
.await;
assert!(result.is_err());
match result {
Err(LimiteronError::ValidationError(msg)) => {
assert!(msg.contains("过长"));
}
_ => panic!("期望 ValidationError"),
}
}
#[tokio::test]
async fn test_validate_ban_reason_control_chars() {
let ban_manager = create_test_ban_manager().await;
let result = ban_manager
.create_ban(
BanTarget::Ip("192.168.1.1".to_string()),
"包含控制字符\x00的封禁原因".to_string(),
BanSource::Auto,
serde_json::json!({}),
None,
)
.await;
assert!(result.is_err());
match result {
Err(LimiteronError::ValidationError(msg)) => {
assert!(msg.contains("非法字符"));
}
_ => panic!("期望 ValidationError"),
}
}
#[tokio::test]
async fn test_storage_error_handling() {
let storage = Arc::new(MockBanStorage::with_behavior(MockBanBehavior {
fail_mode: true,
..Default::default()
}));
let ban_manager = BanManager::with_dependencies(storage, BanManagerConfig::default())
.await
.unwrap();
let result = ban_manager
.create_ban(
BanTarget::Ip("192.168.1.1".to_string()),
"测试".to_string(),
BanSource::Auto,
serde_json::json!({}),
None,
)
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_max_entries_limit() {
let storage = Arc::new(MockBanStorage::with_behavior(MockBanBehavior {
max_entries: Some(2),
..Default::default()
}));
let ban_manager = BanManager::with_dependencies(storage, BanManagerConfig::default())
.await
.unwrap();
ban_manager
.create_ban(
BanTarget::Ip("192.168.1.1".to_string()),
"测试1".to_string(),
BanSource::Auto,
serde_json::json!({}),
None,
)
.await
.unwrap();
ban_manager
.create_ban(
BanTarget::Ip("192.168.1.2".to_string()),
"测试2".to_string(),
BanSource::Auto,
serde_json::json!({}),
None,
)
.await
.unwrap();
let result = ban_manager
.create_ban(
BanTarget::Ip("192.168.1.3".to_string()),
"测试3".to_string(),
BanSource::Auto,
serde_json::json!({}),
None,
)
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_add_ban_convenience_method() {
let ban_manager = create_test_ban_manager().await;
let record = BanRecord {
target: BanTarget::Ip("192.168.1.50".to_string()),
ban_times: 1,
duration: StdDuration::from_secs(1800),
banned_at: chrono::Utc::now(),
expires_at: chrono::Utc::now() + chrono::Duration::seconds(1800),
is_manual: true,
reason: "便捷方法测试".to_string(),
};
let result = ban_manager.add_ban(record).await;
assert!(result.is_ok());
let banned = ban_manager
.read_ban(&BanTarget::Ip("192.168.1.50".to_string()))
.await
.unwrap();
assert!(banned.is_some());
}
#[tokio::test]
async fn test_get_ban_convenience_method() {
let ban_manager = create_test_ban_manager().await;
let target = BanTarget::UserId("get_ban_user".to_string());
ban_manager
.create_ban(
target.clone(),
"测试".to_string(),
BanSource::Auto,
serde_json::json!({}),
None,
)
.await
.unwrap();
let record = ban_manager.get_ban(&target).await.unwrap();
assert!(record.is_some());
let record = record.unwrap();
assert_eq!(record.target, target);
}
#[tokio::test]
async fn test_is_banned_convenience_method() {
let ban_manager = create_test_ban_manager().await;
let target = BanTarget::Mac("11:22:33:44:55:66".to_string());
let result = ban_manager.is_banned(&target).await.unwrap();
assert!(result.is_none());
ban_manager
.create_ban(
target.clone(),
"测试".to_string(),
BanSource::Auto,
serde_json::json!({}),
None,
)
.await
.unwrap();
let result = ban_manager.is_banned(&target).await.unwrap();
assert!(result.is_some());
}
#[tokio::test]
async fn test_ban_manager_new_uses_memory_storage() {
let ban_manager = BanManager::new()
.await
.expect("BanManager creation should succeed");
let target = BanTarget::UserId("test_user".to_string());
let result = ban_manager.is_banned(&target).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_ban_manager_builder_with_storage() {
use crate::storage::MemoryBanStorage;
let storage: Arc<dyn BanStorage> = Arc::new(MemoryBanStorage::new());
let ban_manager = BanManager::builder()
.with_storage(storage)
.build()
.await
.expect("BanManager build should succeed");
let target = BanTarget::Ip("192.168.1.1".to_string());
let result = ban_manager.is_banned(&target).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_ban_manager_builder_without_storage() {
let ban_manager = BanManager::builder()
.build()
.await
.expect("BanManager build should succeed with default storage");
let target = BanTarget::UserId("default_test".to_string());
let result = ban_manager.is_banned(&target).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_ban_manager_with_dependencies() {
use crate::storage::MemoryBanStorage;
let storage: Arc<dyn BanStorage> = Arc::new(MemoryBanStorage::new());
let config = BanManagerConfig::default();
let ban_manager = BanManager::with_dependencies(storage, config)
.await
.expect("BanManager creation should succeed");
let target = BanTarget::Mac("aa:bb:cc:dd:ee:ff".to_string());
let result = ban_manager.is_banned(&target).await;
assert!(result.is_ok());
}
async fn create_manager_with_storage() -> (BanManager, Arc<dyn BanStorage>) {
let storage: Arc<dyn BanStorage> = Arc::new(MockBanStorage::new());
let manager = BanManager::with_dependencies(storage.clone(), BanManagerConfig::default())
.await
.unwrap();
(manager, storage)
}
fn make_ban_record(
target: BanTarget,
is_manual: bool,
banned_at: DateTime<Utc>,
duration_secs: u64,
) -> BanRecord {
BanRecord {
target,
ban_times: 1,
duration: StdDuration::from_secs(duration_secs),
banned_at,
expires_at: banned_at + chrono::Duration::seconds(duration_secs as i64),
is_manual,
reason: format!("ban at {}", banned_at),
}
}
#[tokio::test]
async fn test_list_bans_filter_target_type_ip() {
let (manager, storage) = create_manager_with_storage().await;
let now = chrono::Utc::now();
storage
.save(&make_ban_record(
BanTarget::Ip("10.0.0.1".to_string()),
false,
now,
3600,
))
.await
.unwrap();
storage
.save(&make_ban_record(
BanTarget::UserId("user1".to_string()),
false,
now,
3600,
))
.await
.unwrap();
let filter = BanFilter {
target_type: Some("ip".to_string()),
..Default::default()
};
let result = manager.list_bans(filter).await.unwrap();
assert_eq!(result.len(), 1);
assert!(matches!(result[0].target, BanTarget::Ip(_)));
}
#[tokio::test]
async fn test_list_bans_filter_target_type_user() {
let (manager, storage) = create_manager_with_storage().await;
let now = chrono::Utc::now();
storage
.save(&make_ban_record(
BanTarget::Ip("10.0.0.2".to_string()),
false,
now,
3600,
))
.await
.unwrap();
storage
.save(&make_ban_record(
BanTarget::UserId("user2".to_string()),
false,
now,
3600,
))
.await
.unwrap();
let filter = BanFilter {
target_type: Some("user".to_string()),
..Default::default()
};
let result = manager.list_bans(filter).await.unwrap();
assert_eq!(result.len(), 1);
assert!(matches!(result[0].target, BanTarget::UserId(_)));
}
#[tokio::test]
async fn test_list_bans_filter_target_type_mac() {
let (manager, storage) = create_manager_with_storage().await;
let now = chrono::Utc::now();
storage
.save(&make_ban_record(
BanTarget::Mac("aa:bb:cc:dd:ee:ff".to_string()),
false,
now,
3600,
))
.await
.unwrap();
storage
.save(&make_ban_record(
BanTarget::UserId("user3".to_string()),
false,
now,
3600,
))
.await
.unwrap();
let filter = BanFilter {
target_type: Some("mac".to_string()),
..Default::default()
};
let result = manager.list_bans(filter).await.unwrap();
assert_eq!(result.len(), 1);
assert!(matches!(result[0].target, BanTarget::Mac(_)));
}
#[tokio::test]
async fn test_list_bans_filter_target_type_unknown() {
let (manager, storage) = create_manager_with_storage().await;
let now = chrono::Utc::now();
storage
.save(&make_ban_record(
BanTarget::Ip("10.0.0.3".to_string()),
false,
now,
3600,
))
.await
.unwrap();
let filter = BanFilter {
target_type: Some("unknown_type".to_string()),
..Default::default()
};
let result = manager.list_bans(filter).await.unwrap();
assert_eq!(result.len(), 0);
}
#[tokio::test]
async fn test_list_bans_filter_target_type_case_insensitive() {
let (manager, storage) = create_manager_with_storage().await;
let now = chrono::Utc::now();
storage
.save(&make_ban_record(
BanTarget::Ip("10.0.0.4".to_string()),
false,
now,
3600,
))
.await
.unwrap();
let filter = BanFilter {
target_type: Some("IP".to_string()),
..Default::default()
};
let result = manager.list_bans(filter).await.unwrap();
assert_eq!(result.len(), 1);
}
#[tokio::test]
async fn test_list_bans_filter_target_value_match() {
let (manager, storage) = create_manager_with_storage().await;
let now = chrono::Utc::now();
storage
.save(&make_ban_record(
BanTarget::Ip("192.168.1.100".to_string()),
false,
now,
3600,
))
.await
.unwrap();
storage
.save(&make_ban_record(
BanTarget::Ip("10.0.0.5".to_string()),
false,
now,
3600,
))
.await
.unwrap();
let filter = BanFilter {
target_value: Some("192.168".to_string()),
..Default::default()
};
let result = manager.list_bans(filter).await.unwrap();
assert_eq!(result.len(), 1);
assert!(matches!(result[0].target, BanTarget::Ip(ref ip) if ip.contains("192.168")));
}
#[tokio::test]
async fn test_list_bans_filter_target_value_no_match() {
let (manager, storage) = create_manager_with_storage().await;
let now = chrono::Utc::now();
storage
.save(&make_ban_record(
BanTarget::UserId("user_abc".to_string()),
false,
now,
3600,
))
.await
.unwrap();
let filter = BanFilter {
target_value: Some("nonexistent".to_string()),
..Default::default()
};
let result = manager.list_bans(filter).await.unwrap();
assert_eq!(result.len(), 0);
}
#[tokio::test]
async fn test_list_bans_filter_manual_only() {
let (manager, storage) = create_manager_with_storage().await;
let now = chrono::Utc::now();
storage
.save(&make_ban_record(
BanTarget::Ip("10.0.0.6".to_string()),
true,
now,
3600,
))
.await
.unwrap();
storage
.save(&make_ban_record(
BanTarget::UserId("user_auto".to_string()),
false,
now,
3600,
))
.await
.unwrap();
let filter = BanFilter {
manual_only: true,
..Default::default()
};
let result = manager.list_bans(filter).await.unwrap();
assert_eq!(result.len(), 1);
assert!(result[0].is_manual);
}
#[tokio::test]
async fn test_list_bans_filter_start_time() {
let (manager, storage) = create_manager_with_storage().await;
let now = chrono::Utc::now();
let past = now - chrono::Duration::seconds(100);
storage
.save(&make_ban_record(
BanTarget::Ip("10.0.0.7".to_string()),
false,
past,
3600,
))
.await
.unwrap();
storage
.save(&make_ban_record(
BanTarget::Ip("10.0.0.8".to_string()),
false,
now,
3600,
))
.await
.unwrap();
let filter = BanFilter {
start_time: Some(now),
..Default::default()
};
let result = manager.list_bans(filter).await.unwrap();
assert!(!result.is_empty());
for detail in &result {
assert!(detail.banned_at >= now);
}
}
#[tokio::test]
async fn test_list_bans_filter_end_time() {
let (manager, storage) = create_manager_with_storage().await;
let now = chrono::Utc::now();
let future = now + chrono::Duration::seconds(100);
storage
.save(&make_ban_record(
BanTarget::Ip("10.0.0.9".to_string()),
false,
now,
3600,
))
.await
.unwrap();
let filter = BanFilter {
end_time: Some(future),
..Default::default()
};
let result = manager.list_bans(filter).await.unwrap();
assert!(!result.is_empty());
for detail in &result {
assert!(detail.banned_at <= future);
}
}
#[tokio::test]
async fn test_list_bans_filter_combined() {
let (manager, storage) = create_manager_with_storage().await;
let now = chrono::Utc::now();
storage
.save(&make_ban_record(
BanTarget::Ip("192.168.1.1".to_string()),
true,
now,
3600,
))
.await
.unwrap();
storage
.save(&make_ban_record(
BanTarget::UserId("user_x".to_string()),
false,
now,
3600,
))
.await
.unwrap();
let filter = BanFilter {
target_type: Some("ip".to_string()),
target_value: Some("192.168".to_string()),
manual_only: true,
..Default::default()
};
let result = manager.list_bans(filter).await.unwrap();
assert_eq!(result.len(), 1);
assert!(matches!(result[0].target, BanTarget::Ip(_)));
assert!(result[0].is_manual);
}
#[tokio::test]
async fn test_check_ban_priority_truly_empty_targets() {
let ban_manager = create_test_ban_manager().await;
let targets: Vec<BanTarget> = vec![];
let result = ban_manager.check_ban_priority(&targets).await;
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
#[tokio::test]
async fn test_add_ban_auto_source() {
let ban_manager = create_test_ban_manager().await;
let now = chrono::Utc::now();
let record = BanRecord {
target: BanTarget::Ip("203.0.113.10".to_string()),
ban_times: 1,
duration: StdDuration::from_secs(3600),
banned_at: now,
expires_at: now + chrono::Duration::seconds(3600),
is_manual: false,
reason: "auto ban via add_ban".to_string(),
};
let result = ban_manager.add_ban(record).await;
assert!(result.is_ok());
let banned = ban_manager
.read_ban(&BanTarget::Ip("203.0.113.10".to_string()))
.await
.unwrap();
assert!(banned.is_some());
assert!(!banned.unwrap().is_manual);
}
#[tokio::test]
async fn test_list_bans_filter_mac_target_value() {
let ban_manager = create_test_ban_manager().await;
ban_manager
.create_ban(
BanTarget::Mac("AA:BB:CC:DD:EE:FF".to_string()),
"MAC封禁".to_string(),
BanSource::Auto,
serde_json::json!({}),
Some(StdDuration::from_secs(1800)),
)
.await
.unwrap();
let filter = BanFilter {
target_value: Some("BB:CC".to_string()),
..Default::default()
};
let bans = ban_manager.list_bans(filter).await.unwrap();
assert_eq!(bans.len(), 1);
assert!(matches!(bans[0].target, BanTarget::Mac(_)));
let filter = BanFilter {
target_value: Some("ZZ:ZZ".to_string()),
..Default::default()
};
let bans = ban_manager.list_bans(filter).await.unwrap();
assert!(bans.is_empty());
}
#[tokio::test]
async fn test_create_ban_with_authorization_mac_target() {
use crate::authorization::SimpleAuthorizationProvider;
let storage = Arc::new(MockBanStorage::new());
let auth_provider = Arc::new(SimpleAuthorizationProvider::new(vec!["admin".to_string()]));
let ban_manager = BanManager::builder()
.with_storage(storage)
.with_authorization_provider(auth_provider)
.build()
.await
.unwrap();
let target = BanTarget::Mac("AA:BB:CC:DD:EE:FF".to_string());
let result = ban_manager
.create_ban(
target,
"MAC auth ban".to_string(),
BanSource::Manual {
operator: "admin".to_string(),
},
serde_json::json!({}),
None,
)
.await;
assert!(result.is_ok());
}
}