1use crate::config::server_runtime_config;
2use crate::domain::authentication::{constant_time_eq_str, session_user_id};
3use actix_session::SessionExt;
4use actix_web::{
5 Error, HttpMessage, HttpResponse,
6 body::{EitherBody, MessageBody},
7 dev::{Service, ServiceRequest, ServiceResponse, Transform},
8 http::{StatusCode, header},
9};
10use futures_util::future::{LocalBoxFuture, Ready, ready};
11use governor::{
12 Quota, RateLimiter,
13 clock::{Clock, DefaultClock},
14 state::keyed::DefaultKeyedStateStore,
15};
16use headless_lms_models::library::oauth::bearer::TokenSubject;
17use secrecy::ExposeSecret;
18#[cfg(test)]
19use secrecy::SecretString;
20use std::{
21 num::NonZeroU32,
22 sync::{
23 Arc,
24 atomic::{AtomicU64, Ordering},
25 },
26 task::{Context, Poll},
27 time::Duration,
28};
29
30#[derive(Clone, Debug, Default)]
31pub struct RateLimitConfig {
32 pub per_second: Option<u64>,
33 pub per_minute: Option<u64>,
34 pub per_hour: Option<u64>,
35 pub per_day: Option<u64>,
36 pub per_month: Option<u64>,
37}
38
39#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
41pub enum RateLimitKey {
42 #[default]
43 ClientIp,
44 User,
48 Grant,
52}
53
54pub const RATELIMIT_PROTECTION_SAFE_API_KEY_HEADER: &str = "RATELIMIT-PROTECTION-SAFE-API-KEY";
57
58#[derive(Clone)]
60enum Bypass {
61 Off,
62 RuntimeConfigKey,
63 #[cfg(test)]
64 Key(SecretString),
65}
66
67type Key = String;
68type Limiter = RateLimiter<Key, DefaultKeyedStateStore<Key>, DefaultClock>;
69
70#[derive(Clone)]
71struct EndpointLimiters {
72 month: Option<Arc<Limiter>>,
73 day: Option<Arc<Limiter>>,
74 hour: Option<Arc<Limiter>>,
75 minute: Option<Arc<Limiter>>,
76 second: Option<Arc<Limiter>>,
77}
78
79impl EndpointLimiters {
80 fn from_config(cfg: &RateLimitConfig) -> Self {
81 Self {
82 month: cfg.per_month.and_then(|n| {
83 build_custom_period_limiter(n, Duration::from_secs(30 * 24 * 60 * 60))
84 }),
85 day: cfg
86 .per_day
87 .and_then(|n| build_custom_period_limiter(n, Duration::from_secs(24 * 60 * 60))),
88 hour: cfg.per_hour.and_then(|n| build_limiter(n, Quota::per_hour)),
89 minute: cfg
90 .per_minute
91 .and_then(|n| build_limiter(n, Quota::per_minute)),
92 second: cfg
93 .per_second
94 .and_then(|n| build_limiter(n, Quota::per_second)),
95 }
96 }
97
98 fn iter(&self) -> impl Iterator<Item = &Arc<Limiter>> {
99 self.month
100 .iter()
101 .chain(self.day.iter())
102 .chain(self.hour.iter())
103 .chain(self.minute.iter())
104 .chain(self.second.iter())
105 }
106
107 fn is_empty(&self) -> bool {
108 self.second.is_none()
109 && self.minute.is_none()
110 && self.hour.is_none()
111 && self.day.is_none()
112 && self.month.is_none()
113 }
114}
115
116fn build_limiter<F>(n: u64, quota_fn: F) -> Option<Arc<Limiter>>
117where
118 F: FnOnce(NonZeroU32) -> Quota,
119{
120 let n32 = NonZeroU32::new(u32::try_from(n).ok()?)?;
121 Some(Arc::new(RateLimiter::keyed(quota_fn(n32))))
122}
123
124fn build_custom_period_limiter(n: u64, period: Duration) -> Option<Arc<Limiter>> {
125 let n32 = NonZeroU32::new(u32::try_from(n).ok()?)?;
126 let quota = Quota::with_period(period)?.allow_burst(n32);
127 Some(Arc::new(RateLimiter::keyed(quota)))
128}
129
130#[derive(Clone)]
131pub struct RateLimit {
132 limiters: Arc<EndpointLimiters>,
133 key: RateLimitKey,
134 bypass: Bypass,
135 calls: Arc<AtomicU64>,
136}
137
138impl RateLimit {
139 pub fn global_api_rate_limit_config(test_mode: bool) -> RateLimitConfig {
141 if test_mode {
142 RateLimitConfig {
143 per_second: Some(10000),
144 per_minute: Some(200000),
145 ..Default::default()
146 }
147 } else {
148 RateLimitConfig {
149 per_second: Some(20),
150 per_minute: Some(1000),
151 per_hour: Some(10000),
152 ..Default::default()
153 }
154 }
155 }
156
157 pub fn new(cfg: RateLimitConfig) -> Self {
158 Self {
159 limiters: Arc::new(EndpointLimiters::from_config(&cfg)),
160 key: RateLimitKey::default(),
161 bypass: Bypass::Off,
162 calls: Arc::new(AtomicU64::new(0)),
163 }
164 }
165
166 pub fn keyed_by(self, key: RateLimitKey) -> Self {
168 Self { key, ..self }
169 }
170
171 pub fn bypassable(self) -> Self {
175 Self {
176 bypass: Bypass::RuntimeConfigKey,
177 ..self
178 }
179 }
180
181 #[cfg(test)]
182 fn bypassable_with(self, safe_api_key: SecretString) -> Self {
183 Self {
184 bypass: Bypass::Key(safe_api_key),
185 ..self
186 }
187 }
188}
189
190impl<S, B> Transform<S, ServiceRequest> for RateLimit
191where
192 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
193 B: MessageBody + 'static,
194{
195 type Response = ServiceResponse<EitherBody<B>>;
196 type Error = Error;
197 type InitError = ();
198 type Transform = RateLimitInner<S>;
199 type Future = Ready<Result<Self::Transform, Self::InitError>>;
200
201 fn new_transform(&self, service: S) -> Self::Future {
202 ready(Ok(RateLimitInner {
203 service,
204 limiters: self.limiters.clone(),
205 key: self.key,
206 bypass: self.bypass.clone(),
207 calls: self.calls.clone(),
208 }))
209 }
210}
211
212pub struct RateLimitInner<S> {
213 service: S,
214 limiters: Arc<EndpointLimiters>,
215 key: RateLimitKey,
216 bypass: Bypass,
217 calls: Arc<AtomicU64>,
218}
219
220impl<S> RateLimitInner<S> {
221 fn carries_bypass_key(&self, req: &ServiceRequest) -> bool {
222 let Some(provided) = req
223 .headers()
224 .get(RATELIMIT_PROTECTION_SAFE_API_KEY_HEADER)
225 .and_then(|value| value.to_str().ok())
226 else {
227 return false;
228 };
229 let expected = match &self.bypass {
230 Bypass::Off => return false,
231 Bypass::RuntimeConfigKey => server_runtime_config()
232 .ratelimit_protection_safe_api_key
233 .expose_secret(),
234 #[cfg(test)]
235 Bypass::Key(key) => key.expose_secret(),
236 };
237 !expected.is_empty() && constant_time_eq_str(provided, expected)
238 }
239}
240
241impl<S, B> Service<ServiceRequest> for RateLimitInner<S>
242where
243 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
244 B: MessageBody + 'static,
245{
246 type Response = ServiceResponse<EitherBody<B>>;
247 type Error = Error;
248 type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
249
250 fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
251 self.service.poll_ready(cx)
252 }
253
254 fn call(&self, req: ServiceRequest) -> Self::Future {
255 if self.limiters.is_empty() || self.carries_bypass_key(&req) {
256 let fut = self.service.call(req);
257 return Box::pin(async move { fut.await.map(|r| r.map_into_left_body()) });
258 }
259
260 const RETAIN_EVERY: u64 = 1024;
261 const SHRINK_EVERY: u64 = 65_536;
262
263 let n = self.calls.fetch_add(1, Ordering::Relaxed) + 1;
264 if n.is_multiple_of(RETAIN_EVERY) {
265 for limiter in self.limiters.iter() {
266 limiter.retain_recent();
267 }
268 if n.is_multiple_of(SHRINK_EVERY) {
269 for limiter in self.limiters.iter() {
270 limiter.shrink_to_fit();
271 }
272 }
273 }
274
275 let clock = DefaultClock::default();
276 let token_subject = req.extensions().get::<TokenSubject>().copied();
277 let key = match self.key {
278 RateLimitKey::User => token_subject
279 .map(|subject| subject.user_id)
280 .or_else(|| session_user_id(&req.get_session()))
281 .map(|id| format!("user:{id}"))
282 .unwrap_or_else(|| extract_client_ip_key(&req)),
283 RateLimitKey::Grant => token_subject
284 .map(|subject| format!("grant:{}", subject.grant_id))
285 .unwrap_or_else(|| extract_client_ip_key(&req)),
286 RateLimitKey::ClientIp => extract_client_ip_key(&req),
287 };
288
289 let mut retry_after: Option<Duration> = None;
290 for limiter in self.limiters.iter() {
291 if let Err(negative) = limiter.check_key(&key) {
292 let wait = negative.wait_time_from(clock.now());
293 retry_after = Some(retry_after.map_or(wait, |cur| cur.max(wait)));
294 }
295 }
296
297 if let Some(wait) = retry_after {
298 let secs = wait.as_secs().max(1);
299 let resp = HttpResponse::build(StatusCode::TOO_MANY_REQUESTS)
300 .insert_header((header::RETRY_AFTER, secs.to_string()))
301 .content_type("application/json")
302 .body(r#"{"type":"rate_limit","message_key":"rate_limited","message":"Too many requests. Please try again later."}"#.to_string());
303 return Box::pin(async move { Ok(req.into_response(resp).map_into_right_body()) });
304 }
305
306 let fut = self.service.call(req);
307 Box::pin(async move { fut.await.map(|r| r.map_into_left_body()) })
308 }
309}
310
311fn extract_client_ip_key(req: &ServiceRequest) -> String {
312 if let Some(s) = req.connection_info().realip_remote_addr() {
313 let s = s.trim();
314 if !s.is_empty() {
315 return s.to_string();
316 }
317 }
318
319 if let Some(sa) = req.peer_addr() {
320 return sa.ip().to_string();
321 }
322
323 format!("unknown:{}|{}", req.connection_info().host(), req.path())
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329 use actix_http::Request;
330 use actix_web::{
331 App, HttpResponse,
332 body::{BoxBody, EitherBody},
333 dev::{Service, ServiceResponse},
334 http::header,
335 test, web,
336 };
337 use std::net::{IpAddr, Ipv4Addr, SocketAddr};
338
339 fn mw(
340 per_minute: Option<u64>,
341 per_hour: Option<u64>,
342 per_day: Option<u64>,
343 per_month: Option<u64>,
344 ) -> RateLimit {
345 RateLimit::new(RateLimitConfig {
346 per_minute,
347 per_hour,
348 per_day,
349 per_month,
350 ..Default::default()
351 })
352 }
353
354 #[actix_web::test]
355 async fn global_api_rate_limit_config_uses_test_mode_argument() {
356 let test_cfg = RateLimit::global_api_rate_limit_config(true);
357 assert_eq!(test_cfg.per_second, Some(10000));
358 assert_eq!(test_cfg.per_minute, Some(200000));
359 assert_eq!(test_cfg.per_hour, None);
360
361 let production_cfg = RateLimit::global_api_rate_limit_config(false);
362 assert_eq!(production_cfg.per_second, Some(20));
363 assert_eq!(production_cfg.per_minute, Some(1000));
364 assert_eq!(production_cfg.per_hour, Some(10000));
365 }
366
367 async fn call_get<S>(
368 app: &S,
369 uri: &str,
370 xff: Option<&str>,
371 peer: Option<SocketAddr>,
372 ) -> ServiceResponse<EitherBody<BoxBody>>
373 where
374 S: Service<Request, Response = ServiceResponse<EitherBody<BoxBody>>, Error = Error>,
375 {
376 let mut tr = test::TestRequest::get().uri(uri);
377 if let Some(v) = xff {
378 tr = tr.insert_header(("x-forwarded-for", v));
379 }
380 if let Some(p) = peer {
381 tr = tr.peer_addr(p);
382 }
383 test::call_service(app, tr.to_request()).await
384 }
385
386 fn retry_after_secs(resp: &ServiceResponse<EitherBody<BoxBody>>) -> Option<u64> {
387 resp.headers()
388 .get(header::RETRY_AFTER)
389 .and_then(|v| v.to_str().ok())
390 .and_then(|s| s.parse::<u64>().ok())
391 }
392
393 async fn app(
394 mw: RateLimit,
395 ) -> impl Service<Request, Response = ServiceResponse<EitherBody<BoxBody>>, Error = Error> {
396 test::init_service(
397 App::new()
398 .wrap(mw)
399 .route("/", web::get().to(|| async { HttpResponse::Ok().finish() }))
400 .route(
401 "/other",
402 web::get().to(|| async { HttpResponse::Ok().finish() }),
403 ),
404 )
405 .await
406 }
407
408 #[actix_web::test]
409 async fn key_trims_realip_value() {
410 let req = test::TestRequest::get()
411 .uri("/x")
412 .insert_header(("x-forwarded-for", " 9.9.9.9 "))
413 .to_srv_request();
414
415 assert_eq!(super::extract_client_ip_key(&req), "9.9.9.9");
416 }
417
418 #[actix_web::test]
419 async fn key_empty_realip_falls_back_to_peer_ip() {
420 let peer = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), 5555);
421 let req = test::TestRequest::get()
422 .uri("/x")
423 .insert_header(("x-forwarded-for", " "))
424 .peer_addr(peer)
425 .to_srv_request();
426
427 assert_eq!(super::extract_client_ip_key(&req), "1.2.3.4");
428 }
429
430 #[actix_web::test]
431 async fn key_no_realip_uses_peer_ip() {
432 let peer = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 9)), 1234);
433 let req = test::TestRequest::get()
434 .uri("/x")
435 .peer_addr(peer)
436 .to_srv_request();
437 assert_eq!(super::extract_client_ip_key(&req), "10.0.0.9");
438 }
439
440 #[actix_web::test]
441 async fn key_no_realip_and_no_peer_uses_unknown_host_and_path() {
442 let req = test::TestRequest::get().uri("/path123").to_srv_request();
443 let k = super::extract_client_ip_key(&req);
444 assert!(k.starts_with("unknown:"), "key={k}");
445 assert!(k.contains("|/path123"), "key={k}");
446 }
447
448 #[actix_web::test]
449 async fn passthrough_when_no_limits() {
450 let app = app(mw(None, None, None, None)).await;
451
452 let r1 = call_get(&app, "/", Some("1.2.3.4"), None).await;
453 let r2 = call_get(&app, "/", Some("1.2.3.4"), None).await;
454
455 assert_eq!(r1.status(), StatusCode::OK);
456 assert_eq!(r2.status(), StatusCode::OK);
457 assert!(r2.headers().get(header::RETRY_AFTER).is_none());
458 }
459
460 #[actix_web::test]
461 async fn per_minute_zero_disables_window() {
462 let app = app(mw(Some(0), None, None, None)).await;
463
464 let r1 = call_get(&app, "/", Some("1.2.3.4"), None).await;
465 let r2 = call_get(&app, "/", Some("1.2.3.4"), None).await;
466
467 assert_eq!(r1.status(), StatusCode::OK);
468 assert_eq!(r2.status(), StatusCode::OK);
469 }
470
471 #[actix_web::test]
472 async fn per_minute_over_u32_disables_window() {
473 let app = app(mw(Some(u64::from(u32::MAX) + 1), None, None, None)).await;
474
475 let r1 = call_get(&app, "/", Some("1.2.3.4"), None).await;
476 let r2 = call_get(&app, "/", Some("1.2.3.4"), None).await;
477
478 assert_eq!(r1.status(), StatusCode::OK);
479 assert_eq!(r2.status(), StatusCode::OK);
480 }
481
482 #[actix_web::test]
483 async fn blocks_second_request_same_key() {
484 let app = app(mw(Some(1), None, None, None)).await;
485
486 let ok = call_get(&app, "/", Some("1.2.3.4"), None).await;
487 assert_eq!(ok.status(), StatusCode::OK);
488
489 let blocked = call_get(&app, "/", Some("1.2.3.4"), None).await;
490 assert_eq!(blocked.status(), StatusCode::TOO_MANY_REQUESTS);
491
492 let ra = retry_after_secs(&blocked).expect("missing Retry-After");
493 assert!(ra >= 1);
494 }
495
496 #[actix_web::test]
497 async fn retry_after_is_integer_seconds_and_body_is_json() {
498 let app = app(mw(Some(1), None, None, None)).await;
499
500 let _ = call_get(&app, "/", Some("1.2.3.4"), None).await;
501 let blocked = call_get(&app, "/", Some("1.2.3.4"), None).await;
502
503 assert_eq!(blocked.status(), StatusCode::TOO_MANY_REQUESTS);
504
505 let ra_hdr = blocked.headers().get(header::RETRY_AFTER).unwrap();
506 let ra_str = ra_hdr.to_str().unwrap();
507 assert!(
508 ra_str.parse::<u64>().is_ok(),
509 "Retry-After not int: {ra_str}"
510 );
511
512 let bytes = test::read_body(blocked).await;
513 let body = std::str::from_utf8(&bytes).unwrap();
514 assert!(body.contains(r#""type":"rate_limit""#), "body={body}");
515 let v: serde_json::Value = serde_json::from_str(body).unwrap();
516 assert_eq!(v["type"], "rate_limit");
517 assert_eq!(v["message_key"], "rate_limited");
518 assert!(v.get("retry_after").is_none());
519 }
520
521 #[actix_web::test]
522 async fn different_keys_independent() {
523 let app = app(mw(Some(1), None, None, None)).await;
524
525 let a1 = call_get(&app, "/", Some("10.0.0.1"), None).await;
526 let b1 = call_get(&app, "/", Some("10.0.0.2"), None).await;
527 assert_eq!(a1.status(), StatusCode::OK);
528 assert_eq!(b1.status(), StatusCode::OK);
529
530 let a2 = call_get(&app, "/", Some("10.0.0.1"), None).await;
531 assert_eq!(a2.status(), StatusCode::TOO_MANY_REQUESTS);
532 }
533
534 #[actix_web::test]
535 async fn same_key_shared_across_routes_in_same_app() {
536 let app = app(mw(Some(1), None, None, None)).await;
537
538 let r1 = call_get(&app, "/", Some("1.2.3.4"), None).await;
539 assert_eq!(r1.status(), StatusCode::OK);
540
541 let r2 = call_get(&app, "/other", Some("1.2.3.4"), None).await;
542 assert_eq!(r2.status(), StatusCode::TOO_MANY_REQUESTS);
543 }
544
545 #[actix_web::test]
546 async fn max_retry_after_prefers_longer_window() {
547 let app = app(mw(Some(1), Some(1), None, None)).await;
548
549 let r1 = call_get(&app, "/", Some("1.2.3.4"), None).await;
550 assert_eq!(r1.status(), StatusCode::OK);
551
552 let r2 = call_get(&app, "/", Some("1.2.3.4"), None).await;
553 assert_eq!(r2.status(), StatusCode::TOO_MANY_REQUESTS);
554
555 let ra = retry_after_secs(&r2).expect("missing Retry-After");
556 assert!(ra >= 120, "expected hour-dominated Retry-After, got {ra}");
558 }
559
560 #[actix_web::test]
561 async fn peer_addr_used_when_no_forwarded_headers() {
562 let app = app(mw(Some(1), None, None, None)).await;
563
564 let peer = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(7, 7, 7, 7)), 9999);
565 let r1 = call_get(&app, "/", None, Some(peer)).await;
566 let r2 = call_get(&app, "/", None, Some(peer)).await;
567
568 assert_eq!(r1.status(), StatusCode::OK);
569 assert_eq!(r2.status(), StatusCode::TOO_MANY_REQUESTS);
570 }
571
572 #[actix_web::test]
573 async fn empty_forwarded_header_does_not_create_empty_key_bucket() {
574 let app = app(mw(Some(1), None, None, None)).await;
575
576 let peer = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8)), 1111);
577 let r1 = call_get(&app, "/", Some(" "), Some(peer)).await;
578 let r2 = call_get(&app, "/", Some(" "), Some(peer)).await;
579
580 assert_eq!(r1.status(), StatusCode::OK);
581 assert_eq!(r2.status(), StatusCode::TOO_MANY_REQUESTS);
582 }
583
584 #[actix_web::test]
585 async fn unknown_bucket_includes_path_to_reduce_collisions() {
586 let app = app(mw(Some(1), None, None, None)).await;
587
588 let r1 = call_get(&app, "/", None, None).await;
589 let r2 = call_get(&app, "/other", None, None).await;
590
591 assert_eq!(r1.status(), StatusCode::OK);
592 assert_eq!(r2.status(), StatusCode::OK);
593
594 let r1b = call_get(&app, "/", None, None).await;
595 assert_eq!(r1b.status(), StatusCode::TOO_MANY_REQUESTS);
596 }
597
598 #[actix_web::test]
599 async fn housekeeping_retain_recent_path_executes() {
600 let app = app(mw(Some(1_000_000), None, None, None)).await;
602
603 for i in 0..=1024 {
604 let ip = format!("192.0.2.{}", (i % 250) + 1);
605 let resp = call_get(&app, "/", Some(&ip), None).await;
606 assert_eq!(resp.status(), StatusCode::OK, "i={i} ip={ip}");
607 }
608 }
609
610 #[actix_web::test]
611 async fn keys_are_exact_strings_no_normalization_means_ports_are_distinct() {
612 let app = app(mw(Some(1), None, None, None)).await;
613
614 let a = call_get(&app, "/", Some("1.2.3.4"), None).await;
615 let b = call_get(&app, "/", Some("1.2.3.4:12345"), None).await;
616
617 assert_eq!(a.status(), StatusCode::OK);
618 assert_eq!(b.status(), StatusCode::OK);
619
620 let a2 = call_get(&app, "/", Some("1.2.3.4"), None).await;
621 let b2 = call_get(&app, "/", Some("1.2.3.4:12345"), None).await;
622
623 assert_eq!(a2.status(), StatusCode::TOO_MANY_REQUESTS);
624 assert_eq!(b2.status(), StatusCode::TOO_MANY_REQUESTS);
625 }
626
627 async fn call_get_with_safe_api_key<S>(
628 app: &S,
629 safe_api_key: &str,
630 ) -> ServiceResponse<EitherBody<BoxBody>>
631 where
632 S: Service<Request, Response = ServiceResponse<EitherBody<BoxBody>>, Error = Error>,
633 {
634 let req = test::TestRequest::get()
635 .uri("/")
636 .insert_header(("x-forwarded-for", "1.2.3.4"))
637 .insert_header((RATELIMIT_PROTECTION_SAFE_API_KEY_HEADER, safe_api_key))
638 .to_request();
639 test::call_service(app, req).await
640 }
641
642 #[actix_web::test]
643 async fn bypassable_limiter_skips_requests_with_safe_api_key() {
644 let app = app(mw(Some(1), None, None, None).bypassable_with("safe-key".into())).await;
645
646 for _ in 0..3 {
647 let resp = call_get_with_safe_api_key(&app, "safe-key").await;
648 assert_eq!(resp.status(), StatusCode::OK);
649 }
650 }
651
652 #[actix_web::test]
653 async fn bypassable_limiter_limits_wrong_safe_api_key() {
654 let app = app(mw(Some(1), None, None, None).bypassable_with("safe-key".into())).await;
655
656 let r1 = call_get_with_safe_api_key(&app, "wrong-key").await;
657 let r2 = call_get_with_safe_api_key(&app, "wrong-key").await;
658
659 assert_eq!(r1.status(), StatusCode::OK);
660 assert_eq!(r2.status(), StatusCode::TOO_MANY_REQUESTS);
661 }
662
663 #[actix_web::test]
664 async fn non_bypassable_limiter_limits_safe_api_key() {
665 let app = app(mw(Some(1), None, None, None)).await;
666
667 let r1 = call_get_with_safe_api_key(&app, "safe-key").await;
668 let r2 = call_get_with_safe_api_key(&app, "safe-key").await;
669
670 assert_eq!(r1.status(), StatusCode::OK);
671 assert_eq!(r2.status(), StatusCode::TOO_MANY_REQUESTS);
672 }
673
674 #[actix_web::test]
675 async fn empty_configured_safe_api_key_never_bypasses() {
676 let app = app(mw(Some(1), None, None, None).bypassable_with("".into())).await;
677
678 let r1 = call_get_with_safe_api_key(&app, "").await;
679 let r2 = call_get_with_safe_api_key(&app, "").await;
680
681 assert_eq!(r1.status(), StatusCode::OK);
682 assert_eq!(r2.status(), StatusCode::TOO_MANY_REQUESTS);
683 }
684}