use crate::error::LimiteronError;
use crate::matchers::RequestContext;
use ahash::AHashMap as HashMap;
use async_trait::async_trait;
use chrono::Timelike;
use log::{debug, error, info, warn};
use serde_json::Value;
use std::sync::Arc;
use tokio::sync::RwLock;
const MAX_MATCHER_NAME_LENGTH: usize = 100;
const MAX_HEADER_NAME_LENGTH: usize = 256;
const MAX_HEADER_VALUE_LENGTH: usize = 4096;
const MAX_ALLOWED_VALUES_COUNT: usize = 100;
fn validate_matcher_name(name: &str) -> Result<(), LimiteronError> {
if name.is_empty() {
return Err(LimiteronError::ConfigError(
"匹配器名称不能为空".to_string(),
));
}
if name.len() > MAX_MATCHER_NAME_LENGTH {
return Err(LimiteronError::ConfigError(format!(
"匹配器名称长度超过限制(最大 {} 字符)",
MAX_MATCHER_NAME_LENGTH
)));
}
if !name
.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '-')
{
return Err(LimiteronError::ConfigError(
"匹配器名称只能包含字母、数字、下划线和连字符".to_string(),
));
}
Ok(())
}
fn validate_header_name(name: &str) -> Result<(), LimiteronError> {
if name.is_empty() {
return Err(LimiteronError::ConfigError(
"HTTP头名称不能为空".to_string(),
));
}
if name.len() > MAX_HEADER_NAME_LENGTH {
return Err(LimiteronError::ConfigError(format!(
"HTTP头名称长度超过限制(最大 {} 字符)",
MAX_HEADER_NAME_LENGTH
)));
}
if !name.chars().all(|c| c.is_alphanumeric() || c == '-') {
return Err(LimiteronError::ConfigError(
"HTTP头名称只能包含字母、数字和连字符".to_string(),
));
}
Ok(())
}
fn validate_header_value(value: &str) -> Result<(), LimiteronError> {
if value.len() > MAX_HEADER_VALUE_LENGTH {
return Err(LimiteronError::ConfigError(format!(
"HTTP头值长度超过限制(最大 {} 字符)",
MAX_HEADER_VALUE_LENGTH
)));
}
Ok(())
}
#[async_trait]
pub trait CustomMatcher: Send + Sync {
fn name(&self) -> &str;
async fn matches(&self, context: &RequestContext) -> Result<bool, LimiteronError>;
fn load_config(&mut self, config: Value) -> Result<(), LimiteronError>;
}
#[derive(Clone)]
pub struct CustomMatcherRegistry {
matchers: Arc<RwLock<HashMap<String, Box<dyn CustomMatcher>>>>,
}
impl std::fmt::Debug for CustomMatcherRegistry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CustomMatcherRegistry")
.field("matchers", &"<custom matchers>")
.finish()
}
}
impl CustomMatcherRegistry {
pub fn new() -> Self {
Self {
matchers: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn builder() -> CustomMatcherRegistryBuilder {
CustomMatcherRegistryBuilder::new()
}
pub fn with_dependencies() -> Self {
Self::new()
}
pub async fn register(
&self,
name: String,
matcher: Box<dyn CustomMatcher>,
) -> Result<(), LimiteronError> {
validate_matcher_name(&name)?;
let mut matchers = self.matchers.write().await;
if matchers.contains_key(&name) {
let error_msg = format!("匹配器 '{}' 已存在", name);
warn!("{}", error_msg);
return Err(LimiteronError::ConfigError(error_msg));
}
info!("注册自定义匹配器: {}", name);
matchers.insert(name.clone(), matcher);
debug!("当前注册的匹配器数量: {}", matchers.len());
Ok(())
}
pub async fn get(&self, name: &str) -> Option<Box<dyn CustomMatcher>> {
let matchers = self.matchers.read().await;
if let Some(_matcher) = matchers.get(name) {
debug!("查询匹配器: {}", name);
None
} else {
debug!("未找到匹配器: {}", name);
None
}
}
pub async fn contains(&self, name: &str) -> bool {
let matchers = self.matchers.read().await;
matchers.contains_key(name)
}
pub async fn unregister(&self, name: &str) -> Result<(), LimiteronError> {
let mut matchers = self.matchers.write().await;
if !matchers.contains_key(name) {
let error_msg = format!("匹配器 '{}' 不存在", name);
warn!("{}", error_msg);
return Err(LimiteronError::ConfigError(error_msg));
}
info!("注销自定义匹配器: {}", name);
matchers.remove(name);
debug!("当前注册的匹配器数量: {}", matchers.len());
Ok(())
}
#[allow(clippy::map_clone)]
pub async fn list(&self) -> Vec<String> {
let matchers = self.matchers.read().await;
matchers.keys().map(|k| k.clone()).collect()
}
pub async fn count(&self) -> usize {
let matchers = self.matchers.read().await;
matchers.len()
}
pub async fn clear(&self) {
let mut matchers = self.matchers.write().await;
info!("清空所有自定义匹配器");
matchers.clear();
}
pub async fn match_with(
&self,
name: &str,
context: &RequestContext,
) -> Result<bool, LimiteronError> {
let matchers = self.matchers.read().await;
let matcher = matchers.get(name).ok_or_else(|| {
let error_msg = format!("匹配器 '{}' 不存在", name);
error!("{}", error_msg);
LimiteronError::ConfigError(error_msg)
})?;
debug!("使用匹配器 '{}' 检查请求", name);
matcher.matches(context).await
}
}
impl Default for CustomMatcherRegistry {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Default)]
pub struct CustomMatcherRegistryBuilder;
impl CustomMatcherRegistryBuilder {
pub fn new() -> Self {
Self
}
pub fn build(self) -> CustomMatcherRegistry {
CustomMatcherRegistry::new()
}
}
#[derive(Debug, Clone)]
pub struct TimeWindowMatcher {
start_hour: u8,
end_hour: u8,
}
impl TimeWindowMatcher {
pub fn new(start_hour: u8, end_hour: u8) -> Self {
assert!(start_hour <= 23, "开始小时必须在 0-23 范围内");
assert!(end_hour <= 23, "结束小时必须在 0-23 范围内");
Self {
start_hour,
end_hour,
}
}
pub fn start_hour(&self) -> u8 {
self.start_hour
}
pub fn end_hour(&self) -> u8 {
self.end_hour
}
pub fn builder() -> TimeWindowMatcherBuilder {
TimeWindowMatcherBuilder::new()
}
pub fn with_dependencies(start_hour: u8, end_hour: u8) -> Self {
Self::new(start_hour, end_hour)
}
}
#[async_trait]
impl CustomMatcher for TimeWindowMatcher {
fn name(&self) -> &str {
"time_window"
}
async fn matches(&self, _context: &RequestContext) -> Result<bool, LimiteronError> {
let now = chrono::Utc::now();
let hour = now.hour() as u8;
let matches = if self.start_hour <= self.end_hour {
hour >= self.start_hour && hour <= self.end_hour
} else {
hour >= self.start_hour || hour <= self.end_hour
};
debug!(
"时间窗口匹配: 当前时间 {}小时, 窗口 {}-{}小时, 结果: {}",
hour, self.start_hour, self.end_hour, matches
);
Ok(matches)
}
fn load_config(&mut self, config: Value) -> Result<(), LimiteronError> {
let start_hour_u64 = config["start_hour"]
.as_u64()
.ok_or_else(|| LimiteronError::ConfigError("缺少 start_hour 配置".to_string()))?;
if start_hour_u64 > 23 {
return Err(LimiteronError::ConfigError(
"start_hour 必须在 0-23 范围内".to_string(),
));
}
let start_hour = start_hour_u64 as u8;
let end_hour_u64 = config["end_hour"]
.as_u64()
.ok_or_else(|| LimiteronError::ConfigError("缺少 end_hour 配置".to_string()))?;
if end_hour_u64 > 23 {
return Err(LimiteronError::ConfigError(
"end_hour 必须在 0-23 范围内".to_string(),
));
}
let end_hour = end_hour_u64 as u8;
self.start_hour = start_hour;
self.end_hour = end_hour;
info!(
"加载时间窗口匹配器配置: {}-{}小时",
self.start_hour, self.end_hour
);
Ok(())
}
}
#[derive(Debug, Clone, Default)]
pub struct TimeWindowMatcherBuilder {
start_hour: u8,
end_hour: u8,
}
impl TimeWindowMatcherBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn start_hour(mut self, start_hour: u8) -> Self {
self.start_hour = start_hour;
self
}
pub fn end_hour(mut self, end_hour: u8) -> Self {
self.end_hour = end_hour;
self
}
pub fn build(self) -> TimeWindowMatcher {
TimeWindowMatcher::new(self.start_hour, self.end_hour)
}
}
#[derive(Debug, Clone)]
pub struct HeaderMatcher {
header_name: String,
allowed_values: Vec<String>,
case_sensitive: bool,
}
impl HeaderMatcher {
pub fn new(header_name: &str, allowed_values: Vec<String>) -> Result<Self, LimiteronError> {
validate_header_name(header_name)?;
if allowed_values.len() > MAX_ALLOWED_VALUES_COUNT {
return Err(LimiteronError::ValidationError(format!(
"允许的值数量超过限制(最大 {})",
MAX_ALLOWED_VALUES_COUNT
)));
}
for value in &allowed_values {
validate_header_value(value)?;
}
Ok(Self {
header_name: header_name.to_lowercase(),
allowed_values,
case_sensitive: false,
})
}
pub fn with_case_sensitive(mut self, case_sensitive: bool) -> Self {
self.case_sensitive = case_sensitive;
self
}
pub fn header_name(&self) -> &str {
&self.header_name
}
pub fn allowed_values(&self) -> &[String] {
&self.allowed_values
}
pub fn builder() -> HeaderMatcherBuilder {
HeaderMatcherBuilder::new()
}
pub fn with_dependencies(
header_name: &str,
allowed_values: Vec<String>,
case_sensitive: bool,
) -> Result<Self, LimiteronError> {
Self::new(header_name, allowed_values).map(|mut m| {
m.case_sensitive = case_sensitive;
m
})
}
}
#[async_trait]
impl CustomMatcher for HeaderMatcher {
fn name(&self) -> &str {
"header"
}
async fn matches(&self, context: &RequestContext) -> Result<bool, LimiteronError> {
let header_value = match context.get_header(&self.header_name) {
Some(value) => value,
None => {
debug!("HTTP头 '{}' 不存在", self.header_name);
return Ok(false);
}
};
let matches = if self.case_sensitive {
self.allowed_values.contains(header_value)
} else {
let lower_value = header_value.to_lowercase();
self.allowed_values
.iter()
.any(|v| v.to_lowercase() == lower_value)
};
debug!(
"HTTP头匹配: 头='{}', 值='{}', 结果: {}",
self.header_name, header_value, matches
);
Ok(matches)
}
fn load_config(&mut self, config: Value) -> Result<(), LimiteronError> {
if let Some(header_name) = config["header_name"].as_str() {
validate_header_name(header_name)?;
self.header_name = header_name.to_lowercase();
}
if let Some(values) = config["allowed_values"].as_array() {
if values.len() > MAX_ALLOWED_VALUES_COUNT {
return Err(LimiteronError::ConfigError(format!(
"允许的值数量超过限制(最大 {})",
MAX_ALLOWED_VALUES_COUNT
)));
}
self.allowed_values = values
.iter()
.filter_map(|v| v.as_str())
.map(|s| {
validate_header_value(s)?;
Ok(s.to_string())
})
.collect::<Result<Vec<_>, LimiteronError>>()?;
}
if let Some(case_sensitive) = config["case_sensitive"].as_bool() {
self.case_sensitive = case_sensitive;
}
info!(
"加载HTTP头匹配器配置: 头='{}', 允许值={:?}, 区分大小写={}",
self.header_name, self.allowed_values, self.case_sensitive
);
Ok(())
}
}
#[derive(Debug, Clone, Default)]
pub struct HeaderMatcherBuilder {
header_name: Option<String>,
allowed_values: Vec<String>,
case_sensitive: bool,
}
impl HeaderMatcherBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn header_name(mut self, header_name: &str) -> Self {
self.header_name = Some(header_name.to_string());
self
}
pub fn allowed_values(mut self, allowed_values: Vec<String>) -> Self {
self.allowed_values = allowed_values;
self
}
pub fn add_allowed_value(mut self, value: &str) -> Self {
self.allowed_values.push(value.to_string());
self
}
pub fn case_sensitive(mut self, case_sensitive: bool) -> Self {
self.case_sensitive = case_sensitive;
self
}
pub fn build(self) -> Result<HeaderMatcher, LimiteronError> {
HeaderMatcher::new(
self.header_name.as_deref().unwrap_or(""),
self.allowed_values,
)
.map(|mut m| {
m.case_sensitive = self.case_sensitive;
m
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::matchers::RequestContext;
#[tokio::test]
async fn test_registry_new() {
let registry = CustomMatcherRegistry::new();
assert_eq!(registry.count().await, 0);
}
#[tokio::test]
async fn test_registry_register() {
let registry = CustomMatcherRegistry::new();
let matcher = TimeWindowMatcher::new(9, 18);
assert!(
registry
.register("time_window".to_string(), Box::new(matcher))
.await
.is_ok()
);
assert_eq!(registry.count().await, 1);
assert!(registry.contains("time_window").await);
}
#[tokio::test]
async fn test_registry_register_duplicate() {
let registry = CustomMatcherRegistry::new();
let matcher = TimeWindowMatcher::new(9, 18);
assert!(
registry
.register("time_window".to_string(), Box::new(matcher))
.await
.is_ok()
);
let result = registry
.register(
"time_window".to_string(),
Box::new(TimeWindowMatcher::new(10, 20)),
)
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_registry_unregister() {
let registry = CustomMatcherRegistry::new();
let matcher = TimeWindowMatcher::new(9, 18);
registry
.register("time_window".to_string(), Box::new(matcher))
.await
.unwrap();
assert!(registry.unregister("time_window").await.is_ok());
assert_eq!(registry.count().await, 0);
assert!(!registry.contains("time_window").await);
}
#[tokio::test]
async fn test_registry_unregister_nonexistent() {
let registry = CustomMatcherRegistry::new();
let result = registry.unregister("nonexistent").await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_registry_list() {
let registry = CustomMatcherRegistry::new();
registry
.register(
"matcher1".to_string(),
Box::new(TimeWindowMatcher::new(9, 18)),
)
.await
.unwrap();
registry
.register(
"matcher2".to_string(),
Box::new(HeaderMatcher::new("X-API-Key", vec!["secret".to_string()]).unwrap()),
)
.await
.unwrap();
let list = registry.list().await;
assert_eq!(list.len(), 2);
assert!(list.contains(&"matcher1".to_string()));
assert!(list.contains(&"matcher2".to_string()));
}
#[tokio::test]
async fn test_registry_clear() {
let registry = CustomMatcherRegistry::new();
registry
.register(
"matcher1".to_string(),
Box::new(TimeWindowMatcher::new(9, 18)),
)
.await
.unwrap();
registry.clear().await;
assert_eq!(registry.count().await, 0);
}
#[tokio::test]
async fn test_registry_match_with() {
let registry = CustomMatcherRegistry::new();
let matcher = TimeWindowMatcher::new(0, 23);
registry
.register("time_window".to_string(), Box::new(matcher))
.await
.unwrap();
let context = RequestContext::new();
let result = registry.match_with("time_window", &context).await;
assert!(result.is_ok());
assert!(result.unwrap());
}
#[tokio::test]
async fn test_registry_match_with_nonexistent() {
let registry = CustomMatcherRegistry::new();
let context = RequestContext::new();
let result = registry.match_with("nonexistent", &context).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_time_window_matcher_new() {
let matcher = TimeWindowMatcher::new(9, 18);
assert_eq!(matcher.name(), "time_window");
assert_eq!(matcher.start_hour(), 9);
assert_eq!(matcher.end_hour(), 18);
}
#[tokio::test]
async fn test_time_window_matcher_matches() {
let matcher = TimeWindowMatcher::new(0, 23); let context = RequestContext::new();
let result = matcher.matches(&context).await.unwrap();
assert!(result);
}
#[tokio::test]
async fn test_time_window_matcher_load_config() {
let mut matcher = TimeWindowMatcher::new(9, 18);
let config = serde_json::json!({
"start_hour": 10,
"end_hour": 20
});
assert!(matcher.load_config(config).is_ok());
assert_eq!(matcher.start_hour(), 10);
assert_eq!(matcher.end_hour(), 20);
}
#[tokio::test]
async fn test_time_window_matcher_load_config_invalid() {
let mut matcher = TimeWindowMatcher::new(9, 18);
let config = serde_json::json!({
"start_hour": 25
});
let result = matcher.load_config(config);
assert!(result.is_err());
}
#[test]
#[should_panic(expected = "开始小时必须在 0-23 范围内")]
fn test_time_window_matcher_invalid_start_hour() {
TimeWindowMatcher::new(25, 18);
}
#[test]
#[should_panic(expected = "结束小时必须在 0-23 范围内")]
fn test_time_window_matcher_invalid_end_hour() {
TimeWindowMatcher::new(9, 25);
}
#[tokio::test]
async fn test_header_matcher_new() {
let matcher = HeaderMatcher::new("X-API-Key", vec!["secret123".to_string()]).unwrap();
assert_eq!(matcher.name(), "header");
assert_eq!(matcher.header_name(), "x-api-key");
assert_eq!(matcher.allowed_values().len(), 1);
}
#[tokio::test]
async fn test_header_matcher_matches() {
let matcher = HeaderMatcher::new("X-API-Key", vec!["secret123".to_string()]).unwrap();
let context = RequestContext::new().with_header("X-API-Key", "secret123");
let result = matcher.matches(&context).await.unwrap();
assert!(result);
}
#[tokio::test]
async fn test_header_matcher_not_matches() {
let matcher = HeaderMatcher::new("X-API-Key", vec!["secret123".to_string()]).unwrap();
let context = RequestContext::new().with_header("X-API-Key", "wrong");
let result = matcher.matches(&context).await.unwrap();
assert!(!result);
}
#[tokio::test]
async fn test_header_matcher_missing_header() {
let matcher = HeaderMatcher::new("X-API-Key", vec!["secret123".to_string()]).unwrap();
let context = RequestContext::new();
let result = matcher.matches(&context).await.unwrap();
assert!(!result);
}
#[tokio::test]
async fn test_header_matcher_case_insensitive() {
let matcher = HeaderMatcher::new("X-API-Key", vec!["SECRET123".to_string()]).unwrap();
let context = RequestContext::new().with_header("X-API-Key", "secret123");
let result = matcher.matches(&context).await.unwrap();
assert!(result);
}
#[tokio::test]
async fn test_header_header_matcher_case_sensitive() {
let matcher = HeaderMatcher::new("X-API-Key", vec!["SECRET123".to_string()])
.unwrap()
.with_case_sensitive(true);
let context = RequestContext::new().with_header("X-API-Key", "secret123");
let result = matcher.matches(&context).await.unwrap();
assert!(!result);
}
#[tokio::test]
async fn test_header_matcher_multiple_values() {
let matcher = HeaderMatcher::new(
"X-API-Key",
vec!["secret123".to_string(), "secret456".to_string()],
)
.unwrap();
let context1 = RequestContext::new().with_header("X-API-Key", "secret123");
let context2 = RequestContext::new().with_header("X-API-Key", "secret456");
assert!(matcher.matches(&context1).await.unwrap());
assert!(matcher.matches(&context2).await.unwrap());
}
#[tokio::test]
async fn test_header_matcher_load_config() {
let mut matcher = HeaderMatcher::new("X-API-Key", vec!["secret123".to_string()]).unwrap();
let config = serde_json::json!({
"header_name": "Authorization",
"allowed_values": ["Bearer token123"],
"case_sensitive": true
});
assert!(matcher.load_config(config).is_ok());
assert_eq!(matcher.header_name(), "authorization");
assert_eq!(matcher.allowed_values().len(), 1);
}
#[tokio::test]
async fn test_registry_concurrent_register() {
let registry = Arc::new(CustomMatcherRegistry::new());
let mut handles = vec![];
for i in 0..10 {
let registry_clone = Arc::clone(®istry);
handles.push(tokio::spawn(async move {
let matcher = TimeWindowMatcher::new(i as u8, (i + 10) as u8);
registry_clone
.register(format!("matcher_{}", i), Box::new(matcher))
.await
}));
}
let mut success_count = 0;
for handle in handles {
if handle.await.unwrap().is_ok() {
success_count += 1;
}
}
assert_eq!(success_count, 10);
assert_eq!(registry.count().await, 10);
}
#[tokio::test]
async fn test_registry_concurrent_match() {
let registry = Arc::new(CustomMatcherRegistry::new());
let matcher = TimeWindowMatcher::new(0, 23);
registry
.register("time_window".to_string(), Box::new(matcher))
.await
.unwrap();
let mut handles = vec![];
for _ in 0..100 {
let registry_clone = Arc::clone(®istry);
handles.push(tokio::spawn(async move {
let context = RequestContext::new();
registry_clone.match_with("time_window", &context).await
}));
}
let mut success_count = 0;
for handle in handles {
if let Ok(Ok(true)) = handle.await {
success_count += 1;
}
}
assert_eq!(success_count, 100);
}
#[test]
fn test_validate_matcher_name_empty() {
let result = validate_matcher_name("");
assert!(result.is_err());
}
#[test]
fn test_validate_matcher_name_too_long() {
let long_name = "a".repeat(MAX_MATCHER_NAME_LENGTH + 1);
let result = validate_matcher_name(&long_name);
assert!(result.is_err());
}
#[test]
fn test_validate_matcher_name_invalid_chars() {
let result = validate_matcher_name("invalid@name!");
assert!(result.is_err());
}
#[test]
fn test_validate_header_name_empty() {
let result = validate_header_name("");
assert!(result.is_err());
}
#[test]
fn test_validate_header_name_too_long() {
let long_name = "a".repeat(MAX_HEADER_NAME_LENGTH + 1);
let result = validate_header_name(&long_name);
assert!(result.is_err());
}
#[test]
fn test_validate_header_name_invalid_chars() {
let result = validate_header_name("X-Header@Name");
assert!(result.is_err());
}
#[test]
fn test_validate_header_value_too_long() {
let long_value = "a".repeat(MAX_HEADER_VALUE_LENGTH + 1);
let result = validate_header_value(&long_value);
assert!(result.is_err());
assert!(validate_header_value("").is_ok());
assert!(validate_header_value("valid-value").is_ok());
}
#[tokio::test]
async fn test_registry_default() {
let registry = CustomMatcherRegistry::default();
assert_eq!(registry.count().await, 0);
}
#[test]
fn test_registry_debug() {
let registry = CustomMatcherRegistry::new();
let debug_str = format!("{:?}", registry);
assert!(debug_str.contains("CustomMatcherRegistry"));
assert!(debug_str.contains("<custom matchers>"));
}
#[tokio::test]
async fn test_registry_builder() {
let registry = CustomMatcherRegistry::builder().build();
assert_eq!(registry.count().await, 0);
}
#[tokio::test]
async fn test_registry_with_dependencies() {
let registry = CustomMatcherRegistry::with_dependencies();
assert_eq!(registry.count().await, 0);
}
#[tokio::test]
async fn test_registry_register_with_invalid_name() {
let registry = CustomMatcherRegistry::new();
let matcher = TimeWindowMatcher::new(9, 18);
let result = registry.register("".to_string(), Box::new(matcher)).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_registry_get_found() {
let registry = CustomMatcherRegistry::new();
registry
.register("test".to_string(), Box::new(TimeWindowMatcher::new(9, 18)))
.await
.unwrap();
let result = registry.get("test").await;
assert!(result.is_none());
}
#[tokio::test]
async fn test_registry_get_not_found() {
let registry = CustomMatcherRegistry::new();
let result = registry.get("nonexistent").await;
assert!(result.is_none());
}
#[tokio::test]
async fn test_registry_list_empty() {
let registry = CustomMatcherRegistry::new();
let list = registry.list().await;
assert!(list.is_empty());
}
#[test]
fn test_time_window_matcher_builder() {
let matcher = TimeWindowMatcher::builder()
.start_hour(8)
.end_hour(20)
.build();
assert_eq!(matcher.start_hour(), 8);
assert_eq!(matcher.end_hour(), 20);
}
#[test]
fn test_time_window_matcher_with_dependencies() {
let matcher = TimeWindowMatcher::with_dependencies(7, 19);
assert_eq!(matcher.start_hour(), 7);
assert_eq!(matcher.end_hour(), 19);
}
#[tokio::test]
async fn test_time_window_matcher_cross_midnight() {
let matcher = TimeWindowMatcher::new(12, 11);
let context = RequestContext::new();
let result = matcher.matches(&context).await.unwrap();
assert!(result);
}
#[test]
fn test_time_window_matcher_load_config_missing_start_hour() {
let mut matcher = TimeWindowMatcher::new(9, 18);
let config = serde_json::json!({ "end_hour": 20 });
let result = matcher.load_config(config);
assert!(result.is_err());
}
#[test]
fn test_time_window_matcher_load_config_missing_end_hour() {
let mut matcher = TimeWindowMatcher::new(9, 18);
let config = serde_json::json!({ "start_hour": 10 });
let result = matcher.load_config(config);
assert!(result.is_err());
}
#[test]
fn test_time_window_matcher_load_config_start_out_of_range() {
let mut matcher = TimeWindowMatcher::new(9, 18);
let config = serde_json::json!({
"start_hour": 25,
"end_hour": 20,
});
let result = matcher.load_config(config);
assert!(result.is_err());
}
#[test]
fn test_time_window_matcher_load_config_end_out_of_range() {
let mut matcher = TimeWindowMatcher::new(9, 18);
let config = serde_json::json!({
"start_hour": 10,
"end_hour": 25,
});
let result = matcher.load_config(config);
assert!(result.is_err());
}
#[test]
fn test_header_matcher_new_empty_name() {
let result = HeaderMatcher::new("", vec!["value".to_string()]);
assert!(result.is_err());
}
#[test]
fn test_header_matcher_new_invalid_name_chars() {
let result = HeaderMatcher::new("X-Header@Name", vec!["value".to_string()]);
assert!(result.is_err());
}
#[test]
fn test_header_matcher_new_too_many_values() {
let values: Vec<String> = (0..=MAX_ALLOWED_VALUES_COUNT)
.map(|i| format!("val_{}", i))
.collect();
let result = HeaderMatcher::new("X-Test", values);
assert!(result.is_err());
}
#[test]
fn test_header_matcher_new_value_too_long() {
let long_value = "a".repeat(MAX_HEADER_VALUE_LENGTH + 1);
let result = HeaderMatcher::new("X-Test", vec![long_value]);
assert!(result.is_err());
}
#[test]
fn test_header_matcher_builder() {
let matcher = HeaderMatcher::builder()
.header_name("X-Custom")
.add_allowed_value("val1")
.add_allowed_value("val2")
.case_sensitive(true)
.build()
.unwrap();
assert_eq!(matcher.header_name(), "x-custom");
assert_eq!(matcher.allowed_values().len(), 2);
}
#[test]
fn test_header_matcher_with_dependencies() {
let matcher =
HeaderMatcher::with_dependencies("X-Custom", vec!["value1".to_string()], true).unwrap();
assert_eq!(matcher.header_name(), "x-custom");
assert_eq!(matcher.allowed_values().len(), 1);
}
#[tokio::test]
async fn test_header_matcher_case_sensitive_match() {
let matcher = HeaderMatcher::new("X-Key", vec!["ExactMatch".to_string()])
.unwrap()
.with_case_sensitive(true);
let context = RequestContext::new().with_header("x-key", "ExactMatch");
let result = matcher.matches(&context).await.unwrap();
assert!(result);
}
#[test]
fn test_header_matcher_load_config_partial_header_name() {
let mut matcher = HeaderMatcher::new("X-Original", vec!["value".to_string()]).unwrap();
let config = serde_json::json!({ "header_name": "Authorization" });
assert!(matcher.load_config(config).is_ok());
assert_eq!(matcher.header_name(), "authorization");
assert_eq!(matcher.allowed_values().len(), 1);
}
#[test]
fn test_header_matcher_load_config_partial_values() {
let mut matcher = HeaderMatcher::new("X-Original", vec!["old_value".to_string()]).unwrap();
let config = serde_json::json!({ "allowed_values": ["new_value"] });
assert!(matcher.load_config(config).is_ok());
assert_eq!(matcher.header_name(), "x-original");
assert_eq!(matcher.allowed_values().len(), 1);
assert_eq!(matcher.allowed_values()[0], "new_value");
}
#[test]
fn test_header_matcher_load_config_partial_case_sensitive() {
let mut matcher = HeaderMatcher::new("X-Test", vec!["value".to_string()]).unwrap();
let config = serde_json::json!({ "case_sensitive": true });
assert!(matcher.load_config(config).is_ok());
assert!(matcher.case_sensitive);
}
#[test]
fn test_header_matcher_load_config_too_many_values() {
let mut matcher = HeaderMatcher::new("X-Test", vec!["value".to_string()]).unwrap();
let values: Vec<String> = (0..=MAX_ALLOWED_VALUES_COUNT)
.map(|i| format!("value_{}", i))
.collect();
let config = serde_json::json!({ "allowed_values": values });
let result = matcher.load_config(config);
assert!(result.is_err());
}
#[test]
fn test_header_matcher_load_config_non_string_value() {
let mut matcher = HeaderMatcher::new("X-Test", vec!["valid".to_string()]).unwrap();
let config = serde_json::json!({
"allowed_values": ["valid1", 123, "valid2"],
});
assert!(matcher.load_config(config).is_ok());
assert_eq!(matcher.allowed_values().len(), 2);
assert_eq!(matcher.allowed_values()[0], "valid1");
assert_eq!(matcher.allowed_values()[1], "valid2");
}
#[test]
fn test_header_matcher_builder_allowed_values() {
let values = vec!["v1".to_string(), "v2".to_string(), "v3".to_string()];
let matcher = HeaderMatcher::builder()
.header_name("X-Test")
.allowed_values(values)
.build()
.unwrap();
assert_eq!(matcher.allowed_values().len(), 3);
assert_eq!(matcher.allowed_values()[0], "v1");
assert_eq!(matcher.allowed_values()[2], "v3");
}
}