mod builder;
mod error;
mod gcra;
mod middleware;
mod types;
pub use builder::{HostBuilder, HostRouteBuilder, RateLimitBuilder, RouteBuilder};
pub use error::RateLimitError;
pub use middleware::RateLimitMiddleware;
pub use types::{RateLimit, Route, ThrottleBehavior};
#[cfg(test)]
mod tests {
use super::*;
use http::Method;
use std::time::Duration;
#[test]
fn test_route_matching_all() {
let route = Route {
host: None,
method: None,
path_prefix: String::new(),
limits: vec![],
on_limit: ThrottleBehavior::Delay,
};
let req = reqwest::Client::new()
.get("https://example.com/test")
.build()
.unwrap();
assert!(route.matches(&req));
}
#[test]
fn test_route_matching_host() {
let route = Route {
host: Some("api.example.com".to_string()),
method: None,
path_prefix: String::new(),
limits: vec![],
on_limit: ThrottleBehavior::Delay,
};
let req_match = reqwest::Client::new()
.get("https://api.example.com/test")
.build()
.unwrap();
let req_no_match = reqwest::Client::new()
.get("https://other.example.com/test")
.build()
.unwrap();
assert!(route.matches(&req_match));
assert!(!route.matches(&req_no_match));
}
#[test]
fn test_route_matching_method() {
let route = Route {
host: None,
method: Some(Method::POST),
path_prefix: String::new(),
limits: vec![],
on_limit: ThrottleBehavior::Delay,
};
let req_match = reqwest::Client::new()
.post("https://example.com/test")
.build()
.unwrap();
let req_no_match = reqwest::Client::new()
.get("https://example.com/test")
.build()
.unwrap();
assert!(route.matches(&req_match));
assert!(!route.matches(&req_no_match));
}
#[test]
fn test_route_matching_path_prefix() {
let route = Route {
host: None,
method: None,
path_prefix: "/api/v1".to_string(),
limits: vec![],
on_limit: ThrottleBehavior::Delay,
};
let req_match = reqwest::Client::new()
.get("https://example.com/api/v1/users")
.build()
.unwrap();
let req_no_match = reqwest::Client::new()
.get("https://example.com/api/v2/users")
.build()
.unwrap();
assert!(route.matches(&req_match));
assert!(!route.matches(&req_no_match));
}
#[test]
fn test_route_matching_path_segment_boundary() {
let route = Route {
host: None,
method: None,
path_prefix: "/order".to_string(),
limits: vec![],
on_limit: ThrottleBehavior::Delay,
};
let req_exact = reqwest::Client::new()
.get("https://example.com/order")
.build()
.unwrap();
let req_trailing = reqwest::Client::new()
.get("https://example.com/order/")
.build()
.unwrap();
let req_subpath = reqwest::Client::new()
.get("https://example.com/order/123")
.build()
.unwrap();
assert!(route.matches(&req_exact), "/order should match /order");
assert!(route.matches(&req_trailing), "/order should match /order/");
assert!(
route.matches(&req_subpath),
"/order should match /order/123"
);
let req_orders = reqwest::Client::new()
.get("https://example.com/orders")
.build()
.unwrap();
let req_order_dash = reqwest::Client::new()
.get("https://example.com/order-test")
.build()
.unwrap();
assert!(
!route.matches(&req_orders),
"/order should NOT match /orders"
);
assert!(
!route.matches(&req_order_dash),
"/order should NOT match /order-test"
);
}
#[test]
fn test_emission_interval() {
let limit = RateLimit::new(100, Duration::from_secs(10));
assert_eq!(limit.emission_interval(), Duration::from_millis(100));
let limit = RateLimit::new(1000, Duration::from_secs(60));
assert_eq!(limit.emission_interval(), Duration::from_millis(60));
}
#[test]
#[should_panic(expected = "requests must be greater than 0")]
fn test_zero_requests_panics() {
RateLimit::new(0, Duration::from_secs(10));
}
#[test]
#[should_panic(expected = "window must be greater than 0")]
fn test_zero_window_panics() {
RateLimit::new(100, Duration::ZERO);
}
#[test]
#[should_panic(expected = "window must not exceed u64::MAX nanoseconds")]
fn test_overflow_window_panics() {
RateLimit::new(100, Duration::from_secs(600 * 365 * 24 * 60 * 60));
}
}