1
use super::*;
2
use std::cell::Cell;
3

            
4
/// A clock the test drives by hand, so window behaviour is asserted rather
5
/// than slept through.
6
struct MockClock {
7
    now: Cell<Instant>,
8
}
9

            
10
impl MockClock {
11
7
    fn new() -> Self {
12
7
        Self {
13
7
            now: Cell::new(Instant::now()),
14
7
        }
15
7
    }
16

            
17
6
    fn advance(&self, by: Duration) {
18
6
        self.now.set(self.now.get() + by);
19
6
    }
20
}
21

            
22
impl Clock for &MockClock {
23
123
    fn now(&self) -> Instant {
24
123
        self.now.get()
25
123
    }
26
}
27

            
28
const WINDOW: Duration = Duration::from_secs(60);
29

            
30
7
fn limits(max_attempts: u32, capacity: usize) -> Limits {
31
7
    Limits {
32
7
        max_attempts,
33
7
        window: WINDOW,
34
7
        capacity,
35
7
    }
36
7
}
37

            
38
#[test]
39
1
fn attempts_within_the_limit_are_allowed_and_the_next_is_not() {
40
1
    let clock = MockClock::new();
41
1
    let mut bucket = Bucket::with_clock(limits(3, 16), &clock);
42

            
43
1
    assert_eq!(bucket.check(&"a"), Decision::Allow);
44
1
    assert_eq!(bucket.check(&"a"), Decision::Allow);
45
1
    assert_eq!(bucket.check(&"a"), Decision::Allow);
46
1
    assert_eq!(bucket.check(&"a"), Decision::Deny);
47
1
}
48

            
49
#[test]
50
1
fn the_window_slides_so_old_attempts_stop_counting() {
51
1
    let clock = MockClock::new();
52
1
    let mut bucket = Bucket::with_clock(limits(2, 16), &clock);
53

            
54
1
    assert_eq!(bucket.check(&"a"), Decision::Allow);
55
1
    assert_eq!(bucket.check(&"a"), Decision::Allow);
56
1
    assert_eq!(bucket.check(&"a"), Decision::Deny);
57

            
58
1
    clock.advance(WINDOW + Duration::from_secs(1));
59

            
60
1
    assert_eq!(
61
1
        bucket.check(&"a"),
62
        Decision::Allow,
63
        "attempts older than the window must not count against a client forever"
64
    );
65
1
}
66

            
67
#[test]
68
1
fn a_partly_elapsed_window_only_forgives_the_attempts_that_aged_out() {
69
1
    let clock = MockClock::new();
70
1
    let mut bucket = Bucket::with_clock(limits(2, 16), &clock);
71

            
72
1
    assert_eq!(bucket.check(&"a"), Decision::Allow);
73
1
    clock.advance(Duration::from_secs(50));
74
1
    assert_eq!(bucket.check(&"a"), Decision::Allow);
75
1
    assert_eq!(bucket.check(&"a"), Decision::Deny);
76

            
77
    // Only the first attempt has aged out.
78
1
    clock.advance(Duration::from_secs(11));
79
1
    assert_eq!(bucket.check(&"a"), Decision::Allow);
80
1
    assert_eq!(bucket.check(&"a"), Decision::Deny);
81
1
}
82

            
83
#[test]
84
1
fn keys_are_counted_separately() {
85
1
    let clock = MockClock::new();
86
1
    let mut bucket = Bucket::with_clock(limits(1, 16), &clock);
87

            
88
1
    assert_eq!(bucket.check(&"a"), Decision::Allow);
89
1
    assert_eq!(bucket.check(&"a"), Decision::Deny);
90
1
    assert_eq!(bucket.check(&"b"), Decision::Allow);
91
1
}
92

            
93
#[test]
94
1
fn the_map_never_grows_past_its_capacity() {
95
1
    let clock = MockClock::new();
96
1
    let mut bucket = Bucket::with_clock(limits(5, 4), &clock);
97

            
98
100
    for key in 0..100 {
99
100
        bucket.check(&key);
100
100
    }
101

            
102
1
    assert!(
103
1
        bucket.tracked() <= 4,
104
        "an unbounded map is a memory DoS: tracked {} keys",
105
        bucket.tracked()
106
    );
107
1
}
108

            
109
#[test]
110
1
fn a_full_map_evicts_the_stalest_key_rather_than_refusing_everyone() {
111
1
    let clock = MockClock::new();
112
1
    let mut bucket = Bucket::with_clock(limits(1, 2), &clock);
113

            
114
1
    bucket.check(&"stale");
115
1
    clock.advance(Duration::from_secs(1));
116
1
    bucket.check(&"recent");
117

            
118
    // A newcomer must get in; refusing here would let anyone who can fill the
119
    // map deny service to everyone else.
120
1
    clock.advance(Duration::from_secs(1));
121
1
    assert_eq!(bucket.check(&"newcomer"), Decision::Allow);
122

            
123
1
    assert_eq!(
124
1
        bucket.check(&"recent"),
125
        Decision::Deny,
126
        "the more recently seen key must be the one that survived"
127
    );
128
1
}
129

            
130
#[test]
131
1
fn expired_keys_are_reclaimed_before_anything_is_evicted() {
132
1
    let clock = MockClock::new();
133
1
    let mut bucket = Bucket::with_clock(limits(1, 2), &clock);
134

            
135
1
    bucket.check(&"a");
136
1
    bucket.check(&"b");
137
1
    assert_eq!(bucket.tracked(), 2);
138

            
139
1
    clock.advance(WINDOW + Duration::from_secs(1));
140
1
    bucket.check(&"c");
141

            
142
1
    assert_eq!(
143
1
        bucket.tracked(),
144
        1,
145
        "keys whose window has fully elapsed should be dropped, not evicted one at a time"
146
    );
147
1
}
148

            
149
#[test]
150
1
fn decision_reports_denial() {
151
1
    assert!(Decision::Deny.is_denied());
152
1
    assert!(!Decision::Allow.is_denied());
153
1
}
154

            
155
#[test]
156
1
fn the_system_clock_moves_forward() {
157
1
    let first = SystemClock.now();
158
1
    assert!(SystemClock.now() >= first);
159
1
}