1use crate::auth::{bearer_token, decode_hs256, JwtClaims, ProjectedClaims};
2use actix_web::{
3 body::{BoxBody, MessageBody},
4 dev::{Service, ServiceRequest, ServiceResponse, Transform},
5 http::{header, Method, StatusCode},
6 Error, HttpMessage, HttpResponse,
7};
8use futures::future::{ok, LocalBoxFuture, Ready};
9use rust_zero_core::{AuthFailure, JwtClaimProjection};
10use serde::{Deserialize, Serialize};
11use std::{
12 collections::{HashMap, HashSet},
13 fmt,
14 future::Future,
15 rc::Rc,
16 sync::Arc,
17 task::{Context, Poll},
18 time::Duration,
19};
20
21#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct RouteJwtConfig {
24 pub secret: String,
25 #[serde(default, skip_serializing_if = "Option::is_none")]
26 pub previous_secret: Option<String>,
27 #[serde(default)]
28 pub leeway_seconds: u64,
29 #[serde(default)]
30 pub claim_projection: JwtClaimProjection,
31}
32
33impl fmt::Debug for RouteJwtConfig {
34 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
35 formatter
36 .debug_struct("RouteJwtConfig")
37 .field("secret", &"[REDACTED]")
38 .field(
39 "previous_secret",
40 &self.previous_secret.as_ref().map(|_| "[REDACTED]"),
41 )
42 .field("leeway_seconds", &self.leeway_seconds)
43 .field("claim_projection", &self.claim_projection)
44 .finish()
45 }
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50pub struct RoutePolicyConfig {
51 pub method: String,
52 pub path: String,
53 #[serde(default)]
54 pub public: bool,
55 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub jwt: Option<RouteJwtConfig>,
57 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub timeout_ms: Option<u64>,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub max_body_bytes: Option<usize>,
61 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub priority: Option<bool>,
63 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub sse: Option<bool>,
65}
66
67#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
69pub struct RouteGroupConfig {
70 #[serde(default)]
71 pub prefix: String,
72 #[serde(default, skip_serializing_if = "Option::is_none")]
73 pub jwt: Option<RouteJwtConfig>,
74 #[serde(default, skip_serializing_if = "Option::is_none")]
75 pub timeout_ms: Option<u64>,
76 #[serde(default, skip_serializing_if = "Option::is_none")]
77 pub max_body_bytes: Option<usize>,
78 #[serde(default)]
79 pub priority: bool,
80 #[serde(default)]
81 pub sse: bool,
82 #[serde(default)]
84 pub middleware: Vec<String>,
85 #[serde(default)]
86 pub routes: Vec<RoutePolicyConfig>,
87}
88
89pub type RouteMiddlewareFuture = LocalBoxFuture<'static, Result<ServiceResponse<BoxBody>, Error>>;
91
92#[derive(Clone)]
94pub struct RouteMiddlewareNext {
95 inner: Rc<dyn Fn(ServiceRequest) -> RouteMiddlewareFuture>,
96}
97
98impl RouteMiddlewareNext {
99 pub fn call(&self, request: ServiceRequest) -> RouteMiddlewareFuture {
100 (self.inner)(request)
101 }
102}
103
104pub trait RouteMiddleware: Send + Sync + 'static {
106 fn call(&self, request: ServiceRequest, next: RouteMiddlewareNext) -> RouteMiddlewareFuture;
107}
108
109impl<F, Fut> RouteMiddleware for F
110where
111 F: Fn(ServiceRequest, RouteMiddlewareNext) -> Fut + Send + Sync + 'static,
112 Fut: Future<Output = Result<ServiceResponse<BoxBody>, Error>> + 'static,
113{
114 fn call(&self, request: ServiceRequest, next: RouteMiddlewareNext) -> RouteMiddlewareFuture {
115 Box::pin((self)(request, next))
116 }
117}
118
119#[derive(Debug, Clone)]
120struct CompiledPolicy {
121 method: Method,
122 pattern: String,
123 jwt_secrets: Option<Vec<Arc<[u8]>>>,
124 jwt_leeway_seconds: u64,
125 jwt_claim_projection: JwtClaimProjection,
126 timeout: Option<Duration>,
127 max_body_bytes: Option<usize>,
128 priority: bool,
129 sse: bool,
130 middleware: Arc<[String]>,
131}
132
133#[derive(Debug, Clone, Default)]
135pub(crate) struct RequestPolicy {
136 pub timeout: Option<Duration>,
137 pub max_body_bytes: Option<usize>,
138 pub priority: bool,
139 pub sse: bool,
140}
141
142#[derive(Clone, Default)]
144pub(crate) struct RoutePolicies {
145 routes: Arc<[CompiledPolicy]>,
146 middleware: Arc<HashMap<String, Arc<dyn RouteMiddleware>>>,
147}
148
149impl fmt::Debug for RoutePolicies {
150 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
151 formatter
152 .debug_struct("RoutePolicies")
153 .field("routes", &self.routes)
154 .field("middleware_names", &self.middleware.keys())
155 .finish()
156 }
157}
158
159impl RoutePolicies {
160 pub fn compile(groups: &[RouteGroupConfig]) -> Result<Self, String> {
161 let mut routes = Vec::new();
162 let mut unique = HashSet::new();
163
164 for group in groups {
165 validate_prefix(&group.prefix)?;
166 validate_optional_limits(group.timeout_ms, group.max_body_bytes)?;
167 validate_middleware_names(&group.middleware)?;
168 if let Some(jwt) = &group.jwt {
169 validate_jwt(jwt)?;
170 }
171
172 for route in &group.routes {
173 if !route.path.starts_with('/') {
174 return Err(format!("route path must start with '/': {}", route.path));
175 }
176 validate_optional_limits(route.timeout_ms, route.max_body_bytes)?;
177 if let Some(jwt) = &route.jwt {
178 validate_jwt(jwt)?;
179 }
180 if route.public && route.jwt.is_some() {
181 return Err(format!(
182 "route {} cannot be public and define JWT authentication",
183 route.path
184 ));
185 }
186
187 let method = Method::from_bytes(route.method.as_bytes())
188 .map_err(|_| format!("invalid route method: {}", route.method))?;
189 let pattern = join_route(&group.prefix, &route.path);
190 if !unique.insert((method.clone(), pattern.clone())) {
191 return Err(format!("duplicate route policy: {method} {pattern}"));
192 }
193
194 let jwt = if route.public {
195 None
196 } else {
197 route.jwt.as_ref().or(group.jwt.as_ref())
198 };
199 let (jwt_secrets, jwt_leeway_seconds, jwt_claim_projection) = match jwt {
200 Some(jwt) => (
201 Some(jwt_secrets(jwt)),
202 jwt.leeway_seconds,
203 jwt.claim_projection.clone(),
204 ),
205 None => (None, 0, JwtClaimProjection::default()),
206 };
207 let sse = route.sse.unwrap_or(group.sse);
208 routes.push(CompiledPolicy {
209 method,
210 pattern,
211 jwt_secrets,
212 jwt_leeway_seconds,
213 jwt_claim_projection,
214 timeout: if sse {
215 None
216 } else {
217 route
218 .timeout_ms
219 .or(group.timeout_ms)
220 .map(Duration::from_millis)
221 },
222 max_body_bytes: route.max_body_bytes.or(group.max_body_bytes),
223 priority: route.priority.unwrap_or(group.priority),
224 sse,
225 middleware: group.middleware.clone().into(),
226 });
227 }
228 }
229
230 Ok(Self {
231 routes: routes.into(),
232 middleware: Arc::new(HashMap::new()),
233 })
234 }
235
236 pub fn with_middleware(
237 mut self,
238 middleware: HashMap<String, Arc<dyn RouteMiddleware>>,
239 ) -> Result<Self, String> {
240 for route in self.routes.iter() {
241 for name in route.middleware.iter() {
242 if !middleware.contains_key(name) {
243 return Err(format!("route middleware '{name}' is not registered"));
244 }
245 }
246 }
247 self.middleware = Arc::new(middleware);
248 Ok(self)
249 }
250
251 fn find(&self, method: &Method, pattern: &str) -> Option<&CompiledPolicy> {
252 self.routes
253 .iter()
254 .find(|route| route.method == *method && route.pattern == pattern)
255 }
256}
257
258fn validate_middleware_names(names: &[String]) -> Result<(), String> {
259 let mut unique = HashSet::new();
260 for name in names {
261 if name.trim().is_empty() {
262 return Err("route middleware name must not be empty".to_owned());
263 }
264 if !unique.insert(name) {
265 return Err(format!("duplicate route middleware name: {name}"));
266 }
267 }
268 Ok(())
269}
270
271fn validate_prefix(prefix: &str) -> Result<(), String> {
272 if !prefix.is_empty() && !prefix.starts_with('/') {
273 return Err(format!("route group prefix must start with '/': {prefix}"));
274 }
275 Ok(())
276}
277
278fn validate_optional_limits(
279 timeout_ms: Option<u64>,
280 max_body_bytes: Option<usize>,
281) -> Result<(), String> {
282 if timeout_ms == Some(0) {
283 return Err("route timeout_ms must be greater than zero".to_owned());
284 }
285 if max_body_bytes == Some(0) {
286 return Err("route max_body_bytes must be greater than zero".to_owned());
287 }
288 Ok(())
289}
290
291fn validate_jwt(jwt: &RouteJwtConfig) -> Result<(), String> {
292 if jwt.secret.is_empty() {
293 return Err("route JWT secret must not be empty".to_owned());
294 }
295 if jwt.previous_secret.as_deref() == Some("") {
296 return Err("route previous JWT secret must not be empty".to_owned());
297 }
298 Ok(())
299}
300
301fn jwt_secrets(jwt: &RouteJwtConfig) -> Vec<Arc<[u8]>> {
302 let mut secrets = vec![Arc::from(jwt.secret.as_bytes())];
303 if let Some(previous) = &jwt.previous_secret {
304 secrets.push(Arc::from(previous.as_bytes()));
305 }
306 secrets
307}
308
309fn join_route(prefix: &str, path: &str) -> String {
310 if prefix.is_empty() || prefix == "/" {
311 return path.to_owned();
312 }
313 format!("{}{}", prefix.trim_end_matches('/'), path)
314}
315
316impl<S, B> Transform<S, ServiceRequest> for RoutePolicies
317where
318 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
319 S::Future: 'static,
320 B: MessageBody + 'static,
321{
322 type Response = ServiceResponse<BoxBody>;
323 type Error = Error;
324 type Transform = RoutePoliciesMiddleware<S>;
325 type InitError = ();
326 type Future = Ready<Result<Self::Transform, Self::InitError>>;
327
328 fn new_transform(&self, service: S) -> Self::Future {
329 ok(RoutePoliciesMiddleware {
330 service: Rc::new(service),
331 policies: self.clone(),
332 })
333 }
334}
335
336pub(crate) struct RoutePoliciesMiddleware<S> {
337 service: Rc<S>,
338 policies: RoutePolicies,
339}
340
341impl<S, B> Service<ServiceRequest> for RoutePoliciesMiddleware<S>
342where
343 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
344 S::Future: 'static,
345 B: MessageBody + 'static,
346{
347 type Response = ServiceResponse<BoxBody>;
348 type Error = Error;
349 type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
350
351 fn poll_ready(&self, context: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
352 self.service.poll_ready(context)
353 }
354
355 fn call(&self, request: ServiceRequest) -> Self::Future {
356 let pattern = request
357 .match_pattern()
358 .unwrap_or_else(|| request.path().to_owned());
359 let policy = self.policies.find(request.method(), &pattern).cloned();
360
361 if let Some(policy) = &policy {
362 if let Some(secrets) = &policy.jwt_secrets {
363 let result = request
364 .headers()
365 .get(header::AUTHORIZATION)
366 .and_then(|value| value.to_str().ok())
367 .and_then(bearer_token)
368 .ok_or(AuthFailure::MissingCredentials)
369 .and_then(|token| {
370 decode_hs256::<serde_json::Value>(token, secrets, policy.jwt_leeway_seconds)
371 .map_err(AuthFailure::from)
372 });
373 let claims = match result {
374 Ok(claims) => claims,
375 Err(failure) => {
376 return Box::pin(async move {
377 Ok(request.into_response(
378 HttpResponse::build(StatusCode::UNAUTHORIZED)
379 .insert_header((header::WWW_AUTHENTICATE, "Bearer"))
380 .json(serde_json::json!({
381 "code": failure.code(),
382 "message": failure.message(),
383 }))
384 .map_into_boxed_body(),
385 ))
386 });
387 }
388 };
389 request.extensions_mut().insert(ProjectedClaims(
390 policy.jwt_claim_projection.project(&claims),
391 ));
392 request.extensions_mut().insert(JwtClaims(claims));
393 }
394
395 request.extensions_mut().insert(RequestPolicy {
396 timeout: policy.timeout,
397 max_body_bytes: policy.max_body_bytes,
398 priority: policy.priority,
399 sse: policy.sse,
400 });
401 }
402
403 let service = Rc::clone(&self.service);
404 let middleware = policy
405 .as_ref()
406 .map(|policy| policy.middleware.clone())
407 .unwrap_or_default();
408 let registry = Arc::clone(&self.policies.middleware);
409 Box::pin(async move {
410 let terminal = RouteMiddlewareNext {
411 inner: Rc::new(move |request| {
412 let service = Rc::clone(&service);
413 Box::pin(async move { Ok(service.call(request).await?.map_into_boxed_body()) })
414 }),
415 };
416 let chain = middleware.iter().rev().fold(terminal, |next, name| {
417 let route_middleware = Arc::clone(
418 registry
419 .get(name)
420 .expect("route middleware registry was validated"),
421 );
422 RouteMiddlewareNext {
423 inner: Rc::new(move |request| route_middleware.call(request, next.clone())),
424 }
425 });
426 let mut response = chain.call(request).await?;
427 if policy.is_some_and(|policy| policy.sse) {
428 response.headers_mut().insert(
429 header::CONTENT_TYPE,
430 header::HeaderValue::from_static("text/event-stream"),
431 );
432 response.headers_mut().insert(
433 header::CACHE_CONTROL,
434 header::HeaderValue::from_static("no-cache, no-transform"),
435 );
436 response.headers_mut().insert(
437 header::HeaderName::from_static("x-accel-buffering"),
438 header::HeaderValue::from_static("no"),
439 );
440 }
441 Ok(response)
442 })
443 }
444}
445
446#[cfg(test)]
447mod tests {
448 use super::*;
449 use crate::{encode_hs256, ConcurrencyLimit, JwtAuth, RequestBodyLimit, Timeout};
450 use actix_web::{
451 http::{header::HeaderValue, StatusCode},
452 test, web, App, HttpRequest, HttpResponse,
453 };
454 use std::sync::Arc;
455 use tokio::sync::Notify;
456
457 fn policy_set() -> RoutePolicies {
458 RoutePolicies::compile(&[RouteGroupConfig {
459 prefix: "/api".to_owned(),
460 jwt: Some(RouteJwtConfig {
461 secret: "secret".to_owned(),
462 previous_secret: None,
463 leeway_seconds: 0,
464 claim_projection: JwtClaimProjection::new([(
465 "caller".to_owned(),
466 "sub".to_owned(),
467 )]),
468 }),
469 timeout_ms: Some(50),
470 max_body_bytes: Some(16),
471 routes: vec![
472 RoutePolicyConfig {
473 method: "GET".to_owned(),
474 path: "/private/{id}".to_owned(),
475 public: false,
476 jwt: None,
477 timeout_ms: None,
478 max_body_bytes: None,
479 priority: None,
480 sse: None,
481 },
482 RoutePolicyConfig {
483 method: "GET".to_owned(),
484 path: "/slow".to_owned(),
485 public: true,
486 jwt: None,
487 timeout_ms: Some(1),
488 max_body_bytes: None,
489 priority: None,
490 sse: None,
491 },
492 RoutePolicyConfig {
493 method: "POST".to_owned(),
494 path: "/body".to_owned(),
495 public: true,
496 jwt: None,
497 timeout_ms: None,
498 max_body_bytes: Some(2),
499 priority: None,
500 sse: None,
501 },
502 RoutePolicyConfig {
503 method: "GET".to_owned(),
504 path: "/events".to_owned(),
505 public: true,
506 jwt: None,
507 timeout_ms: Some(1),
508 max_body_bytes: None,
509 priority: None,
510 sse: Some(true),
511 },
512 ],
513 ..RouteGroupConfig::default()
514 }])
515 .unwrap()
516 }
517
518 #[actix_web::test]
519 async fn enforces_inherited_and_per_route_policies() {
520 let app = test::init_service(
521 App::new()
522 .wrap(Timeout::new(Duration::from_secs(1)))
523 .wrap(RequestBodyLimit::new(1_024))
524 .wrap(policy_set())
525 .route(
526 "/api/private/{id}",
527 web::get().to(|request: HttpRequest| async move {
528 HttpResponse::Ok().json(serde_json::json!({
529 "claims": JwtAuth::<serde_json::Value>::claims(&request),
530 "projected": JwtAuth::<serde_json::Value>::projected_claims(&request),
531 }))
532 }),
533 )
534 .route(
535 "/api/slow",
536 web::get().to(|| async {
537 actix_rt::time::sleep(Duration::from_millis(20)).await;
538 HttpResponse::Ok().finish()
539 }),
540 )
541 .route(
542 "/api/body",
543 web::post().to(|body: web::Bytes| async move { HttpResponse::Ok().body(body) }),
544 )
545 .route(
546 "/api/events",
547 web::get().to(|| async {
548 actix_rt::time::sleep(Duration::from_millis(10)).await;
549 HttpResponse::Ok().body("data: ready\n\n")
550 }),
551 ),
552 )
553 .await;
554
555 let unauthorized = test::call_service(
556 &app,
557 test::TestRequest::get().uri("/api/private/7").to_request(),
558 )
559 .await;
560 assert_eq!(unauthorized.status(), StatusCode::UNAUTHORIZED);
561
562 let token = encode_hs256(&serde_json::json!({ "sub": "42" }), b"secret").unwrap();
563 let authorized = test::call_service(
564 &app,
565 test::TestRequest::get()
566 .uri("/api/private/7")
567 .insert_header((header::AUTHORIZATION, format!("Bearer {token}")))
568 .to_request(),
569 )
570 .await;
571 assert_eq!(authorized.status(), StatusCode::OK);
572 let claims: serde_json::Value = test::read_body_json(authorized).await;
573 assert_eq!(claims["claims"]["sub"], "42");
574 assert_eq!(claims["projected"]["caller"], "42");
575
576 let slow =
577 test::try_call_service(&app, test::TestRequest::get().uri("/api/slow").to_request())
578 .await
579 .expect_err("route timeout should reject slow work");
580 assert_eq!(
581 actix_web::error::ResponseError::status_code(slow.as_response_error()),
582 StatusCode::GATEWAY_TIMEOUT
583 );
584
585 let oversized = test::call_service(
586 &app,
587 test::TestRequest::post()
588 .uri("/api/body")
589 .set_payload("abc")
590 .to_request(),
591 )
592 .await;
593 assert_eq!(oversized.status(), StatusCode::PAYLOAD_TOO_LARGE);
594
595 let events = test::call_service(
596 &app,
597 test::TestRequest::get().uri("/api/events").to_request(),
598 )
599 .await;
600 assert_eq!(events.status(), StatusCode::OK);
601 assert_eq!(
602 events.headers().get(header::CONTENT_TYPE).unwrap(),
603 "text/event-stream"
604 );
605 assert_eq!(
606 events.headers().get(header::CACHE_CONTROL).unwrap(),
607 "no-cache, no-transform"
608 );
609 }
610
611 #[actix_web::test]
612 async fn priority_routes_use_reserved_capacity() {
613 let policies = RoutePolicies::compile(&[RouteGroupConfig {
614 routes: vec![RoutePolicyConfig {
615 method: "GET".to_owned(),
616 path: "/priority".to_owned(),
617 public: true,
618 jwt: None,
619 timeout_ms: None,
620 max_body_bytes: None,
621 priority: Some(true),
622 sse: None,
623 }],
624 ..RouteGroupConfig::default()
625 }])
626 .unwrap();
627 let started = Arc::new(Notify::new());
628 let release = Arc::new(Notify::new());
629 let app = test::init_service(
630 App::new()
631 .wrap(ConcurrencyLimit::new(1).with_priority_reserve(1))
632 .wrap(policies)
633 .route(
634 "/normal",
635 web::get().to({
636 let started = Arc::clone(&started);
637 let release = Arc::clone(&release);
638 move || {
639 let started = Arc::clone(&started);
640 let release = Arc::clone(&release);
641 async move {
642 started.notify_one();
643 release.notified().await;
644 HttpResponse::Ok().finish()
645 }
646 }
647 }),
648 )
649 .route("/priority", web::get().to(HttpResponse::Ok)),
650 )
651 .await;
652
653 let normal = test::call_service(&app, test::TestRequest::get().uri("/normal").to_request());
654 let priority = async {
655 started.notified().await;
656 let response =
657 test::call_service(&app, test::TestRequest::get().uri("/priority").to_request())
658 .await;
659 release.notify_one();
660 response
661 };
662 let (normal, priority) = futures::future::join(normal, priority).await;
663 assert_eq!(normal.status(), StatusCode::OK);
664 assert_eq!(priority.status(), StatusCode::OK);
665 }
666
667 #[actix_web::test]
668 async fn named_group_middleware_wraps_only_declared_routes() {
669 let policies = RoutePolicies::compile(&[RouteGroupConfig {
670 prefix: "/api".to_owned(),
671 middleware: vec!["api-key".to_owned(), "response-header".to_owned()],
672 routes: vec![RoutePolicyConfig {
673 method: "GET".to_owned(),
674 path: "/private".to_owned(),
675 public: true,
676 jwt: None,
677 timeout_ms: None,
678 max_body_bytes: None,
679 priority: None,
680 sse: None,
681 }],
682 ..RouteGroupConfig::default()
683 }])
684 .unwrap();
685 let mut middleware: HashMap<String, Arc<dyn RouteMiddleware>> = HashMap::new();
686 middleware.insert(
687 "api-key".to_owned(),
688 Arc::new(
689 |request: ServiceRequest, next: RouteMiddlewareNext| async move {
690 if request
691 .headers()
692 .get("x-api-key")
693 .and_then(|value| value.to_str().ok())
694 != Some("valid")
695 {
696 return Ok(request
697 .into_response(HttpResponse::Forbidden().finish())
698 .map_into_boxed_body());
699 }
700 next.call(request).await
701 },
702 ),
703 );
704 middleware.insert(
705 "response-header".to_owned(),
706 Arc::new(
707 |request: ServiceRequest, next: RouteMiddlewareNext| async move {
708 let mut response = next.call(request).await?;
709 response.headers_mut().insert(
710 "x-application-middleware".parse().unwrap(),
711 HeaderValue::from_static("yes"),
712 );
713 Ok(response)
714 },
715 ),
716 );
717 let app = test::init_service(
718 App::new()
719 .wrap(policies.with_middleware(middleware).unwrap())
720 .route("/api/private", web::get().to(HttpResponse::Ok))
721 .route("/public", web::get().to(HttpResponse::Ok)),
722 )
723 .await;
724
725 let forbidden = test::call_service(
726 &app,
727 test::TestRequest::get().uri("/api/private").to_request(),
728 )
729 .await;
730 assert_eq!(forbidden.status(), StatusCode::FORBIDDEN);
731 assert!(!forbidden.headers().contains_key("x-application-middleware"));
732
733 let accepted = test::call_service(
734 &app,
735 test::TestRequest::get()
736 .uri("/api/private")
737 .insert_header(("x-api-key", "valid"))
738 .to_request(),
739 )
740 .await;
741 assert_eq!(accepted.status(), StatusCode::OK);
742 assert_eq!(
743 accepted.headers().get("x-application-middleware").unwrap(),
744 "yes"
745 );
746
747 let public =
748 test::call_service(&app, test::TestRequest::get().uri("/public").to_request()).await;
749 assert_eq!(public.status(), StatusCode::OK);
750 assert!(!public.headers().contains_key("x-application-middleware"));
751 }
752
753 #[actix_web::test]
754 async fn rejects_ambiguous_or_invalid_route_policies() {
755 let duplicate = RoutePolicyConfig {
756 method: "GET".to_owned(),
757 path: "/users/{id}".to_owned(),
758 public: true,
759 jwt: None,
760 timeout_ms: None,
761 max_body_bytes: None,
762 priority: None,
763 sse: None,
764 };
765 let error = RoutePolicies::compile(&[RouteGroupConfig {
766 routes: vec![duplicate.clone(), duplicate],
767 ..RouteGroupConfig::default()
768 }])
769 .unwrap_err();
770 assert!(error.contains("duplicate"));
771
772 let missing = RoutePolicies::compile(&[RouteGroupConfig {
773 middleware: vec!["missing".to_owned()],
774 routes: vec![RoutePolicyConfig {
775 method: "GET".to_owned(),
776 path: "/route".to_owned(),
777 public: true,
778 jwt: None,
779 timeout_ms: None,
780 max_body_bytes: None,
781 priority: None,
782 sse: None,
783 }],
784 ..RouteGroupConfig::default()
785 }])
786 .unwrap()
787 .with_middleware(HashMap::new())
788 .unwrap_err();
789 assert!(missing.contains("not registered"));
790 }
791}