use super::*;
mod rate_limit_info {
use super::*;
use chrono::{DateTime, Duration as ChronoDuration, Utc};
#[test]
fn test_from_headers_valid() {
let info = RateLimitInfo::from_headers(Some("5000"), Some("4999"), Some("1640000000"));
assert!(info.is_some());
let info = info.unwrap();
assert_eq!(info.limit, 5000);
assert_eq!(info.remaining, 4999);
assert_eq!(
info.reset_at,
DateTime::from_timestamp(1640000000, 0).unwrap()
);
assert!(!info.is_limited);
}
#[test]
fn test_from_headers_missing() {
let info = RateLimitInfo::from_headers(None, Some("4999"), Some("1640000000"));
assert!(info.is_none());
let info = RateLimitInfo::from_headers(Some("5000"), None, Some("1640000000"));
assert!(info.is_none());
let info = RateLimitInfo::from_headers(Some("5000"), Some("4999"), None);
assert!(info.is_none());
}
#[test]
fn test_from_headers_invalid() {
let info = RateLimitInfo::from_headers(Some("invalid"), Some("4999"), Some("1640000000"));
assert!(info.is_none());
let info = RateLimitInfo::from_headers(Some("5000"), Some("invalid"), Some("1640000000"));
assert!(info.is_none());
let info = RateLimitInfo::from_headers(Some("5000"), Some("4999"), Some("invalid"));
assert!(info.is_none());
}
#[test]
fn test_is_limited() {
let info = RateLimitInfo::from_headers(Some("5000"), Some("0"), Some("1640000000"));
assert!(info.is_some());
let info = info.unwrap();
assert_eq!(info.remaining, 0);
assert!(info.is_limited);
}
#[test]
fn test_is_not_limited() {
let info = RateLimitInfo::from_headers(Some("5000"), Some("100"), Some("1640000000"));
assert!(info.is_some());
let info = info.unwrap();
assert_eq!(info.remaining, 100);
assert!(!info.is_limited);
}
#[test]
fn test_is_near_limit_true() {
let info = RateLimitInfo::from_headers(Some("5000"), Some("100"), Some("1640000000"))
.expect("Valid headers");
assert!(info.is_near_limit(0.10));
assert!(info.is_near_limit(0.05));
}
#[test]
fn test_is_near_limit_false() {
let info = RateLimitInfo::from_headers(Some("5000"), Some("3000"), Some("1640000000"))
.expect("Valid headers");
assert!(!info.is_near_limit(0.10));
}
#[test]
fn test_time_until_reset_future() {
let future_timestamp = (Utc::now() + ChronoDuration::try_hours(1).unwrap()).timestamp();
let info = RateLimitInfo::from_headers(
Some("5000"),
Some("100"),
Some(&future_timestamp.to_string()),
)
.expect("Valid headers");
let duration = info.time_until_reset();
assert!(duration.as_secs() >= 3590);
assert!(duration.as_secs() <= 3610);
}
#[test]
fn test_time_until_reset_past() {
let info = RateLimitInfo::from_headers(Some("5000"), Some("100"), Some("1640000000"))
.expect("Valid headers");
let duration = info.time_until_reset();
assert_eq!(duration.as_secs(), 0);
}
}
mod retry_policy {
use super::*;
#[test]
fn test_default() {
let policy = RetryPolicy::default();
assert_eq!(policy.max_retries, 3);
assert_eq!(policy.initial_delay, Duration::from_millis(100));
assert_eq!(policy.max_delay, Duration::from_secs(60));
assert_eq!(policy.backoff_multiplier, 2.0);
assert!(policy.use_jitter);
}
#[test]
fn test_new() {
let policy = RetryPolicy::new(5, Duration::from_millis(500), Duration::from_secs(30));
assert_eq!(policy.max_retries, 5);
assert_eq!(policy.initial_delay, Duration::from_millis(500));
assert_eq!(policy.max_delay, Duration::from_secs(30));
assert_eq!(policy.backoff_multiplier, 2.0);
assert!(policy.use_jitter); }
#[test]
fn test_with_jitter() {
let policy = RetryPolicy::default().with_jitter();
assert!(policy.use_jitter);
}
#[test]
fn test_without_jitter() {
let policy = RetryPolicy::default().without_jitter();
assert!(!policy.use_jitter);
}
#[test]
fn test_calculate_delay_attempt_zero() {
let policy = RetryPolicy::default();
let delay = policy.calculate_delay(0);
assert_eq!(delay, Duration::from_secs(0));
}
#[test]
fn test_calculate_delay_exponential_backoff() {
let policy = RetryPolicy::default().without_jitter();
assert_eq!(policy.calculate_delay(1), Duration::from_millis(100));
assert_eq!(policy.calculate_delay(2), Duration::from_millis(200));
assert_eq!(policy.calculate_delay(3), Duration::from_millis(400));
assert_eq!(policy.calculate_delay(4), Duration::from_millis(800));
}
#[test]
fn test_calculate_delay_max_cap() {
let policy = RetryPolicy {
max_retries: 100,
initial_delay: Duration::from_millis(100),
max_delay: Duration::from_secs(5),
backoff_multiplier: 2.0,
use_jitter: false,
};
let delay = policy.calculate_delay(20);
assert_eq!(delay, Duration::from_secs(5));
}
#[test]
fn test_calculate_delay_with_jitter() {
let policy = RetryPolicy::default().with_jitter();
let mut delays = Vec::new();
for _ in 0..100 {
let delay = policy.calculate_delay(1);
delays.push(delay.as_millis());
}
for delay_ms in &delays {
assert!(*delay_ms >= 75, "Delay {}ms below minimum 75ms", delay_ms);
assert!(*delay_ms <= 125, "Delay {}ms above maximum 125ms", delay_ms);
}
let min = *delays.iter().min().unwrap();
let max = *delays.iter().max().unwrap();
assert!(max > min, "Jitter should produce variation in delays");
}
#[test]
fn test_calculate_delay_without_jitter() {
let policy = RetryPolicy::default().without_jitter();
let delay1 = policy.calculate_delay(1);
let delay2 = policy.calculate_delay(1);
let delay3 = policy.calculate_delay(1);
assert_eq!(delay1, delay2);
assert_eq!(delay2, delay3);
assert_eq!(delay1, Duration::from_millis(100));
}
#[test]
fn test_calculate_delay_jitter_respects_bounds() {
let policy = RetryPolicy {
max_retries: 10,
initial_delay: Duration::from_millis(5000),
max_delay: Duration::from_secs(10),
backoff_multiplier: 2.0,
use_jitter: true,
};
let mut delays = Vec::new();
for _ in 0..50 {
let delay = policy.calculate_delay(5);
delays.push(delay.as_millis());
}
for delay_ms in &delays {
assert!(*delay_ms >= 7500, "Delay {}ms below minimum", delay_ms);
assert!(*delay_ms <= 12500, "Delay {}ms above maximum", delay_ms);
}
}
#[test]
fn test_should_retry_true() {
let policy = RetryPolicy::default();
assert!(policy.should_retry(0));
assert!(policy.should_retry(1));
assert!(policy.should_retry(2));
}
#[test]
fn test_should_retry_false() {
let policy = RetryPolicy::default();
assert!(!policy.should_retry(3));
assert!(!policy.should_retry(4));
assert!(!policy.should_retry(100));
}
#[test]
fn test_builder_pattern() {
let policy = RetryPolicy::new(5, Duration::from_millis(200), Duration::from_secs(30))
.without_jitter();
assert_eq!(policy.max_retries, 5);
assert_eq!(policy.initial_delay, Duration::from_millis(200));
assert_eq!(policy.max_delay, Duration::from_secs(30));
assert!(!policy.use_jitter);
let policy2 = policy.clone().with_jitter();
assert!(policy2.use_jitter);
}
}
mod serialization {
use super::*;
#[test]
fn test_rate_limit_info_serialize() {
let reset_time = DateTime::parse_from_rfc3339("2024-01-01T12:00:00Z")
.unwrap()
.with_timezone(&Utc);
let info = RateLimitInfo {
limit: 5000,
remaining: 4999,
reset_at: reset_time,
is_limited: false,
};
let json = serde_json::to_value(&info).unwrap();
assert_eq!(json["limit"], 5000);
assert_eq!(json["remaining"], 4999);
assert_eq!(json["is_limited"], false);
assert!(json.get("reset_at").is_some());
}
#[test]
fn test_rate_limit_info_deserialize() {
let json = r#"{
"limit": 5000,
"remaining": 4999,
"reset_at": "2024-01-01T12:00:00Z",
"is_limited": false
}"#;
let info: RateLimitInfo = serde_json::from_str(json).unwrap();
assert_eq!(info.limit, 5000);
assert_eq!(info.remaining, 4999);
assert!(!info.is_limited);
assert_eq!(
info.reset_at,
DateTime::parse_from_rfc3339("2024-01-01T12:00:00Z")
.unwrap()
.with_timezone(&Utc)
);
}
#[test]
fn test_retry_policy_serialize() {
let policy = RetryPolicy {
max_retries: 5,
initial_delay: Duration::from_secs(2),
max_delay: Duration::from_secs(60),
backoff_multiplier: 2.0,
use_jitter: true,
};
let json = serde_json::to_value(&policy).unwrap();
assert_eq!(json["max_retries"], 5);
assert_eq!(json["backoff_multiplier"], 2.0);
assert_eq!(json["use_jitter"], true);
assert!(json.get("initial_delay").is_some());
assert!(json.get("max_delay").is_some());
}
#[test]
fn test_retry_policy_deserialize() {
let json = r#"{
"max_retries": 5,
"initial_delay": {"secs": 2, "nanos": 0},
"max_delay": {"secs": 60, "nanos": 0},
"backoff_multiplier": 2.0,
"use_jitter": true
}"#;
let policy: RetryPolicy = serde_json::from_str(json).unwrap();
assert_eq!(policy.max_retries, 5);
assert_eq!(policy.initial_delay, Duration::from_secs(2));
assert_eq!(policy.max_delay, Duration::from_secs(60));
assert_eq!(policy.backoff_multiplier, 2.0);
assert!(policy.use_jitter);
}
}
mod retry_after_parsing {
use super::*;
#[test]
fn test_parse_retry_after_delta_seconds() {
assert_eq!(parse_retry_after("60"), Some(Duration::from_secs(60)));
assert_eq!(parse_retry_after("120"), Some(Duration::from_secs(120)));
assert_eq!(parse_retry_after("0"), Some(Duration::from_secs(0)));
}
#[test]
fn test_parse_retry_after_http_date() {
let future = Utc::now() + chrono::Duration::seconds(60);
let http_date = future.to_rfc2822();
let delay = parse_retry_after(&http_date);
assert!(delay.is_some());
let delay_secs = delay.unwrap().as_secs();
assert!((59..=61).contains(&delay_secs), "Delay was {}s", delay_secs);
}
#[test]
fn test_parse_retry_after_invalid() {
assert_eq!(parse_retry_after("invalid"), None);
assert_eq!(parse_retry_after("not a number"), None);
assert_eq!(parse_retry_after(""), None);
assert_eq!(parse_retry_after("-10"), None); }
#[test]
fn test_parse_retry_after_past_date() {
let past = Utc::now() - chrono::Duration::seconds(60);
let http_date = past.to_rfc2822();
let delay = parse_retry_after(&http_date);
assert_eq!(delay, None);
}
#[test]
fn test_parse_retry_after_large_values() {
assert_eq!(parse_retry_after("3600"), Some(Duration::from_secs(3600)));
assert_eq!(parse_retry_after("86400"), Some(Duration::from_secs(86400)));
}
}
mod rate_limit_delay_calculation {
use super::*;
#[test]
fn test_calculate_rate_limit_delay_retry_after_priority() {
let future_timestamp = (Utc::now().timestamp() + 300).to_string();
let delay = calculate_rate_limit_delay(
Some("60"), Some(&future_timestamp), );
assert_eq!(delay, Duration::from_secs(60));
}
#[test]
fn test_calculate_rate_limit_delay_uses_reset() {
let future_timestamp = (Utc::now().timestamp() + 120).to_string();
let delay = calculate_rate_limit_delay(None, Some(&future_timestamp));
let delay_secs = delay.as_secs();
assert!(
(119..=121).contains(&delay_secs),
"Delay was {}s",
delay_secs
);
}
#[test]
fn test_calculate_rate_limit_delay_default() {
let delay = calculate_rate_limit_delay(None, None);
assert_eq!(delay, Duration::from_secs(60));
}
#[test]
fn test_calculate_rate_limit_delay_invalid_retry_after() {
let future_timestamp = (Utc::now().timestamp() + 90).to_string();
let delay = calculate_rate_limit_delay(Some("invalid"), Some(&future_timestamp));
let delay_secs = delay.as_secs();
assert!((89..=91).contains(&delay_secs), "Delay was {}s", delay_secs);
}
#[test]
fn test_calculate_rate_limit_delay_invalid_reset() {
let delay = calculate_rate_limit_delay(None, Some("not-a-timestamp"));
assert_eq!(delay, Duration::from_secs(60));
}
#[test]
fn test_calculate_rate_limit_delay_past_reset() {
let past_timestamp = (Utc::now().timestamp() - 60).to_string();
let delay = calculate_rate_limit_delay(None, Some(&past_timestamp));
assert_eq!(delay, Duration::from_secs(60));
}
#[test]
fn test_calculate_rate_limit_delay_zero() {
let delay = calculate_rate_limit_delay(Some("0"), None);
assert_eq!(delay, Duration::from_secs(0));
}
#[test]
fn test_calculate_rate_limit_delay_http_date() {
let future = Utc::now() + chrono::Duration::seconds(180); let http_date = future.to_rfc2822();
let delay = calculate_rate_limit_delay(Some(&http_date), None);
let delay_secs = delay.as_secs();
assert!(
(179..=181).contains(&delay_secs),
"Delay was {}s",
delay_secs
);
}
#[test]
fn test_calculate_rate_limit_delay_large_value() {
let delay = calculate_rate_limit_delay(Some("3600"), None);
assert_eq!(delay, Duration::from_secs(3600));
}
}
mod secondary_rate_limit_detection {
use super::*;
#[test]
fn test_detect_secondary_rate_limit_rate_limit_message() {
let body = r#"{"message":"You have exceeded a secondary rate limit. Please wait a few minutes before you try again."}"#;
assert!(detect_secondary_rate_limit(403, body));
}
#[test]
fn test_detect_secondary_rate_limit_rate_limit_underscore() {
let body = r#"{"message":"API rate_limit exceeded for user"}"#;
assert!(detect_secondary_rate_limit(403, body));
}
#[test]
fn test_detect_secondary_rate_limit_abuse_message() {
let body = r#"{"message":"You have triggered an abuse detection mechanism. Please retry your request later."}"#;
assert!(detect_secondary_rate_limit(403, body));
}
#[test]
fn test_detect_secondary_rate_limit_too_many_requests() {
let body = r#"{"message":"Too many requests. Please slow down."}"#;
assert!(detect_secondary_rate_limit(403, body));
}
#[test]
fn test_detect_secondary_rate_limit_case_insensitive() {
let body = r#"{"message":"RATE LIMIT exceeded"}"#;
assert!(detect_secondary_rate_limit(403, body));
}
#[test]
fn test_detect_secondary_rate_limit_permission_denied() {
let body = r#"{"message":"Resource not accessible by integration"}"#;
assert!(!detect_secondary_rate_limit(403, body));
}
#[test]
fn test_detect_secondary_rate_limit_unrelated_403() {
let body = r#"{"message":"This repository has been archived"}"#;
assert!(!detect_secondary_rate_limit(403, body));
}
#[test]
fn test_detect_secondary_rate_limit_not_403() {
let body = r#"{"message":"You have exceeded a secondary rate limit"}"#;
assert!(!detect_secondary_rate_limit(429, body));
assert!(!detect_secondary_rate_limit(404, body));
assert!(!detect_secondary_rate_limit(500, body));
assert!(!detect_secondary_rate_limit(200, body));
}
#[test]
fn test_detect_secondary_rate_limit_empty_body() {
assert!(!detect_secondary_rate_limit(403, ""));
}
#[test]
fn test_detect_secondary_rate_limit_non_json_body() {
let body = "Plain text error: rate limit exceeded";
assert!(detect_secondary_rate_limit(403, body));
}
#[test]
fn test_detect_secondary_rate_limit_substring_match() {
let body = r#"
{
"message": "Request forbidden by administrative rules. The request has triggered GitHub's abuse detection mechanisms. Please wait before retrying.",
"documentation_url": "https://docs.github.com"
}
"#;
assert!(detect_secondary_rate_limit(403, body));
}
#[test]
fn test_detect_secondary_rate_limit_real_github_response() {
let body = r#"{
"message": "You have exceeded a secondary rate limit. Please wait a few minutes before you try again.",
"documentation_url": "https://docs.github.com/rest/overview/resources-in-the-rest-api#secondary-rate-limits"
}"#;
assert!(detect_secondary_rate_limit(403, body));
}
#[test]
fn test_detect_secondary_rate_limit_no_partial_match() {
let body = r#"{"message":"Preliminary check failed"}"#;
assert!(!detect_secondary_rate_limit(403, body));
}
}