Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 11m16s
83 lines
3.4 KiB
Rust
83 lines
3.4 KiB
Rust
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<u16> = 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);
|
|
} |