use std::sync::Arc;
use axum::body::Body;
use axum::http::Request;
use limiteron::Governor;
use crate::security::ratelimit::{LimiteronAdapter, RateLimiter};
#[tokio::test]
async fn new_returns_non_panic_instance() {
let adapter = LimiteronAdapter::new().await;
drop(adapter);
}
#[tokio::test]
async fn default_matches_new_behavior() {
let from_new = LimiteronAdapter::new().await;
let from_default = LimiteronAdapter::default().await;
drop(from_new);
drop(from_default);
}
#[tokio::test]
async fn builder_build_with_default_config_succeeds() {
let adapter = LimiteronAdapter::builder()
.build()
.await
.expect("default_config must produce a valid FlowControlConfig");
let result = adapter.check("127.0.0.1").await;
assert!(
result.is_ok(),
"builder-built adapter should allow: {:?}",
result
);
}
#[tokio::test]
async fn builder_build_returns_err_on_invalid_config() {
use limiteron::config::GlobalConfig;
use limiteron::FlowControlConfig;
let invalid_config = FlowControlConfig {
version: "0.1.0".to_string(),
global: GlobalConfig::default(),
rules: vec![], };
let result = LimiteronAdapter::builder()
.with_config(invalid_config)
.build()
.await;
assert!(
result.is_err(),
"build() with empty rules must return Err, not panic — got Ok"
);
}
#[tokio::test]
async fn with_dependencies_accepts_prebuilt_governor() {
use limiteron::config::{GlobalConfig, Matcher, Rule};
use limiteron::storage::{MemoryBanStorage, MemoryStorage};
use limiteron::{ActionConfig, BanStorage, FlowControlConfig, LimiterConfig, Storage};
let config = FlowControlConfig {
version: "0.1.0".to_string(),
global: GlobalConfig::default(),
rules: vec![Rule {
id: "di-test".to_string(),
name: "DI test rule".to_string(),
priority: 100,
matchers: vec![Matcher::Ip {
ip_ranges: vec!["0.0.0.0/0".to_string()],
}],
limiters: vec![LimiterConfig::TokenBucket {
capacity: 5,
refill_rate: 1,
}],
action: ActionConfig::default(),
}],
};
let storage: Arc<dyn Storage> = Arc::new(MemoryStorage::new());
let ban_storage: Arc<dyn BanStorage> = Arc::new(MemoryBanStorage::new());
let governor = Governor::builder()
.with_config(config)
.with_storage(storage)
.with_ban_storage(ban_storage)
.build()
.await
.expect("DI test config must be valid");
let adapter = LimiteronAdapter::with_dependencies(Arc::new(governor));
let result = adapter.check("10.0.0.1").await;
assert!(
result.is_ok(),
"DI adapter should allow first request: {:?}",
result
);
}
#[tokio::test]
async fn check_allows_first_request_with_default_config() {
let adapter = LimiteronAdapter::new().await;
let result = adapter.check("127.0.0.1").await;
assert!(
result.is_ok(),
"first request should be allowed: {:?}",
result
);
}
#[tokio::test]
async fn check_returns_exceeded_after_capacity_exhausted() {
use limiteron::config::{GlobalConfig, Matcher, Rule};
use limiteron::storage::{MemoryBanStorage, MemoryStorage};
use limiteron::{ActionConfig, BanStorage, FlowControlConfig, LimiterConfig, Storage};
let config = FlowControlConfig {
version: "0.1.0".to_string(),
global: GlobalConfig::default(),
rules: vec![Rule {
id: "tight".to_string(),
name: "tight bucket".to_string(),
priority: 100,
matchers: vec![Matcher::Ip {
ip_ranges: vec!["0.0.0.0/0".to_string()],
}],
limiters: vec![LimiterConfig::TokenBucket {
capacity: 2,
refill_rate: 1,
}],
action: ActionConfig::default(),
}],
};
let storage: Arc<dyn Storage> = Arc::new(MemoryStorage::new());
let ban_storage: Arc<dyn BanStorage> = Arc::new(MemoryBanStorage::new());
let governor = Governor::builder()
.with_config(config)
.with_storage(storage)
.with_ban_storage(ban_storage)
.with_l1_cache_enabled(false)
.build()
.await
.expect("tight config must be valid");
let adapter = LimiteronAdapter::with_dependencies(Arc::new(governor));
let mut allowed = 0;
let mut rejected_with_exceeded = 0;
let mut other = 0;
for _ in 0..5 {
match adapter.check("192.0.2.1").await {
Ok(()) => allowed += 1,
Err(crate::security::ratelimit::RateLimitError::Exceeded { .. }) => {
rejected_with_exceeded += 1;
}
Err(other_err) => {
other += 1;
eprintln!("unexpected error: {:?}", other_err);
}
}
}
assert_eq!(other, 0, "no unexpected error variants allowed");
assert!(
rejected_with_exceeded >= 2,
"expected at least 2 Exceeded rejections (capacity=2, 5 requests), got allowed={}, exceeded={}",
allowed,
rejected_with_exceeded
);
}
#[tokio::test]
async fn check_request_extracts_ip_from_x_forwarded_for() {
let adapter = LimiteronAdapter::new().await;
let req = Request::builder()
.header("x-forwarded-for", "203.0.113.7, 10.0.0.1")
.body(Body::empty())
.expect("request build");
let result = adapter.check_request(&req).await;
assert!(result.is_ok(), "check_request should allow: {:?}", result);
}
#[tokio::test]
async fn check_request_falls_back_to_x_real_ip() {
let adapter = LimiteronAdapter::new().await;
let req = Request::builder()
.header("x-real-ip", "198.51.100.42")
.body(Body::empty())
.expect("request build");
let result = adapter.check_request(&req).await;
assert!(
result.is_ok(),
"check_request should allow via X-Real-IP: {:?}",
result
);
}
#[tokio::test]
async fn check_request_falls_back_to_unknown_identifier() {
let adapter = LimiteronAdapter::new().await;
let req = Request::builder()
.body(Body::empty())
.expect("request build");
let result = adapter.check_request(&req).await;
assert!(
result.is_ok(),
"check_request should allow unknown: {:?}",
result
);
}
#[tokio::test]
async fn check_request_ignores_spoofed_x_forwarded_for_from_non_trusted_source() {
use axum::extract::connect_info::ConnectInfo;
use limiteron::config::{GlobalConfig, Matcher, Rule};
use limiteron::storage::{MemoryBanStorage, MemoryStorage};
use limiteron::{ActionConfig, BanStorage, FlowControlConfig, LimiterConfig, Storage};
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
let config = FlowControlConfig {
version: "0.1.0".to_string(),
global: GlobalConfig::default(),
rules: vec![Rule {
id: "anti-spoof".to_string(),
name: "anti-spoofing rule".to_string(),
priority: 100,
matchers: vec![Matcher::Ip {
ip_ranges: vec!["0.0.0.0/0".to_string()],
}],
limiters: vec![LimiterConfig::TokenBucket {
capacity: 1,
refill_rate: 1,
}],
action: ActionConfig::default(),
}],
};
let storage: Arc<dyn Storage> = Arc::new(MemoryStorage::new());
let ban_storage: Arc<dyn BanStorage> = Arc::new(MemoryBanStorage::new());
let governor = Governor::builder()
.with_config(config)
.with_storage(storage)
.with_ban_storage(ban_storage)
.with_l1_cache_enabled(false)
.build()
.await
.expect("anti-spoof config must be valid");
let adapter = LimiteronAdapter::with_dependencies(Arc::new(governor));
let mut req1 = Request::builder()
.body(Body::empty())
.expect("request build");
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8)), 8080);
req1.extensions_mut().insert(ConnectInfo(addr));
req1.headers_mut()
.insert("X-Forwarded-For", "1.2.3.4".parse().unwrap());
let mut req2 = Request::builder()
.body(Body::empty())
.expect("request build");
req2.extensions_mut().insert(ConnectInfo(addr));
req2.headers_mut()
.insert("X-Forwarded-For", "5.6.7.8".parse().unwrap());
let r1 = adapter.check_request(&req1).await;
let r2 = adapter.check_request(&req2).await;
let rejections = [r1, r2].iter().filter(|r| r.is_err()).count();
assert!(
rejections >= 1,
"expected at least 1 rejection (same direct IP, capacity=1), got 0 — spoofed X-Forwarded-For was likely trusted"
);
}
#[tokio::test]
async fn check_request_trusts_x_forwarded_for_from_trusted_proxy() {
use axum::extract::connect_info::ConnectInfo;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
let adapter = LimiteronAdapter::new().await;
let mut req = Request::builder()
.body(Body::empty())
.expect("request build");
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), 8080);
req.extensions_mut().insert(ConnectInfo(addr));
req.headers_mut()
.insert("X-Forwarded-For", "203.0.113.5".parse().unwrap());
let result = adapter.check_request(&req).await;
assert!(
result.is_ok(),
"request via trusted proxy with valid X-Forwarded-For should be allowed: {:?}",
result
);
}