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);
}
#[test]
fn circuit_breaker_concurrent_check_and_failure_make_progress() {
use std::sync::mpsc;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
let cb = Arc::new(CircuitBreaker::new(1, 0));
cb.record_failure();
assert_eq!(cb.state(), CircuitState::Open, "precondition: breaker open");
let (tx, rx) = mpsc::channel();
let mut handles = Vec::new();
for t in 0..4 {
let cb = Arc::clone(&cb);
let tx = tx.clone();
handles.push(thread::spawn(move || {
for _ in 0..2_000 {
if t % 2 == 0 {
let _ = cb.check();
} else {
cb.record_failure();
}
}
let _ = tx.send(t);
}));
}
drop(tx);
for _ in 0..4 {
rx.recv_timeout(Duration::from_secs(30))
.expect("circuit breaker deadlocked: a worker never finished (lock-order regression)");
}
for h in handles {
h.join().expect("worker panicked");
}
}