use api_tests::*;
use reqwest::StatusCode;
use serde_json::json;
use std::time::Duration;
#[tokio::test]
async fn test_rate_limit_by_api_key() -> TestResult {
let client = build_test_client();
let mock_server = wiremock::MockServer::start().await;
for i in 0..10 {
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/api/v1/data"))
.and(wiremock::matchers::header("X-API-Key", MOCK_API_KEY))
.respond_with(
wiremock::ResponseTemplate::new(200)
.set_body_json(json!({"data": "success"}))
.insert_header("X-RateLimit-Limit", "10")
.insert_header("X-RateLimit-Remaining", format!("{}", 9 - i))
.insert_header("X-RateLimit-Reset", "1699660800")
)
.up_to_n_times(1)
.mount(&mock_server)
.await;
}
let url = format!("{}/api/v1/data", mock_server.uri());
for _ in 0..10 {
let res = client
.get(&url)
.header("X-API-Key", MOCK_API_KEY)
.send()
.await?;
assert_eq!(res.status(), StatusCode::OK);
assert!(res.headers().contains_key("X-RateLimit-Limit"));
}
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/api/v1/data"))
.and(wiremock::matchers::header("X-API-Key", MOCK_API_KEY))
.respond_with(
wiremock::ResponseTemplate::new(429)
.set_body_json(json!({
"error": "Rate limit exceeded",
"code": "RATE_LIMIT_EXCEEDED"
}))
.insert_header("X-RateLimit-Limit", "10")
.insert_header("X-RateLimit-Remaining", "0")
.insert_header("X-RateLimit-Reset", "1699660800")
.insert_header("Retry-After", "60")
)
.mount(&mock_server)
.await;
let res = client
.get(&url)
.header("X-API-Key", MOCK_API_KEY)
.send()
.await?;
assert_eq!(res.status(), StatusCode::TOO_MANY_REQUESTS);
assert!(res.headers().contains_key("Retry-After"));
Ok(())
}
#[tokio::test]
async fn test_rate_limit_by_ip_address() -> TestResult {
let client = build_test_client();
let mock_server = wiremock::MockServer::start().await;
for i in 0..100 {
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/api/v1/public"))
.respond_with(
wiremock::ResponseTemplate::new(200)
.set_body_json(json!({"data": "success"}))
.insert_header("X-RateLimit-Limit", "100")
.insert_header("X-RateLimit-Remaining", format!("{}", 99 - i))
)
.up_to_n_times(1)
.mount(&mock_server)
.await;
}
let url = format!("{}/api/v1/public", mock_server.uri());
for _ in 0..100 {
let res = client.get(&url).send().await?;
assert_eq!(res.status(), StatusCode::OK);
}
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/api/v1/public"))
.respond_with(
wiremock::ResponseTemplate::new(429)
.set_body_json(json!({
"error": "Rate limit exceeded",
"code": "RATE_LIMIT_EXCEEDED"
}))
)
.mount(&mock_server)
.await;
let res = client.get(&url).send().await?;
assert_eq!(res.status(), StatusCode::TOO_MANY_REQUESTS);
Ok(())
}
#[tokio::test]
async fn test_rate_limit_different_tiers() -> TestResult {
let client = build_test_client();
let mock_server = wiremock::MockServer::start().await;
let free_key = "free_api_key";
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/api/v1/data"))
.and(wiremock::matchers::header("X-API-Key", free_key))
.respond_with(
wiremock::ResponseTemplate::new(200)
.insert_header("X-RateLimit-Limit", "10")
.insert_header("X-RateLimit-Tier", "free")
)
.up_to_n_times(10)
.mount(&mock_server)
.await;
let premium_key = "premium_api_key";
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/api/v1/data"))
.and(wiremock::matchers::header("X-API-Key", premium_key))
.respond_with(
wiremock::ResponseTemplate::new(200)
.insert_header("X-RateLimit-Limit", "1000")
.insert_header("X-RateLimit-Tier", "premium")
)
.up_to_n_times(100)
.mount(&mock_server)
.await;
let url = format!("{}/api/v1/data", mock_server.uri());
let res = client.get(&url).header("X-API-Key", free_key).send().await?;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get("X-RateLimit-Limit").unwrap(), "10");
let res = client.get(&url).header("X-API-Key", premium_key).send().await?;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get("X-RateLimit-Limit").unwrap(), "1000");
Ok(())
}
#[tokio::test]
async fn test_rate_limit_headers_present() -> TestResult {
let client = build_test_client();
let mock_server = wiremock::MockServer::start().await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/api/v1/data"))
.respond_with(
wiremock::ResponseTemplate::new(200)
.set_body_json(json!({"data": "test"}))
.insert_header("X-RateLimit-Limit", "100")
.insert_header("X-RateLimit-Remaining", "99")
.insert_header("X-RateLimit-Reset", "1699660800")
)
.mount(&mock_server)
.await;
let url = format!("{}/api/v1/data", mock_server.uri());
let res = client.get(&url).send().await?;
assert_eq!(res.status(), StatusCode::OK);
assert!(res.headers().contains_key("X-RateLimit-Limit"));
assert!(res.headers().contains_key("X-RateLimit-Remaining"));
assert!(res.headers().contains_key("X-RateLimit-Reset"));
Ok(())
}
#[tokio::test]
async fn test_rate_limit_reset_window() -> TestResult {
let client = build_test_client();
let mock_server = wiremock::MockServer::start().await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/api/v1/data"))
.respond_with(
wiremock::ResponseTemplate::new(429)
.set_body_json(json!({
"error": "Rate limit exceeded",
"code": "RATE_LIMIT_EXCEEDED"
}))
.insert_header("Retry-After", "1")
)
.up_to_n_times(1)
.mount(&mock_server)
.await;
let url = format!("{}/api/v1/data", mock_server.uri());
let res = client.get(&url).send().await?;
assert_eq!(res.status(), StatusCode::TOO_MANY_REQUESTS);
tokio::time::sleep(Duration::from_secs(2)).await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/api/v1/data"))
.respond_with(
wiremock::ResponseTemplate::new(200)
.set_body_json(json!({"data": "success"}))
.insert_header("X-RateLimit-Remaining", "99")
)
.mount(&mock_server)
.await;
let res = client.get(&url).send().await?;
assert_eq!(res.status(), StatusCode::OK);
Ok(())
}
#[tokio::test]
async fn test_rate_limit_burst_protection() -> TestResult {
let client = build_test_client();
let mock_server = wiremock::MockServer::start().await;
for i in 0..5 {
wiremock::Mock::given(wiremock::matchers::method("POST"))
.and(wiremock::matchers::path("/api/v1/process"))
.respond_with(
wiremock::ResponseTemplate::new(200)
.insert_header("X-RateLimit-Burst", "5")
.insert_header("X-RateLimit-Burst-Remaining", format!("{}", 4 - i))
)
.up_to_n_times(1)
.mount(&mock_server)
.await;
}
let url = format!("{}/api/v1/process", mock_server.uri());
let mut tasks = vec![];
for _ in 0..5 {
let client = client.clone();
let url = url.clone();
tasks.push(tokio::spawn(async move {
client.post(&url).json(&json!({"data": "test"})).send().await
}));
}
let results = futures::future::join_all(tasks).await;
let success_count = results
.into_iter()
.filter(|r| r.is_ok() && r.as_ref().unwrap().as_ref().unwrap().status() == StatusCode::OK)
.count();
assert_eq!(success_count, 5);
Ok(())
}
#[tokio::test]
async fn test_rate_limit_per_endpoint() -> TestResult {
let client = build_test_client();
let mock_server = wiremock::MockServer::start().await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/api/v1/cheap"))
.respond_with(
wiremock::ResponseTemplate::new(200)
.insert_header("X-RateLimit-Limit", "1000")
)
.mount(&mock_server)
.await;
wiremock::Mock::given(wiremock::matchers::method("POST"))
.and(wiremock::matchers::path("/api/v1/expensive"))
.respond_with(
wiremock::ResponseTemplate::new(200)
.insert_header("X-RateLimit-Limit", "10")
)
.mount(&mock_server)
.await;
let cheap_url = format!("{}/api/v1/cheap", mock_server.uri());
let expensive_url = format!("{}/api/v1/expensive", mock_server.uri());
let cheap_res = client.get(&cheap_url).send().await?;
assert_eq!(cheap_res.headers().get("X-RateLimit-Limit").unwrap(), "1000");
let expensive_res = client.post(&expensive_url).send().await?;
assert_eq!(expensive_res.headers().get("X-RateLimit-Limit").unwrap(), "10");
Ok(())
}