use rate_guard::{Nanos, Millis, Secs, MockTimeSource, RateLimit};
use rate_guard::limits::FixedWindowCounterBuilder;
use rate_guard::error::{RateLimitError};
use std::time::Duration;
#[cfg(test)]
mod fixed_window_counter_basic_functionality {
use super::*;
#[test]
fn test_initial_capacity() {
let counter = FixedWindowCounterBuilder::builder()
.capacity(100)
.window_duration(Duration::from_secs(60))
.with_time(MockTimeSource::new())
.with_precision::<Secs>()
.build()
.unwrap();
assert_eq!(counter.capacity_remaining().unwrap(), 100);
}
#[test]
fn test_acquire_within_window() {
let counter = FixedWindowCounterBuilder::builder()
.capacity(100)
.window_duration(Duration::from_secs(60))
.with_time(MockTimeSource::new())
.with_precision::<Secs>()
.build()
.unwrap();
assert!(counter.try_acquire(30).is_ok());
assert_eq!(counter.capacity_remaining().unwrap(), 70);
assert!(counter.try_acquire(20).is_ok());
assert_eq!(counter.capacity_remaining().unwrap(), 50);
}
#[test]
fn test_acquire_all_tokens_in_window() {
let counter = FixedWindowCounterBuilder::builder()
.capacity(50)
.window_duration(Duration::from_secs(60))
.with_time(MockTimeSource::new())
.with_precision::<Secs>()
.build()
.unwrap();
assert!(counter.try_acquire(50).is_ok());
assert_eq!(counter.capacity_remaining().unwrap(), 0);
}
#[test]
fn test_insufficient_tokens_in_window() {
let counter = FixedWindowCounterBuilder::builder()
.capacity(10)
.window_duration(Duration::from_secs(60))
.with_time(MockTimeSource::new())
.with_precision::<Secs>()
.build()
.unwrap();
assert!(counter.try_acquire(10).is_ok());
assert!(counter.try_acquire(5).is_err());
assert_eq!(counter.capacity_remaining().unwrap(), 0);
}
#[test]
fn test_acquire_beyond_capacity() {
let counter = FixedWindowCounterBuilder::builder()
.capacity(10)
.window_duration(Duration::from_secs(60))
.with_time(MockTimeSource::new())
.with_precision::<Secs>()
.build()
.unwrap();
assert!(counter.try_acquire(15).is_err());
assert_eq!(counter.capacity_remaining().unwrap(), 10); }
}
#[cfg(test)]
mod fixed_window_counter_window_behavior {
use super::*;
#[test]
fn test_window_reset() {
let time_source = MockTimeSource::new();
let counter = FixedWindowCounterBuilder::builder()
.capacity(100)
.window_duration(Duration::from_secs(60))
.with_time(time_source.clone())
.with_precision::<Secs>()
.build()
.unwrap();
assert!(counter.try_acquire(80).is_ok());
assert_eq!(counter.capacity_remaining().unwrap(), 20);
time_source.advance(Duration::from_secs(60));
assert_eq!(counter.capacity_remaining().unwrap(), 100);
assert!(counter.try_acquire(100).is_ok());
}
#[test]
fn test_partial_window_progression() {
let time_source = MockTimeSource::new();
let counter = FixedWindowCounterBuilder::builder()
.capacity(100)
.window_duration(Duration::from_secs(60))
.with_time(time_source.clone())
.with_precision::<Secs>()
.build()
.unwrap();
assert!(counter.try_acquire(50).is_ok());
assert_eq!(counter.capacity_remaining().unwrap(), 50);
time_source.advance(Duration::from_secs(30));
assert_eq!(counter.capacity_remaining().unwrap(), 50);
assert!(counter.try_acquire(30).is_ok());
assert_eq!(counter.capacity_remaining().unwrap(), 20);
}
#[test]
fn test_multiple_window_cycles() {
let time_source = MockTimeSource::new();
let counter = FixedWindowCounterBuilder::builder()
.capacity(50)
.window_duration(Duration::from_secs(30))
.with_time(time_source.clone())
.with_precision::<Secs>()
.build()
.unwrap();
assert!(counter.try_acquire(40).is_ok());
assert_eq!(counter.capacity_remaining().unwrap(), 10);
time_source.advance(Duration::from_secs(30));
assert_eq!(counter.capacity_remaining().unwrap(), 50);
assert!(counter.try_acquire(25).is_ok());
time_source.advance(Duration::from_secs(30));
assert_eq!(counter.capacity_remaining().unwrap(), 50);
assert!(counter.try_acquire(50).is_ok());
assert_eq!(counter.capacity_remaining().unwrap(), 0);
time_source.advance(Duration::from_secs(30));
assert_eq!(counter.capacity_remaining().unwrap(), 50);
}
#[test]
fn test_large_time_skip() {
let time_source = MockTimeSource::new();
let counter = FixedWindowCounterBuilder::builder()
.capacity(100)
.window_duration(Duration::from_secs(60))
.with_time(time_source.clone())
.with_precision::<Secs>()
.build()
.unwrap();
assert!(counter.try_acquire(90).is_ok());
assert_eq!(counter.capacity_remaining().unwrap(), 10);
time_source.advance(Duration::from_secs(600));
assert_eq!(counter.capacity_remaining().unwrap(), 100);
}
}
#[cfg(test)]
mod fixed_window_counter_precision_tests {
use super::*;
#[test]
fn test_millisecond_precision_windows() {
let time_source = MockTimeSource::new();
let counter = FixedWindowCounterBuilder::builder()
.capacity(100)
.window_duration(Duration::from_millis(1000))
.with_time(time_source.clone())
.with_precision::<Millis>()
.build()
.unwrap();
assert!(counter.try_acquire(50).is_ok());
time_source.advance(Duration::from_millis(999));
assert_eq!(counter.capacity_remaining().unwrap(), 50);
time_source.advance(Duration::from_millis(1));
assert_eq!(counter.capacity_remaining().unwrap(), 100); }
#[test]
fn test_nanosecond_precision_windows() {
let time_source = MockTimeSource::new();
let counter = FixedWindowCounterBuilder::builder()
.capacity(10)
.window_duration(Duration::from_nanos(1_000_000)) .with_time(time_source.clone())
.with_precision::<Nanos>()
.build()
.unwrap();
assert!(counter.try_acquire(5).is_ok());
time_source.advance(Duration::from_nanos(999_999));
assert_eq!(counter.capacity_remaining().unwrap(), 5);
time_source.advance(Duration::from_nanos(1));
assert_eq!(counter.capacity_remaining().unwrap(), 10); }
#[test]
fn test_second_precision_windows() {
let time_source = MockTimeSource::new();
let counter = FixedWindowCounterBuilder::builder()
.capacity(100)
.window_duration(Duration::from_secs(5))
.with_time(time_source.clone())
.with_precision::<Secs>()
.build()
.unwrap();
assert!(counter.try_acquire(30).is_ok());
time_source.advance(Duration::from_secs(4));
assert_eq!(counter.capacity_remaining().unwrap(), 70);
time_source.advance(Duration::from_secs(1));
assert_eq!(counter.capacity_remaining().unwrap(), 100); }
}
#[cfg(test)]
mod fixed_window_counter_verbose_errors {
use super::*;
#[test]
fn test_verbose_insufficient_capacity() {
let counter = FixedWindowCounterBuilder::builder()
.capacity(10)
.window_duration(Duration::from_secs(60))
.with_time(MockTimeSource::new())
.with_precision::<Secs>()
.build()
.unwrap();
assert!(counter.try_acquire(10).is_ok());
match counter.try_acquire_verbose(5) {
Err(RateLimitError::InsufficientCapacity { acquiring, available, retry_after }) => {
assert_eq!(acquiring, 5);
assert_eq!(available, 0);
assert!(retry_after > Duration::ZERO);
}
_ => panic!("Expected InsufficientCapacity error"),
}
}
#[test]
fn test_verbose_beyond_capacity() {
let counter = FixedWindowCounterBuilder::builder()
.capacity(10)
.window_duration(Duration::from_secs(60))
.with_time(MockTimeSource::new())
.with_precision::<Secs>()
.build()
.unwrap();
match counter.try_acquire_verbose(15) {
Err(RateLimitError::BeyondCapacity { acquiring, capacity }) => {
assert_eq!(acquiring, 15);
assert_eq!(capacity, 10);
}
_ => panic!("Expected BeyondCapacity error"),
}
}
#[test]
fn test_verbose_success() {
let counter = FixedWindowCounterBuilder::builder()
.capacity(100)
.window_duration(Duration::from_secs(60))
.with_time(MockTimeSource::new())
.with_precision::<Secs>()
.build()
.unwrap();
assert!(counter.try_acquire_verbose(50).is_ok());
assert_eq!(counter.capacity_remaining().unwrap(), 50);
}
#[test]
fn test_verbose_retry_after_timing() {
let time_source = MockTimeSource::new();
let counter = FixedWindowCounterBuilder::builder()
.capacity(10)
.window_duration(Duration::from_secs(60))
.with_time(time_source.clone())
.with_precision::<Secs>()
.build()
.unwrap();
time_source.advance(Duration::from_secs(20));
assert!(counter.try_acquire(10).is_ok());
match counter.try_acquire_verbose(1) {
Err(RateLimitError::InsufficientCapacity { retry_after, .. }) => {
assert!(retry_after >= Duration::from_secs(39));
assert!(retry_after <= Duration::from_secs(40));
}
_ => panic!("Expected InsufficientCapacity error"),
}
}
}
#[cfg(test)]
mod fixed_window_counter_edge_cases {
use super::*;
#[test]
fn test_zero_token_acquire() {
let counter = FixedWindowCounterBuilder::builder()
.capacity(100)
.window_duration(Duration::from_secs(60))
.with_time(MockTimeSource::new())
.with_precision::<Secs>()
.build()
.unwrap();
assert!(counter.try_acquire(0).is_ok());
assert_eq!(counter.capacity_remaining().unwrap(), 100); }
#[test]
fn test_window_boundary_exact_timing() {
let time_source = MockTimeSource::new();
let counter = FixedWindowCounterBuilder::builder()
.capacity(100)
.window_duration(Duration::from_millis(1000))
.with_time(time_source.clone())
.with_precision::<Millis>()
.build()
.unwrap();
assert!(counter.try_acquire(80).is_ok());
time_source.advance(Duration::from_millis(1000));
assert_eq!(counter.capacity_remaining().unwrap(), 100);
}
#[test]
fn test_rapid_consecutive_acquires() {
let counter = FixedWindowCounterBuilder::builder()
.capacity(100)
.window_duration(Duration::from_secs(60))
.with_time(MockTimeSource::new())
.with_precision::<Secs>()
.build()
.unwrap();
for i in 1..=10 {
assert!(counter.try_acquire(5).is_ok());
assert_eq!(counter.capacity_remaining().unwrap(), 100 - (i * 5));
}
assert_eq!(counter.capacity_remaining().unwrap(), 50);
assert!(counter.try_acquire(50).is_ok());
assert!(counter.try_acquire(1).is_err()); }
#[test]
fn test_very_small_window_duration() {
let time_source = MockTimeSource::new();
let counter = FixedWindowCounterBuilder::builder()
.capacity(10)
.window_duration(Duration::from_nanos(1))
.with_time(time_source.clone())
.with_precision::<Nanos>()
.build()
.unwrap();
assert!(counter.try_acquire(5).is_ok());
time_source.advance(Duration::from_nanos(1));
assert_eq!(counter.capacity_remaining().unwrap(), 10);
}
}
#[cfg(test)]
mod fixed_window_counter_window_alignment {
use super::*;
#[test]
fn test_window_alignment_from_start() {
let time_source = MockTimeSource::new();
let counter = FixedWindowCounterBuilder::builder()
.capacity(100)
.window_duration(Duration::from_secs(10))
.with_time(time_source.clone())
.with_precision::<Secs>()
.build()
.unwrap();
assert!(counter.try_acquire(50).is_ok());
time_source.advance(Duration::from_secs(9));
assert_eq!(counter.capacity_remaining().unwrap(), 50);
time_source.advance(Duration::from_secs(1));
assert_eq!(counter.capacity_remaining().unwrap(), 100);
assert!(counter.try_acquire(30).is_ok());
time_source.advance(Duration::from_secs(9));
assert_eq!(counter.capacity_remaining().unwrap(), 70);
time_source.advance(Duration::from_secs(1));
assert_eq!(counter.capacity_remaining().unwrap(), 100);
}
#[test]
fn test_non_aligned_start_time() {
let time_source = MockTimeSource::new();
time_source.advance(Duration::from_secs(5));
let counter = FixedWindowCounterBuilder::builder()
.capacity(100)
.window_duration(Duration::from_secs(10))
.with_time(time_source.clone())
.with_precision::<Secs>()
.build()
.unwrap();
assert!(counter.try_acquire(50).is_ok());
time_source.advance(Duration::from_secs(5));
assert_eq!(counter.capacity_remaining().unwrap(), 100);
assert!(counter.try_acquire(25).is_ok());
time_source.advance(Duration::from_secs(10));
assert_eq!(counter.capacity_remaining().unwrap(), 100); }
}