use super::{PeriodicStore, RateLimiter};
use std::time::{Duration, SystemTime};
#[test]
fn test_basic_rate_limiting() {
let mut limiter = RateLimiter::new(PeriodicStore::new());
let now = SystemTime::now();
let (allowed, result) = limiter.rate_limit("test", 5, 10, 60, 1, now).unwrap();
assert!(allowed);
assert_eq!(result.limit, 5);
assert_eq!(result.remaining, 4);
}
#[test]
fn test_burst_capacity() {
let mut limiter = RateLimiter::new(PeriodicStore::new());
let now = SystemTime::now();
for i in 0..5 {
let (allowed, result) = limiter.rate_limit("burst_test", 5, 10, 60, 1, now).unwrap();
assert!(allowed, "Request {} should be allowed", i + 1);
assert_eq!(result.remaining, 5 - (i + 1) as i64);
}
let (allowed, result) = limiter.rate_limit("burst_test", 5, 10, 60, 1, now).unwrap();
assert!(!allowed);
assert_eq!(result.remaining, 0);
assert!(result.retry_after.as_secs() > 0);
}
#[test]
fn test_rate_replenishment() {
let mut limiter = RateLimiter::new(PeriodicStore::new());
let now = SystemTime::now();
let (allowed1, _) = limiter
.rate_limit("replenish_test", 2, 60, 60, 1, now)
.unwrap();
let (allowed2, _) = limiter
.rate_limit("replenish_test", 2, 60, 60, 1, now)
.unwrap();
assert!(allowed1);
assert!(allowed2);
let (allowed3, _result) = limiter
.rate_limit("replenish_test", 2, 60, 60, 1, now)
.unwrap();
assert!(!allowed3);
let later = now + Duration::from_secs(1);
let (allowed4, _) = limiter
.rate_limit("replenish_test", 2, 60, 60, 1, later)
.unwrap();
assert!(allowed4);
}
#[test]
fn test_different_keys() {
let mut limiter = RateLimiter::new(PeriodicStore::new());
let now = SystemTime::now();
let (allowed1, _) = limiter.rate_limit("key1", 2, 2, 60, 1, now).unwrap();
let (allowed2, _) = limiter.rate_limit("key2", 2, 2, 60, 1, now).unwrap();
assert!(allowed1);
assert!(allowed2);
let (allowed3, _) = limiter.rate_limit("key1", 2, 2, 60, 1, now).unwrap();
assert!(allowed3);
let (allowed4, _) = limiter.rate_limit("key1", 2, 2, 60, 1, now).unwrap();
assert!(!allowed4);
let (allowed5, _) = limiter.rate_limit("key2", 2, 2, 60, 1, now).unwrap();
assert!(allowed5);
let (allowed6, _) = limiter.rate_limit("key2", 2, 2, 60, 1, now).unwrap();
assert!(!allowed6);
}
#[test]
fn test_quantity_parameter() {
let mut limiter = RateLimiter::new(PeriodicStore::new());
let now = SystemTime::now();
let (allowed1, result1) = limiter
.rate_limit("quantity_test", 10, 10, 60, 5, now)
.unwrap();
assert!(allowed1);
assert_eq!(result1.remaining, 5);
let (allowed2, result2) = limiter
.rate_limit("quantity_test", 10, 10, 60, 6, now)
.unwrap();
assert!(!allowed2);
assert_eq!(result2.remaining, 5);
let (allowed3, result3) = limiter
.rate_limit("quantity_test", 10, 10, 60, 5, now)
.unwrap();
assert!(allowed3);
assert_eq!(result3.remaining, 0);
}
#[test]
fn test_negative_quantity_error() {
let mut limiter = RateLimiter::new(PeriodicStore::new());
let now = SystemTime::now();
let result = limiter.rate_limit("negative_test", 10, 10, 60, -1, now);
assert!(result.is_err());
}
#[test]
fn test_invalid_parameters() {
let mut limiter = RateLimiter::new(PeriodicStore::new());
let now = SystemTime::now();
let result = limiter.rate_limit("test", 0, 10, 60, 1, now);
assert!(result.is_err());
let result = limiter.rate_limit("test", 10, 0, 60, 1, now);
assert!(result.is_err());
let result = limiter.rate_limit("test", 10, 10, 0, 1, now);
assert!(result.is_err());
}
#[test]
fn test_large_quantity_overflow_protection() {
let mut limiter = RateLimiter::new(PeriodicStore::new());
let now = SystemTime::now();
let result = limiter.rate_limit("overflow_test", 10, 10, 60, i64::MAX / 2, now);
assert!(result.is_ok());
let (allowed, _) = result.unwrap();
assert!(!allowed);
}
#[test]
fn test_saturating_arithmetic() {
let mut limiter = RateLimiter::new(PeriodicStore::new());
let now = SystemTime::now();
let result = limiter.rate_limit("saturate_test", i64::MAX / 1000, 100, 60, 1, now);
assert!(result.is_ok());
let result = limiter.rate_limit("saturate_test2", 10, i64::MAX / 1000, 60, 1, now);
assert!(result.is_ok());
}
#[test]
fn test_remaining_count_accuracy() {
let mut limiter = RateLimiter::new(PeriodicStore::new());
let start_time = SystemTime::now();
let burst = 5;
let rate = 10;
let period = 60;
let (allowed, result) = limiter
.rate_limit("remaining_test", burst, rate, period, 1, start_time)
.unwrap();
assert!(allowed);
assert_eq!(
result.remaining, 4,
"First request should leave 4 remaining"
);
for i in 2..=5 {
let (allowed, result) = limiter
.rate_limit("remaining_test", burst, rate, period, 1, start_time)
.unwrap();
assert!(allowed, "Request {i} should be allowed");
assert_eq!(
result.remaining,
(5 - i) as i64,
"Request {} should leave {} remaining",
i,
5 - i
);
}
let (allowed, result) = limiter
.rate_limit("remaining_test", burst, rate, period, 1, start_time)
.unwrap();
assert!(!allowed, "Should be rate limited after burst");
assert_eq!(result.remaining, 0, "Should have 0 remaining when blocked");
assert!(
result.retry_after.as_secs() > 0,
"Should have positive retry_after"
);
let after_replenish = start_time + Duration::from_secs(6);
let (allowed, result) = limiter
.rate_limit("remaining_test", burst, rate, period, 1, after_replenish)
.unwrap();
assert!(allowed, "Should allow after replenishment");
assert_eq!(
result.remaining, 0,
"Should have 0 remaining after using replenished token"
);
let (allowed, result) = limiter
.rate_limit("remaining_test", burst, rate, period, 1, after_replenish)
.unwrap();
assert!(!allowed, "Should be blocked again");
assert_eq!(result.remaining, 0);
let (allowed, result) = limiter
.rate_limit("quantity_remaining", burst, rate, period, 3, start_time)
.unwrap();
assert!(allowed);
assert_eq!(
result.remaining, 2,
"Using quantity=3 should leave 2 remaining"
);
let (allowed, result) = limiter
.rate_limit("quantity_remaining", burst, rate, period, 3, start_time)
.unwrap();
assert!(!allowed, "Quantity larger than remaining should be blocked");
assert_eq!(
result.remaining, 2,
"Remaining should not change on blocked request"
);
let (allowed, result) = limiter
.rate_limit("quantity_remaining", burst, rate, period, 2, start_time)
.unwrap();
assert!(allowed, "Quantity equal to remaining should be allowed");
assert_eq!(result.remaining, 0);
let (allowed, result) = limiter
.rate_limit("high_rate", 10, 600, 60, 1, start_time)
.unwrap();
assert!(allowed);
assert_eq!(result.remaining, 9);
let one_sec_later = start_time + Duration::from_secs(1);
for _ in 0..9 {
limiter
.rate_limit("high_rate", 10, 600, 60, 1, start_time)
.unwrap();
}
let (allowed, result) = limiter
.rate_limit("high_rate", 10, 600, 60, 1, one_sec_later)
.unwrap();
assert!(allowed, "Should have replenished tokens");
assert!(
result.remaining < 10,
"Should not be at full capacity immediately"
);
}
#[test]
fn test_remaining_count_all_stores() {
use super::{AdaptiveStore, ProbabilisticStore};
fn test_scenario<S: super::Store>(mut limiter: RateLimiter<S>) {
let now = SystemTime::now();
let burst = 3;
let rate = 6;
let period = 60;
for i in 1..=3 {
let (allowed, result) = limiter
.rate_limit("test_key", burst, rate, period, 1, now)
.unwrap();
assert!(allowed, "Request {i} should be allowed");
assert_eq!(result.remaining, (3 - i) as i64);
}
let (allowed, result) = limiter
.rate_limit("test_key", burst, rate, period, 1, now)
.unwrap();
assert!(!allowed);
assert_eq!(result.remaining, 0);
let later = now + Duration::from_secs(10);
let (allowed, result) = limiter
.rate_limit("test_key", burst, rate, period, 1, later)
.unwrap();
assert!(allowed);
assert_eq!(
result.remaining, 0,
"Should use the replenished token immediately"
);
}
test_scenario(RateLimiter::new(PeriodicStore::new()));
test_scenario(RateLimiter::new(AdaptiveStore::new()));
test_scenario(RateLimiter::new(ProbabilisticStore::new()));
}
#[test]
fn test_edge_cases_zero_remaining() {
let mut limiter = RateLimiter::new(PeriodicStore::new());
let now = SystemTime::now();
let (allowed, result) = limiter
.rate_limit("exact_timing", 2, 120, 60, 1, now)
.unwrap();
assert!(allowed);
assert_eq!(result.remaining, 1);
let (allowed, result) = limiter
.rate_limit("exact_timing", 2, 120, 60, 1, now)
.unwrap();
assert!(allowed);
assert_eq!(result.remaining, 0);
let half_sec = now + Duration::from_millis(500);
let (allowed, result) = limiter
.rate_limit("exact_timing", 2, 120, 60, 1, half_sec)
.unwrap();
assert!(allowed);
assert_eq!(result.remaining, 0);
let result = limiter.rate_limit("zero_period", 10, 10, 0, 1, now);
assert!(result.is_err(), "Zero period should error");
let (allowed, result) = limiter.rate_limit("fractional", 3, 7, 60, 1, now).unwrap();
assert!(allowed);
assert_eq!(result.remaining, 2);
limiter.rate_limit("fractional", 3, 7, 60, 1, now).unwrap();
limiter.rate_limit("fractional", 3, 7, 60, 1, now).unwrap();
let eight_sec = now + Duration::from_secs(8);
let (allowed, _) = limiter
.rate_limit("fractional", 3, 7, 60, 1, eight_sec)
.unwrap();
assert!(!allowed, "Should not have token after 8 seconds");
let nine_sec = now + Duration::from_secs(9);
let (allowed, result) = limiter
.rate_limit("fractional", 3, 7, 60, 1, nine_sec)
.unwrap();
assert!(allowed, "Should have token after 9 seconds");
assert_eq!(result.remaining, 0);
let (allowed, result) = limiter
.rate_limit("max_burst", i64::MAX / 1000, 100, 60, 1, now)
.unwrap();
assert!(allowed);
assert!(result.remaining > 0, "Should handle large burst values");
}
#[test]
fn test_quantity_variations_and_replenishment() {
let mut limiter = RateLimiter::new(PeriodicStore::new());
let start_time = SystemTime::now();
let (allowed, result) = limiter
.rate_limit("multi_quantity", 10, 60, 60, 5, start_time)
.unwrap();
assert!(allowed);
assert_eq!(result.remaining, 5, "Should have 5 remaining after using 5");
let (allowed, result) = limiter
.rate_limit("multi_quantity", 10, 60, 60, 6, start_time)
.unwrap();
assert!(!allowed, "Should not allow quantity larger than remaining");
assert_eq!(
result.remaining, 5,
"Remaining should not change on failure"
);
let (allowed, result) = limiter
.rate_limit("multi_quantity", 10, 60, 60, 5, start_time)
.unwrap();
assert!(allowed);
assert_eq!(result.remaining, 0);
let three_sec = start_time + Duration::from_secs(3);
let (allowed, result) = limiter
.rate_limit("multi_quantity", 10, 60, 60, 2, three_sec)
.unwrap();
assert!(allowed, "Should have replenished tokens");
assert_eq!(
result.remaining, 1,
"Should have 1 remaining after using 2 of 3"
);
let key = "gradual_replenish";
for _ in 0..5 {
limiter.rate_limit(key, 5, 120, 60, 1, start_time).unwrap();
}
let test_cases = vec![
(500, 1, 0), (1000, 2, 1), (1500, 3, 2), (2000, 4, 3), (2500, 5, 4), ];
for (millis, expected_available, expected_remaining) in test_cases {
let test_key = format!("gradual_replenish_{millis}");
for _ in 0..5 {
limiter
.rate_limit(&test_key, 5, 120, 60, 1, start_time)
.unwrap();
}
let time = start_time + Duration::from_millis(millis);
let (allowed, result) = limiter.rate_limit(&test_key, 5, 120, 60, 1, time).unwrap();
if expected_available > 0 {
assert!(allowed, "At {millis}ms should be allowed");
assert_eq!(
result.remaining, expected_remaining,
"At {millis}ms, should have {expected_available} tokens available, {expected_remaining} remaining after use"
);
} else {
assert!(!allowed, "At {millis}ms should be blocked");
}
}
}
#[test]
fn test_complex_replenishment_scenarios() {
let mut limiter = RateLimiter::new(PeriodicStore::new());
let start = SystemTime::now();
let key = "partial_burst";
let (allowed, result) = limiter.rate_limit(key, 8, 240, 60, 6, start).unwrap();
assert!(allowed);
assert_eq!(result.remaining, 2);
let half_sec = start + Duration::from_millis(500);
let (allowed, result) = limiter.rate_limit(key, 8, 240, 60, 1, half_sec).unwrap();
assert!(allowed);
assert_eq!(result.remaining, 3);
let one_half_sec = start + Duration::from_millis(1500);
let (allowed, result) = limiter
.rate_limit(key, 8, 240, 60, 1, one_half_sec)
.unwrap();
assert!(allowed);
assert_eq!(
result.remaining, 6,
"Should have 6 remaining after using 1 of 7"
);
let key2 = "slow_replenish";
for _ in 0..3 {
limiter.rate_limit(key2, 3, 6, 60, 1, start).unwrap();
}
let five_sec = start + Duration::from_secs(5);
let (allowed, _) = limiter.rate_limit(key2, 3, 6, 60, 1, five_sec).unwrap();
assert!(!allowed, "Should not have token after 5 seconds");
let ten_sec = start + Duration::from_secs(10);
let (allowed, result) = limiter.rate_limit(key2, 3, 6, 60, 1, ten_sec).unwrap();
assert!(allowed);
assert_eq!(result.remaining, 0);
let twenty_sec = start + Duration::from_secs(20);
let (allowed, result) = limiter.rate_limit(key2, 3, 6, 60, 1, twenty_sec).unwrap();
assert!(allowed);
assert_eq!(result.remaining, 0);
let key3 = "fractional_accumulation";
for _ in 0..5 {
limiter.rate_limit(key3, 5, 100, 60, 1, start).unwrap();
}
let replenish_tests = vec![
(600, true, 0), (1200, true, 1), (1800, true, 2), (2400, true, 3), (3000, true, 4), ];
for (millis, should_allow, expected_remaining) in replenish_tests {
let test_key = format!("fractional_accumulation_{millis}");
for _ in 0..5 {
limiter.rate_limit(&test_key, 5, 100, 60, 1, start).unwrap();
}
let time = start + Duration::from_millis(millis);
let (allowed, result) = limiter.rate_limit(&test_key, 5, 100, 60, 1, time).unwrap();
assert_eq!(
allowed,
should_allow,
"At {}ms, request should be {}",
millis,
if should_allow { "allowed" } else { "blocked" }
);
if allowed {
assert_eq!(
result.remaining, expected_remaining,
"At {millis}ms, should have {expected_remaining} remaining"
);
}
}
}
#[test]
fn test_quantity_edge_cases() {
let mut limiter = RateLimiter::new(PeriodicStore::new());
let now = SystemTime::now();
let (allowed, result) = limiter
.rate_limit("zero_quantity", 10, 100, 60, 0, now)
.unwrap();
assert!(allowed, "Zero quantity should be allowed");
assert_eq!(result.remaining, 10, "Should not consume any tokens");
let result = limiter.rate_limit("neg_quantity", 10, 100, 60, -5, now);
assert!(result.is_err(), "Negative quantity should be invalid");
let (allowed, result) = limiter
.rate_limit("large_quantity", 5, 100, 60, 10, now)
.unwrap();
assert!(!allowed, "Quantity larger than burst should be blocked");
assert_eq!(
result.remaining, 5,
"Should still have full burst available"
);
let (allowed, result) = limiter
.rate_limit("exact_burst", 10, 100, 60, 10, now)
.unwrap();
assert!(allowed, "Should allow quantity equal to burst");
assert_eq!(result.remaining, 0, "Should have 0 remaining");
let key = "large_quantity_replenish";
let (allowed, result) = limiter.rate_limit(key, 20, 600, 60, 15, now).unwrap();
assert!(allowed);
assert_eq!(result.remaining, 5);
let one_sec = now + Duration::from_secs(1);
let (allowed, result) = limiter.rate_limit(key, 20, 600, 60, 12, one_sec).unwrap();
assert!(allowed, "Should allow 12 of 15 available");
assert_eq!(result.remaining, 3);
let (allowed, result) = limiter.rate_limit(key, 20, 600, 60, 5, one_sec).unwrap();
assert!(!allowed);
assert_eq!(result.remaining, 3);
}
#[test]
fn test_rapid_time_changes() {
let mut limiter = RateLimiter::new(PeriodicStore::new());
let base_time = SystemTime::now();
let (allowed1, _) = limiter
.rate_limit("time_jump", 3, 10, 60, 1, base_time)
.unwrap();
assert!(allowed1);
let time_back = base_time - Duration::from_secs(5);
let result_back = limiter.rate_limit("time_jump", 3, 10, 60, 1, time_back);
assert!(result_back.is_ok());
let time_forward = base_time + Duration::from_secs(10);
let (allowed2, _) = limiter
.rate_limit("time_jump", 3, 10, 60, 1, time_forward)
.unwrap();
assert!(allowed2);
for i in 0..5 {
let jittered_time = if i % 2 == 0 {
base_time + Duration::from_secs(i)
} else {
base_time - Duration::from_secs(i)
};
let result = limiter.rate_limit("time_jitter", 10, 10, 60, 1, jittered_time);
assert!(result.is_ok());
}
}