use super::*;
use reqwest::header::{HeaderMap, HeaderValue};
fn headers(pairs: &[(&str, &str)]) -> HeaderMap {
let mut h = HeaderMap::new();
for (k, v) in pairs {
let name: reqwest::header::HeaderName =
k.parse().expect("test header name is a valid header name");
h.insert(
name,
HeaderValue::from_str(v).expect("test header value is valid"),
);
}
h
}
#[test]
fn retry_after_is_preferred_over_every_other_hint() {
let h = headers(&[
("retry-after", "7"),
("x-ratelimit-remaining", "0"),
("x-ratelimit-reset", "99999999999"),
]);
assert_eq!(rate_limit_delay(429, &h), Some(Duration::from_secs(7)));
}
#[test]
fn a_403_without_rate_limit_evidence_is_not_a_rate_limit() {
let h = headers(&[("x-ratelimit-remaining", "4999")]);
assert_eq!(rate_limit_delay(403, &h), None);
}
#[test]
fn a_drained_quota_makes_a_403_a_rate_limit() {
let h = headers(&[("x-ratelimit-remaining", "0")]);
assert!(rate_limit_delay(403, &h).is_some());
}
#[test]
fn a_reset_epoch_is_used_when_retry_after_is_absent() {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock is after the epoch")
.as_secs();
let h = headers(&[
("x-ratelimit-remaining", "0"),
("x-ratelimit-reset", &(now + 5).to_string()),
]);
let delay = rate_limit_delay(403, &h).expect("a drained quota is a rate limit");
assert!(
delay <= Duration::from_secs(5) && delay >= Duration::from_secs(3),
"expected ~5s, got {delay:?}"
);
}
#[test]
fn a_very_long_retry_after_is_clamped() {
let h = headers(&[("retry-after", "3600")]);
assert_eq!(rate_limit_delay(429, &h), Some(MAX_RETRY_AFTER));
}
#[test]
fn a_429_with_no_hints_falls_back_to_the_default_delay() {
assert_eq!(
rate_limit_delay(429, &HeaderMap::new()),
Some(DEFAULT_RATE_LIMIT_DELAY)
);
}
#[test]
fn a_success_is_never_a_rate_limit() {
let h = headers(&[("retry-after", "5")]);
assert_eq!(rate_limit_delay(200, &h), None);
}
#[test]
fn the_budget_latches_after_the_allowance_is_spent() {
let budget = FetchBudget::with_sleep_budget(Duration::from_secs(3));
assert!(budget.reserve(Duration::from_secs(2), 429).is_ok());
assert!(budget.tripped_error().is_none());
let refused = budget.reserve(Duration::from_secs(2), 429);
assert!(
matches!(refused, Err(CollectError::Throttled { status: 429, .. })),
"expected Throttled, got {refused:?}"
);
assert!(budget.tripped_error().is_some());
}
#[test]
fn a_latched_budget_refuses_every_later_reservation() {
let budget = FetchBudget::with_sleep_budget(Duration::from_millis(1));
assert!(budget.reserve(Duration::from_secs(1), 429).is_err());
for _ in 0..5 {
assert!(budget.reserve(Duration::from_millis(0), 429).is_err());
}
}
#[test]
fn tripping_on_the_attempt_cap_latches_the_breaker() {
let budget = FetchBudget::new();
let err = budget.trip(429, Some(Duration::from_secs(2)));
assert!(matches!(err, CollectError::Throttled { status: 429, .. }));
assert!(budget.tripped_error().is_some());
}
#[test]
fn truncation_notices_are_recorded_in_order() {
let budget = FetchBudget::new();
assert!(budget.notices().is_empty());
budget.note_truncation("first");
budget.note_truncation("second");
assert_eq!(budget.notices(), vec!["first", "second"]);
}
#[test]
fn the_shipped_bounds_are_the_documented_ones() {
assert_eq!(MAX_PAGES, 100);
assert_eq!(MAX_REFERENCE_LOOKUPS, 500);
assert_eq!(RATE_LIMIT_SLEEP_BUDGET, Duration::from_secs(120));
assert_eq!(MAX_RETRY_AFTER, Duration::from_secs(60));
}