use super::resilience::{CircuitBreaker, CircuitState, RateLimiter};
use crate::guardrails::limits::GuardRailViolation;
#[test]
fn rate_limiter_allows_within_limit() {
let limiter = RateLimiter::new(10);
for i in 0..10 {
assert!(
limiter.check("client-a").is_ok(),
"request {i} should be allowed"
);
}
}
#[test]
fn rate_limiter_rejects_over_limit() {
let limiter = RateLimiter::new(3);
for _ in 0..3 {
limiter.check("client-b").expect("should be allowed");
}
let result = limiter.check("client-b");
assert!(result.is_err());
match result.unwrap_err() {
GuardRailViolation::RateLimitExceeded { limit_qps } => {
assert_eq!(limit_qps, 3);
}
other => panic!("expected RateLimitExceeded, got: {other}"),
}
}
#[test]
fn rate_limiter_isolates_clients() {
let limiter = RateLimiter::new(2);
limiter.check("client-a").unwrap();
limiter.check("client-a").unwrap();
assert!(limiter.check("client-a").is_err());
assert!(limiter.check("client-b").is_ok());
}
#[test]
fn rate_limiter_bounds_client_map_under_id_rotation() {
use crate::guardrails::resilience::MAX_TRACKED_CLIENTS;
let limiter = RateLimiter::new(10);
let total = MAX_TRACKED_CLIENTS + 500;
for i in 0..total {
let _ = limiter.check(&format!("attacker-{i}"));
assert!(
limiter.tracked_clients() <= MAX_TRACKED_CLIENTS,
"client map exceeded cap at iteration {i}: {}",
limiter.tracked_clients()
);
}
assert_eq!(limiter.tracked_clients(), MAX_TRACKED_CLIENTS);
for _ in 0..10 {
let _ = limiter.check("active");
}
assert!(
limiter.check("active").is_err(),
"active client should be rate-limited after exhausting tokens"
);
}
#[test]
fn rate_limiter_bounded_eviction_under_low_qps_rotation() {
use crate::guardrails::resilience::MAX_TRACKED_CLIENTS;
let limiter = RateLimiter::new(1);
let total = MAX_TRACKED_CLIENTS + 500;
for i in 0..total {
let _ = limiter.check(&format!("rot-{i}"));
assert!(
limiter.tracked_clients() <= MAX_TRACKED_CLIENTS,
"client map exceeded cap at iteration {i}: {}",
limiter.tracked_clients()
);
}
assert_eq!(limiter.tracked_clients(), MAX_TRACKED_CLIENTS);
assert!(limiter.check("victim").is_ok(), "first token available");
assert!(
limiter.check("victim").is_err(),
"re-used id must remain throttled (no self-reset via eviction)"
);
}
#[test]
fn circuit_starts_closed() {
let cb = CircuitBreaker::new(3, 60);
assert_eq!(cb.state(), CircuitState::Closed);
assert!(cb.check().is_ok());
}
#[test]
fn circuit_opens_after_failure_threshold() {
let cb = CircuitBreaker::new(3, 60);
cb.record_failure();
assert_eq!(cb.state(), CircuitState::Closed);
cb.record_failure();
assert_eq!(cb.state(), CircuitState::Closed);
cb.record_failure();
assert_eq!(cb.state(), CircuitState::Open);
}
#[test]
fn circuit_open_rejects_requests() {
let cb = CircuitBreaker::new(1, 9999);
cb.record_failure(); assert_eq!(cb.state(), CircuitState::Open);
let result = cb.check();
assert!(result.is_err());
match result.unwrap_err() {
GuardRailViolation::CircuitOpen {
recovery_in_seconds,
} => {
assert!(recovery_in_seconds > 0);
}
other => panic!("expected CircuitOpen, got: {other}"),
}
}
#[test]
fn circuit_success_resets_failure_count() {
let cb = CircuitBreaker::new(3, 60);
cb.record_failure();
cb.record_failure();
cb.record_success();
assert_eq!(cb.state(), CircuitState::Closed);
cb.record_failure();
cb.record_failure();
assert_eq!(cb.state(), CircuitState::Closed);
cb.record_failure();
assert_eq!(cb.state(), CircuitState::Open);
}
#[test]
fn circuit_halfopen_success_closes() {
let cb = CircuitBreaker::new(1, 0); cb.record_failure();
assert_eq!(cb.state(), CircuitState::Open);
assert!(cb.check().is_ok());
assert_eq!(cb.state(), CircuitState::HalfOpen);
cb.record_success();
assert_eq!(cb.state(), CircuitState::Closed);
}
#[test]
fn circuit_halfopen_failure_reopens() {
let cb = CircuitBreaker::new(1, 0);
cb.record_failure();
assert_eq!(cb.state(), CircuitState::Open);
assert!(cb.check().is_ok());
assert_eq!(cb.state(), CircuitState::HalfOpen);
cb.record_failure();
assert_eq!(cb.state(), CircuitState::Open);
}