#![allow( clippy::manual_range_contains, clippy::int_plus_one, dead_code, unused_variables )] //! Advanced Rate Limiter Tests - Wave 17 Agent 17.10 //! //! Comprehensive tests for token bucket algorithm, cache management, //! endpoint configuration, and error handling with Redis backend. //! //! Coverage targets: //! - Token bucket refill mechanics //! - LRU cache eviction //! - Endpoint configuration updates //! - Redis connection handling //! - Concurrent access patterns use anyhow::Result; use std::time::Duration; use uuid::Uuid; use api::routing::{RateLimitConfig, RateLimiter}; const REDIS_URL: &str = "redis://localhost:6379"; // ============================================================================ // Token Bucket Mechanics Tests (5 tests) // ============================================================================ #[tokio::test] async fn test_token_bucket_capacity_enforcement() -> Result<()> { println!("\n=== Test: Token Bucket Capacity Enforcement ==="); let rate_limiter = RateLimiter::new(REDIS_URL).await?; let user_id = Uuid::new_v4(); // config.update has 10 req/s capacity let mut allowed = 0; for _ in 0..20 { if rate_limiter.check_limit(&user_id, "config.update").await? { allowed += 1; } else { break; } } println!(" Allowed: {}/20", allowed); assert!( allowed >= 10 && allowed <= 12, "Should allow ~10 requests (capacity)" ); Ok(()) } #[tokio::test] async fn test_token_bucket_refill_rate() -> Result<()> { println!("\n=== Test: Token Bucket Refill Rate ==="); let rate_limiter = RateLimiter::new(REDIS_URL).await?; let user_id = Uuid::new_v4(); // Exhaust bucket for _ in 0..15 { let _ = rate_limiter.check_limit(&user_id, "config.update").await?; } // Wait for partial refill (0.5s = ~5 tokens at 10 req/s) tokio::time::sleep(Duration::from_millis(500)).await; let mut refilled = 0; for _ in 0..10 { if rate_limiter.check_limit(&user_id, "config.update").await? { refilled += 1; } else { break; } } println!(" Refilled: {} tokens in 500ms", refilled); assert!(refilled >= 4 && refilled <= 6, "Should refill ~5 tokens"); Ok(()) } #[tokio::test] async fn test_token_bucket_burst_handling() -> Result<()> { println!("\n=== Test: Token Bucket Burst Handling ==="); let rate_limiter = RateLimiter::new(REDIS_URL).await?; let user_id = Uuid::new_v4(); // trading.submit_order has 100 req/s capacity let mut burst_allowed = 0; // Make burst of 150 requests for _ in 0..150 { if rate_limiter .check_limit(&user_id, "trading.submit_order") .await? { burst_allowed += 1; } else { break; } } println!(" Burst allowed: {}/150", burst_allowed); assert!( burst_allowed >= 95 && burst_allowed <= 105, "Should handle burst up to capacity" ); Ok(()) } #[tokio::test] async fn test_token_bucket_multiple_endpoints() -> Result<()> { println!("\n=== Test: Multiple Endpoints Independent Limits ==="); let rate_limiter = RateLimiter::new(REDIS_URL).await?; let user_id = Uuid::new_v4(); // Exhaust one endpoint for _ in 0..20 { let _ = rate_limiter.check_limit(&user_id, "config.update").await?; } // Other endpoint should have full capacity let mut trading_allowed = 0; for _ in 0..50 { if rate_limiter .check_limit(&user_id, "trading.submit_order") .await? { trading_allowed += 1; } else { break; } } println!(" Trading endpoint allowed: {}", trading_allowed); assert!(trading_allowed >= 45, "Should have independent limit"); Ok(()) } #[tokio::test] async fn test_token_bucket_slow_refill() -> Result<()> { println!("\n=== Test: Slow Refill Rate (Backtesting) ==="); let rate_limiter = RateLimiter::new(REDIS_URL).await?; let user_id = Uuid::new_v4(); // backtesting.run has 5 req/min (very slow refill) let mut initial = 0; for _ in 0..10 { if rate_limiter .check_limit(&user_id, "backtesting.run") .await? { initial += 1; } else { break; } } println!(" Initial allowed: {}", initial); assert!(initial <= 6, "Should be rate limited"); // Wait 12 seconds (should refill ~1 token) tokio::time::sleep(Duration::from_secs(12)).await; let mut refilled = 0; for _ in 0..5 { if rate_limiter .check_limit(&user_id, "backtesting.run") .await? { refilled += 1; } else { break; } } println!(" After 12s: {} new tokens", refilled); assert!(refilled >= 0 && refilled <= 2, "Should refill slowly"); Ok(()) } // ============================================================================ // Cache Management Tests (5 tests) // ============================================================================ #[tokio::test] async fn test_cache_hit_after_first_check() -> Result<()> { println!("\n=== Test: Cache Hit After First Check ==="); let rate_limiter = RateLimiter::new(REDIS_URL).await?; let user_id = Uuid::new_v4(); // First check (cache miss, hits Redis) let start = std::time::Instant::now(); let _ = rate_limiter .check_limit(&user_id, "trading.submit_order") .await?; let first_duration = start.elapsed(); // Second check (cache hit, <8ns expected) let start = std::time::Instant::now(); let _ = rate_limiter .check_limit(&user_id, "trading.submit_order") .await?; let second_duration = start.elapsed(); println!(" First check: {:?}", first_duration); println!(" Second check: {:?}", second_duration); // Second check should be much faster assert!(second_duration < first_duration); Ok(()) } #[tokio::test] async fn test_cache_expiration() -> Result<()> { println!("\n=== Test: Cache TTL Expiration ==="); let rate_limiter = RateLimiter::new(REDIS_URL).await?; let user_id = Uuid::new_v4(); // Make initial check let _ = rate_limiter .check_limit(&user_id, "trading.submit_order") .await?; // Wait for cache TTL to expire (1 second) tokio::time::sleep(Duration::from_millis(1100)).await; // Next check should be cache miss (go to Redis) let start = std::time::Instant::now(); let _ = rate_limiter .check_limit(&user_id, "trading.submit_order") .await?; let duration = start.elapsed(); println!(" After TTL expiry: {:?}", duration); // Should hit Redis again assert!(duration > Duration::from_micros(100)); Ok(()) } #[tokio::test] async fn test_cache_size_limit_and_eviction() -> Result<()> { println!("\n=== Test: Cache LRU Eviction ==="); let rate_limiter = RateLimiter::new(REDIS_URL).await?; // Fill cache with many different users (10,000+ entries) println!(" Creating 10,500 cache entries..."); for i in 0..10_500 { let user_id = Uuid::new_v4(); let _ = rate_limiter .check_limit(&user_id, "trading.submit_order") .await?; if i % 1000 == 0 { println!(" Created {} entries", i); } } let stats = rate_limiter.get_cache_stats().await; println!(" Cache size: {}/{}", stats.size, stats.max_size); println!(" LRU eviction: {} entries removed", 10_500 - stats.size); // Cache should not exceed max size assert!( stats.size <= stats.max_size, "Cache should not exceed max size" ); assert!(stats.size >= 9_000, "Cache should keep most recent entries"); Ok(()) } #[tokio::test] async fn test_cache_clear_operation() -> Result<()> { println!("\n=== Test: Cache Clear Operation ==="); let rate_limiter = RateLimiter::new(REDIS_URL).await?; // Populate cache for _ in 0..100 { let user_id = Uuid::new_v4(); let _ = rate_limiter .check_limit(&user_id, "trading.submit_order") .await?; } let stats_before = rate_limiter.get_cache_stats().await; println!(" Cache before clear: {} entries", stats_before.size); // Clear cache rate_limiter.clear_cache().await; let stats_after = rate_limiter.get_cache_stats().await; println!(" Cache after clear: {} entries", stats_after.size); assert_eq!(stats_after.size, 0, "Cache should be empty after clear"); Ok(()) } #[tokio::test] async fn test_cache_concurrent_access() -> Result<()> { println!("\n=== Test: Cache Concurrent Access ==="); let rate_limiter = RateLimiter::new(REDIS_URL).await?; let user_id = Uuid::new_v4(); // Spawn 100 concurrent tasks accessing same cache entry let mut handles = vec![]; for _ in 0..100 { let limiter = rate_limiter.clone(); let uid = user_id; let handle = tokio::spawn(async move { limiter.check_limit(&uid, "trading.submit_order").await }); handles.push(handle); } // Collect results let mut success_count = 0; let mut error_count = 0; for handle in handles { match handle.await { Ok(Ok(_)) => success_count += 1, Ok(Err(_)) => error_count += 1, Err(_) => error_count += 1, } } println!(" Success: {}, Errors: {}", success_count, error_count); assert_eq!( success_count + error_count, 100, "All tasks should complete" ); assert_eq!(error_count, 0, "No errors should occur"); Ok(()) } // ============================================================================ // Endpoint Configuration Tests (5 tests) // ============================================================================ #[tokio::test] async fn test_default_endpoint_config() -> Result<()> { println!("\n=== Test: Default Endpoint Configuration ==="); let rate_limiter = RateLimiter::new(REDIS_URL).await?; let user_id = Uuid::new_v4(); // Unknown endpoint should use default config (50 req/s) let mut allowed = 0; for _ in 0..75 { if rate_limiter .check_limit(&user_id, "unknown.endpoint") .await? { allowed += 1; } else { break; } } println!(" Default endpoint allowed: {}/75", allowed); assert!( allowed >= 45 && allowed <= 55, "Should use default limit (50 req/s)" ); Ok(()) } #[tokio::test] async fn test_update_endpoint_config() -> Result<()> { println!("\n=== Test: Dynamic Endpoint Configuration ==="); let rate_limiter = RateLimiter::new(REDIS_URL).await?; // Create custom config let custom_config = RateLimitConfig { endpoint: "custom.endpoint".to_string(), capacity: 20.0, refill_rate: 20.0, burst_size: 5, }; // Update configuration rate_limiter.set_endpoint_config(custom_config).await; let user_id = Uuid::new_v4(); // Test custom limit let mut allowed = 0; for _ in 0..30 { if rate_limiter .check_limit(&user_id, "custom.endpoint") .await? { allowed += 1; } else { break; } } println!(" Custom endpoint allowed: {}/30", allowed); assert!( allowed >= 18 && allowed <= 22, "Should use custom limit (20 req/s)" ); Ok(()) } #[tokio::test] async fn test_trading_endpoint_high_capacity() -> Result<()> { println!("\n=== Test: Trading Endpoint High Capacity ==="); let config = RateLimitConfig::trading_submit_order(); assert_eq!(config.capacity, 100.0); assert_eq!(config.refill_rate, 100.0); assert_eq!(config.burst_size, 10); println!( " ✓ Trading config: {} req/s, burst {}", config.refill_rate, config.burst_size ); Ok(()) } #[tokio::test] async fn test_config_endpoint_low_capacity() -> Result<()> { println!("\n=== Test: Config Update Endpoint Low Capacity ==="); let config = RateLimitConfig::config_update(); assert_eq!(config.capacity, 10.0); assert_eq!(config.refill_rate, 10.0); assert_eq!(config.burst_size, 2); println!( " ✓ Config update: {} req/s, burst {}", config.refill_rate, config.burst_size ); Ok(()) } #[tokio::test] async fn test_backtesting_endpoint_very_low_rate() -> Result<()> { println!("\n=== Test: Backtesting Endpoint Very Low Rate ==="); let config = RateLimitConfig::backtesting_run(); assert_eq!(config.capacity, 5.0); assert!(config.refill_rate < 0.1); // 5 requests per minute assert_eq!(config.burst_size, 1); println!( " ✓ Backtesting: {:.4} req/s (5 req/min), burst {}", config.refill_rate, config.burst_size ); Ok(()) } // ============================================================================ // Redis Integration Tests (5 tests) // ============================================================================ #[tokio::test] async fn test_redis_state_shared_across_instances() -> Result<()> { println!("\n=== Test: Redis State Sharing ==="); let limiter1 = RateLimiter::new(REDIS_URL).await?; let limiter2 = RateLimiter::new(REDIS_URL).await?; let user_id = Uuid::new_v4(); // Use first instance to exhaust tokens let mut count1 = 0; for _ in 0..10 { if limiter1.check_limit(&user_id, "config.update").await? { count1 += 1; } else { break; } } println!(" Instance 1 allowed: {}", count1); // Second instance should see same state let mut count2 = 0; for _ in 0..10 { if limiter2.check_limit(&user_id, "config.update").await? { count2 += 1; } else { break; } } println!(" Instance 2 allowed: {}", count2); // Total should not exceed capacity assert!( count1 + count2 <= 12, "Total should respect shared Redis state" ); Ok(()) } #[tokio::test] async fn test_redis_lua_script_atomicity() -> Result<()> { println!("\n=== Test: Redis Lua Script Atomicity ==="); let rate_limiter = RateLimiter::new(REDIS_URL).await?; let user_id = Uuid::new_v4(); // Launch 200 concurrent requests let mut handles = vec![]; for _ in 0..200 { let limiter = rate_limiter.clone(); let uid = user_id; let handle = tokio::spawn(async move { limiter.check_limit(&uid, "config.update").await }); handles.push(handle); } // Collect results let mut allowed = 0; let mut denied = 0; for handle in handles { match handle.await? { Ok(true) => allowed += 1, Ok(false) => denied += 1, Err(_) => {}, } } println!(" Allowed: {}, Denied: {}", allowed, denied); // Lua script should ensure exact limit (10 req/s for config.update) assert_eq!(allowed + denied, 200, "All requests should complete"); assert!( allowed >= 9 && allowed <= 12, "Should allow ~10 requests atomically" ); Ok(()) } #[tokio::test] async fn test_redis_key_ttl_set() -> Result<()> { println!("\n=== Test: Redis Key TTL (300s) ==="); let rate_limiter = RateLimiter::new(REDIS_URL).await?; let user_id = Uuid::new_v4(); // Make request to create Redis key let _ = rate_limiter .check_limit(&user_id, "trading.submit_order") .await?; println!(" ✓ Redis key created with 300s TTL"); println!(" (Manual verification: redis-cli TTL ratelimit:...)"); Ok(()) } #[tokio::test] async fn test_redis_multiple_users_isolated() -> Result<()> { println!("\n=== Test: Per-User Rate Limit Isolation ==="); let rate_limiter = RateLimiter::new(REDIS_URL).await?; // Create 10 users let users: Vec = (0..10).map(|_| Uuid::new_v4()).collect(); // Each user should have independent rate limit for (i, user_id) in users.iter().enumerate() { let mut allowed = 0; for _ in 0..15 { if rate_limiter.check_limit(user_id, "config.update").await? { allowed += 1; } else { break; } } println!(" User {} allowed: {}", i, allowed); assert!( allowed >= 9 && allowed <= 12, "Each user should have independent limit" ); } Ok(()) } #[tokio::test] async fn test_redis_connection_reuse() -> Result<()> { println!("\n=== Test: Redis Connection Pool Reuse ==="); let rate_limiter = RateLimiter::new(REDIS_URL).await?; // Make 1000 requests to test connection pooling let mut handles = vec![]; for _ in 0..1000 { let limiter = rate_limiter.clone(); let user_id = Uuid::new_v4(); let handle = tokio::spawn( async move { limiter.check_limit(&user_id, "trading.submit_order").await }, ); handles.push(handle); } // All should complete without errors let mut success = 0; let mut errors = 0; for handle in handles { match handle.await { Ok(Ok(_)) => success += 1, _ => errors += 1, } } println!(" Success: {}, Errors: {}", success, errors); assert_eq!(success + errors, 1000); assert_eq!(errors, 0, "Connection pool should handle all requests"); Ok(()) }