use crate::common::{setup_app, spawn_server}; use std::time::{Duration, Instant}; #[tokio::test] async fn test_rate_limiting_blocks_spam() { let (app, _db) = setup_app().await; let (base_url, client) = spawn_server(app).await; // Send 6 requests CONCURRENTLY so they arrive nearly simultaneously // Capacity is 5, so exactly 5 should pass and 1 should fail. let mut handles = Vec::new(); for _ in 0..6 { let c = client.clone(); let url = format!("{}/api/v1/auth/login", base_url); handles.push(tokio::spawn(async move { c.post(&url) .header("x-client-ip", "1.2.3.4") .json(&serde_json::json!({"email": "a@test.com", "password": "b"})) .send().await.unwrap() .status().as_u16() })); } let statuses: Vec = futures_util::future::join_all(handles).await .into_iter().map(|r| r.unwrap()).collect(); let successes = statuses.iter().filter(|&&s| s != 429).count(); let blocked = statuses.iter().filter(|&&s| s == 429).count(); assert!(successes <= 5, "At most 5 rapid requests should be allowed, got {} passing", successes); assert!(blocked >= 1, "At least 1 request should be rate-limited (429)"); // Different IP should still work let ok = client .post(format!("{}/api/v1/auth/login", base_url)) .header("x-client-ip", "5.6.7.8") .json(&serde_json::json!({"email": "a@test.com", "password": "b"})) .send().await.unwrap(); assert_ne!(ok.status(), 429, "Different IP should not be rate-limited"); } #[tokio::test] async fn test_anti_enumeration_timing() { let (app, _db) = setup_app().await; let (base_url, client) = spawn_server(app).await; let start = Instant::now(); let _ = client .post(format!("{}/api/v1/auth/login", base_url)) .header("x-client-ip", "1.1.1.1") // Different IP to avoid rate limits from previous requests in other tests if run concurrently .json(&serde_json::json!({"email": "ghost_not_real@test.com", "password": "irrelevant"})) .send().await.unwrap(); let duration_nonexistent = start.elapsed(); let email = format!("timing_{}@test.com", uuid::Uuid::new_v4()); let resp = client .post(format!("{}/api/v1/auth/register", base_url)) .header("x-client-ip", "2.2.2.2") .json(&serde_json::json!({"email": email, "password": "SuperSecureP@ssw0rd2024!"})) .send().await.unwrap(); assert!(resp.status().is_success()); let start = Instant::now(); let _ = client .post(format!("{}/api/v1/auth/login", base_url)) .header("x-client-ip", "3.3.3.3") .json(&serde_json::json!({"email": email, "password": "WrongPassword123!"})) .send().await.unwrap(); let duration_existent = start.elapsed(); // Anti-enumeration middleware ensures BOTH take >= 150ms assert!(duration_nonexistent >= Duration::from_millis(150), "Fast path should be padded to >= 150ms"); assert!(duration_existent >= Duration::from_millis(150), "Slow path should be padded to >= 150ms"); let diff = if duration_nonexistent > duration_existent { duration_nonexistent - duration_existent } else { duration_existent - duration_nonexistent }; assert!(diff < Duration::from_millis(300), "Timing difference should be <300ms because both paths are padded by random delay: got {:?}", diff); }