1use axum::extract::Request;
84use axum::http::{header, HeaderMap, HeaderValue, StatusCode};
85use axum::middleware::Next;
86use axum::response::{IntoResponse, Response};
87use serde_json::json;
88use std::sync::Arc;
89use std::time::{Duration, SystemTime, UNIX_EPOCH};
90
91use sz_orm_limit::RateLimiter;
92
93use crate::middleware::auth::AuthenticatedUser;
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
97pub enum KeyExtractor {
98 #[default]
100 Ip,
101 UserId,
106 IpPlusRoute,
108}
109
110impl KeyExtractor {
111 pub fn as_str(self) -> &'static str {
113 match self {
114 KeyExtractor::Ip => "ip",
115 KeyExtractor::UserId => "user_id",
116 KeyExtractor::IpPlusRoute => "ip_plus_route",
117 }
118 }
119}
120
121impl std::fmt::Display for KeyExtractor {
122 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123 f.write_str(self.as_str())
124 }
125}
126
127#[derive(Clone)]
132pub struct RateLimitConfig {
133 pub limiter: Arc<dyn RateLimiter + Send + Sync>,
135 pub key_extractor: KeyExtractor,
137 pub exclude_paths: Vec<String>,
139 pub key_prefix: String,
141}
142
143impl std::fmt::Debug for RateLimitConfig {
144 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145 f.debug_struct("RateLimitConfig")
146 .field("key_extractor", &self.key_extractor)
147 .field("exclude_paths", &self.exclude_paths)
148 .field("key_prefix", &self.key_prefix)
149 .finish_non_exhaustive()
150 }
151}
152
153impl RateLimitConfig {
154 pub fn new(limiter: Arc<dyn RateLimiter + Send + Sync>) -> Self {
156 Self {
157 limiter,
158 key_extractor: KeyExtractor::default(),
159 exclude_paths: Vec::new(),
160 key_prefix: String::new(),
161 }
162 }
163
164 pub fn with_key_extractor(mut self, extractor: KeyExtractor) -> Self {
166 self.key_extractor = extractor;
167 self
168 }
169
170 pub fn with_exclude_paths(mut self, paths: Vec<String>) -> Self {
172 self.exclude_paths = paths;
173 self
174 }
175
176 pub fn with_key_prefix(mut self, prefix: impl Into<String>) -> Self {
178 self.key_prefix = prefix.into();
179 self
180 }
181
182 pub fn is_excluded(&self, path: &str) -> bool {
184 crate::middleware::auth::is_route_allowed(path, &self.exclude_paths)
185 }
186}
187
188pub fn extract_client_ip(headers: &HeaderMap) -> String {
195 if let Some(forwarded) = headers.get("x-forwarded-for") {
196 if let Ok(value) = forwarded.to_str() {
197 if let Some(first) = value.split(',').next() {
199 let trimmed = first.trim();
200 if !trimmed.is_empty() {
201 return trimmed.to_string();
202 }
203 }
204 }
205 }
206 if let Some(real_ip) = headers.get("x-real-ip") {
207 if let Ok(value) = real_ip.to_str() {
208 let trimmed = value.trim();
209 if !trimmed.is_empty() {
210 return trimmed.to_string();
211 }
212 }
213 }
214 "unknown".to_string()
215}
216
217pub fn extract_rate_limit_key(req: &Request, config: &RateLimitConfig) -> String {
221 let inner_key = match config.key_extractor {
222 KeyExtractor::Ip => extract_client_ip(req.headers()),
223 KeyExtractor::UserId => req
224 .extensions()
225 .get::<AuthenticatedUser>()
226 .map(|u| u.user_id.to_string())
227 .unwrap_or_else(|| extract_client_ip(req.headers())),
228 KeyExtractor::IpPlusRoute => {
229 let ip = extract_client_ip(req.headers());
230 let path = req.uri().path();
231 format!("{}:{}", ip, path)
232 }
233 };
234 if config.key_prefix.is_empty() {
235 inner_key
236 } else {
237 format!("{}:{}", config.key_prefix, inner_key)
238 }
239}
240
241pub fn rate_limit_rejected_response(result: &sz_orm_limit::RateLimitResult) -> Response {
246 let now_ms = current_unix_ms();
247 let retry_after_seconds = ((result.reset_at - now_ms) / 1000).max(1) as u64;
248
249 let body = json!({
250 "code": 429,
251 "msg": "Too Many Requests",
252 "data": {
253 "retry_after_seconds": retry_after_seconds,
254 "reset_at_ms": result.reset_at
255 }
256 })
257 .to_string();
258
259 let mut response = (
260 StatusCode::TOO_MANY_REQUESTS,
261 [(header::CONTENT_TYPE, "application/json; charset=utf-8")],
262 body,
263 )
264 .into_response();
265
266 insert_rate_limit_headers(&mut response, result, retry_after_seconds);
267 response
268}
269
270fn current_unix_ms() -> i64 {
272 SystemTime::now()
273 .duration_since(UNIX_EPOCH)
274 .map(|d| d.as_millis() as i64)
275 .unwrap_or(0)
276}
277
278fn insert_rate_limit_headers(
280 response: &mut Response,
281 result: &sz_orm_limit::RateLimitResult,
282 retry_after_seconds: u64,
283) {
284 let headers = response.headers_mut();
285 headers.insert(
286 "x-ratelimit-remaining",
287 HeaderValue::from_str(&result.remaining.to_string())
288 .unwrap_or_else(|_| HeaderValue::from_static("0")),
289 );
290 headers.insert(
291 "x-ratelimit-reset",
292 HeaderValue::from_str(&result.reset_at.to_string())
293 .unwrap_or_else(|_| HeaderValue::from_static("0")),
294 );
295 headers.insert(
296 "retry-after",
297 HeaderValue::from_str(&retry_after_seconds.to_string())
298 .unwrap_or_else(|_| HeaderValue::from_static("1")),
299 );
300}
301
302pub async fn rate_limit_middleware(
313 axum::extract::State(config): axum::extract::State<RateLimitConfig>,
314 req: Request,
315 next: Next,
316) -> Response {
317 let path = req.uri().path().to_string();
318
319 if config.is_excluded(&path) {
321 return next.run(req).await;
322 }
323
324 let key = extract_rate_limit_key(&req, &config);
326
327 match config.limiter.acquire(&key) {
329 Ok(result) if result.allowed => {
330 let mut response = next.run(req).await;
332 let retry_after_seconds = ((result.reset_at - current_unix_ms()) / 1000).max(1) as u64;
333 insert_rate_limit_headers(&mut response, &result, retry_after_seconds);
334 response
335 }
336 Ok(result) => {
337 rate_limit_rejected_response(&result)
339 }
340 Err(err) => {
341 tracing::error!(
343 error = %err,
344 key = %key,
345 "rate_limit limiter error, fail-open"
346 );
347 next.run(req).await
348 }
349 }
350}
351
352pub fn sliding_window_config(max_requests: u64, window_size: Duration) -> RateLimitConfig {
361 let limiter = Arc::new(sz_orm_limit::SlidingWindowRateLimiter::new(
362 max_requests,
363 window_size,
364 ));
365 RateLimitConfig::new(limiter)
366}
367
368pub fn token_bucket_config(capacity: u64, refill_per_second: f64) -> RateLimitConfig {
377 let limiter = Arc::new(sz_orm_limit::TokenBucketRateLimiter::new(
378 capacity,
379 refill_per_second,
380 ));
381 RateLimitConfig::new(limiter)
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387 use axum::body::Body;
388 use axum::Router;
389 use http_body_util::BodyExt;
390 use tower::ServiceExt;
391
392 async fn read_body(resp: Response) -> String {
397 let bytes = resp.into_body().collect().await.unwrap().to_bytes();
398 String::from_utf8(bytes.to_vec()).unwrap()
399 }
400
401 fn make_request(method: &str, uri: &str) -> Request {
402 Request::builder()
403 .method(method)
404 .uri(uri)
405 .body(Body::empty())
406 .unwrap()
407 }
408
409 fn make_request_with_ip(method: &str, uri: &str, ip: &str) -> Request {
410 Request::builder()
411 .method(method)
412 .uri(uri)
413 .header("x-forwarded-for", ip)
414 .body(Body::empty())
415 .unwrap()
416 }
417
418 fn build_app_sliding_window() -> Router {
420 let config = sliding_window_config(2, Duration::from_secs(60));
421 Router::new()
422 .route(
423 "/api",
424 axum::routing::get(|| async { axum::http::StatusCode::OK }),
425 )
426 .layer(axum::middleware::from_fn_with_state(
427 config,
428 rate_limit_middleware,
429 ))
430 }
431
432 fn build_app_token_bucket() -> Router {
434 let config = token_bucket_config(2, 1.0);
435 Router::new()
436 .route(
437 "/api",
438 axum::routing::get(|| async { axum::http::StatusCode::OK }),
439 )
440 .layer(axum::middleware::from_fn_with_state(
441 config,
442 rate_limit_middleware,
443 ))
444 }
445
446 #[test]
451 fn test_key_extractor_as_str() {
452 assert_eq!(KeyExtractor::Ip.as_str(), "ip");
453 assert_eq!(KeyExtractor::UserId.as_str(), "user_id");
454 assert_eq!(KeyExtractor::IpPlusRoute.as_str(), "ip_plus_route");
455 }
456
457 #[test]
458 fn test_key_extractor_display() {
459 assert_eq!(KeyExtractor::Ip.to_string(), "ip");
460 assert_eq!(KeyExtractor::UserId.to_string(), "user_id");
461 assert_eq!(KeyExtractor::IpPlusRoute.to_string(), "ip_plus_route");
462 }
463
464 #[test]
465 fn test_key_extractor_default_is_ip() {
466 assert_eq!(KeyExtractor::default(), KeyExtractor::Ip);
467 }
468
469 #[test]
470 fn test_key_extractor_equality() {
471 assert_eq!(KeyExtractor::Ip, KeyExtractor::Ip);
472 assert_ne!(KeyExtractor::Ip, KeyExtractor::UserId);
473 assert_ne!(KeyExtractor::UserId, KeyExtractor::IpPlusRoute);
474 }
475
476 #[test]
477 fn test_key_extractor_copy_clone() {
478 let extractor = KeyExtractor::UserId;
479 let copied = extractor; assert_eq!(extractor, copied);
481 }
482
483 #[test]
488 fn test_extract_client_ip_from_x_forwarded_for() {
489 let mut headers = HeaderMap::new();
490 headers.insert("x-forwarded-for", "1.2.3.4".parse().unwrap());
491 assert_eq!(extract_client_ip(&headers), "1.2.3.4");
492 }
493
494 #[test]
495 fn test_extract_client_ip_from_x_forwarded_for_multi() {
496 let mut headers = HeaderMap::new();
498 headers.insert(
499 "x-forwarded-for",
500 "1.2.3.4, 5.6.7.8, 9.10.11.12".parse().unwrap(),
501 );
502 assert_eq!(extract_client_ip(&headers), "1.2.3.4");
503 }
504
505 #[test]
506 fn test_extract_client_ip_from_x_real_ip() {
507 let mut headers = HeaderMap::new();
508 headers.insert("x-real-ip", "1.2.3.4".parse().unwrap());
509 assert_eq!(extract_client_ip(&headers), "1.2.3.4");
510 }
511
512 #[test]
513 fn test_extract_client_ip_x_forwarded_for_takes_priority() {
514 let mut headers = HeaderMap::new();
515 headers.insert("x-forwarded-for", "1.1.1.1".parse().unwrap());
516 headers.insert("x-real-ip", "2.2.2.2".parse().unwrap());
517 assert_eq!(extract_client_ip(&headers), "1.1.1.1");
518 }
519
520 #[test]
521 fn test_extract_client_ip_no_headers() {
522 let headers = HeaderMap::new();
523 assert_eq!(extract_client_ip(&headers), "unknown");
524 }
525
526 #[test]
527 fn test_extract_client_ip_empty_x_forwarded_for() {
528 let mut headers = HeaderMap::new();
529 headers.insert("x-forwarded-for", "".parse().unwrap());
530 assert_eq!(extract_client_ip(&headers), "unknown");
532 }
533
534 #[test]
535 fn test_extract_client_ip_empty_x_forwarded_for_falls_back_to_x_real_ip() {
536 let mut headers = HeaderMap::new();
537 headers.insert("x-forwarded-for", "".parse().unwrap());
538 headers.insert("x-real-ip", "3.3.3.3".parse().unwrap());
539 assert_eq!(extract_client_ip(&headers), "3.3.3.3");
540 }
541
542 #[test]
543 fn test_extract_client_ip_trims_whitespace() {
544 let mut headers = HeaderMap::new();
545 headers.insert("x-forwarded-for", " 1.2.3.4 ".parse().unwrap());
546 assert_eq!(extract_client_ip(&headers), "1.2.3.4");
547 }
548
549 #[test]
554 fn test_extract_rate_limit_key_ip_strategy() {
555 let config = sliding_window_config(10, Duration::from_secs(60));
556 let req = make_request_with_ip("GET", "/api", "1.2.3.4");
557 assert_eq!(extract_rate_limit_key(&req, &config), "1.2.3.4");
558 }
559
560 #[test]
561 fn test_extract_rate_limit_key_ip_strategy_no_ip_header() {
562 let config = sliding_window_config(10, Duration::from_secs(60));
563 let req = make_request("GET", "/api");
564 assert_eq!(extract_rate_limit_key(&req, &config), "unknown");
565 }
566
567 #[test]
568 fn test_extract_rate_limit_key_user_id_strategy_with_auth() {
569 let config = sliding_window_config(10, Duration::from_secs(60))
570 .with_key_extractor(KeyExtractor::UserId);
571 let mut req = make_request_with_ip("GET", "/api", "1.2.3.4");
572 req.extensions_mut()
573 .insert(AuthenticatedUser { user_id: 42 });
574 assert_eq!(extract_rate_limit_key(&req, &config), "42");
575 }
576
577 #[test]
578 fn test_extract_rate_limit_key_user_id_strategy_fallback_to_ip() {
579 let config = sliding_window_config(10, Duration::from_secs(60))
581 .with_key_extractor(KeyExtractor::UserId);
582 let req = make_request_with_ip("GET", "/api", "1.2.3.4");
583 assert_eq!(extract_rate_limit_key(&req, &config), "1.2.3.4");
584 }
585
586 #[test]
587 fn test_extract_rate_limit_key_ip_plus_route_strategy() {
588 let config = sliding_window_config(10, Duration::from_secs(60))
589 .with_key_extractor(KeyExtractor::IpPlusRoute);
590 let req = make_request_with_ip("GET", "/api/users", "1.2.3.4");
591 assert_eq!(extract_rate_limit_key(&req, &config), "1.2.3.4:/api/users");
592 }
593
594 #[test]
595 fn test_extract_rate_limit_key_with_prefix() {
596 let config = sliding_window_config(10, Duration::from_secs(60)).with_key_prefix("login");
597 let req = make_request_with_ip("GET", "/api", "1.2.3.4");
598 assert_eq!(extract_rate_limit_key(&req, &config), "login:1.2.3.4");
599 }
600
601 #[test]
602 fn test_extract_rate_limit_key_with_prefix_and_user_id() {
603 let config = sliding_window_config(10, Duration::from_secs(60))
604 .with_key_extractor(KeyExtractor::UserId)
605 .with_key_prefix("api");
606 let mut req = make_request("GET", "/api");
607 req.extensions_mut()
608 .insert(AuthenticatedUser { user_id: 100 });
609 assert_eq!(extract_rate_limit_key(&req, &config), "api:100");
610 }
611
612 #[test]
617 fn test_rate_limit_config_default() {
618 let config = sliding_window_config(10, Duration::from_secs(60));
619 assert_eq!(config.key_extractor, KeyExtractor::Ip);
620 assert!(config.exclude_paths.is_empty());
621 assert!(config.key_prefix.is_empty());
622 }
623
624 #[test]
625 fn test_rate_limit_config_with_key_extractor() {
626 let config = sliding_window_config(10, Duration::from_secs(60))
627 .with_key_extractor(KeyExtractor::UserId);
628 assert_eq!(config.key_extractor, KeyExtractor::UserId);
629 }
630
631 #[test]
632 fn test_rate_limit_config_with_exclude_paths() {
633 let config = sliding_window_config(10, Duration::from_secs(60))
634 .with_exclude_paths(vec!["/health".to_string()]);
635 assert_eq!(config.exclude_paths, vec!["/health".to_string()]);
636 }
637
638 #[test]
639 fn test_rate_limit_config_with_key_prefix() {
640 let config = sliding_window_config(10, Duration::from_secs(60)).with_key_prefix("sms");
641 assert_eq!(config.key_prefix, "sms");
642 }
643
644 #[test]
645 fn test_rate_limit_config_is_excluded_exact_match() {
646 let config = sliding_window_config(10, Duration::from_secs(60))
647 .with_exclude_paths(vec!["/health".to_string()]);
648 assert!(config.is_excluded("/health"));
649 assert!(!config.is_excluded("/api"));
650 }
651
652 #[test]
653 fn test_rate_limit_config_is_excluded_wildcard_match() {
654 let config = sliding_window_config(10, Duration::from_secs(60))
655 .with_exclude_paths(vec!["/public/*".to_string()]);
656 assert!(config.is_excluded("/public/anything"));
657 assert!(!config.is_excluded("/api"));
658 }
659
660 #[test]
661 fn test_rate_limit_config_is_excluded_empty_list() {
662 let config = sliding_window_config(10, Duration::from_secs(60));
663 assert!(!config.is_excluded("/any"));
664 }
665
666 #[test]
667 fn test_rate_limit_config_clone() {
668 let config = sliding_window_config(10, Duration::from_secs(60)).with_key_prefix("test");
669 let cloned = config.clone();
670 assert_eq!(config.key_extractor, cloned.key_extractor);
671 assert_eq!(config.key_prefix, cloned.key_prefix);
672 }
673
674 #[tokio::test]
679 async fn test_rate_limit_rejected_response_status_code() {
680 let result = sz_orm_limit::RateLimitResult::rejected(0, current_unix_ms() + 60_000);
681 let response = rate_limit_rejected_response(&result);
682 assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);
683 }
684
685 #[tokio::test]
686 async fn test_rate_limit_rejected_response_headers() {
687 let reset_at = current_unix_ms() + 60_000;
688 let result = sz_orm_limit::RateLimitResult::rejected(0, reset_at);
689 let response = rate_limit_rejected_response(&result);
690 let headers = response.headers();
691 assert_eq!(headers.get("x-ratelimit-remaining").unwrap(), "0");
692 assert_eq!(
693 headers.get("x-ratelimit-reset").unwrap().to_str().unwrap(),
694 reset_at.to_string()
695 );
696 let retry_after: u64 = headers
698 .get("retry-after")
699 .unwrap()
700 .to_str()
701 .unwrap()
702 .parse()
703 .unwrap();
704 assert!(retry_after > 0);
705 }
706
707 #[tokio::test]
708 async fn test_rate_limit_rejected_response_body_format() {
709 let reset_at = current_unix_ms() + 60_000;
710 let result = sz_orm_limit::RateLimitResult::rejected(0, reset_at);
711 let response = rate_limit_rejected_response(&result);
712 let body = read_body(response).await;
713 let json: serde_json::Value = serde_json::from_str(&body).unwrap();
714 assert_eq!(json["code"], 429);
715 assert_eq!(json["msg"], "Too Many Requests");
716 assert_eq!(json["data"]["reset_at_ms"], reset_at);
717 assert!(json["data"]["retry_after_seconds"].as_u64().unwrap() > 0);
718 }
719
720 #[tokio::test]
721 async fn test_rate_limit_rejected_response_content_type() {
722 let result = sz_orm_limit::RateLimitResult::rejected(0, current_unix_ms() + 60_000);
723 let response = rate_limit_rejected_response(&result);
724 assert_eq!(
725 response.headers().get("content-type").unwrap(),
726 "application/json; charset=utf-8"
727 );
728 }
729
730 #[tokio::test]
735 async fn test_rate_limit_middleware_allows_first_request() {
736 let app = build_app_sliding_window();
737 let resp = app
738 .oneshot(make_request_with_ip("GET", "/api", "1.1.1.1"))
739 .await
740 .unwrap();
741 assert_eq!(resp.status(), StatusCode::OK);
742 }
743
744 #[tokio::test]
745 async fn test_rate_limit_middleware_allows_second_request() {
746 let app = build_app_sliding_window();
747 let resp = app
749 .clone()
750 .oneshot(make_request_with_ip("GET", "/api", "2.2.2.2"))
751 .await
752 .unwrap();
753 assert_eq!(resp.status(), StatusCode::OK);
754 let resp = app
756 .oneshot(make_request_with_ip("GET", "/api", "2.2.2.2"))
757 .await
758 .unwrap();
759 assert_eq!(resp.status(), StatusCode::OK);
760 }
761
762 #[tokio::test]
763 async fn test_rate_limit_middleware_rejects_third_request() {
764 let app = build_app_sliding_window();
765 let _ = app
767 .clone()
768 .oneshot(make_request_with_ip("GET", "/api", "3.3.3.3"))
769 .await
770 .unwrap();
771 let _ = app
773 .clone()
774 .oneshot(make_request_with_ip("GET", "/api", "3.3.3.3"))
775 .await
776 .unwrap();
777 let resp = app
779 .oneshot(make_request_with_ip("GET", "/api", "3.3.3.3"))
780 .await
781 .unwrap();
782 assert_eq!(resp.status(), StatusCode::TOO_MANY_REQUESTS);
783 }
784
785 #[tokio::test]
786 async fn test_rate_limit_middleware_different_ips_independent() {
787 let app = build_app_sliding_window();
789 let _ = app
791 .clone()
792 .oneshot(make_request_with_ip("GET", "/api", "4.4.4.4"))
793 .await
794 .unwrap();
795 let _ = app
796 .clone()
797 .oneshot(make_request_with_ip("GET", "/api", "4.4.4.4"))
798 .await
799 .unwrap();
800 let resp = app
802 .oneshot(make_request_with_ip("GET", "/api", "5.5.5.5"))
803 .await
804 .unwrap();
805 assert_eq!(resp.status(), StatusCode::OK);
806 }
807
808 #[tokio::test]
809 async fn test_rate_limit_middleware_adds_remaining_header_on_success() {
810 let app = build_app_sliding_window();
811 let resp = app
812 .oneshot(make_request_with_ip("GET", "/api", "6.6.6.6"))
813 .await
814 .unwrap();
815 assert_eq!(resp.status(), StatusCode::OK);
816 let remaining = resp
817 .headers()
818 .get("x-ratelimit-remaining")
819 .expect("X-RateLimit-Remaining header should be present");
820 let remaining: u64 = remaining.to_str().unwrap().parse().unwrap();
821 assert_eq!(remaining, 1);
823 }
824
825 #[tokio::test]
826 async fn test_rate_limit_middleware_adds_reset_header_on_success() {
827 let app = build_app_sliding_window();
828 let resp = app
829 .oneshot(make_request_with_ip("GET", "/api", "7.7.7.7"))
830 .await
831 .unwrap();
832 assert_eq!(resp.status(), StatusCode::OK);
833 let reset = resp
834 .headers()
835 .get("x-ratelimit-reset")
836 .expect("X-RateLimit-Reset header should be present");
837 let reset: i64 = reset.to_str().unwrap().parse().unwrap();
838 assert!(reset > current_unix_ms());
840 }
841
842 #[tokio::test]
843 async fn test_rate_limit_middleware_rejected_response_has_retry_after() {
844 let app = build_app_sliding_window();
845 let _ = app
847 .clone()
848 .oneshot(make_request_with_ip("GET", "/api", "8.8.8.8"))
849 .await
850 .unwrap();
851 let _ = app
852 .clone()
853 .oneshot(make_request_with_ip("GET", "/api", "8.8.8.8"))
854 .await
855 .unwrap();
856 let resp = app
858 .oneshot(make_request_with_ip("GET", "/api", "8.8.8.8"))
859 .await
860 .unwrap();
861 assert_eq!(resp.status(), StatusCode::TOO_MANY_REQUESTS);
862 let retry_after = resp
863 .headers()
864 .get("retry-after")
865 .expect("Retry-After header should be present");
866 let retry_after: u64 = retry_after.to_str().unwrap().parse().unwrap();
867 assert!(retry_after > 0);
868 }
869
870 #[tokio::test]
871 async fn test_rate_limit_middleware_excluded_path_bypasses_limit() {
872 let config = sliding_window_config(1, Duration::from_secs(60))
873 .with_exclude_paths(vec!["/health".to_string()]);
874 let app = Router::new()
875 .route(
876 "/health",
877 axum::routing::get(|| async { axum::http::StatusCode::OK }),
878 )
879 .layer(axum::middleware::from_fn_with_state(
880 config,
881 rate_limit_middleware,
882 ));
883
884 for _ in 0..5 {
886 let resp = app
887 .clone()
888 .oneshot(make_request("GET", "/health"))
889 .await
890 .unwrap();
891 assert_eq!(resp.status(), StatusCode::OK);
892 }
893 }
894
895 #[tokio::test]
896 async fn test_rate_limit_middleware_wildcard_exclude() {
897 let config = sliding_window_config(1, Duration::from_secs(60))
898 .with_exclude_paths(vec!["/public/*".to_string()]);
899 let app = Router::new()
900 .route(
901 "/public/asset1",
902 axum::routing::get(|| async { axum::http::StatusCode::OK }),
903 )
904 .route(
905 "/public/asset2",
906 axum::routing::get(|| async { axum::http::StatusCode::OK }),
907 )
908 .layer(axum::middleware::from_fn_with_state(
909 config,
910 rate_limit_middleware,
911 ));
912
913 let resp = app
915 .clone()
916 .oneshot(make_request("GET", "/public/asset1"))
917 .await
918 .unwrap();
919 assert_eq!(resp.status(), StatusCode::OK);
920 let resp = app
921 .oneshot(make_request("GET", "/public/asset2"))
922 .await
923 .unwrap();
924 assert_eq!(resp.status(), StatusCode::OK);
925 }
926
927 #[tokio::test]
928 async fn test_rate_limit_middleware_unknown_ip_shared_bucket() {
929 let app = build_app_sliding_window();
931 let _ = app
933 .clone()
934 .oneshot(make_request("GET", "/api"))
935 .await
936 .unwrap();
937 let _ = app
939 .clone()
940 .oneshot(make_request("GET", "/api"))
941 .await
942 .unwrap();
943 let resp = app.oneshot(make_request("GET", "/api")).await.unwrap();
945 assert_eq!(resp.status(), StatusCode::TOO_MANY_REQUESTS);
946 }
947
948 #[tokio::test]
949 async fn test_rate_limit_middleware_preserves_response_body() {
950 let config = sliding_window_config(10, Duration::from_secs(60));
951 let app = Router::new()
952 .route("/body", axum::routing::get(|| async { "hello" }))
953 .layer(axum::middleware::from_fn_with_state(
954 config,
955 rate_limit_middleware,
956 ));
957 let resp = app.oneshot(make_request("GET", "/body")).await.unwrap();
958 let body = read_body(resp).await;
959 assert_eq!(body, "hello");
960 }
961
962 #[tokio::test]
963 async fn test_rate_limit_middleware_handles_post_request() {
964 let config = sliding_window_config(10, Duration::from_secs(60));
965 let app = Router::new()
966 .route(
967 "/submit",
968 axum::routing::post(|| async { axum::http::StatusCode::CREATED }),
969 )
970 .layer(axum::middleware::from_fn_with_state(
971 config,
972 rate_limit_middleware,
973 ));
974 let req = Request::builder()
975 .method("POST")
976 .uri("/submit")
977 .header("x-forwarded-for", "9.9.9.9")
978 .body(Body::empty())
979 .unwrap();
980 let resp = app.oneshot(req).await.unwrap();
981 assert_eq!(resp.status(), StatusCode::CREATED);
982 }
983
984 #[tokio::test]
989 async fn test_token_bucket_allows_within_capacity() {
990 let app = build_app_token_bucket();
991 let resp = app
993 .clone()
994 .oneshot(make_request_with_ip("GET", "/api", "10.0.0.1"))
995 .await
996 .unwrap();
997 assert_eq!(resp.status(), StatusCode::OK);
998 let resp = app
999 .oneshot(make_request_with_ip("GET", "/api", "10.0.0.1"))
1000 .await
1001 .unwrap();
1002 assert_eq!(resp.status(), StatusCode::OK);
1003 }
1004
1005 #[tokio::test]
1006 async fn test_token_bucket_rejects_over_capacity() {
1007 let app = build_app_token_bucket();
1008 let _ = app
1010 .clone()
1011 .oneshot(make_request_with_ip("GET", "/api", "10.0.0.2"))
1012 .await
1013 .unwrap();
1014 let _ = app
1015 .clone()
1016 .oneshot(make_request_with_ip("GET", "/api", "10.0.0.2"))
1017 .await
1018 .unwrap();
1019 let resp = app
1021 .oneshot(make_request_with_ip("GET", "/api", "10.0.0.2"))
1022 .await
1023 .unwrap();
1024 assert_eq!(resp.status(), StatusCode::TOO_MANY_REQUESTS);
1025 }
1026
1027 #[test]
1032 fn test_sliding_window_config_creates_valid_config() {
1033 let config = sliding_window_config(100, Duration::from_secs(60));
1034 assert_eq!(config.key_extractor, KeyExtractor::Ip);
1035 assert!(config.exclude_paths.is_empty());
1036 }
1037
1038 #[test]
1039 fn test_token_bucket_config_creates_valid_config() {
1040 let config = token_bucket_config(100, 10.0);
1041 assert_eq!(config.key_extractor, KeyExtractor::Ip);
1042 }
1043
1044 #[tokio::test]
1049 async fn test_rate_limit_middleware_with_key_prefix_isolates_buckets() {
1050 let config1 = sliding_window_config(1, Duration::from_secs(60)).with_key_prefix("api1");
1052 let config2 = sliding_window_config(1, Duration::from_secs(60)).with_key_prefix("api2");
1053
1054 let app1 = Router::new()
1055 .route(
1056 "/api",
1057 axum::routing::get(|| async { axum::http::StatusCode::OK }),
1058 )
1059 .layer(axum::middleware::from_fn_with_state(
1060 config1,
1061 rate_limit_middleware,
1062 ));
1063 let app2 = Router::new()
1064 .route(
1065 "/api",
1066 axum::routing::get(|| async { axum::http::StatusCode::OK }),
1067 )
1068 .layer(axum::middleware::from_fn_with_state(
1069 config2,
1070 rate_limit_middleware,
1071 ));
1072
1073 let _ = app1
1075 .clone()
1076 .oneshot(make_request_with_ip("GET", "/api", "1.1.1.1"))
1077 .await
1078 .unwrap();
1079 let resp = app1
1081 .oneshot(make_request_with_ip("GET", "/api", "1.1.1.1"))
1082 .await
1083 .unwrap();
1084 assert_eq!(resp.status(), StatusCode::TOO_MANY_REQUESTS);
1085
1086 let resp = app2
1088 .oneshot(make_request_with_ip("GET", "/api", "1.1.1.1"))
1089 .await
1090 .unwrap();
1091 assert_eq!(resp.status(), StatusCode::OK);
1092 }
1093
1094 #[tokio::test]
1095 async fn test_rate_limit_middleware_chains_with_other_middleware() {
1096 async fn add_header_middleware(req: Request, next: Next) -> Response {
1097 let mut resp = next.run(req).await;
1098 resp.headers_mut()
1099 .insert("X-Custom", "value".parse().unwrap());
1100 resp
1101 }
1102
1103 let config = sliding_window_config(10, Duration::from_secs(60));
1104 let app = Router::new()
1105 .route("/", axum::routing::get(|| async { "ok" }))
1106 .layer(axum::middleware::from_fn(add_header_middleware))
1107 .layer(axum::middleware::from_fn_with_state(
1108 config,
1109 rate_limit_middleware,
1110 ));
1111
1112 let resp = app.oneshot(make_request("GET", "/")).await.unwrap();
1113 assert_eq!(resp.status(), StatusCode::OK);
1114 assert_eq!(resp.headers().get("X-Custom").unwrap(), "value");
1115 }
1116
1117 #[test]
1122 fn test_php_no_rate_limit_implementation() {
1123 let config = sliding_window_config(10, Duration::from_secs(60));
1129 assert_eq!(config.key_extractor, KeyExtractor::Ip);
1131 }
1132
1133 #[test]
1134 fn test_rate_limit_response_format_aligns_with_render_json() {
1135 let result = sz_orm_limit::RateLimitResult::rejected(0, current_unix_ms() + 60_000);
1138 let response = rate_limit_rejected_response(&result);
1139 let headers = response.headers().clone();
1140 let _body = response.into_body();
1141 assert_eq!(
1143 headers.get("content-type").unwrap(),
1144 "application/json; charset=utf-8"
1145 );
1146 }
1147
1148 #[test]
1149 fn test_http_429_status_code_alignment() {
1150 let result = sz_orm_limit::RateLimitResult::rejected(0, current_unix_ms() + 60_000);
1153 let response = rate_limit_rejected_response(&result);
1154 assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);
1155 assert_eq!(response.status().as_u16(), 429);
1156 }
1157}