use std::time::Duration;
use super::*;
fn at(limiter: &FixedWindowRateLimiter, offset: Duration) -> Instant {
limiter.base + offset
}
fn limiter(config: RateLimitConfig) -> FixedWindowRateLimiter {
FixedWindowRateLimiter::with_config(config)
}
fn client_failures(l: &FixedWindowRateLimiter, client_id: &str) -> u64 {
l.lock().clients.get(client_id).map_or(0, |b| b.failures)
}
fn authorization_failures(l: &FixedWindowRateLimiter, client_id: &str) -> u64 {
l.lock()
.authorization
.get(client_id)
.map_or(0, |b| b.failures)
}
#[test]
fn a_budget_is_spent_one_unit_per_allowed_attempt() {
let l = limiter(RateLimitConfig::default().with_device_user_code_budget(3, 0));
let now = at(&l, Duration::ZERO);
for i in 0..3 {
assert_eq!(
l.check_at(Attempt::DeviceUserCodeEntry, now),
RateLimitDecision::Allow,
"attempt {i} is inside the budget"
);
}
assert_eq!(
l.check_at(Attempt::DeviceUserCodeEntry, now),
RateLimitDecision::Deny
);
}
#[test]
fn failures_cost_ten_times_what_successes_cost() {
let successes = {
let l = limiter(RateLimitConfig::default());
let now = at(&l, Duration::ZERO);
let mut n = 0;
while l.check_at(Attempt::DeviceUserCodeEntry, now) == RateLimitDecision::Allow {
l.record_at(Attempt::DeviceUserCodeEntry, AttemptOutcome::Succeeded, now);
n += 1;
}
n
};
let failures = {
let l = limiter(RateLimitConfig::default());
let now = at(&l, Duration::ZERO);
let mut n = 0;
while l.check_at(Attempt::DeviceUserCodeEntry, now) == RateLimitDecision::Allow {
l.record_at(Attempt::DeviceUserCodeEntry, AttemptOutcome::Failed, now);
n += 1;
}
n
};
assert_eq!(
(successes, failures),
(DEFAULT_DEVICE_USER_CODE_CAPACITY, 20),
"the documented default is 200 correct entries a minute or 20 wrong ones"
);
}
#[test]
fn the_shipped_default_permits_twenty_wrong_user_codes_per_window() {
let cost_of_a_failure = ATTEMPT_COST + DEFAULT_DEVICE_USER_CODE_FAILURE_COST;
assert_eq!(
DEFAULT_DEVICE_USER_CODE_CAPACITY / cost_of_a_failure,
20,
"the module docs derive the 2^34.6 guessing odds from 20 wrong codes per 60s window"
);
let cost_of_a_failed_auth = ATTEMPT_COST + DEFAULT_CLIENT_AUTHENTICATION_FAILURE_COST;
assert_eq!(
cost_of_a_failed_auth, 200,
"the module docs price a failed client authentication at 200 units"
);
let config = RateLimitConfig::default();
assert_eq!(
config.client_authentication_failure_ceiling() / cost_of_a_failed_auth,
15,
"the module docs derive the RFC 9700 s4.13 weighting from 15 penalised failures per client"
);
assert_eq!(
config
.client_authentication_failure_ceiling()
.div_ceil(DEFAULT_CLIENT_AUTHENTICATION_FAILURE_COST),
16,
"the failure counter climbs by 199 a failure, so 16 of them reach the 3000 ceiling"
);
assert_eq!(
config.client_authentication_failure_ceiling(),
DEFAULT_CLIENT_AUTHENTICATION_CAPACITY / 2,
"half of every client's budget is reserved for attempts and cannot be spent by failures"
);
}
#[test]
fn a_successful_outcome_costs_nothing_beyond_the_attempt() {
let l = limiter(RateLimitConfig::default().with_device_user_code_budget(2, 1_000_000));
let now = at(&l, Duration::ZERO);
assert_eq!(
l.check_at(Attempt::DeviceUserCodeEntry, now),
RateLimitDecision::Allow
);
l.record_at(Attempt::DeviceUserCodeEntry, AttemptOutcome::Succeeded, now);
assert_eq!(
l.check_at(Attempt::DeviceUserCodeEntry, now),
RateLimitDecision::Allow,
"a success must not consume the failure penalty as well"
);
}
#[test]
fn the_budget_rolls_exactly_at_the_window_boundary() {
let window = Duration::from_secs(60);
let l = limiter(
RateLimitConfig::default()
.with_window(window)
.with_device_user_code_budget(1, 0),
);
assert_eq!(
l.check_at(Attempt::DeviceUserCodeEntry, at(&l, Duration::ZERO)),
RateLimitDecision::Allow
);
assert_eq!(
l.check_at(
Attempt::DeviceUserCodeEntry,
at(&l, window - Duration::from_nanos(1))
),
RateLimitDecision::Deny,
"one nanosecond before the boundary is still the same budget"
);
assert_eq!(
l.check_at(Attempt::DeviceUserCodeEntry, at(&l, window)),
RateLimitDecision::Allow,
"the boundary itself starts a fresh budget"
);
assert_eq!(
l.check_at(Attempt::DeviceUserCodeEntry, at(&l, window * 1_000)),
RateLimitDecision::Allow
);
}
#[test]
fn a_penalty_reported_after_the_roll_lands_in_the_new_window() {
let window = Duration::from_secs(60);
let l = limiter(
RateLimitConfig::default()
.with_window(window)
.with_device_user_code_budget(10, 10),
);
assert_eq!(
l.check_at(Attempt::DeviceUserCodeEntry, at(&l, Duration::ZERO)),
RateLimitDecision::Allow
);
l.record_at(
Attempt::DeviceUserCodeEntry,
AttemptOutcome::Failed,
at(&l, window),
);
assert_eq!(
l.check_at(Attempt::DeviceUserCodeEntry, at(&l, window)),
RateLimitDecision::Deny,
"the penalty was charged to the window it was reported in, not discarded"
);
}
#[test]
fn client_budgets_are_independent_of_each_other() {
let l = limiter(RateLimitConfig::default().with_client_authentication_budget(1, 0));
let now = at(&l, Duration::ZERO);
let a = Attempt::ClientAuthentication { client_id: "app-a" };
let b = Attempt::ClientAuthentication { client_id: "app-b" };
assert_eq!(l.check_at(a, now), RateLimitDecision::Allow);
assert_eq!(l.check_at(a, now), RateLimitDecision::Deny);
assert_eq!(
l.check_at(b, now),
RateLimitDecision::Allow,
"app-b's budget is its own"
);
}
#[test]
fn the_cheapest_complete_failure_spray_leaves_a_client_id_on_the_air() {
let l = limiter(RateLimitConfig::default());
let now = at(&l, Duration::ZERO);
let victim = Attempt::ClientAuthentication {
client_id: "real-app",
};
let ceiling = l.config().client_authentication_failure_ceiling();
let mut sprayed = 0;
while client_failures(&l, "real-app") < ceiling {
assert_eq!(
l.check_at(victim, now),
RateLimitDecision::Allow,
"spray request {sprayed} is itself inside the budget"
);
l.record_at(victim, AttemptOutcome::Failed, now);
sprayed += 1;
}
assert_eq!(
sprayed, 16,
"the failure counter climbs by 199 and clamps at 3000, so 16 failures fill it and not 15"
);
let mut admitted = 0;
while l.check_at(victim, now) == RateLimitDecision::Allow {
admitted += 1;
}
assert_eq!(
admitted,
DEFAULT_CLIENT_AUTHENTICATION_CAPACITY / 2 - sprayed,
"the reserved half less the 16 attempt units the spray itself spent is 2984 further \
authentications, which is 49 a second: a per-client_id budget a cheap failure spray can \
exhaust IS a lockout an attacker triggers for free"
);
}
#[test]
fn a_within_window_denial_costs_the_whole_reserved_half_in_requests() {
let l = limiter(RateLimitConfig::default());
let now = at(&l, Duration::ZERO);
let victim = Attempt::ClientAuthentication {
client_id: "real-app",
};
let reserved = DEFAULT_CLIENT_AUTHENTICATION_CAPACITY / 2;
for i in 0..reserved - 1 {
assert_eq!(
l.check_at(victim, now),
RateLimitDecision::Allow,
"wrong secret {i} of {reserved} is still admitted"
);
l.record_at(victim, AttemptOutcome::Failed, now);
}
assert_eq!(
l.check_at(victim, now),
RateLimitDecision::Allow,
"request 3000 is the last one the reserved half pays for"
);
assert_eq!(
l.check_at(victim, now),
RateLimitDecision::Deny,
"3000 wrong secrets in one window, and not 6000 requests of real volume, is what taking a \
client_id off the air for the rest of the window costs"
);
}
#[test]
fn the_cheapest_complete_failure_spray_leaves_the_shared_overflow_budget_usable() {
let l = limiter(RateLimitConfig::default().with_max_tracked_clients(1));
let now = at(&l, Duration::ZERO);
let ceiling = l.config().client_authentication_failure_ceiling();
assert_eq!(
l.check_at(
Attempt::ClientAuthentication {
client_id: "the-tracked-one"
},
now
),
RateLimitDecision::Allow
);
let mut sprayed = 0u64;
while l.lock().overflow.failures < ceiling {
let id = format!("sprayed-{sprayed}");
let attempt = Attempt::ClientAuthentication { client_id: &id };
assert_eq!(l.check_at(attempt, now), RateLimitDecision::Allow);
l.record_at(attempt, AttemptOutcome::Failed, now);
sprayed += 1;
}
assert_eq!(sprayed, 16, "same clamp, same 16 failures to reach it");
let latecomer = Attempt::ClientAuthentication {
client_id: "a-legitimate-latecomer",
};
let mut admitted = 0;
while l.check_at(latecomer, now) == RateLimitDecision::Allow {
admitted += 1;
}
assert_eq!(
admitted,
DEFAULT_CLIENT_AUTHENTICATION_CAPACITY / 2 - sprayed,
"a client whose first authentication of the window arrives after the map filled shares the \
overflow budget, and the cheapest failure spray must leave it nearly whole"
);
}
#[test]
fn the_failure_penalty_still_costs_two_hundred_units_up_to_the_ceiling() {
assert_eq!(
authentications_after_failures(0),
DEFAULT_CLIENT_AUTHENTICATION_CAPACITY,
"with no failures the capacity is a plain attempt ceiling"
);
assert_eq!(
authentications_after_failures(1),
DEFAULT_CLIENT_AUTHENTICATION_CAPACITY - 200,
"one failure costs 1 + 199, so it is worth 200 attempts"
);
assert_eq!(
authentications_after_failures(10),
DEFAULT_CLIENT_AUTHENTICATION_CAPACITY - 2_000,
"ten failures cost 2000 units, still short of the 3000-unit ceiling"
);
}
fn authentications_after_failures(failures: u64) -> u64 {
let l = limiter(RateLimitConfig::default());
let now = at(&l, Duration::ZERO);
let a = Attempt::ClientAuthentication { client_id: "app" };
for _ in 0..failures {
assert_eq!(l.check_at(a, now), RateLimitDecision::Allow);
l.record_at(a, AttemptOutcome::Failed, now);
}
let mut n = 0;
while l.check_at(a, now) == RateLimitDecision::Allow {
n += 1;
}
n
}
#[test]
fn the_sixteenth_failure_costs_sixteen_units_and_the_seventeenth_costs_one() {
let fifteen = authentications_after_failures(15);
let sixteen = authentications_after_failures(16);
let seventeen = authentications_after_failures(17);
assert_eq!(
fifteen,
DEFAULT_CLIENT_AUTHENTICATION_CAPACITY / 2,
"15 failures put the failure counter at 15 * 199 = 2985 and cost 15 attempt units, which \
leaves exactly the reserved half"
);
assert_eq!(
fifteen - sixteen,
16,
"the sixteenth failure costs 1 + the 15 units left under the 3000 ceiling, not 200 and not \
ATTEMPT_COST"
);
assert_eq!(
sixteen - seventeen,
ATTEMPT_COST,
"the seventeenth is the first failure that costs nothing beyond its attempt unit"
);
}
#[test]
fn the_device_budget_is_shared_by_every_caller() {
let l = limiter(RateLimitConfig::default().with_device_user_code_budget(1, 0));
let now = at(&l, Duration::ZERO);
assert_eq!(
l.check_at(Attempt::DeviceUserCodeEntry, now),
RateLimitDecision::Allow
);
assert_eq!(
l.check_at(Attempt::DeviceUserCodeEntry, now),
RateLimitDecision::Deny
);
}
#[test]
fn the_shipped_default_permits_three_thousand_authorization_requests_or_fifteen_hundred_refusals() {
let drive = |outcome: AttemptOutcome| {
let l = limiter(RateLimitConfig::default());
let now = at(&l, Duration::ZERO);
let a = Attempt::AuthorizationRequest { client_id: "app" };
let mut n = 0;
while l.check_at(a, now) == RateLimitDecision::Allow {
l.record_at(a, outcome, now);
n += 1;
}
n
};
assert_eq!(
(
drive(AttemptOutcome::Succeeded),
drive(AttemptOutcome::Failed)
),
(DEFAULT_AUTHORIZATION_REQUEST_CAPACITY, 1_500),
"the documented default is 3000 authorization requests a minute for one client_id or 1500 \
refused ones"
);
}
#[test]
fn the_authorization_refusal_penalty_saturates_at_half_the_capacity() {
let l = limiter(RateLimitConfig::default());
let now = at(&l, Duration::ZERO);
let a = Attempt::AuthorizationRequest { client_id: "app" };
let ceiling = l.config().authorization_request_failure_ceiling();
assert_eq!(
ceiling,
DEFAULT_AUTHORIZATION_REQUEST_CAPACITY / 2,
"the same divisor governs both budgets"
);
let mut refused = 0;
while authorization_failures(&l, "app") < ceiling {
assert_eq!(
l.check_at(a, now),
RateLimitDecision::Allow,
"refusal {refused} is itself inside the budget"
);
l.record_at(a, AttemptOutcome::Failed, now);
refused += 1;
}
assert_eq!(
refused, 167,
"9 units a refusal against a 1500-unit ceiling: 166 reach 1494 and the 167th clamps"
);
let mut admitted = 0;
while l.check_at(a, now) == RateLimitDecision::Allow {
admitted += 1;
}
assert_eq!(
admitted,
DEFAULT_AUTHORIZATION_REQUEST_CAPACITY / 2 - refused,
"the reserved half less the 167 attempt units the walk itself spent is 1333 further \
arrivals: a client's users can still reach its login page"
);
}
#[test]
fn the_authorization_budget_is_separate_per_client_and_from_client_authentication() {
let l = limiter(RateLimitConfig {
authorization_request_capacity: 1,
authorization_request_failure_cost: 0,
client_authentication_capacity: 1,
client_authentication_failure_cost: 0,
..RateLimitConfig::default()
});
let now = at(&l, Duration::ZERO);
let authorize_a = Attempt::AuthorizationRequest { client_id: "app-a" };
let authorize_b = Attempt::AuthorizationRequest { client_id: "app-b" };
let authenticate_a = Attempt::ClientAuthentication { client_id: "app-a" };
assert_eq!(l.check_at(authorize_a, now), RateLimitDecision::Allow);
assert_eq!(l.check_at(authorize_a, now), RateLimitDecision::Deny);
assert_eq!(
l.check_at(authorize_b, now),
RateLimitDecision::Allow,
"app-b's login page is on its own budget"
);
assert_eq!(
l.check_at(authenticate_a, now),
RateLimitDecision::Allow,
"app-a's token traffic is not spent by the traffic at its authorization endpoint"
);
}
#[test]
fn the_authorization_map_never_exceeds_its_cap() {
let l = limiter(RateLimitConfig {
max_tracked_clients: 4,
authorization_request_capacity: u64::MAX,
..RateLimitConfig::default()
});
let now = at(&l, Duration::ZERO);
for i in 0..10_000 {
let id = format!("sprayed-{i}");
l.check_at(Attempt::AuthorizationRequest { client_id: &id }, now);
}
assert_eq!(l.tracked_authorization_clients(), 4);
assert_eq!(
l.tracked_clients(),
0,
"and the spray never touched the client-authentication map, which is exactly why a gate \
that only reads `tracked_clients` proves nothing about this one"
);
}
#[test]
fn an_oversized_client_id_never_gets_an_authorization_entry_of_its_own() {
let l = limiter(RateLimitConfig::default());
let now = at(&l, Duration::ZERO);
let huge = "z".repeat(MAX_TRACKED_CLIENT_ID_LEN + 1);
let ok = "z".repeat(MAX_TRACKED_CLIENT_ID_LEN);
l.check_at(Attempt::AuthorizationRequest { client_id: &huge }, now);
assert_eq!(l.tracked_authorization_clients(), 0, "too long to store");
l.check_at(Attempt::AuthorizationRequest { client_id: &ok }, now);
assert_eq!(
l.tracked_authorization_clients(),
1,
"exactly at the cap is still stored"
);
}
#[test]
fn the_authorization_map_and_its_overflow_are_emptied_when_the_window_rolls() {
let window = Duration::from_secs(60);
let l = limiter(RateLimitConfig {
window,
max_tracked_clients: 1,
..RateLimitConfig::default()
});
let now = at(&l, Duration::ZERO);
let tracked = Attempt::AuthorizationRequest { client_id: "app-a" };
let untracked = Attempt::AuthorizationRequest {
client_id: "past-the-cap",
};
assert_eq!(l.check_at(tracked, now), RateLimitDecision::Allow);
assert_eq!(l.check_at(untracked, now), RateLimitDecision::Allow);
l.record_at(untracked, AttemptOutcome::Failed, now);
assert_eq!(l.tracked_authorization_clients(), 1);
assert!(l.lock().authorization_overflow.failures > 0);
l.check_at(Attempt::DeviceUserCodeEntry, at(&l, window));
assert_eq!(
l.tracked_authorization_clients(),
0,
"the roll drops every key in this map too"
);
assert_eq!(
l.lock().authorization_overflow.failures,
0,
"and resets its shared counter, which is a separate field from the other map's"
);
}
#[test]
fn authorization_identifiers_past_the_cap_share_one_budget() {
let l = limiter(RateLimitConfig {
max_tracked_clients: 1,
authorization_request_capacity: 3,
authorization_request_failure_cost: 0,
..RateLimitConfig::default()
});
let now = at(&l, Duration::ZERO);
assert_eq!(
l.check_at(Attempt::AuthorizationRequest { client_id: "first" }, now),
RateLimitDecision::Allow
);
for i in 0..3 {
let id = format!("overflow-{i}");
assert_eq!(
l.check_at(Attempt::AuthorizationRequest { client_id: &id }, now),
RateLimitDecision::Allow,
"overflow arrival {i}"
);
}
assert_eq!(
l.check_at(
Attempt::AuthorizationRequest {
client_id: "overflow-brand-new"
},
now
),
RateLimitDecision::Deny,
"the shared overflow budget is spent, so the spray throttles itself"
);
assert_eq!(
l.check_at(Attempt::AuthorizationRequest { client_id: "first" }, now),
RateLimitDecision::Allow,
"the tracked client is untouched by the spray"
);
}
#[test]
fn the_tracked_client_map_never_exceeds_its_cap() {
let l = limiter(
RateLimitConfig::default()
.with_max_tracked_clients(4)
.with_client_authentication_budget(u64::MAX, 0),
);
let now = at(&l, Duration::ZERO);
for i in 0..10_000 {
let id = format!("sprayed-{i}");
l.check_at(Attempt::ClientAuthentication { client_id: &id }, now);
}
assert_eq!(l.tracked_clients(), 4);
}
#[test]
fn a_tracked_entry_costs_the_bytes_the_bounding_argument_says_it_does() {
assert_eq!(
std::mem::size_of::<Box<str>>(),
16,
"a Box<str> is a pointer and a length; a String would carry a spare capacity word"
);
assert_eq!(
std::mem::size_of::<ClientBudget>(),
16,
"two u64 counters, which is what makes a table slot 32 bytes"
);
let mut map: HashMap<Box<str>, ClientBudget> = HashMap::new();
for i in 0..DEFAULT_MAX_TRACKED_CLIENTS {
map.insert(format!("k{i}").into_boxed_str(), ClientBudget::default());
}
assert!(
map.capacity() >= DEFAULT_MAX_TRACKED_CLIENTS,
"4096 entries fit without the table having to grow past the next power of two"
);
}
#[test]
fn an_oversized_client_id_never_gets_an_entry_of_its_own() {
let l = limiter(RateLimitConfig::default());
let now = at(&l, Duration::ZERO);
let huge = "z".repeat(MAX_TRACKED_CLIENT_ID_LEN + 1);
let ok = "z".repeat(MAX_TRACKED_CLIENT_ID_LEN);
l.check_at(Attempt::ClientAuthentication { client_id: &huge }, now);
assert_eq!(l.tracked_clients(), 0, "too long to store");
l.check_at(Attempt::ClientAuthentication { client_id: &ok }, now);
assert_eq!(l.tracked_clients(), 1, "exactly at the cap is still stored");
}
#[test]
fn the_tracked_client_map_is_emptied_when_the_window_rolls() {
let window = Duration::from_secs(60);
let l = limiter(RateLimitConfig::default().with_window(window));
l.check_at(
Attempt::ClientAuthentication { client_id: "app-a" },
at(&l, Duration::ZERO),
);
assert_eq!(l.tracked_clients(), 1);
l.check_at(Attempt::DeviceUserCodeEntry, at(&l, window));
assert_eq!(
l.tracked_clients(),
0,
"the roll drops every key, which costs no information since every counter was being reset"
);
}
#[test]
fn identifiers_past_the_cap_share_one_budget_and_are_refused_together() {
let l = limiter(
RateLimitConfig::default()
.with_max_tracked_clients(1)
.with_client_authentication_budget(3, 0),
);
let now = at(&l, Duration::ZERO);
assert_eq!(
l.check_at(Attempt::ClientAuthentication { client_id: "first" }, now),
RateLimitDecision::Allow
);
for i in 0..3 {
let id = format!("overflow-{i}");
assert_eq!(
l.check_at(Attempt::ClientAuthentication { client_id: &id }, now),
RateLimitDecision::Allow,
"overflow attempt {i}"
);
}
assert_eq!(
l.check_at(
Attempt::ClientAuthentication {
client_id: "overflow-brand-new"
},
now
),
RateLimitDecision::Deny,
"the shared overflow budget is spent, so the spray throttles itself"
);
assert_eq!(
l.check_at(Attempt::ClientAuthentication { client_id: "first" }, now),
RateLimitDecision::Allow
);
}
#[test]
fn a_denied_flood_pins_the_counter_rather_than_overflowing_it() {
let l = limiter(RateLimitConfig::default().with_device_user_code_budget(1, u64::MAX));
let now = at(&l, Duration::ZERO);
assert_eq!(
l.check_at(Attempt::DeviceUserCodeEntry, now),
RateLimitDecision::Allow
);
for _ in 0..1_000 {
l.record_at(Attempt::DeviceUserCodeEntry, AttemptOutcome::Failed, now);
assert_eq!(
l.check_at(Attempt::DeviceUserCodeEntry, now),
RateLimitDecision::Deny
);
}
assert_eq!(l.lock().device_user_code, 1, "clamped at the capacity");
}
#[test]
fn a_zero_capacity_refuses_rather_than_admitting_everything() {
let l = limiter(RateLimitConfig::default().with_device_user_code_budget(0, 0));
assert_eq!(
l.check_at(Attempt::DeviceUserCodeEntry, at(&l, Duration::ZERO)),
RateLimitDecision::Deny
);
}
#[test]
fn a_zero_window_is_clamped_rather_than_dividing_by_zero() {
let l = limiter(RateLimitConfig::default().with_window(Duration::ZERO));
assert_eq!(l.config().window, MIN_WINDOW);
assert_eq!(l.window_index(at(&l, Duration::ZERO)), 0);
assert_eq!(l.window_index(at(&l, MIN_WINDOW)), 1);
let l = limiter(RateLimitConfig {
window: Duration::ZERO,
..RateLimitConfig::default()
});
assert_eq!(l.window_index(at(&l, MIN_WINDOW)), 1);
}
#[test]
fn the_window_index_saturates_rather_than_panicking() {
let mut l = limiter(RateLimitConfig::default());
assert_eq!(l.window_index(l.base), 0);
assert_eq!(l.window_index(at(&l, DEFAULT_WINDOW * 3)), 3);
let before_base = l.base;
l.base = before_base + DEFAULT_WINDOW * 3;
assert_eq!(
l.window_index(before_base),
0,
"an instant three windows BEFORE the base must land in window 0, not panic and not wrap \
to a far-future index that would hand out a fresh budget"
);
assert_eq!(
l.window_index(before_base + DEFAULT_WINDOW),
0,
"still before the base, so still window 0"
);
assert_eq!(
l.check_at(Attempt::DeviceUserCodeEntry, before_base),
RateLimitDecision::Allow
);
l.record_at(
Attempt::DeviceUserCodeEntry,
AttemptOutcome::Failed,
before_base,
);
}
#[test]
fn a_poisoned_lock_is_recovered_from_rather_than_propagated() {
let l = std::sync::Arc::new(limiter(RateLimitConfig::default()));
let poisoner = std::sync::Arc::clone(&l);
let _ = std::thread::spawn(move || {
let _guard = poisoner.lock();
panic!("poison the limiter's mutex");
})
.join();
assert_eq!(
l.check_at(Attempt::DeviceUserCodeEntry, at(&l, Duration::ZERO)),
RateLimitDecision::Allow,
"the limiter still answers after its mutex was poisoned"
);
}
#[test]
fn an_override_raises_one_client_ids_budget_and_nobody_elses() {
let l = limiter(
RateLimitConfig::default()
.with_client_authentication_budget(2, 0)
.with_client_authentication_capacity_for("resource-server", 5),
);
let now = at(&l, Duration::ZERO);
let rs = Attempt::ClientAuthentication {
client_id: "resource-server",
};
let app = Attempt::ClientAuthentication { client_id: "app" };
let mut admitted = 0;
while l.check_at(rs, now) == RateLimitDecision::Allow {
admitted += 1;
}
assert_eq!(
admitted, 5,
"the resource server gets the budget it was given"
);
let mut admitted = 0;
while l.check_at(app, now) == RateLimitDecision::Allow {
admitted += 1;
}
assert_eq!(
admitted, 2,
"every other client id is still on the configured capacity"
);
}
#[test]
fn an_override_moves_the_failure_reserve_with_the_capacity() {
let config = RateLimitConfig::default()
.with_client_authentication_budget(100, 9)
.with_client_authentication_capacity_for("resource-server", 1000);
assert_eq!(
config.client_authentication_failure_ceiling_for("resource-server"),
500
);
assert_eq!(config.client_authentication_failure_ceiling_for("app"), 50);
let l = limiter(config);
let now = at(&l, Duration::ZERO);
let rs = Attempt::ClientAuthentication {
client_id: "resource-server",
};
while client_failures(&l, "resource-server") < 500 {
assert_eq!(l.check_at(rs, now), RateLimitDecision::Allow);
l.record_at(rs, AttemptOutcome::Failed, now);
}
assert_eq!(
client_failures(&l, "resource-server"),
500,
"the penalty clamps at half the OVERRIDDEN capacity, not at half the global one"
);
}
#[test]
fn an_override_never_raises_the_shared_overflow_budget() {
let l = limiter(
RateLimitConfig::default()
.with_max_tracked_clients(1)
.with_client_authentication_budget(2, 0)
.with_client_authentication_capacity_for("resource-server", 1000),
);
let now = at(&l, Duration::ZERO);
assert_eq!(
l.check_at(Attempt::ClientAuthentication { client_id: "first" }, now),
RateLimitDecision::Allow
);
let rs = Attempt::ClientAuthentication {
client_id: "resource-server",
};
let mut admitted = 0;
while l.check_at(rs, now) == RateLimitDecision::Allow {
admitted += 1;
assert!(
admitted <= 8,
"the overflow budget is not the override's 1000"
);
}
assert_eq!(
admitted, 2,
"an untracked identifier is charged the SHARED capacity whatever its override says"
);
}