use super::*;
use a2a_protocol_types::error::A2aError;
struct Unreachable;
impl RateLimitCounter for Unreachable {
fn count<'a>(
&'a self,
_key: &'a str,
_window: u64,
_window_secs: u64,
) -> Pin<Box<dyn Future<Output = A2aResult<u64>> + Send + 'a>> {
Box::pin(async { Err(A2aError::internal("counter unreachable")) })
}
}
struct Reports(u64);
impl RateLimitCounter for Reports {
fn count<'a>(
&'a self,
_key: &'a str,
_window: u64,
_window_secs: u64,
) -> Pin<Box<dyn Future<Output = A2aResult<u64>> + Send + 'a>> {
let n = self.0;
Box::pin(async move { Ok(n) })
}
}
fn limiter(limit: u64) -> RateLimitInterceptor {
RateLimitInterceptor::new(RateLimitConfig {
requests_per_window: limit,
window_secs: 300,
..RateLimitConfig::default()
})
.expect("limiter builds")
}
fn ctx() -> CallContext {
CallContext::new("SendMessage").with_caller_identity("caller".to_string())
}
#[tokio::test]
async fn a_count_over_the_limit_is_refused_even_on_a_fresh_replica() {
let limiter = limiter(5).with_shared_counter(std::sync::Arc::new(Reports(6)));
let err = limiter
.before(&ctx())
.await
.expect_err("the deployment's budget is spent");
assert!(
err.message.contains("rate limit exceeded"),
"the rejection should name the limit, got: {}",
err.message
);
}
#[tokio::test]
async fn a_count_at_the_limit_is_still_admitted() {
let limiter = limiter(5).with_shared_counter(std::sync::Arc::new(Reports(5)));
assert!(limiter.before(&ctx()).await.is_ok());
}
#[tokio::test]
async fn an_unreachable_counter_degrades_to_local_counting() {
const LIMIT: u64 = 4;
let limiter = limiter(LIMIT).with_shared_counter(std::sync::Arc::new(Unreachable));
let mut admitted = 0;
for _ in 0..20 {
if limiter.before(&ctx()).await.is_err() {
break;
}
admitted += 1;
}
assert_eq!(
admitted, LIMIT,
"with the counter gone the limiter must still enforce its limit \
locally — neither refusing everything nor admitting everything"
);
}