use super::*;
use crate::auth::InstallationId;
use chrono::{Duration, Utc};
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
mod rate_limit_tests {
use super::*;
#[test]
fn test_rate_limit_creation_and_accessors() {
let reset_time = Utc::now() + Duration::hours(1);
let rate_limit = RateLimit::new(5000, 4500, reset_time, "core");
assert_eq!(rate_limit.limit(), 5000);
assert_eq!(rate_limit.remaining(), 4500);
assert_eq!(rate_limit.reset_at(), reset_time);
assert_eq!(rate_limit.resource(), "core");
}
#[test]
fn test_rate_limit_is_exhausted_when_zero_remaining() {
let rate_limit = RateLimit::new(5000, 0, Utc::now(), "core");
assert!(rate_limit.is_exhausted());
}
#[test]
fn test_rate_limit_not_exhausted_when_remaining() {
let rate_limit = RateLimit::new(5000, 100, Utc::now(), "core");
assert!(!rate_limit.is_exhausted());
}
#[test]
fn test_rate_limit_near_exhaustion_below_margin() {
let rate_limit = RateLimit::new(5000, 400, Utc::now(), "core");
assert!(rate_limit.is_near_exhaustion(0.1));
}
#[test]
fn test_rate_limit_not_near_exhaustion_above_margin() {
let rate_limit = RateLimit::new(5000, 600, Utc::now(), "core");
assert!(!rate_limit.is_near_exhaustion(0.1));
}
#[test]
fn test_rate_limit_has_not_reset_before_time() {
let reset_time = Utc::now() + Duration::hours(1);
let rate_limit = RateLimit::new(5000, 4500, reset_time, "core");
assert!(!rate_limit.has_reset());
}
#[test]
fn test_rate_limit_has_reset_after_time() {
let reset_time = Utc::now() - Duration::minutes(1);
let rate_limit = RateLimit::new(5000, 0, reset_time, "core");
assert!(rate_limit.has_reset());
}
}
mod parse_rate_limit_tests {
use super::*;
#[test]
fn test_parse_valid_rate_limit_headers() {
let mut headers = HeaderMap::new();
headers.insert(
HeaderName::from_static("x-ratelimit-limit"),
HeaderValue::from_static("5000"),
);
headers.insert(
HeaderName::from_static("x-ratelimit-remaining"),
HeaderValue::from_static("4999"),
);
headers.insert(
HeaderName::from_static("x-ratelimit-reset"),
HeaderValue::from_str(&(Utc::now().timestamp() + 3600).to_string()).unwrap(),
);
headers.insert(
HeaderName::from_static("x-ratelimit-resource"),
HeaderValue::from_static("core"),
);
let rate_limit = parse_rate_limit_from_headers(&headers);
assert!(rate_limit.is_some());
let rate_limit = rate_limit.unwrap();
assert_eq!(rate_limit.limit(), 5000);
assert_eq!(rate_limit.remaining(), 4999);
assert_eq!(rate_limit.resource(), "core");
}
#[test]
fn test_parse_rate_limit_defaults_to_core_resource() {
let mut headers = HeaderMap::new();
headers.insert(
HeaderName::from_static("x-ratelimit-limit"),
HeaderValue::from_static("5000"),
);
headers.insert(
HeaderName::from_static("x-ratelimit-remaining"),
HeaderValue::from_static("4999"),
);
headers.insert(
HeaderName::from_static("x-ratelimit-reset"),
HeaderValue::from_str(&(Utc::now().timestamp() + 3600).to_string()).unwrap(),
);
let rate_limit = parse_rate_limit_from_headers(&headers);
assert!(rate_limit.is_some());
let rate_limit = rate_limit.unwrap();
assert_eq!(rate_limit.resource(), "core");
}
#[test]
fn test_parse_rate_limit_returns_none_when_headers_missing() {
let mut headers = HeaderMap::new();
headers.insert(
HeaderName::from_static("x-ratelimit-limit"),
HeaderValue::from_static("5000"),
);
let rate_limit = parse_rate_limit_from_headers(&headers);
assert!(rate_limit.is_none());
}
#[test]
fn test_parse_rate_limit_returns_none_when_headers_invalid() {
let mut headers = HeaderMap::new();
headers.insert(
HeaderName::from_static("x-ratelimit-limit"),
HeaderValue::from_static("not-a-number"),
);
headers.insert(
HeaderName::from_static("x-ratelimit-remaining"),
HeaderValue::from_static("4999"),
);
headers.insert(
HeaderName::from_static("x-ratelimit-reset"),
HeaderValue::from_static("1234567890"),
);
let rate_limit = parse_rate_limit_from_headers(&headers);
assert!(rate_limit.is_none());
}
}
mod rate_limiter_tests {
use super::*;
#[test]
fn test_rate_limiter_creation_with_margin() {
let limiter = RateLimiter::new(0.15);
assert_eq!(limiter.margin, 0.15);
}
#[test]
fn test_rate_limiter_clamps_margin_to_valid_range() {
let limiter_too_low = RateLimiter::new(-0.5);
assert_eq!(limiter_too_low.margin, 0.0);
let limiter_too_high = RateLimiter::new(1.5);
assert_eq!(limiter_too_high.margin, 1.0);
}
#[test]
fn test_rate_limiter_updates_from_headers_app_context() {
let limiter = RateLimiter::new(0.1);
let mut headers = HeaderMap::new();
headers.insert(
HeaderName::from_static("x-ratelimit-limit"),
HeaderValue::from_static("5000"),
);
headers.insert(
HeaderName::from_static("x-ratelimit-remaining"),
HeaderValue::from_static("4500"),
);
headers.insert(
HeaderName::from_static("x-ratelimit-reset"),
HeaderValue::from_str(&(Utc::now().timestamp() + 3600).to_string()).unwrap(),
);
headers.insert(
HeaderName::from_static("x-ratelimit-resource"),
HeaderValue::from_static("core"),
);
limiter.update_from_headers(&RateLimitContext::App, &headers);
let rate_limit = limiter.get_limit(&RateLimitContext::App, "core");
assert!(rate_limit.is_some());
let rate_limit = rate_limit.unwrap();
assert_eq!(rate_limit.limit(), 5000);
assert_eq!(rate_limit.remaining(), 4500);
}
#[test]
fn test_rate_limiter_updates_from_headers_installation_context() {
let limiter = RateLimiter::new(0.1);
let install_id = InstallationId::new(12345);
let mut headers = HeaderMap::new();
headers.insert(
HeaderName::from_static("x-ratelimit-limit"),
HeaderValue::from_static("5000"),
);
headers.insert(
HeaderName::from_static("x-ratelimit-remaining"),
HeaderValue::from_static("3500"),
);
headers.insert(
HeaderName::from_static("x-ratelimit-reset"),
HeaderValue::from_str(&(Utc::now().timestamp() + 3600).to_string()).unwrap(),
);
headers.insert(
HeaderName::from_static("x-ratelimit-resource"),
HeaderValue::from_static("core"),
);
limiter.update_from_headers(&RateLimitContext::Installation(install_id), &headers);
let rate_limit = limiter.get_limit(&RateLimitContext::Installation(install_id), "core");
assert!(rate_limit.is_some());
let rate_limit = rate_limit.unwrap();
assert_eq!(rate_limit.limit(), 5000);
assert_eq!(rate_limit.remaining(), 3500);
}
#[test]
fn test_rate_limiter_tracks_installations_separately() {
let limiter = RateLimiter::new(0.1);
let install_1 = InstallationId::new(111);
let install_2 = InstallationId::new(222);
let mut headers_1 = HeaderMap::new();
headers_1.insert(
HeaderName::from_static("x-ratelimit-limit"),
HeaderValue::from_static("5000"),
);
headers_1.insert(
HeaderName::from_static("x-ratelimit-remaining"),
HeaderValue::from_static("4000"),
);
headers_1.insert(
HeaderName::from_static("x-ratelimit-reset"),
HeaderValue::from_str(&(Utc::now().timestamp() + 3600).to_string()).unwrap(),
);
limiter.update_from_headers(&RateLimitContext::Installation(install_1), &headers_1);
let mut headers_2 = HeaderMap::new();
headers_2.insert(
HeaderName::from_static("x-ratelimit-limit"),
HeaderValue::from_static("5000"),
);
headers_2.insert(
HeaderName::from_static("x-ratelimit-remaining"),
HeaderValue::from_static("2000"),
);
headers_2.insert(
HeaderName::from_static("x-ratelimit-reset"),
HeaderValue::from_str(&(Utc::now().timestamp() + 3600).to_string()).unwrap(),
);
limiter.update_from_headers(&RateLimitContext::Installation(install_2), &headers_2);
let limit_1 = limiter
.get_limit(&RateLimitContext::Installation(install_1), "core")
.unwrap();
let limit_2 = limiter
.get_limit(&RateLimitContext::Installation(install_2), "core")
.unwrap();
assert_eq!(limit_1.remaining(), 4000);
assert_eq!(limit_2.remaining(), 2000);
}
#[test]
fn test_rate_limiter_tracks_app_and_installation_separately() {
let limiter = RateLimiter::new(0.1);
let install_id = InstallationId::new(12345);
let mut app_headers = HeaderMap::new();
app_headers.insert(
HeaderName::from_static("x-ratelimit-limit"),
HeaderValue::from_static("5000"),
);
app_headers.insert(
HeaderName::from_static("x-ratelimit-remaining"),
HeaderValue::from_static("4500"),
);
app_headers.insert(
HeaderName::from_static("x-ratelimit-reset"),
HeaderValue::from_str(&(Utc::now().timestamp() + 3600).to_string()).unwrap(),
);
limiter.update_from_headers(&RateLimitContext::App, &app_headers);
let mut install_headers = HeaderMap::new();
install_headers.insert(
HeaderName::from_static("x-ratelimit-limit"),
HeaderValue::from_static("5000"),
);
install_headers.insert(
HeaderName::from_static("x-ratelimit-remaining"),
HeaderValue::from_static("3000"),
);
install_headers.insert(
HeaderName::from_static("x-ratelimit-reset"),
HeaderValue::from_str(&(Utc::now().timestamp() + 3600).to_string()).unwrap(),
);
limiter.update_from_headers(
&RateLimitContext::Installation(install_id),
&install_headers,
);
let app_limit = limiter.get_limit(&RateLimitContext::App, "core").unwrap();
let install_limit = limiter
.get_limit(&RateLimitContext::Installation(install_id), "core")
.unwrap();
assert_eq!(app_limit.remaining(), 4500);
assert_eq!(install_limit.remaining(), 3000);
}
#[test]
fn test_rate_limiter_can_proceed_when_healthy_app() {
let limiter = RateLimiter::new(0.1);
let mut headers = HeaderMap::new();
headers.insert(
HeaderName::from_static("x-ratelimit-limit"),
HeaderValue::from_static("5000"),
);
headers.insert(
HeaderName::from_static("x-ratelimit-remaining"),
HeaderValue::from_static("4000"),
);
headers.insert(
HeaderName::from_static("x-ratelimit-reset"),
HeaderValue::from_str(&(Utc::now().timestamp() + 3600).to_string()).unwrap(),
);
limiter.update_from_headers(&RateLimitContext::App, &headers);
assert!(limiter.can_proceed(&RateLimitContext::App, "core"));
}
#[test]
fn test_rate_limiter_can_proceed_when_healthy_installation() {
let limiter = RateLimiter::new(0.1);
let install_id = InstallationId::new(12345);
let mut headers = HeaderMap::new();
headers.insert(
HeaderName::from_static("x-ratelimit-limit"),
HeaderValue::from_static("5000"),
);
headers.insert(
HeaderName::from_static("x-ratelimit-remaining"),
HeaderValue::from_static("4000"),
);
headers.insert(
HeaderName::from_static("x-ratelimit-reset"),
HeaderValue::from_str(&(Utc::now().timestamp() + 3600).to_string()).unwrap(),
);
limiter.update_from_headers(&RateLimitContext::Installation(install_id), &headers);
assert!(limiter.can_proceed(&RateLimitContext::Installation(install_id), "core"));
}
#[test]
fn test_rate_limiter_blocks_when_near_exhaustion() {
let limiter = RateLimiter::new(0.1);
let mut headers = HeaderMap::new();
headers.insert(
HeaderName::from_static("x-ratelimit-limit"),
HeaderValue::from_static("5000"),
);
headers.insert(
HeaderName::from_static("x-ratelimit-remaining"),
HeaderValue::from_static("400"),
);
headers.insert(
HeaderName::from_static("x-ratelimit-reset"),
HeaderValue::from_str(&(Utc::now().timestamp() + 3600).to_string()).unwrap(),
);
limiter.update_from_headers(&RateLimitContext::App, &headers);
assert!(!limiter.can_proceed(&RateLimitContext::App, "core"));
}
#[test]
fn test_rate_limiter_blocks_when_exhausted() {
let limiter = RateLimiter::new(0.1);
let mut headers = HeaderMap::new();
headers.insert(
HeaderName::from_static("x-ratelimit-limit"),
HeaderValue::from_static("5000"),
);
headers.insert(
HeaderName::from_static("x-ratelimit-remaining"),
HeaderValue::from_static("0"),
);
headers.insert(
HeaderName::from_static("x-ratelimit-reset"),
HeaderValue::from_str(&(Utc::now().timestamp() + 3600).to_string()).unwrap(),
);
limiter.update_from_headers(&RateLimitContext::App, &headers);
assert!(!limiter.can_proceed(&RateLimitContext::App, "core"));
}
#[test]
fn test_rate_limiter_allows_when_no_data() {
let limiter = RateLimiter::new(0.1);
assert!(limiter.can_proceed(&RateLimitContext::App, "core"));
}
#[test]
fn test_rate_limiter_allows_after_reset() {
let limiter = RateLimiter::new(0.1);
let mut headers = HeaderMap::new();
headers.insert(
HeaderName::from_static("x-ratelimit-limit"),
HeaderValue::from_static("5000"),
);
headers.insert(
HeaderName::from_static("x-ratelimit-remaining"),
HeaderValue::from_static("0"),
);
headers.insert(
HeaderName::from_static("x-ratelimit-reset"),
HeaderValue::from_str(&(Utc::now().timestamp() - 60).to_string()).unwrap(),
);
limiter.update_from_headers(&RateLimitContext::App, &headers);
assert!(limiter.can_proceed(&RateLimitContext::App, "core"));
}
#[test]
fn test_rate_limiter_contexts_independent() {
let limiter = RateLimiter::new(0.1);
let install_1 = InstallationId::new(111);
let install_2 = InstallationId::new(222);
let mut headers_1 = HeaderMap::new();
headers_1.insert(
HeaderName::from_static("x-ratelimit-limit"),
HeaderValue::from_static("5000"),
);
headers_1.insert(
HeaderName::from_static("x-ratelimit-remaining"),
HeaderValue::from_static("0"),
);
headers_1.insert(
HeaderName::from_static("x-ratelimit-reset"),
HeaderValue::from_str(&(Utc::now().timestamp() + 3600).to_string()).unwrap(),
);
limiter.update_from_headers(&RateLimitContext::Installation(install_1), &headers_1);
let mut headers_2 = HeaderMap::new();
headers_2.insert(
HeaderName::from_static("x-ratelimit-limit"),
HeaderValue::from_static("5000"),
);
headers_2.insert(
HeaderName::from_static("x-ratelimit-remaining"),
HeaderValue::from_static("4000"),
);
headers_2.insert(
HeaderName::from_static("x-ratelimit-reset"),
HeaderValue::from_str(&(Utc::now().timestamp() + 3600).to_string()).unwrap(),
);
limiter.update_from_headers(&RateLimitContext::Installation(install_2), &headers_2);
assert!(!limiter.can_proceed(&RateLimitContext::Installation(install_1), "core"));
assert!(limiter.can_proceed(&RateLimitContext::Installation(install_2), "core"));
}
#[test]
fn test_rate_limiter_get_limit_returns_none_when_no_data() {
let limiter = RateLimiter::new(0.1);
assert!(limiter.get_limit(&RateLimitContext::App, "core").is_none());
}
#[test]
fn test_rate_limiter_get_limit_returns_stored_data() {
let limiter = RateLimiter::new(0.1);
let mut headers = HeaderMap::new();
headers.insert(
HeaderName::from_static("x-ratelimit-limit"),
HeaderValue::from_static("5000"),
);
headers.insert(
HeaderName::from_static("x-ratelimit-remaining"),
HeaderValue::from_static("4500"),
);
headers.insert(
HeaderName::from_static("x-ratelimit-reset"),
HeaderValue::from_str(&(Utc::now().timestamp() + 3600).to_string()).unwrap(),
);
limiter.update_from_headers(&RateLimitContext::App, &headers);
let rate_limit = limiter.get_limit(&RateLimitContext::App, "core");
assert!(rate_limit.is_some());
let rate_limit = rate_limit.unwrap();
assert_eq!(rate_limit.limit(), 5000);
assert_eq!(rate_limit.remaining(), 4500);
}
}