use limiteron::config::{
Action, ActionConfig, CacheBackend, ChangeSource, ConfigBuilder, ConfigChangeRecord,
ConfigHistory, FlowControlConfig, LimiterConfig, Matcher, MetricsBackend, Rule, StorageType,
};
fn make_valid_rule(id: &str, name: &str) -> Rule {
Rule {
id: id.to_string(),
name: name.to_string(),
priority: 100,
matchers: vec![Matcher::User {
user_ids: vec!["*".to_string()],
}],
limiters: vec![LimiterConfig::TokenBucket {
capacity: 1000,
refill_rate: 100,
}],
action: ActionConfig {
on_exceed: Action::Reject,
ban: None,
},
}
}
fn make_valid_config(version: &str, rules: Vec<Rule>) -> FlowControlConfig {
FlowControlConfig {
version: version.to_string(),
global: limiteron::config::GlobalConfig {
storage: StorageType::Memory,
cache: CacheBackend::Memory,
metrics: MetricsBackend::Prometheus,
trusted_proxies: Default::default(),
},
rules,
}
}
#[tokio::test]
async fn test_valid_config_passes_validation() {
let config = make_valid_config("1.0.0", vec![make_valid_rule("rule_001", "Test Rule")]);
assert!(config.validate().is_ok(), "有效配置应通过验证");
}
#[tokio::test]
async fn test_empty_version_fails_validation() {
let config = FlowControlConfig {
version: "".to_string(),
global: limiteron::config::GlobalConfig {
storage: StorageType::Memory,
cache: CacheBackend::Memory,
metrics: MetricsBackend::Prometheus,
trusted_proxies: Default::default(),
},
rules: vec![make_valid_rule("rule_001", "Test Rule")],
};
let result = config.validate();
assert!(result.is_err(), "空版本号配置应校验失败");
assert_eq!(result.unwrap_err(), "版本号不能为空");
}
#[tokio::test]
async fn test_duplicate_rule_ids_fail_validation() {
let rule = make_valid_rule("duplicate_id", "Rule");
let config = make_valid_config("1.0.0", vec![rule.clone(), rule]);
let result = config.validate();
assert!(result.is_err(), "重复规则ID应校验失败");
assert!(result.unwrap_err().contains("规则ID重复: duplicate_id"));
}
#[tokio::test]
async fn test_storage_type_default() {
let config = FlowControlConfig {
version: "1.0.0".to_string(),
global: limiteron::config::GlobalConfig {
storage: StorageType::Memory,
cache: CacheBackend::Memory,
metrics: MetricsBackend::Prometheus,
trusted_proxies: Default::default(),
},
rules: vec![make_valid_rule("rule_001", "Test Rule")],
};
let result = config.validate();
assert!(result.is_ok(), "有效配置应校验通过");
}
#[tokio::test]
async fn test_version_compare_equal() {
let config1 = make_valid_config("1.0.0", vec![]);
let config2 = make_valid_config("1.0.0", vec![]);
assert_eq!(
config1.compare_version(&config2),
std::cmp::Ordering::Equal,
"相同版本应返回 Equal"
);
}
#[tokio::test]
async fn test_version_compare_greater() {
let config1 = make_valid_config("0.2.0", vec![]);
let config2 = make_valid_config("0.1.0", vec![]);
assert_eq!(
config1.compare_version(&config2),
std::cmp::Ordering::Greater,
"较新版本应返回 Greater"
);
}
#[tokio::test]
async fn test_version_compare_less() {
let config1 = make_valid_config("0.1.0", vec![]);
let config2 = make_valid_config("0.2.0", vec![]);
assert_eq!(
config1.compare_version(&config2),
std::cmp::Ordering::Less,
"较旧版本应返回 Less"
);
}
#[tokio::test]
async fn test_invalid_version_format_no_panic() {
let config1 = FlowControlConfig {
version: "invalid".to_string(),
global: limiteron::config::GlobalConfig::default(),
rules: vec![make_valid_rule("r", "R")],
};
let config2 = FlowControlConfig {
version: "also-invalid".to_string(),
global: limiteron::config::GlobalConfig::default(),
rules: vec![make_valid_rule("r", "R")],
};
let ordering = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
config1.compare_version(&config2)
}));
assert!(ordering.is_ok(), "无效版本格式比较不应 panic");
let ordering2 = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let c1 = FlowControlConfig {
version: "zzz".to_string(),
global: limiteron::config::GlobalConfig::default(),
rules: vec![make_valid_rule("r", "R")],
};
let c2 = FlowControlConfig {
version: "aaa".to_string(),
global: limiteron::config::GlobalConfig::default(),
rules: vec![make_valid_rule("r", "R")],
};
c1.compare_version(&c2)
}));
assert!(ordering2.is_ok(), "纯字母版本字符串比较不应 panic");
}
#[tokio::test]
async fn test_config_builder_minimal_valid() {
let config = ConfigBuilder::new()
.with_rule(|rule| {
rule.id("minimal")
.name("Minimal Rule")
.user_matcher(vec!["*".to_string()])
.token_bucket(100, 10)
})
.build();
assert!(config.is_ok(), "最小有效配置应构建成功");
let cfg = config.unwrap();
assert_eq!(cfg.version, "0.1.0");
assert_eq!(cfg.global.storage, StorageType::Memory);
assert_eq!(cfg.rules.len(), 1);
assert_eq!(cfg.rules[0].id, "minimal");
}
#[tokio::test]
async fn test_config_builder_with_multiple_rules() {
let config = ConfigBuilder::new()
.with_storage(StorageType::Memory)
.with_cache(CacheBackend::Memory)
.with_metrics(MetricsBackend::Prometheus)
.with_rule(|rule| {
rule.id("rule_token_bucket")
.name("Token Bucket Rule")
.priority(100)
.user_matcher(vec!["user1".to_string(), "user2".to_string()])
.token_bucket(1000, 100)
})
.with_rule(|rule| {
rule.id("rule_fixed_window")
.name("Fixed Window Rule")
.priority(90)
.ip_matcher(vec!["192.168.1.0/24".to_string()])
.fixed_window("60s", 500)
.on_reject()
})
.with_rule(|rule| {
rule.id("rule_sliding_window")
.name("Sliding Window Rule")
.priority(80)
.user_matcher(vec!["*".to_string()])
.sliding_window("30s", 200)
.on_degrade()
})
.build();
assert!(config.is_ok(), "多规则配置应构建成功");
let cfg = config.unwrap();
assert_eq!(cfg.rules.len(), 3);
assert_eq!(cfg.rules[0].priority, 100);
assert_eq!(cfg.rules[1].priority, 90);
assert_eq!(cfg.rules[2].priority, 80);
match &cfg.rules[0].limiters[0] {
LimiterConfig::TokenBucket { .. } => {}
_ => panic!("第一个规则应为 TokenBucket"),
}
match &cfg.rules[1].limiters[0] {
LimiterConfig::FixedWindow { .. } => {}
_ => panic!("第二个规则应为 FixedWindow"),
}
match &cfg.rules[2].limiters[0] {
LimiterConfig::SlidingWindow { .. } => {}
_ => panic!("第三个规则应为 SlidingWindow"),
}
}
#[tokio::test]
async fn test_config_builder_validation_during_construction() {
let result = ConfigBuilder::new().build();
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "至少需要一个规则");
let result = ConfigBuilder::new()
.with_rule(|rule| {
rule.name("No ID")
.user_matcher(vec!["*".to_string()])
.token_bucket(100, 10)
})
.build();
assert!(result.is_err());
assert!(result.unwrap_err().contains("规则ID不能为空"));
let result = ConfigBuilder::new()
.with_rule(|rule| {
rule.id("no_matcher")
.name("No Matcher")
.token_bucket(100, 10)
})
.build();
assert!(result.is_err());
assert!(result.unwrap_err().contains("规则至少需要一个匹配器"));
let result = ConfigBuilder::new()
.with_rule(|rule| {
rule.id("no_limiter")
.name("No Limiter")
.user_matcher(vec!["*".to_string()])
})
.build();
assert!(result.is_err());
assert!(result.unwrap_err().contains("规则至少需要一个限流器"));
}
#[tokio::test]
async fn test_config_builder_method_chaining() {
let config = ConfigBuilder::new()
.with_storage(StorageType::Memory)
.with_cache(CacheBackend::Memory)
.with_metrics(MetricsBackend::Prometheus)
.with_rule(|rule| {
rule.id("chained")
.name("Chained Rule")
.priority(50)
.user_matcher(vec!["*".to_string()])
.token_bucket(500, 50)
.on_reject()
})
.build();
assert!(config.is_ok(), "链式调用应正常工作");
let cfg = config.unwrap();
assert_eq!(cfg.global.storage, StorageType::Memory);
assert_eq!(cfg.global.cache, CacheBackend::Memory);
assert_eq!(cfg.global.metrics, MetricsBackend::Prometheus);
assert_eq!(cfg.rules[0].priority, 50);
}
#[tokio::test]
async fn test_identical_configs_same_hash() {
let config1 = make_valid_config("1.0.0", vec![make_valid_rule("rule1", "Rule 1")]);
let config2 = make_valid_config("1.0.0", vec![make_valid_rule("rule1", "Rule 1")]);
assert_eq!(
config1.compute_hash(),
config2.compute_hash(),
"相同配置应产生相同哈希"
);
}
#[tokio::test]
async fn test_different_configs_different_hashes() {
let config1 = make_valid_config("1.0.0", vec![make_valid_rule("rule1", "Rule 1")]);
let config2 = make_valid_config(
"1.0.0",
vec![
make_valid_rule("rule1", "Rule 1"),
make_valid_rule("rule2", "Rule 2"),
],
);
assert_ne!(
config1.compute_hash(),
config2.compute_hash(),
"不同配置应产生不同哈希"
);
}
#[tokio::test]
async fn test_hash_is_deterministic() {
let config = make_valid_config("1.0.0", vec![make_valid_rule("deterministic", "Test")]);
let hash1 = config.compute_hash();
let hash2 = config.compute_hash();
let hash3 = config.compute_hash();
assert_eq!(hash1, hash2, "哈希第一次和第二次应一致");
assert_eq!(hash2, hash3, "哈希第二次和第三次应一致");
assert_eq!(hash1, hash3, "哈希三次都应一致");
}
#[tokio::test]
async fn test_change_record_version_upgrade() {
let old_config = make_valid_config("0.1.0", vec![make_valid_rule("r1", "Old Rule")]);
let new_config = make_valid_config("0.2.0", vec![make_valid_rule("r1", "Old Rule")]);
let record = new_config.create_change_record(Some(&old_config), ChangeSource::Poll);
assert_eq!(record.old_version, Some("0.1.0".to_string()));
assert_eq!(record.new_version, "0.2.0");
assert!(record.old_hash.is_some());
assert!(!record.new_hash.is_empty());
assert!(!record.changes.is_empty());
}
#[tokio::test]
async fn test_change_record_identifies_added_rules() {
let old_config = make_valid_config("1.0.0", vec![make_valid_rule("existing", "Existing")]);
let new_config = make_valid_config(
"1.0.0",
vec![
make_valid_rule("existing", "Existing"),
make_valid_rule("new_rule", "New Rule"),
],
);
let record = new_config.create_change_record(Some(&old_config), ChangeSource::Api);
assert!(
record.changes.iter().any(|c| c.contains("新增规则")),
"变更记录应包含新增规则: {:?}",
record.changes
);
}
#[tokio::test]
async fn test_change_record_identifies_removed_rules() {
let old_config = make_valid_config(
"1.0.0",
vec![
make_valid_rule("keep", "Keep"),
make_valid_rule("remove", "Remove"),
],
);
let new_config = make_valid_config("1.0.0", vec![make_valid_rule("keep", "Keep")]);
let record = new_config.create_change_record(
Some(&old_config),
ChangeSource::Manual {
operator: "admin".to_string(),
},
);
assert!(
record.changes.iter().any(|c| c.contains("移除规则")),
"变更记录应包含移除规则: {:?}",
record.changes
);
}
#[tokio::test]
async fn test_change_record_identifies_modified_rules() {
let old_config = FlowControlConfig {
version: "1.0.0".to_string(),
global: limiteron::config::GlobalConfig {
storage: StorageType::Memory,
cache: CacheBackend::Memory,
metrics: MetricsBackend::Prometheus,
trusted_proxies: Default::default(),
},
rules: vec![make_valid_rule("same", "Same Rule")],
};
let new_config = FlowControlConfig {
version: "1.0.0".to_string(),
global: limiteron::config::GlobalConfig {
storage: StorageType::PostgreSQL,
cache: CacheBackend::Memory,
metrics: MetricsBackend::Prometheus,
trusted_proxies: Default::default(),
},
rules: vec![make_valid_rule("same", "Same Rule")],
};
let record = new_config.create_change_record(Some(&old_config), ChangeSource::Reload);
assert!(
record.changes.iter().any(|c| c.contains("全局配置")),
"变更记录应包含全局配置变更: {:?}",
record.changes
);
}
#[tokio::test]
async fn test_unique_rule_ids_accepted() {
let config = make_valid_config(
"1.0.0",
vec![
make_valid_rule("rule_a", "Rule A"),
make_valid_rule("rule_b", "Rule B"),
make_valid_rule("rule_c", "Rule C"),
],
);
assert!(config.validate().is_ok(), "唯一规则ID应被接受");
}
#[tokio::test]
async fn test_duplicate_rule_ids_detected() {
let rule1 = make_valid_rule("my_rule", "Rule 1");
let rule2 = make_valid_rule("my_rule", "Rule 2");
let config = make_valid_config("1.0.0", vec![rule1, rule2]);
let result = config.validate();
assert!(result.is_err());
assert!(result.unwrap_err().contains("规则ID重复: my_rule"));
}
#[tokio::test]
async fn test_case_sensitive_rule_id_comparison() {
let config = make_valid_config(
"1.0.0",
vec![
make_valid_rule("rule1", "Lowercase Rule"),
make_valid_rule("Rule1", "Capitalized Rule"),
],
);
assert!(
config.validate().is_ok(),
"大小写不同的规则ID应被视为不同的规则"
);
}
#[tokio::test]
async fn test_config_history_basic_operations() {
let mut history = ConfigHistory::new(10);
assert!(history.get_records().is_empty());
assert!(history.get_latest().is_none());
let record1 = ConfigChangeRecord {
timestamp: chrono::Utc::now(),
old_version: None,
new_version: "0.1.0".to_string(),
old_hash: None,
new_hash: "abc123".to_string(),
source: ChangeSource::Poll,
changes: vec!["初始配置".to_string()],
};
let record2 = ConfigChangeRecord {
timestamp: chrono::Utc::now(),
old_version: Some("0.1.0".to_string()),
new_version: "0.2.0".to_string(),
old_hash: Some("abc123".to_string()),
new_hash: "def456".to_string(),
source: ChangeSource::Api,
changes: vec!["版本变更: 0.1.0 -> 0.2.0".to_string()],
};
history.add_record(record1.clone());
assert_eq!(history.get_records().len(), 1);
assert_eq!(
history.get_latest().map(|r| r.new_version.as_str()),
Some("0.1.0")
);
history.add_record(record2.clone());
assert_eq!(history.get_records().len(), 2);
assert_eq!(
history.get_latest().map(|r| r.new_version.as_str()),
Some("0.2.0")
);
history.clear();
assert!(history.get_records().is_empty());
assert!(history.get_latest().is_none());
}
#[tokio::test]
async fn test_config_history_eviction() {
let mut history = ConfigHistory::new(3);
for i in 0..5 {
let record = ConfigChangeRecord {
timestamp: chrono::Utc::now(),
old_version: Some(format!("0.{}", i)),
new_version: format!("0.{}", i + 1),
old_hash: None,
new_hash: format!("hash_{}", i),
source: ChangeSource::Poll,
changes: vec![format!("change_{}", i)],
};
history.add_record(record);
}
assert_eq!(history.get_records().len(), 3);
assert_eq!(
history.get_latest().map(|r| r.new_version.as_str()),
Some("0.5")
);
}
#[tokio::test]
async fn test_clone_configuration_independent() {
let original = make_valid_config("1.0.0", vec![make_valid_rule("original_rule", "Original")]);
let cloned = original.clone();
assert_eq!(original.compute_hash(), cloned.compute_hash());
assert_eq!(original.version, cloned.version);
assert_eq!(original.rules.len(), cloned.rules.len());
}
#[tokio::test]
async fn test_modify_clone_without_affecting_original() {
let original = make_valid_config(
"1.0.0",
vec![
make_valid_rule("keep", "Keep"),
make_valid_rule("to_remove", "To Remove"),
],
);
let mut cloned = original.clone();
cloned.version = "2.0.0".to_string();
cloned.rules.retain(|r| r.id == "keep");
assert_eq!(original.version, "1.0.0");
assert_eq!(original.rules.len(), 2);
assert!(original.rules.iter().any(|r| r.id == "to_remove"));
assert_eq!(cloned.version, "2.0.0");
assert_eq!(cloned.rules.len(), 1);
assert!(cloned.rules.iter().all(|r| r.id == "keep"));
assert_ne!(original.compute_hash(), cloned.compute_hash());
}
#[cfg(feature = "quota-control")]
#[tokio::test]
async fn test_deep_clone_nested_structures() {
let original = FlowControlConfig {
version: "1.0.0".to_string(),
global: limiteron::config::GlobalConfig {
storage: StorageType::Memory,
cache: CacheBackend::Redis,
metrics: MetricsBackend::Prometheus,
trusted_proxies: Default::default(),
},
rules: vec![Rule {
id: "nested".to_string(),
name: "Nested Rule".to_string(),
priority: 200,
matchers: vec![
Matcher::User {
user_ids: vec!["user1".to_string(), "user2".to_string()],
},
Matcher::Ip {
ip_ranges: vec!["10.0.0.0/8".to_string()],
},
],
limiters: vec![
LimiterConfig::TokenBucket {
capacity: 5000,
refill_rate: 500,
},
LimiterConfig::Concurrency {
max_concurrent: 100,
},
],
action: ActionConfig {
on_exceed: Action::Degrade,
ban: None,
},
}],
};
let mut cloned = original.clone();
let cloned_rule = &mut cloned.rules[0];
cloned_rule.matchers.push(Matcher::Device {
device_types: vec!["mobile".to_string()],
});
cloned_rule.limiters.push(LimiterConfig::Quota {
quota_type: limiteron::quota::QuotaType::Count,
limit: 1000,
window: "1h".to_string(),
alert_threshold: Some(80),
overdraft: None,
});
assert_eq!(original.rules[0].matchers.len(), 2);
assert_eq!(original.rules[0].limiters.len(), 2);
assert!(matches!(
original.rules[0].limiters[0],
LimiterConfig::TokenBucket {
capacity: 5000,
refill_rate: 500
}
));
assert_eq!(cloned.rules[0].matchers.len(), 3);
assert_eq!(cloned.rules[0].limiters.len(), 3);
}
#[tokio::test]
async fn test_serialization_roundtrip_json() {
let original = FlowControlConfig {
version: "1.0.0".to_string(),
global: limiteron::config::GlobalConfig {
storage: StorageType::Memory,
cache: CacheBackend::Memory,
metrics: MetricsBackend::Prometheus,
trusted_proxies: Default::default(),
},
rules: vec![
make_valid_rule("serial_rule", "Serialization Test Rule"),
Rule {
id: "complex_rule".to_string(),
name: "Complex Rule".to_string(),
priority: 50,
matchers: vec![
Matcher::Geo {
countries: vec!["US".to_string(), "CN".to_string()],
},
Matcher::ApiVersion {
versions: vec!["v1".to_string(), "v2".to_string()],
},
],
limiters: vec![
LimiterConfig::SlidingWindow {
window_size: "60s".to_string(),
max_requests: 1000,
},
LimiterConfig::FixedWindow {
window_size: "1h".to_string(),
max_requests: 5000,
},
],
action: ActionConfig {
on_exceed: Action::Allow,
ban: None,
},
},
],
};
let json = serde_json::to_string(&original).expect("序列化应成功");
let deserialized: FlowControlConfig = serde_json::from_str(&json).expect("反序列化应成功");
assert_eq!(deserialized.version, original.version);
assert_eq!(deserialized.global.storage, original.global.storage);
assert_eq!(deserialized.global.cache, original.global.cache);
assert_eq!(deserialized.global.metrics, original.global.metrics);
assert_eq!(deserialized.rules.len(), original.rules.len());
assert_eq!(deserialized.rules[0].id, "serial_rule");
assert_eq!(deserialized.rules[0].name, "Serialization Test Rule");
assert!(matches!(
deserialized.rules[0].limiters[0],
LimiterConfig::TokenBucket { .. }
));
let complex = &deserialized.rules[1];
assert_eq!(complex.id, "complex_rule");
assert_eq!(complex.matchers.len(), 2);
assert_eq!(complex.limiters.len(), 2);
if let Matcher::Geo { countries } = &complex.matchers[0] {
assert_eq!(countries.len(), 2);
} else {
panic!("第一个匹配器应为 Geo");
}
assert_eq!(
deserialized.compute_hash(),
original.compute_hash(),
"往返序列化后哈希应一致"
);
}
#[tokio::test]
async fn test_deserialize_invalid_json_fails() {
let invalid_json = r#"{ "version": "1.0.0", "global": "not an object" }"#;
let result: Result<FlowControlConfig, _> = serde_json::from_str(invalid_json);
assert!(result.is_err(), "无效JSON应反序列化失败");
let malformed_json = r#"this is not json at all"#;
let result2: Result<FlowControlConfig, _> = serde_json::from_str(malformed_json);
assert!(result2.is_err(), "畸形JSON应反序列化失败");
}
#[tokio::test]
async fn test_deserialize_missing_optional_fields() {
let minimal_json = serde_json::json!({
"version": "1.0.0",
"global": {
"storage": "memory",
"cache": "memory",
"metrics": "prometheus"
},
"rules": [
{
"id": "minimal",
"name": "Minimal Rule",
"priority": 100,
"matchers": [
{ "type": "User", "user_ids": ["*"] }
],
"limiters": [
{ "type": "TokenBucket", "capacity": 100, "refill_rate": 10 }
],
"action": { "on_exceed": "reject" }
}
]
});
let config: FlowControlConfig =
serde_json::from_value(minimal_json).expect("最小化JSON应能反序列化");
assert_eq!(config.version, "1.0.0");
assert_eq!(config.rules[0].id, "minimal");
assert!(config.rules[0].action.ban.is_none());
}
#[tokio::test]
async fn test_change_source_variants() {
let sources = vec![
ChangeSource::Poll,
ChangeSource::Watch,
ChangeSource::Api,
ChangeSource::Reload,
ChangeSource::Manual {
operator: "admin".to_string(),
},
ChangeSource::Rollback {
target_version: "1.0.0".to_string(),
},
];
for source in sources {
let record = ConfigChangeRecord {
timestamp: chrono::Utc::now(),
old_version: Some("1.0.0".to_string()),
new_version: "2.0.0".to_string(),
old_hash: Some("old".to_string()),
new_hash: "new".to_string(),
source,
changes: vec![],
};
assert!(!record.new_hash.is_empty());
}
}
#[tokio::test]
async fn test_global_config_validation_valid_types() {
let valid_configs = vec![
(
StorageType::Memory,
CacheBackend::Memory,
MetricsBackend::Prometheus,
),
(
StorageType::PostgreSQL,
CacheBackend::Redis,
MetricsBackend::Prometheus,
),
];
for (storage, cache, metrics) in valid_configs {
let global = limiteron::config::GlobalConfig {
storage,
cache,
metrics,
trusted_proxies: Default::default(),
};
assert!(global.validate().is_ok(), "有效配置应校验通过");
}
}
#[tokio::test]
async fn test_global_config_various_type_combinations() {
let valid_combos = vec![
(
StorageType::Memory,
CacheBackend::Memory,
MetricsBackend::Prometheus,
),
(
StorageType::PostgreSQL,
CacheBackend::Redis,
MetricsBackend::Prometheus,
),
(
StorageType::Redis,
CacheBackend::None,
MetricsBackend::Statsd,
),
];
for (storage, cache, metrics) in valid_combos {
let global = limiteron::config::GlobalConfig {
storage,
cache,
metrics,
trusted_proxies: Default::default(),
};
assert!(global.validate().is_ok(), "有效组合应通过验证");
}
}
#[tokio::test]
async fn test_rule_validation_various_invalid_cases() {
let rule = Rule {
id: "".to_string(),
name: "Test".to_string(),
priority: 100,
matchers: vec![Matcher::User {
user_ids: vec!["*".to_string()],
}],
limiters: vec![LimiterConfig::TokenBucket {
capacity: 100,
refill_rate: 10,
}],
action: ActionConfig::default(),
};
assert!(rule.validate().is_err());
let rule = Rule {
id: "valid_id".to_string(),
name: "".to_string(),
priority: 100,
matchers: vec![Matcher::User {
user_ids: vec!["*".to_string()],
}],
limiters: vec![LimiterConfig::TokenBucket {
capacity: 100,
refill_rate: 10,
}],
action: ActionConfig::default(),
};
assert!(rule.validate().is_err());
let rule = Rule {
id: "valid_id".to_string(),
name: "Valid Name".to_string(),
priority: 100,
matchers: vec![],
limiters: vec![LimiterConfig::TokenBucket {
capacity: 100,
refill_rate: 10,
}],
action: ActionConfig::default(),
};
assert!(rule.validate().is_err());
let rule = Rule {
id: "valid_id".to_string(),
name: "Valid Name".to_string(),
priority: 100,
matchers: vec![Matcher::User {
user_ids: vec!["*".to_string()],
}],
limiters: vec![],
action: ActionConfig::default(),
};
assert!(rule.validate().is_err());
}
#[tokio::test]
async fn test_is_same_as_method() {
let config1 = make_valid_config("1.0.0", vec![make_valid_rule("same", "Same")]);
let config2 = make_valid_config("1.0.0", vec![make_valid_rule("same", "Same")]);
let config3 = make_valid_config("1.0.0", vec![make_valid_rule("diff", "Diff")]);
assert!(
config1.is_same_as(&config2),
"相同配置 is_same_as 应返回 true"
);
assert!(
!config1.is_same_as(&config3),
"不同配置 is_same_as 应返回 false"
);
}
#[tokio::test]
async fn test_config_builder_with_concurrency_limiter() {
let config = ConfigBuilder::new()
.with_rule(|rule| {
rule.id("concurrent")
.name("Concurrency Rule")
.user_matcher(vec!["*".to_string()])
.concurrency_limit(50)
.on_allow()
})
.build();
assert!(config.is_ok());
let cfg = config.unwrap();
match &cfg.rules[0].limiters[0] {
LimiterConfig::Concurrency { max_concurrent } => {
assert_eq!(*max_concurrent, 50);
}
_ => panic!("应为 Concurrency 限流器"),
}
}
#[tokio::test]
async fn test_change_record_initial_config() {
let new_config = make_valid_config("0.1.0", vec![make_valid_rule("init", "Init")]);
let record = new_config.create_change_record(None, ChangeSource::Poll);
assert_eq!(record.old_version, None);
assert_eq!(record.new_version, "0.1.0");
assert_eq!(record.old_hash, None);
assert!(!record.new_hash.is_empty());
assert!(record.changes.contains(&"初始配置".to_string()));
}