use serde_json::json; // ============================================================================ // M3.5.7 — Rate limiting per-apikey per-endpoint + idempotency // ============================================================================ // // 8 tests covering rate limiting strategy and idempotency // #[test] fn r1_rate_limits_per_endpoint() { let limits = json!({ "POST /memory/ingest": 100, "GET /memory/query": 1000, "GET /memory/skills": -1, "GET /memory/projects": 100 }); assert_eq!(limits["POST /memory/ingest"], 100); assert_eq!(limits["GET /memory/query"], 1000); assert_eq!(limits["GET /memory/skills"], -1); } #[test] fn r2_rate_limits_per_apikey() { let api_key_1 = "key-abc"; let api_key_2 = "key-xyz"; let mut counters = std::collections::HashMap::new(); counters.insert(api_key_1, 5); counters.insert(api_key_2, 2); assert_eq!(counters[api_key_1], 5); assert_eq!(counters[api_key_2], 2); } #[test] fn r3_burst_allowance() { let burst_capacity: u32 = 10; let requests_in_burst = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; assert!(requests_in_burst.len() as u32 <= burst_capacity); let request_11 = 11; assert!(request_11 as u32 > burst_capacity); } #[test] fn r4_429_response_on_rate_limit() { let response = json!({ "status": 429, "error": "rate_limit_exceeded", "reason": "100 requests/hour for POST /memory/ingest", "retry_after_seconds": 47 }); assert_eq!(response["status"], 429); assert_eq!(response["error"], "rate_limit_exceeded"); assert!(response["retry_after_seconds"].is_number()); } #[test] fn r5_retry_after_header() { let retry_after_seconds = 47u32; assert!(retry_after_seconds > 0 && retry_after_seconds <= 3600); } #[test] fn r6_idempotency_by_ingest_id() { let ingest_id = "abc123def456abc123def456abc123def456abc123def456abc123def456ab00"; let job_id_1 = "ingest-uuid-1"; let job_id_2 = "ingest-uuid-1"; assert_eq!(job_id_1, job_id_2); let ingest_id_2 = "abc123def456abc123def456abc123def456abc123def456abc123def456ab01"; let job_id_3 = "ingest-uuid-2"; assert_ne!(job_id_1, job_id_3); } #[test] fn r7_idempotency_ttl_24_hours() { let created_at: u64 = 1000000; let queried_at_fresh: u64 = 1000000 + 3600; // 1 hour later (fresh) let queried_at_expired: u64 = 1000000 + (86400 * 2); // 2 days later (expired) let ttl_seconds: u64 = 86400; // 24 hours let elapsed_fresh = queried_at_fresh - created_at; let elapsed_expired = queried_at_expired - created_at; assert!(elapsed_fresh < ttl_seconds, "1 hour should be within TTL"); assert!(elapsed_expired > ttl_seconds, "2 days should exceed TTL"); } #[test] fn r8_token_bucket_model() { // Token bucket refill model // Capacity: 100 tokens // Refill rate: 100/3600 tokens/sec (100/hour) // Cost per request: 1 token let capacity: f32 = 100.0; let refill_rate: f32 = 100.0 / 3600.0; // ~0.0278 tokens/sec let cost_per_request: f32 = 1.0; // Simulate tokens over time let mut tokens: f32 = capacity; // After 1 hour, bucket refilled let elapsed_1hour: f32 = 3600.0; let refilled_1hour: f32 = (elapsed_1hour * refill_rate).min(capacity); tokens = (tokens + refilled_1hour).min(capacity); assert!(tokens >= 50.0 && tokens <= capacity); // Make a request (costs 1 token) tokens -= cost_per_request; assert!(tokens < capacity); }