1use std::{
8 convert::Infallible,
9 error::Error,
10 panic::{catch_unwind, AssertUnwindSafe},
11 sync::Arc,
12 task::{Context, Poll},
13};
14
15use arc_swap::ArcSwap;
16use async_trait::async_trait;
17use axum::{body::Body as AxumBody, routing::future::RouteFuture, Router};
18use http::{Request, Response};
19use rcgen::{generate_simple_self_signed, CertifiedKey};
20use rustls::{pki_types::PrivateKeyDer, ServerConfig};
21use rustls_pki_types::PrivatePkcs8KeyDer;
22use saluki_api::{APIHandler, DynamicRoute, EndpointType};
23use saluki_common::{collections::FastIndexMap, sync::shutdown::ShutdownHandle};
24use saluki_core::runtime::{
25 state::{DataspaceRegistry, DataspaceUpdate, Identifier, IdentifierFilter, Subscription},
26 InitializationError, Supervisable, Supervisor, SupervisorFuture,
27};
28use saluki_error::{generic_error, GenericError};
29use saluki_io::net::{
30 server::{grpc::unmatched_route, http::HttpServer},
31 ListenAddress,
32};
33use saluki_tls::ensure_server_config_fips_compliant;
34use tokio::{pin, select};
35use tonic::{body::Body as GrpcBody, server::NamedService, service::RoutesBuilder};
36use tower::Service;
37use tracing::{debug, info, warn};
38
39pub struct APIBuilder {
75 endpoint_type: EndpointType,
76 listen_address: ListenAddress,
77 tls_config: Option<ServerConfig>,
78 http_router: Router,
79 grpc_router: RoutesBuilder,
80}
81
82impl APIBuilder {
83 pub fn new(endpoint_type: EndpointType, listen_address: ListenAddress) -> Self {
85 Self {
86 endpoint_type,
87 listen_address,
88 tls_config: None,
89 http_router: Router::new(),
90 grpc_router: RoutesBuilder::default(),
91 }
92 }
93
94 pub fn with_handler<H>(mut self, handler: H) -> Self
99 where
100 H: APIHandler,
101 {
102 let handler_router = handler.generate_routes();
103 let handler_state = handler.generate_initial_state();
104 self.http_router = self.http_router.merge(handler_router.with_state(handler_state));
105 self
106 }
107
108 pub fn with_optional_handler<H>(self, handler: Option<H>) -> Self
113 where
114 H: APIHandler,
115 {
116 if let Some(handler) = handler {
117 self.with_handler(handler)
118 } else {
119 self
120 }
121 }
122
123 pub fn with_grpc_service<S>(mut self, svc: S) -> Self
125 where
126 S: Service<Request<GrpcBody>, Response = Response<GrpcBody>, Error = Infallible>
127 + NamedService
128 + Clone
129 + Send
130 + Sync
131 + 'static,
132 S::Future: Send + 'static,
133 S::Error: Into<Box<dyn Error + Send + Sync>> + Send,
134 {
135 self.grpc_router.add_service(svc);
136 self
137 }
138
139 pub fn with_tls_config(mut self, config: ServerConfig) -> Self {
141 self.tls_config = Some(config);
142 self
143 }
144
145 pub fn with_self_signed_tls(self) -> Self {
147 self.try_with_self_signed_tls()
148 .expect("self-signed server TLS configuration should build and pass FIPS validation")
149 }
150
151 pub fn try_with_self_signed_tls(self) -> Result<Self, GenericError> {
158 let CertifiedKey { cert, signing_key } = generate_simple_self_signed(["localhost".to_owned()])?;
159 let cert_chain = vec![cert.der().clone()];
160 let key = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(signing_key.serialize_der()));
161
162 let mut config = ServerConfig::builder()
163 .with_no_client_auth()
164 .with_single_cert(cert_chain, key)?;
165
166 ensure_server_config_fips_compliant(&mut config)?;
167
168 Ok(self.with_tls_config(config))
169 }
170}
171
172impl APIBuilder {
173 pub fn into_supervisor(self) -> Supervisor {
178 let base = self
185 .http_router
186 .reset_fallback()
187 .merge(self.grpc_router.routes().into_axum_router().reset_fallback());
188
189 let (inner, outer) = create_dynamic_router(apply_fallback(base.clone()));
191
192 let mut http_server = HttpServer::from_listen_address(self.listen_address.clone())
193 .with_routes(outer)
194 .with_server_id(format!("{}-api", self.endpoint_type.name()));
195 if let Some(tls_config) = self.tls_config {
196 http_server = http_server.with_tls_config(tls_config);
197 }
198
199 let endpoint_type = self.endpoint_type;
200 let name = match endpoint_type {
201 EndpointType::Unprivileged => "unprivileged-api",
202 EndpointType::Privileged => "privileged-api",
203 };
204
205 let mut supervisor = Supervisor::new(name).expect("API supervisor name is a non-empty constant");
206
207 supervisor.add_worker(http_server.into_supervisor());
208 supervisor.add_worker(RouteUpdaterWorker {
209 inner,
210 base,
211 endpoint_type,
212 listen_address: self.listen_address,
213 });
214
215 supervisor
216 }
217}
218
219struct RouteUpdaterWorker {
221 inner: Arc<ArcSwap<Router>>,
222 base: Router,
223 endpoint_type: EndpointType,
224 listen_address: ListenAddress,
225}
226
227#[async_trait]
228impl Supervisable for RouteUpdaterWorker {
229 fn name(&self) -> &str {
230 "route_updater"
231 }
232
233 async fn initialize(&self, process_shutdown: ShutdownHandle) -> Result<SupervisorFuture, InitializationError> {
234 let dataspace = DataspaceRegistry::try_current().ok_or_else(|| generic_error!("Dataspace not available."))?;
235
236 let inner = Arc::clone(&self.inner);
237 let base = self.base.clone();
238 let endpoint_type = self.endpoint_type;
239 let listen_address = self.listen_address.clone();
240
241 Ok(Box::pin(async move {
242 info!("Serving {} API on {}.", endpoint_type.name(), listen_address);
243
244 let route_assertions = dataspace.subscribe::<DynamicRoute>(IdentifierFilter::All);
246
247 run_event_loop(process_shutdown, inner, base, route_assertions, endpoint_type).await
248 }))
249 }
250}
251
252#[derive(Clone)]
259struct DynamicRouterService {
260 inner_router: Arc<ArcSwap<Router>>,
261}
262
263impl DynamicRouterService {
264 fn from_inner(inner_router: &Arc<ArcSwap<Router>>) -> Self {
265 Self {
266 inner_router: Arc::clone(inner_router),
267 }
268 }
269}
270
271impl Service<http::Request<AxumBody>> for DynamicRouterService {
272 type Response = Response<AxumBody>;
273 type Error = Infallible;
274 type Future = RouteFuture<Infallible>;
275
276 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
277 Poll::Ready(Ok(()))
278 }
279
280 fn call(&mut self, request: http::Request<AxumBody>) -> Self::Future {
281 let mut router = Arc::unwrap_or_clone(self.inner_router.load_full());
282 router.call(request)
283 }
284}
285
286async fn run_event_loop(
288 process_shutdown: ShutdownHandle, inner: Arc<ArcSwap<Router>>, base: Router,
289 mut route_assertions: Subscription<DynamicRoute>, endpoint_type: EndpointType,
290) -> Result<(), GenericError> {
291 let mut handlers = FastIndexMap::default();
294
295 pin!(process_shutdown);
296
297 loop {
298 select! {
299 _ = &mut process_shutdown => break,
300 maybe_update = route_assertions.recv() => match maybe_update {
301 None => {
302 debug!("Dataspace subscription ended unexpectedly.");
303 break
304 },
305 Some(update) => {
306 match update {
307 DataspaceUpdate::Asserted(id, route) => {
308 if route.endpoint_type() != endpoint_type {
309 continue;
310 }
311
312 debug!(?id, "Registering dynamic {} handler.", route.endpoint_protocol().name());
313
314 handlers.insert(id, route.into_router());
315 }
316 DataspaceUpdate::Retracted(id) => {
317 if handlers.swap_remove(&id).is_none() {
318 continue;
319 }
320
321 debug!(?id, "Withdrawing dynamic handler.");
322 }
323 DataspaceUpdate::Message(..) => continue,
325 }
326
327 rebuild_router(&inner, &base, &handlers);
328 }
329 }
330 }
331 }
332
333 Ok(())
334}
335
336fn create_dynamic_router(initial: Router) -> (Arc<ArcSwap<Router>>, Router) {
339 let inner = Arc::new(ArcSwap::from_pointee(initial));
340 let outer = Router::new().fallback_service(DynamicRouterService::from_inner(&inner));
341 (inner, outer)
342}
343
344fn try_merge_router(base: &Router, id: &Identifier, other: &Router) -> Result<Router, String> {
361 let candidate = base.clone();
362 match catch_unwind(AssertUnwindSafe(|| candidate.merge(other.clone()))) {
363 Ok(merged) => Ok(merged),
364 Err(payload) => {
365 let reason = payload
366 .downcast_ref::<String>()
367 .map(|s| s.as_str())
368 .or_else(|| payload.downcast_ref::<&str>().copied())
369 .unwrap_or("unknown");
370 Err(format!("failed to merge dynamic handler {id:?}: {reason}"))
371 }
372 }
373}
374
375fn rebuild_router(inner_router: &Arc<ArcSwap<Router>>, base: &Router, handlers: &FastIndexMap<Identifier, Router>) {
378 let mut merged = base.clone();
379 let mut skipped = 0usize;
380
381 for (id, router) in handlers.iter() {
382 let resetable = router.clone().reset_fallback();
383 match try_merge_router(&merged, id, &resetable) {
384 Ok(new_merged) => merged = new_merged,
385 Err(reason) => {
386 warn!(%reason, "Skipping dynamic handler due to overlapping route.");
387 skipped += 1;
388 }
389 }
390 }
391
392 inner_router.store(Arc::new(apply_fallback(merged)));
393 debug!(handler_count = handlers.len(), skipped, "Rebuilt inner router.");
394}
395
396fn apply_fallback(router: Router) -> Router {
401 router.fallback(unmatched_route)
402}
403
404#[cfg(test)]
405mod tests {
406 use std::{net::SocketAddr, time::Duration};
407
408 use axum::Router;
409 use http_body_util::{BodyExt as _, Empty};
410 use hyper::{body::Bytes, StatusCode};
411 use hyper_util::{client::legacy::Client, rt::TokioExecutor};
412 use saluki_api::{APIHandler, DynamicRoute, EndpointType};
413 use saluki_core::runtime::{
414 state::{DataspaceUpdate, Identifier, IdentifierFilter},
415 test_util::TestSupervisor,
416 };
417 use saluki_io::net::BoundListenAddress;
418 use tokio::time::{sleep, timeout, Instant};
419 use tonic::service::Routes;
420
421 use super::*;
422
423 struct SimpleHandler {
424 path: &'static str,
425 body: &'static str,
426 }
427
428 impl APIHandler for SimpleHandler {
429 type State = ();
430
431 fn generate_initial_state(&self) -> Self::State {}
432
433 fn generate_routes(&self) -> Router<Self::State> {
434 let body = self.body;
435 Router::new().route(self.path, axum::routing::get(move || async move { body }))
436 }
437 }
438
439 #[derive(Clone)]
441 struct OkService;
442
443 impl NamedService for OkService {
444 const NAME: &'static str = "test.Ok";
445 }
446
447 impl Service<Request<GrpcBody>> for OkService {
448 type Response = Response<GrpcBody>;
449 type Error = Infallible;
450 type Future = std::future::Ready<Result<Self::Response, Self::Error>>;
451
452 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
453 Poll::Ready(Ok(()))
454 }
455
456 fn call(&mut self, _request: Request<GrpcBody>) -> Self::Future {
457 let response = Response::builder()
458 .header("grpc-status", "0")
459 .body(GrpcBody::empty())
460 .unwrap();
461 std::future::ready(Ok(response))
462 }
463 }
464
465 struct TestHarness {
466 addr: SocketAddr,
467 supervisor: TestSupervisor,
468 }
469
470 impl TestHarness {
471 fn assert_route(&self, id: impl Into<Identifier>, route: DynamicRoute) {
472 self.supervisor.dataspace().assert(route, id);
473 }
474
475 fn retract_route(&self, id: impl Into<Identifier>) {
476 self.supervisor.dataspace().retract::<DynamicRoute>(id);
477 }
478 }
479
480 async fn setup_test_harness(endpoint_type: EndpointType) -> TestHarness {
481 setup_test_harness_with(endpoint_type, |b| b).await
482 }
483
484 async fn setup_test_harness_with<F>(endpoint_type: EndpointType, configure: F) -> TestHarness
485 where
486 F: FnOnce(APIBuilder) -> APIBuilder,
487 {
488 let api_builder = configure(APIBuilder::new(endpoint_type, ListenAddress::tcp_any(0)));
489 let supervisor = TestSupervisor::start_with("test-dynamic-api", |supervisor| {
490 supervisor.add_worker(api_builder.into_supervisor());
491 })
492 .await;
493
494 let bound_addr_name = match endpoint_type {
496 EndpointType::Unprivileged => "http-server-unprivileged-api",
497 EndpointType::Privileged => "http-server-privileged-api",
498 };
499 let mut addr_sub = supervisor
500 .dataspace()
501 .subscribe::<BoundListenAddress>(IdentifierFilter::exact(Identifier::named(bound_addr_name)));
502 let addr = match timeout(Duration::from_secs(5), addr_sub.recv()).await {
503 Ok(Some(DataspaceUpdate::Asserted(_, BoundListenAddress::Tcp(mut addr)))) => {
504 if addr.ip().is_unspecified() {
506 addr.set_ip(std::net::Ipv4Addr::LOCALHOST.into());
507 }
508 addr
509 }
510 other => panic!("expected a bound TCP address, got {other:?}"),
511 };
512
513 TestHarness { addr, supervisor }
514 }
515
516 async fn http_get(addr: SocketAddr, path: &str) -> (StatusCode, String) {
517 let client: Client<_, Empty<Bytes>> = Client::builder(TokioExecutor::new()).build_http();
518 let uri = format!("http://{}{}", addr, path);
519 let resp = client.get(uri.parse().unwrap()).await.unwrap();
520 let status = resp.status();
521 let body = resp.into_body().collect().await.unwrap().to_bytes();
522 let body_str = String::from_utf8_lossy(&body).into_owned();
523 (status, body_str)
524 }
525
526 async fn grpc_post(addr: SocketAddr, path: &str) -> (StatusCode, http::HeaderMap) {
527 let client: Client<_, Empty<Bytes>> = Client::builder(TokioExecutor::new()).build_http();
528 let uri: hyper::Uri = format!("http://{}{}", addr, path).parse().unwrap();
529 let req = hyper::Request::builder()
530 .uri(uri)
531 .method(hyper::Method::POST)
532 .header(hyper::header::CONTENT_TYPE, "application/grpc")
533 .body(Empty::<Bytes>::new())
534 .unwrap();
535 let resp = client.request(req).await.unwrap();
536 (resp.status(), resp.headers().clone())
537 }
538
539 async fn assert_grpc_status_eventually(addr: SocketAddr, path: &str, expected: &str) {
540 let deadline = Instant::now() + Duration::from_secs(2);
541 loop {
542 let (_, headers) = grpc_post(addr, path).await;
543 let grpc_status = headers.get("grpc-status").and_then(|v| v.to_str().ok());
544 if grpc_status == Some(expected) {
545 return;
546 }
547 if Instant::now() > deadline {
548 panic!(
549 "expected grpc-status {} for {} but got {:?}",
550 expected, path, grpc_status
551 );
552 }
553 sleep(Duration::from_millis(50)).await;
554 }
555 }
556
557 async fn assert_status_eventually(addr: SocketAddr, path: &str, expected: StatusCode) -> String {
558 let deadline = Instant::now() + Duration::from_secs(2);
559 loop {
560 let (status, body) = http_get(addr, path).await;
561 if status == expected {
562 return body;
563 }
564 if Instant::now() > deadline {
565 panic!("expected {} for {} but got {}", expected, path, status);
566 }
567 sleep(Duration::from_millis(50)).await;
568 }
569 }
570
571 #[tokio::test]
574 async fn serves_asserted_http_route() {
575 let harness = setup_test_harness(EndpointType::Unprivileged).await;
576
577 let route = DynamicRoute::http(
578 EndpointType::Unprivileged,
579 SimpleHandler {
580 path: "/health",
581 body: "ok",
582 },
583 );
584 harness.assert_route("health", route);
585
586 let body = assert_status_eventually(harness.addr, "/health", StatusCode::OK).await;
587 assert_eq!(body, "ok");
588 }
589
590 #[tokio::test]
591 async fn returns_404_for_unknown_route() {
592 let harness = setup_test_harness(EndpointType::Unprivileged).await;
593 let (status, _) = http_get(harness.addr, "/nonexistent").await;
594 assert_eq!(status, StatusCode::NOT_FOUND);
595 }
596
597 #[tokio::test]
598 async fn route_retraction_removes_route() {
599 let harness = setup_test_harness(EndpointType::Unprivileged).await;
600
601 let route = DynamicRoute::http(
602 EndpointType::Unprivileged,
603 SimpleHandler {
604 path: "/temp",
605 body: "temporary",
606 },
607 );
608 harness.assert_route("temp", route);
609 assert_status_eventually(harness.addr, "/temp", StatusCode::OK).await;
610
611 harness.retract_route("temp");
612 assert_status_eventually(harness.addr, "/temp", StatusCode::NOT_FOUND).await;
613 }
614
615 #[tokio::test]
616 async fn multiple_routes_independent_lifecycle() {
617 let harness = setup_test_harness(EndpointType::Unprivileged).await;
618
619 let route_a = DynamicRoute::http(
620 EndpointType::Unprivileged,
621 SimpleHandler {
622 path: "/a",
623 body: "alpha",
624 },
625 );
626 let route_b = DynamicRoute::http(
627 EndpointType::Unprivileged,
628 SimpleHandler {
629 path: "/b",
630 body: "bravo",
631 },
632 );
633 harness.assert_route("a", route_a);
634 harness.assert_route("b", route_b);
635
636 assert_status_eventually(harness.addr, "/a", StatusCode::OK).await;
637 assert_status_eventually(harness.addr, "/b", StatusCode::OK).await;
638
639 harness.retract_route("a");
641 assert_status_eventually(harness.addr, "/a", StatusCode::NOT_FOUND).await;
642
643 let body = assert_status_eventually(harness.addr, "/b", StatusCode::OK).await;
644 assert_eq!(body, "bravo");
645 }
646
647 #[tokio::test]
648 async fn ignores_routes_for_different_endpoint_type() {
649 let harness = setup_test_harness(EndpointType::Unprivileged).await;
650
651 let wrong_route = DynamicRoute::http(
653 EndpointType::Privileged,
654 SimpleHandler {
655 path: "/secret",
656 body: "secret",
657 },
658 );
659 harness.assert_route("secret", wrong_route);
660
661 let (status, _) = http_get(harness.addr, "/secret").await;
662 assert_eq!(status, StatusCode::NOT_FOUND);
663
664 let right_route = DynamicRoute::http(
666 EndpointType::Unprivileged,
667 SimpleHandler {
668 path: "/secret",
669 body: "not secret",
670 },
671 );
672 harness.assert_route("secret-unpriv", right_route);
673
674 let body = assert_status_eventually(harness.addr, "/secret", StatusCode::OK).await;
675 assert_eq!(body, "not secret");
676 }
677
678 #[tokio::test]
679 async fn overlapping_routes_do_not_crash_server() {
680 let harness = setup_test_harness(EndpointType::Unprivileged).await;
681
682 let route_1 = DynamicRoute::http(
684 EndpointType::Unprivileged,
685 SimpleHandler {
686 path: "/health",
687 body: "health-1",
688 },
689 );
690 harness.assert_route("health-1", route_1);
691 let body = assert_status_eventually(harness.addr, "/health", StatusCode::OK).await;
692 assert_eq!(body, "health-1");
693
694 let route_2 = DynamicRoute::http(
697 EndpointType::Unprivileged,
698 SimpleHandler {
699 path: "/health",
700 body: "health-2",
701 },
702 );
703 harness.assert_route("health-2", route_2);
704
705 sleep(Duration::from_millis(200)).await;
707
708 let (status, body) = http_get(harness.addr, "/health").await;
710 assert_eq!(status, StatusCode::OK);
711 assert_eq!(body, "health-1");
712
713 let route_info = DynamicRoute::http(
715 EndpointType::Unprivileged,
716 SimpleHandler {
717 path: "/info",
718 body: "info",
719 },
720 );
721 harness.assert_route("info", route_info);
722 let body = assert_status_eventually(harness.addr, "/info", StatusCode::OK).await;
723 assert_eq!(body, "info");
724
725 harness.retract_route("health-1");
728 let body = assert_status_eventually(harness.addr, "/health", StatusCode::OK).await;
729 assert_eq!(body, "health-2");
730 }
731
732 #[tokio::test]
733 async fn overlapping_route_retraction_then_reassertion() {
734 let harness = setup_test_harness(EndpointType::Unprivileged).await;
735
736 let route_a = DynamicRoute::http(
738 EndpointType::Unprivileged,
739 SimpleHandler {
740 path: "/overlap",
741 body: "a",
742 },
743 );
744 let route_b = DynamicRoute::http(
745 EndpointType::Unprivileged,
746 SimpleHandler {
747 path: "/overlap",
748 body: "b",
749 },
750 );
751 harness.assert_route("ov-a", route_a);
752 harness.assert_route("ov-b", route_b);
753
754 let body = assert_status_eventually(harness.addr, "/overlap", StatusCode::OK).await;
756 assert_eq!(body, "a");
757
758 harness.retract_route("ov-a");
760 harness.retract_route("ov-b");
761 assert_status_eventually(harness.addr, "/overlap", StatusCode::NOT_FOUND).await;
762
763 let route_c = DynamicRoute::http(
765 EndpointType::Unprivileged,
766 SimpleHandler {
767 path: "/overlap",
768 body: "c",
769 },
770 );
771 harness.assert_route("ov-c", route_c);
772 let body = assert_status_eventually(harness.addr, "/overlap", StatusCode::OK).await;
773 assert_eq!(body, "c");
774 }
775
776 #[tokio::test]
777 async fn static_handler_served_without_dynamic_routes() {
778 let harness = setup_test_harness_with(EndpointType::Unprivileged, |b| {
779 b.with_handler(SimpleHandler {
780 path: "/static",
781 body: "static",
782 })
783 })
784 .await;
785
786 let body = assert_status_eventually(harness.addr, "/static", StatusCode::OK).await;
787 assert_eq!(body, "static");
788 }
789
790 #[tokio::test]
791 async fn static_and_dynamic_routes_coexist() {
792 let harness = setup_test_harness_with(EndpointType::Unprivileged, |b| {
793 b.with_handler(SimpleHandler {
794 path: "/static",
795 body: "static",
796 })
797 })
798 .await;
799
800 let body = assert_status_eventually(harness.addr, "/static", StatusCode::OK).await;
802 assert_eq!(body, "static");
803
804 let dynamic_route = DynamicRoute::http(
806 EndpointType::Unprivileged,
807 SimpleHandler {
808 path: "/dynamic",
809 body: "dynamic",
810 },
811 );
812 harness.assert_route("dyn", dynamic_route);
813
814 let body = assert_status_eventually(harness.addr, "/dynamic", StatusCode::OK).await;
815 assert_eq!(body, "dynamic");
816
817 let (status, body) = http_get(harness.addr, "/static").await;
818 assert_eq!(status, StatusCode::OK);
819 assert_eq!(body, "static");
820
821 harness.retract_route("dyn");
823 assert_status_eventually(harness.addr, "/dynamic", StatusCode::NOT_FOUND).await;
824
825 let (status, body) = http_get(harness.addr, "/static").await;
826 assert_eq!(status, StatusCode::OK);
827 assert_eq!(body, "static");
828 }
829
830 #[tokio::test]
831 async fn unknown_grpc_method_returns_unimplemented() {
832 let harness = setup_test_harness(EndpointType::Unprivileged).await;
833 let (status, headers) = grpc_post(harness.addr, "/some.Service/Method").await;
834
835 assert_eq!(status, StatusCode::OK);
837 let grpc_status = headers.get("grpc-status").and_then(|v| v.to_str().ok());
838 assert_eq!(grpc_status, Some("12"));
839 }
840
841 #[tokio::test]
842 async fn serves_asserted_grpc_route() {
843 let harness = setup_test_harness(EndpointType::Unprivileged).await;
844
845 harness.assert_route(
847 "ok",
848 DynamicRoute::grpc(EndpointType::Unprivileged, Routes::new(OkService)),
849 );
850 assert_grpc_status_eventually(harness.addr, "/test.Ok/Call", "0").await;
851
852 harness.retract_route("ok");
853 assert_grpc_status_eventually(harness.addr, "/test.Ok/Call", "12").await;
854 }
855
856 #[tokio::test]
857 async fn static_route_wins_overlap_with_dynamic() {
858 let harness = setup_test_harness_with(EndpointType::Unprivileged, |b| {
859 b.with_handler(SimpleHandler {
860 path: "/overlap",
861 body: "static",
862 })
863 })
864 .await;
865
866 let body = assert_status_eventually(harness.addr, "/overlap", StatusCode::OK).await;
868 assert_eq!(body, "static");
869
870 let dynamic_route = DynamicRoute::http(
872 EndpointType::Unprivileged,
873 SimpleHandler {
874 path: "/overlap",
875 body: "dynamic",
876 },
877 );
878 harness.assert_route("dyn-overlap", dynamic_route);
879
880 sleep(Duration::from_millis(200)).await;
881
882 let (status, body) = http_get(harness.addr, "/overlap").await;
883 assert_eq!(status, StatusCode::OK);
884 assert_eq!(body, "static");
885 }
886}