1use crate::{
8 Html, StatusCode,
9 api::{Api, ApiError, ApiInner, ApiVersion},
10 dispatch::{self, DispatchError, Trie},
11 error::ErrorExt,
12 healthcheck::{HealthCheck, HealthStatus},
13 http,
14 method::Method,
15 middleware::{AddErrorBody, MetricsMiddleware, request_params},
16 request::RequestParams,
17 route::{Handler, Route, RouteError, health_check_response, respond_with},
18 socket::SocketError,
19};
20use async_std::sync::Arc;
21use derive_more::From;
22use futures::future::{BoxFuture, FutureExt};
23use include_dir::{Dir, include_dir};
24use lazy_static::lazy_static;
25use maud::{PreEscaped, html};
26use rand::Rng;
27use semver::Version;
28use serde::{Deserialize, Serialize};
29use serde_with::{DisplayFromStr, serde_as};
30use snafu::{ResultExt, Snafu};
31use std::{
32 collections::btree_map::BTreeMap,
33 convert::Infallible,
34 env, fs, io,
35 ops::{Deref, DerefMut},
36 path::PathBuf,
37};
38use tide::{
39 http::{headers::HeaderValue, mime::HTML},
40 security::{CorsMiddleware, Origin},
41};
42use tide_websockets::WebSocket;
43use vbs::version::StaticVersionType;
44
45pub use tide::listener::{Listener, ToListener};
46
47#[derive(Debug)]
58pub struct App<State, Error> {
59 pub(crate) modules: Trie<ApiInner<State, Error>>,
60 pub(crate) state: Arc<State>,
61 app_version: Option<Version>,
62}
63
64#[derive(Clone, Debug, From, Snafu, PartialEq, Eq)]
66pub enum AppError {
67 Api { source: ApiError },
68 Dispatch { source: DispatchError },
69}
70
71impl<State: Send + Sync + 'static, Error: 'static> App<State, Error> {
72 pub fn with_state(state: State) -> Self {
74 Self {
75 modules: Default::default(),
76 state: Arc::new(state),
77 app_version: None,
78 }
79 }
80
81 pub fn module<'a, ModuleError, ModuleVersion>(
89 &'a mut self,
90 base_url: &'a str,
91 api: impl Into<toml::Value>,
92 ) -> Result<Module<'a, State, Error, ModuleError, ModuleVersion>, AppError>
93 where
94 Error: crate::Error + From<ModuleError>,
95 ModuleError: Send + Sync + 'static,
96 ModuleVersion: StaticVersionType + 'static,
97 {
98 Ok(Module {
99 app: self,
100 base_url,
101 api: Some(Api::new(api).context(ApiSnafu)?),
102 })
103 }
104
105 pub fn register_module<ModuleError, ModuleVersion>(
138 &mut self,
139 base_url: &str,
140 api: Api<State, ModuleError, ModuleVersion>,
141 ) -> Result<&mut Self, AppError>
142 where
143 Error: crate::Error + From<ModuleError>,
144 ModuleError: Send + Sync + 'static,
145 ModuleVersion: StaticVersionType + 'static,
146 {
147 let mut api = api.map_err(Error::from).into_inner();
148 api.set_name(base_url.to_string());
149
150 let major_version = match api.version().api_version {
151 Some(version) => version.major,
152 None => {
153 0
155 }
156 };
157
158 self.modules
159 .insert(dispatch::split(base_url), major_version, api)?;
160 Ok(self)
161 }
162
163 pub fn with_version(&mut self, version: Version) -> &mut Self {
190 self.app_version = Some(version);
191 self
192 }
193
194 pub fn version(&self) -> AppVersion {
196 AppVersion {
197 app_version: self.app_version.clone(),
198 disco_version: env!("CARGO_PKG_VERSION").parse().unwrap(),
199 modules: self
200 .modules
201 .iter()
202 .map(|module| {
203 (
204 module.path(),
205 module
206 .versions
207 .values()
208 .rev()
209 .map(|api| api.version())
210 .collect(),
211 )
212 })
213 .collect(),
214 }
215 }
216
217 pub async fn health(&self, req: RequestParams, state: &State) -> AppHealth {
224 let mut modules_health = BTreeMap::<String, BTreeMap<_, _>>::new();
225 let mut status = HealthStatus::Available;
226 for module in &self.modules {
227 let versions_health = modules_health.entry(module.path()).or_default();
228 for (version, api) in &module.versions {
229 let health = StatusCode::from(api.health(req.clone(), state).await.status());
230 if health != StatusCode::OK {
231 status = HealthStatus::Unhealthy;
232 }
233 versions_health.insert(*version, health);
234 }
235 }
236 AppHealth {
237 status,
238 modules: modules_health,
239 }
240 }
241
242 pub async fn module_health(
254 &self,
255 req: RequestParams,
256 state: &State,
257 module: &str,
258 major_version: Option<u64>,
259 ) -> Option<tide::Response> {
260 let module = self.modules.get(dispatch::split(module))?;
261 let api = match major_version {
262 Some(v) => module.versions.get(&v)?,
263 None => module.versions.last_key_value()?.1,
264 };
265 Some(api.health(req, state).await)
266 }
267}
268
269static DEFAULT_PUBLIC_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/public/media");
270lazy_static! {
271 static ref DEFAULT_PUBLIC_PATH: PathBuf = {
272 let mut rng = rand::thread_rng();
274 let index: u64 = rng.r#gen();
275
276 let path = PathBuf::from(format!("/tmp/tide-disco/{}/public/media", index));
280 let _ = fs::rename(&path, path.with_extension("old"));
282 DEFAULT_PUBLIC_DIR.extract(&path).unwrap();
283 path
284 };
285}
286
287impl<State, Error> App<State, Error>
288where
289 State: Send + Sync + 'static,
290 Error: 'static + crate::Error,
291{
292 pub async fn serve<L, VER>(self, listener: L, bind_version: VER) -> io::Result<()>
299 where
300 L: ToListener<Arc<Self>>,
301 VER: StaticVersionType + 'static,
302 {
303 let state = Arc::new(self);
304 let mut server = tide::Server::with_state(state.clone());
305 server.with(Self::version_middleware);
306 server.with(AddErrorBody::<Error>::with_version::<VER>());
307 server.with(
308 CorsMiddleware::new()
309 .allow_methods("GET, POST".parse::<HeaderValue>().unwrap())
310 .allow_headers("*".parse::<HeaderValue>().unwrap())
311 .allow_origin(Origin::from("*"))
312 .allow_credentials(true),
313 );
314
315 for module in &state.modules {
316 Self::register_api(&mut server, module.prefix.clone(), &module.versions)?;
317 }
318
319 if !state.modules.is_singleton() {
324 server
326 .at("healthcheck")
327 .get(move |req: tide::Request<Arc<Self>>| async move {
328 let state = req.state().clone();
329 let app_state = &*state.state;
330 let req = request_params(req, &[]).await?;
331 let accept = req.accept()?;
332 let res = state.health(req, app_state).await;
333 Ok(health_check_response::<_, VER>(&accept, res))
334 });
335 server
336 .at("version")
337 .get(move |req: tide::Request<Arc<Self>>| async move {
338 let accept = RequestParams::accept_from_headers(&req)?;
339 respond_with(&accept, req.state().version(), bind_version)
340 .map_err(|err| Error::from_route_error::<Infallible>(err).into_tide_error())
341 });
342
343 server
345 .at("/")
346 .all(move |req: tide::Request<Arc<Self>>| async move {
347 Ok(tide::Response::from(Self::top_level_docs(req)))
348 });
349 }
350
351 server.listen(listener).await
352 }
353
354 fn list_apis(&self) -> Html {
355 html! {
356 ul {
357 @for module in &self.modules {
358 li {
359 a href=(format!("/{}", module.path())) {(module.path())}
361 @for version in module.versions.keys().rev() {
364 sup {
365 a href=(format!("/v{version}/{}", module.path())) {
366 (format!("[v{version}]"))
367 }
368 }
369 }
370 " "
371 (PreEscaped(module.versions.last_key_value().unwrap().1.short_description()))
373 }
374 }
375 }
376 }
377 }
378
379 fn register_api(
380 server: &mut tide::Server<Arc<Self>>,
381 prefix: Vec<String>,
382 versions: &BTreeMap<u64, ApiInner<State, Error>>,
383 ) -> io::Result<()> {
384 for (version, api) in versions {
385 Self::register_api_version(server, &prefix, *version, api)?;
386 }
387 Ok(())
388 }
389
390 fn register_api_version(
391 server: &mut tide::Server<Arc<Self>>,
392 prefix: &[String],
393 version: u64,
394 api: &ApiInner<State, Error>,
395 ) -> io::Result<()> {
396 #[allow(clippy::unnecessary_lazy_evaluations)]
401 server
402 .at("/public")
403 .at(&format!("v{version}"))
404 .at(&prefix.join("/"))
405 .serve_dir(api.public().unwrap_or_else(|| &DEFAULT_PUBLIC_PATH))?;
406
407 let mut version_endpoint = server.at(&format!("/v{version}"));
409 let mut api_endpoint = if prefix.is_empty() {
410 version_endpoint
411 } else {
412 version_endpoint.at(&prefix.join("/"))
413 };
414 api_endpoint.with(AddErrorBody::new(api.error_handler()));
415 for (path, routes) in api.routes_by_path() {
416 let mut endpoint = api_endpoint.at(path);
417 let routes = routes.collect::<Vec<_>>();
418
419 if let Some(socket_route) = routes.iter().find(|route| route.method() == Method::Socket)
423 {
424 Self::register_socket(prefix.to_vec(), version, &mut endpoint, socket_route);
428 }
429 if let Some(metrics_route) = routes
430 .iter()
431 .find(|route| route.method() == Method::Metrics)
432 {
433 Self::register_metrics(prefix.to_vec(), version, &mut endpoint, metrics_route);
438 }
439
440 for route in routes {
442 if let Method::Http(method) = route.method() {
443 Self::register_route(prefix.to_vec(), version, &mut endpoint, route, method);
444 }
445 }
446 }
447
448 for path in ["", "/"] {
451 let prefix = prefix.to_vec();
452 api_endpoint
453 .at(path)
454 .all(move |req: tide::Request<Arc<Self>>| {
455 let prefix = prefix.clone();
456 async move {
457 let api = &req.state().clone().modules[&prefix].versions[&version];
458 Ok(api.documentation())
459 }
460 });
461 }
462 {
463 let prefix = prefix.to_vec();
464 api_endpoint
465 .at("*path")
466 .all(move |req: tide::Request<Arc<Self>>| {
467 let prefix = prefix.clone();
468 async move {
469 let api = &req.state().clone().modules[&prefix].versions[&version];
471 let docs = html! {
472 "No route matches /" (req.param("path")?)
473 br{}
474 (api.documentation())
475 };
476 Ok(tide::Response::builder(StatusCode::NOT_FOUND)
477 .body(docs.into_string())
478 .build())
479 }
480 });
481 }
482 {
483 let prefix = prefix.to_vec();
484 api_endpoint
485 .at("healthcheck")
486 .get(move |req: tide::Request<Arc<Self>>| {
487 let prefix = prefix.clone();
488 async move {
489 let api = &req.state().clone().modules[&prefix].versions[&version];
490 let state = req.state().clone();
491 Ok(api
492 .health(request_params(req, &[]).await?, &state.state)
493 .await)
494 }
495 });
496 }
497 {
498 let prefix = prefix.to_vec();
499 api_endpoint
500 .at("version")
501 .get(move |req: tide::Request<Arc<Self>>| {
502 let prefix = prefix.clone();
503 async move {
504 let api = &req.state().modules[&prefix].versions[&version];
505 let accept = RequestParams::accept_from_headers(&req)?;
506 api.version_handler()(&accept, api.version())
507 .map_err(|err| Error::from_route_error(err).into_tide_error())
508 }
509 });
510 }
511
512 Ok(())
513 }
514
515 fn register_route(
516 api: Vec<String>,
517 version: u64,
518 endpoint: &mut tide::Route<Arc<Self>>,
519 route: &Route<State, Error>,
520 method: http::Method,
521 ) {
522 let name = route.name();
523 endpoint.method(method, move |req: tide::Request<Arc<Self>>| {
524 let name = name.clone();
525 let api = api.clone();
526 async move {
527 let route = &req.state().clone().modules[&api].versions[&version][&name];
528 let state = &*req.state().clone().state;
529 let req = request_params(req, route.params()).await?;
530 route
531 .handle(req, state)
532 .await
533 .map_err(|err| match err {
534 RouteError::AppSpecific(err) => err,
535 _ => Error::from_route_error(err),
536 })
537 .map_err(|err| err.into_tide_error())
538 }
539 });
540 }
541
542 fn register_metrics(
543 api: Vec<String>,
544 version: u64,
545 endpoint: &mut tide::Route<Arc<Self>>,
546 route: &Route<State, Error>,
547 ) {
548 let name = route.name();
549 if route.has_handler() {
550 endpoint.with(MetricsMiddleware::new(name.clone(), api.clone(), version));
554 }
555
556 Self::register_fallback(api, version, endpoint, route);
566 }
567
568 fn register_socket(
569 api: Vec<String>,
570 version: u64,
571 endpoint: &mut tide::Route<Arc<Self>>,
572 route: &Route<State, Error>,
573 ) {
574 let name = route.name();
575 if route.has_handler() {
576 let name = name.clone();
579 let api = api.clone();
580 endpoint.with(WebSocket::new(
581 move |req: tide::Request<Arc<Self>>, conn| {
582 let name = name.clone();
583 let api = api.clone();
584 async move {
585 let route = &req.state().clone().modules[&api].versions[&version][&name];
586 let state = &*req.state().clone().state;
587 let req = request_params(req, route.params()).await?;
588 route
589 .handle_socket(req, conn, state)
590 .await
591 .map_err(|err| match err {
592 SocketError::AppSpecific(err) => err,
593 _ => Error::from_socket_error(err),
594 })
595 .map_err(|err| err.into_tide_error())
596 }
597 },
598 ));
599 }
600
601 Self::register_fallback(api, version, endpoint, route);
614 }
615
616 fn register_fallback(
617 api: Vec<String>,
618 version: u64,
619 endpoint: &mut tide::Route<Arc<Self>>,
620 route: &Route<State, Error>,
621 ) {
622 let name = route.name();
623 endpoint.all(move |req: tide::Request<Arc<Self>>| {
624 let name = name.clone();
625 let api = api.clone();
626 async move {
627 let route = &req.state().clone().modules[&api].versions[&version][&name];
628 route
629 .default_handler()
630 .map_err(|err| match err {
631 RouteError::AppSpecific(err) => err,
632 _ => Error::from_route_error(err),
633 })
634 .map_err(|err| err.into_tide_error())
635 }
636 });
637 }
638
639 fn version_middleware(
642 req: tide::Request<Arc<Self>>,
643 next: tide::Next<Arc<Self>>,
644 ) -> BoxFuture<tide::Result> {
645 async move {
646 let Some(path) = req.url().path_segments() else {
647 return Ok(next.run(req).await);
650 };
651 let path = path.collect::<Vec<_>>();
652 let Some(seg1) = path.first() else {
653 return Ok(next.run(req).await);
655 };
656 if seg1.is_empty() {
657 return Ok(next.run(req).await);
659 }
660
661 if let Some(version) = seg1.strip_prefix('v').and_then(|n| n.parse().ok()) {
665 let Some(module) = req.state().modules.search(&path[1..]) else {
669 let message = html! {
670 ("No API matches ")
671 span style = "font-family: monospace" {
672 (format!("/{}", path[1..].join("/")))
673 }
674 };
675 return Ok(Self::top_level_error(req, StatusCode::NOT_FOUND, message));
676 };
677 if !module.versions.contains_key(&version) {
678 return Ok(html! {
680 "Unsupported version v" (version) ". Supported versions are:"
681 ul {
682 @for v in module.versions.keys().rev() {
683 li {
684 a href=(format!("/v{v}/{}", module.path())) { "v" (v) }
685 }
686 }
687 }
688 }
689 .into());
690 }
691
692 Ok(next.run(req).await)
695 } else {
696 if !req.state().modules.is_singleton() && ["version", "healthcheck"].contains(seg1)
700 {
701 return Ok(next.run(req).await);
702 }
703 let Some(module) = req.state().modules.search(&path) else {
704 let message = html! {
705 ("No API matches ")
706 span style = "font-family: monospace" {
707 (format!("/{}", path.join("/")))
708 }
709 };
710 return Ok(Self::top_level_error(req, StatusCode::NOT_FOUND, message));
711 };
712
713 let latest_version = *module.versions.last_key_value().unwrap().0;
714 let path = path.join("/");
715 Ok(tide::Redirect::temporary(format!("/v{latest_version}/{path}")).into())
716 }
717 }
718 .boxed()
719 }
720
721 fn top_level_docs(req: tide::Request<Arc<Self>>) -> PreEscaped<String> {
723 html! {
724 "This is a Tide Disco app composed of the following modules:"
725 (req.state().list_apis())
726 }
727 }
728
729 fn top_level_error(
731 req: tide::Request<Arc<Self>>,
732 status: StatusCode,
733 message: PreEscaped<String>,
734 ) -> tide::Response {
735 let docs = html! {
736 p style = "color:red" {
737 (message)
738 }
739 (Self::top_level_docs(req))
740 };
741 tide::Response::builder(status)
742 .body(docs.into_string())
743 .content_type(HTML)
744 .build()
745 }
746}
747
748#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
750pub struct AppHealth {
751 pub status: HealthStatus,
756 pub modules: BTreeMap<String, BTreeMap<u64, StatusCode>>,
758}
759
760impl HealthCheck for AppHealth {
761 fn status(&self) -> StatusCode {
762 self.status.status()
763 }
764}
765
766#[serde_as]
768#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
769pub struct AppVersion {
770 pub modules: BTreeMap<String, Vec<ApiVersion>>,
774
775 #[serde_as(as = "Option<DisplayFromStr>")]
777 pub app_version: Option<Version>,
778
779 #[serde_as(as = "DisplayFromStr")]
781 pub disco_version: Version,
782}
783
784#[derive(Debug)]
796pub struct Module<'a, State, Error, ModuleError, ModuleVersion>
797where
798 State: Send + Sync + 'static,
799 Error: crate::Error + From<ModuleError> + 'static,
800 ModuleError: Send + Sync + 'static,
801 ModuleVersion: StaticVersionType + 'static,
802{
803 app: &'a mut App<State, Error>,
804 base_url: &'a str,
805 api: Option<Api<State, ModuleError, ModuleVersion>>,
807}
808
809impl<State, Error, ModuleError, ModuleVersion> Deref
810 for Module<'_, State, Error, ModuleError, ModuleVersion>
811where
812 State: Send + Sync + 'static,
813 Error: crate::Error + From<ModuleError> + 'static,
814 ModuleError: Send + Sync + 'static,
815 ModuleVersion: StaticVersionType + 'static,
816{
817 type Target = Api<State, ModuleError, ModuleVersion>;
818
819 fn deref(&self) -> &Self::Target {
820 self.api.as_ref().unwrap()
821 }
822}
823
824impl<State, Error, ModuleError, ModuleVersion> DerefMut
825 for Module<'_, State, Error, ModuleError, ModuleVersion>
826where
827 State: Send + Sync + 'static,
828 Error: crate::Error + From<ModuleError> + 'static,
829 ModuleError: Send + Sync + 'static,
830 ModuleVersion: StaticVersionType + 'static,
831{
832 fn deref_mut(&mut self) -> &mut Self::Target {
833 self.api.as_mut().unwrap()
834 }
835}
836
837impl<State, Error, ModuleError, ModuleVersion> Drop
838 for Module<'_, State, Error, ModuleError, ModuleVersion>
839where
840 State: Send + Sync + 'static,
841 Error: crate::Error + From<ModuleError> + 'static,
842 ModuleError: Send + Sync + 'static,
843 ModuleVersion: StaticVersionType + 'static,
844{
845 fn drop(&mut self) {
846 self.register_impl().unwrap();
847 }
848}
849
850impl<State, Error, ModuleError, ModuleVersion> Module<'_, State, Error, ModuleError, ModuleVersion>
851where
852 State: Send + Sync + 'static,
853 Error: crate::Error + From<ModuleError> + 'static,
854 ModuleError: Send + Sync + 'static,
855 ModuleVersion: StaticVersionType + 'static,
856{
857 pub fn register(mut self) -> Result<(), AppError> {
859 self.register_impl()
860 }
861
862 fn register_impl(&mut self) -> Result<(), AppError> {
865 if let Some(api) = self.api.take() {
866 self.app.register_module(self.base_url, api)?;
867 Ok(())
868 } else {
869 Ok(())
871 }
872 }
873}
874
875#[cfg(test)]
876mod test {
877 use super::*;
878 use crate::{
879 Url,
880 error::{Error, ServerError},
881 metrics::Metrics,
882 socket::Connection,
883 testing::{Client, setup_test, test_ws_client},
884 };
885 use async_std::{sync::RwLock, task::spawn};
886 use async_tungstenite::tungstenite::Message;
887 use futures::{FutureExt, SinkExt, StreamExt};
888 use portpicker::pick_unused_port;
889 use regex::Regex;
890 use serde::de::DeserializeOwned;
891 use std::{borrow::Cow, fmt::Debug};
892 use toml::toml;
893 use vbs::{BinarySerializer, Serializer, version::StaticVersion};
894
895 type StaticVer01 = StaticVersion<0, 1>;
896 type SerializerV01 = Serializer<StaticVer01>;
897
898 type StaticVer02 = StaticVersion<0, 2>;
899 type SerializerV02 = Serializer<StaticVer02>;
900
901 type StaticVer03 = StaticVersion<0, 3>;
902 type SerializerV03 = Serializer<StaticVer03>;
903
904 #[derive(Clone, Copy, Debug)]
905 struct FakeMetrics;
906
907 impl Metrics for FakeMetrics {
908 type Error = ServerError;
909
910 fn export(&self) -> Result<String, Self::Error> {
911 Ok("METRICS".into())
912 }
913 }
914
915 #[async_std::test]
917 async fn test_method_dispatch() {
918 setup_test();
919
920 use crate::http::Method::*;
921
922 let mut app = App::<_, ServerError>::with_state(RwLock::new(FakeMetrics));
923 let api_toml = toml! {
924 [meta]
925 FORMAT_VERSION = "0.1.0"
926
927 [route.get_test]
928 PATH = ["/test"]
929 METHOD = "GET"
930
931 [route.post_test]
932 PATH = ["/test"]
933 METHOD = "POST"
934
935 [route.put_test]
936 PATH = ["/test"]
937 METHOD = "PUT"
938
939 [route.delete_test]
940 PATH = ["/test"]
941 METHOD = "DELETE"
942
943 [route.socket_test]
944 PATH = ["/test"]
945 METHOD = "SOCKET"
946
947 [route.metrics_test]
948 PATH = ["/test"]
949 METHOD = "METRICS"
950 };
951 {
952 let mut api = app
953 .module::<ServerError, StaticVer01>("mod", api_toml)
954 .unwrap();
955 api.get("get_test", |_req, _state| {
956 async move { Ok(Get.to_string()) }.boxed()
957 })
958 .unwrap()
959 .post("post_test", |_req, _state| {
960 async move { Ok(Post.to_string()) }.boxed()
961 })
962 .unwrap()
963 .put("put_test", |_req, _state| {
964 async move { Ok(Put.to_string()) }.boxed()
965 })
966 .unwrap()
967 .delete("delete_test", |_req, _state| {
968 async move { Ok(Delete.to_string()) }.boxed()
969 })
970 .unwrap()
971 .socket(
972 "socket_test",
973 |_req, mut conn: Connection<str, (), _, StaticVer01>, _state| {
974 async move {
975 conn.send("SOCKET").await.unwrap();
976 Ok(())
977 }
978 .boxed()
979 },
980 )
981 .unwrap()
982 .metrics("metrics_test", |_req, state| {
983 async move { Ok(Cow::Borrowed(state)) }.boxed()
984 })
985 .unwrap();
986 }
987 let port = pick_unused_port().unwrap();
988 let url: Url = format!("http://localhost:{}", port).parse().unwrap();
989 spawn(app.serve(format!("0.0.0.0:{}", port), StaticVer01::instance()));
990 let client = Client::new(url.clone()).await;
991
992 for method in [Get, Post, Put, Delete] {
994 let res = client
995 .request(method, "mod/test")
996 .header("Accept", "application/json")
997 .send()
998 .await
999 .unwrap();
1000 assert_eq!(res.status(), StatusCode::OK);
1001 assert_eq!(res.json::<String>().await.unwrap(), method.to_string());
1002 }
1003
1004 let res = client
1006 .get("mod/test")
1007 .header("Accept", "text/plain")
1008 .send()
1009 .await
1010 .unwrap();
1011 assert_eq!(res.status(), StatusCode::OK);
1012 assert_eq!(res.text().await.unwrap(), "METRICS");
1013
1014 let res = client.get("mod/test").send().await.unwrap();
1016 assert_eq!(res.status(), StatusCode::OK);
1017 assert_eq!(res.text().await.unwrap(), "METRICS");
1018
1019 let mut conn = test_ws_client(url.join("mod/test").unwrap()).await;
1021 let msg = conn.next().await.unwrap().unwrap();
1022 let body: String = match msg {
1023 Message::Text(m) => serde_json::from_str(&m).unwrap(),
1024 Message::Binary(m) => SerializerV01::deserialize(&m).unwrap(),
1025 m => panic!("expected Text or Binary message, but got {}", m),
1026 };
1027 assert_eq!(body, "SOCKET");
1028 }
1029
1030 #[async_std::test]
1032 async fn test_param_dispatch() {
1033 setup_test();
1034
1035 let mut app = App::<_, ServerError>::with_state(RwLock::new(()));
1036 let api_toml = toml! {
1037 [meta]
1038 FORMAT_VERSION = "0.1.0"
1039
1040 [route.test]
1041 PATH = ["/test/a/:a", "/test/b/:b"]
1042 ":a" = "Integer"
1043 ":b" = "Boolean"
1044 };
1045 {
1046 let mut api = app
1047 .module::<ServerError, StaticVer01>("mod", api_toml)
1048 .unwrap();
1049 api.get("test", |req, _state| {
1050 async move {
1051 if let Some(a) = req.opt_integer_param::<_, i32>("a")? {
1052 Ok(("a", a.to_string()))
1053 } else {
1054 Ok(("b", req.boolean_param("b")?.to_string()))
1055 }
1056 }
1057 .boxed()
1058 })
1059 .unwrap();
1060 }
1061 let port = pick_unused_port().unwrap();
1062 let url: Url = format!("http://localhost:{}", port).parse().unwrap();
1063 spawn(app.serve(format!("0.0.0.0:{}", port), StaticVer01::instance()));
1064 let client = Client::new(url.clone()).await;
1065
1066 let res = client.get("mod/test/a/42").send().await.unwrap();
1067 assert_eq!(res.status(), StatusCode::OK);
1068 assert_eq!(
1069 res.json::<(String, String)>().await.unwrap(),
1070 ("a".to_string(), "42".to_string())
1071 );
1072
1073 let res = client.get("mod/test/b/true").send().await.unwrap();
1074 assert_eq!(res.status(), StatusCode::OK);
1075 assert_eq!(
1076 res.json::<(String, String)>().await.unwrap(),
1077 ("b".to_string(), "true".to_string())
1078 );
1079 }
1080
1081 #[async_std::test]
1082 async fn test_versions() {
1083 setup_test();
1084
1085 let mut app = App::<_, ServerError>::with_state(RwLock::new(()));
1086
1087 let v1_toml = toml! {
1091 [meta]
1092 FORMAT_VERSION = "0.1.0"
1093
1094 [route.deleted]
1095 PATH = ["/deleted"]
1096
1097 [route.unchanged]
1098 PATH = ["/unchanged"]
1099 };
1100 let v3_toml = toml! {
1101 [meta]
1102 FORMAT_VERSION = "0.1.0"
1103
1104 [route.added]
1105 PATH = ["/added"]
1106
1107 [route.unchanged]
1108 PATH = ["/unchanged"]
1109 };
1110
1111 {
1112 let mut v1 = app
1113 .module::<ServerError, StaticVer01>("mod", v1_toml.clone())
1114 .unwrap();
1115 v1.with_version("1.0.0".parse().unwrap())
1116 .get("deleted", |_req, _state| {
1117 async move { Ok("deleted v1") }.boxed()
1118 })
1119 .unwrap()
1120 .get("unchanged", |_req, _state| {
1121 async move { Ok("unchanged v1") }.boxed()
1122 })
1123 .unwrap()
1124 .with_health_check(|_state| {
1126 async move { HealthStatus::TemporarilyUnavailable }.boxed()
1127 });
1128 }
1129 {
1130 let mut api = app
1132 .module::<ServerError, StaticVer01>("mod", v1_toml)
1133 .unwrap();
1134 api.with_version("1.1.1".parse().unwrap());
1135 assert_eq!(
1136 api.register().unwrap_err(),
1137 DispatchError::ModuleAlreadyExists {
1138 prefix: "mod".into(),
1139 version: 1,
1140 }
1141 .into()
1142 );
1143 }
1144 {
1145 let mut v3 = app
1146 .module::<ServerError, StaticVer01>("mod", v3_toml.clone())
1147 .unwrap();
1148 v3.with_version("3.0.0".parse().unwrap())
1149 .get("added", |_req, _state| {
1150 async move { Ok("added v3") }.boxed()
1151 })
1152 .unwrap()
1153 .get("unchanged", |_req, _state| {
1154 async move { Ok("unchanged v3") }.boxed()
1155 })
1156 .unwrap();
1157 }
1158
1159 let port = pick_unused_port().unwrap();
1160 let url: Url = format!("http://localhost:{}", port).parse().unwrap();
1161 spawn(app.serve(format!("0.0.0.0:{}", port), StaticVer01::instance()));
1162 let client = Client::new(url.clone()).await;
1163
1164 assert_eq!(
1166 "deleted v1",
1167 client
1168 .get("v1/mod/deleted")
1169 .send()
1170 .await
1171 .unwrap()
1172 .json::<String>()
1173 .await
1174 .unwrap()
1175 );
1176 assert_eq!(
1177 "unchanged v1",
1178 client
1179 .get("v1/mod/unchanged")
1180 .send()
1181 .await
1182 .unwrap()
1183 .json::<String>()
1184 .await
1185 .unwrap()
1186 );
1187 for prefix in ["", "/v3"] {
1189 let span = tracing::info_span!("version", prefix);
1190 let _enter = span.enter();
1191
1192 assert_eq!(
1193 "added v3",
1194 client
1195 .get(&format!("{prefix}/mod/added"))
1196 .send()
1197 .await
1198 .unwrap()
1199 .json::<String>()
1200 .await
1201 .unwrap()
1202 );
1203 assert_eq!(
1204 "unchanged v3",
1205 client
1206 .get(&format!("{prefix}/mod/unchanged"))
1207 .send()
1208 .await
1209 .unwrap()
1210 .json::<String>()
1211 .await
1212 .unwrap()
1213 );
1214 }
1215
1216 let check_docs = |version, route: &'static str| {
1218 let client = &client;
1219 async move {
1220 let span = tracing::info_span!("check_docs", ?version, route);
1221 let _enter = span.enter();
1222 tracing::info!("test invalid route docs");
1223
1224 let prefix = match version {
1225 Some(v) => format!("/v{v}"),
1226 None => "".into(),
1227 };
1228
1229 let version = version.unwrap_or(3);
1232
1233 let res = client
1234 .get(&format!("{prefix}/mod/{route}"))
1235 .send()
1236 .await
1237 .unwrap();
1238 let docs = res.text().await.unwrap();
1239 if !route.is_empty() {
1240 assert!(
1241 docs.contains(&format!("No route matches /{route}")),
1242 "{docs}"
1243 );
1244 }
1245 assert!(
1246 docs.contains(&format!("mod API {version}.0.0 Reference")),
1247 "{docs}"
1248 );
1249 }
1250 };
1251
1252 for route in ["", "deleted"] {
1253 check_docs(None, route).await;
1254 }
1255 for route in ["", "deleted"] {
1256 check_docs(Some(3), route).await;
1257 }
1258 for route in ["", "added"] {
1259 check_docs(Some(1), route).await;
1260 }
1261
1262 let expected_html = html! {
1264 "Unsupported version v2. Supported versions are:"
1265 ul {
1266 li {
1267 a href="/v3/mod" {"v3"}
1268 }
1269 li {
1270 a href="/v1/mod" {"v1"}
1271 }
1272 }
1273 }
1274 .into_string();
1275 for route in ["", "/unchanged"] {
1276 let span = tracing::info_span!("unsupported_version_docs", route);
1277 let _enter = span.enter();
1278 tracing::info!("test unsupported version docs");
1279
1280 let res = client.get(&format!("/v2/mod{route}")).send().await.unwrap();
1281 let docs = res.text().await.unwrap();
1282 assert_eq!(docs, expected_html);
1283 }
1284
1285 for version in [None, Some(1), Some(3)] {
1287 let span = tracing::info_span!("version_endpoints", version);
1288 let _enter = span.enter();
1289 tracing::info!("test version endpoints");
1290
1291 let prefix = match version {
1292 Some(v) => format!("/v{v}"),
1293 None => "".into(),
1294 };
1295 let res = client
1296 .get(&format!("{prefix}/mod/version"))
1297 .send()
1298 .await
1299 .unwrap();
1300 assert_eq!(
1301 res.json::<ApiVersion>()
1302 .await
1303 .unwrap()
1304 .api_version
1305 .unwrap()
1306 .major,
1307 version.unwrap_or(3)
1308 );
1309 }
1310
1311 let res = client.get("version").send().await.unwrap();
1313 assert_eq!(
1314 res.json::<AppVersion>().await.unwrap().modules["mod"],
1315 [
1316 ApiVersion {
1317 api_version: Some("3.0.0".parse().unwrap()),
1318 spec_version: "0.1.0".parse().unwrap(),
1319 },
1320 ApiVersion {
1321 api_version: Some("1.0.0".parse().unwrap()),
1322 spec_version: "0.1.0".parse().unwrap(),
1323 }
1324 ]
1325 );
1326
1327 for version in [None, Some(1), Some(3)] {
1329 let span = tracing::info_span!("healthcheck_endpoints", version);
1330 let _enter = span.enter();
1331 tracing::info!("test healthcheck endpoints");
1332
1333 let prefix = match version {
1334 Some(v) => format!("/v{v}"),
1335 None => "".into(),
1336 };
1337 let res = client
1338 .get(&format!("{prefix}/mod/healthcheck"))
1339 .send()
1340 .await
1341 .unwrap();
1342 let status = res.status();
1343 let health: HealthStatus = res.json().await.unwrap();
1344 assert_eq!(health.status(), status);
1345 assert_eq!(
1346 health,
1347 if version == Some(1) {
1348 HealthStatus::TemporarilyUnavailable
1349 } else {
1350 HealthStatus::Available
1351 }
1352 );
1353 }
1354
1355 let res = client.get("healthcheck").send().await.unwrap();
1357 assert_eq!(res.status(), StatusCode::SERVICE_UNAVAILABLE);
1358 let health: AppHealth = res.json().await.unwrap();
1359 assert_eq!(health.status, HealthStatus::Unhealthy);
1360 assert_eq!(
1361 health.modules["mod"],
1362 [(3, StatusCode::OK), (1, StatusCode::SERVICE_UNAVAILABLE)].into()
1363 );
1364 }
1365
1366 #[async_std::test]
1367 async fn test_api_disco() {
1368 setup_test();
1369
1370 let mut app = App::<_, ServerError>::with_state(());
1372 app.module::<ServerError, StaticVer01>(
1373 "the-correct-module",
1374 toml! {
1375 route = {}
1376 },
1377 )
1378 .unwrap()
1379 .with_version("1.0.0".parse().unwrap());
1380
1381 let port = pick_unused_port().unwrap();
1382 let url: Url = format!("http://localhost:{}", port).parse().unwrap();
1383 spawn(app.serve(format!("0.0.0.0:{}", port), StaticVer01::instance()));
1384 let client = Client::new(url.clone()).await;
1385
1386 let expected_list_item = html! {
1387 a href="/the-correct-module" {"the-correct-module"}
1388 sup {
1389 a href="/v1/the-correct-module" {"[v1]"}
1390 }
1391 }
1392 .into_string();
1393
1394 let expected_err = Regex::new("No API matches .*/test").unwrap();
1395 for version_prefix in ["", "/v1"] {
1396 let docs = client
1397 .get(&format!("{version_prefix}/test"))
1398 .send()
1399 .await
1400 .unwrap()
1401 .text()
1402 .await
1403 .unwrap();
1404 expected_err
1405 .find(&docs)
1406 .unwrap_or_else(|| panic!("Docs contains error message:\n{docs}"));
1407 assert!(docs.contains(&expected_list_item), "{docs}");
1408 }
1409
1410 let docs = client.get("").send().await.unwrap().text().await.unwrap();
1412 assert!(!docs.contains("No API matches"), "{docs}");
1413 assert!(docs.contains(&expected_list_item), "{docs}");
1414
1415 let docs = client
1416 .get("/v1")
1417 .send()
1418 .await
1419 .unwrap()
1420 .text()
1421 .await
1422 .unwrap();
1423 Regex::new("No API matches .*/")
1424 .unwrap()
1425 .find(&docs)
1426 .unwrap_or_else(|| panic!("Docs contains error message:\n{docs}"));
1427 assert!(docs.contains(&expected_list_item), "{docs}");
1428 }
1429
1430 #[async_std::test]
1431 async fn test_post_redirect_idempotency() {
1432 setup_test();
1433
1434 let mut app = App::<_, ServerError>::with_state(RwLock::new(0));
1435
1436 let api_toml = toml! {
1437 [meta]
1438 FORMAT_VERSION = "0.1.0"
1439
1440 [route.test]
1441 METHOD = "POST"
1442 PATH = ["/test"]
1443 };
1444 {
1445 let mut api = app
1446 .module::<ServerError, StaticVer01>("mod", api_toml.clone())
1447 .unwrap();
1448 api.post("test", |_req, state| {
1449 async move {
1450 *state += 1;
1451 Ok(*state)
1452 }
1453 .boxed()
1454 })
1455 .unwrap();
1456 }
1457
1458 let port = pick_unused_port().unwrap();
1459 let url: Url = format!("http://localhost:{}", port).parse().unwrap();
1460 spawn(app.serve(format!("0.0.0.0:{}", port), StaticVer01::instance()));
1461 let client = Client::new(url.clone()).await;
1462
1463 for i in 1..3 {
1464 assert_eq!(
1467 client
1468 .post("mod/test")
1469 .send()
1470 .await
1471 .unwrap()
1472 .json::<u64>()
1473 .await
1474 .unwrap(),
1475 i
1476 );
1477 }
1478 }
1479
1480 #[async_std::test]
1481 async fn test_format_versions() {
1482 setup_test();
1483
1484 let mut app = App::<_, ServerError>::with_state(());
1488 let api_toml = toml! {
1489 [meta]
1490 FORMAT_VERSION = "0.1.0"
1491
1492 [route.ok]
1493 METHOD = "GET"
1494 PATH = ["/ok"]
1495
1496 [route.err]
1497 METHOD = "GET"
1498 PATH = ["/err"]
1499 };
1500
1501 fn init_api<VER: StaticVersionType + 'static>(api: &mut Api<(), ServerError, VER>) {
1502 api.get("ok", |_req, _state| async move { Ok("ok") }.boxed())
1503 .unwrap()
1504 .get("err", |_req, _state| {
1505 async move {
1506 Err::<String, _>(ServerError::catch_all(
1507 StatusCode::INTERNAL_SERVER_ERROR,
1508 "err".into(),
1509 ))
1510 }
1511 .boxed()
1512 })
1513 .unwrap();
1514 }
1515
1516 {
1517 let mut api = app
1518 .module::<ServerError, StaticVer02>("mod02", api_toml.clone())
1519 .unwrap();
1520 init_api(&mut api);
1521 }
1522 {
1523 let mut api = app
1524 .module::<ServerError, StaticVer03>("mod03", api_toml.clone())
1525 .unwrap();
1526 init_api(&mut api);
1527 }
1528
1529 let port = pick_unused_port().unwrap();
1530 let url: Url = format!("http://localhost:{}", port).parse().unwrap();
1531 spawn(app.serve(format!("0.0.0.0:{}", port), StaticVer01::instance()));
1532 let client = Client::new(url.clone()).await;
1533
1534 async fn get<S: BinarySerializer, T: DeserializeOwned>(
1535 client: &Client,
1536 endpoint: &str,
1537 expected_status: StatusCode,
1538 ) -> anyhow::Result<T> {
1539 tracing::info!("GET {endpoint} ->");
1540 let res = client
1541 .get(endpoint)
1542 .header("Accept", "application/octet-stream")
1543 .send()
1544 .await
1545 .unwrap();
1546 tracing::info!(?res, "<-");
1547 assert_eq!(res.status(), expected_status);
1548 let bytes = res.bytes().await.unwrap();
1549 anyhow::Context::context(
1550 S::deserialize(&bytes),
1551 format!("failed to deserialize bytes {bytes:?}"),
1552 )
1553 }
1554
1555 #[tracing::instrument(skip(client))]
1556 async fn check_ok<S: BinarySerializer>(
1557 client: &Client,
1558 endpoint: &str,
1559 expected: impl Debug + DeserializeOwned + Eq,
1560 ) {
1561 tracing::info!("checking successful deserialization");
1562 assert_eq!(
1563 expected,
1564 get::<S, _>(client, endpoint, StatusCode::OK).await.unwrap()
1565 );
1566 }
1567
1568 let api_version = ApiVersion {
1569 spec_version: "0.1.0".parse().unwrap(),
1570 api_version: None,
1571 };
1572
1573 check_ok::<SerializerV01>(
1574 &client,
1575 "healthcheck",
1576 AppHealth {
1577 status: HealthStatus::Available,
1578 modules: [
1579 ("mod02".into(), [(0, StatusCode::OK)].into()),
1580 ("mod03".into(), [(0, StatusCode::OK)].into()),
1581 ]
1582 .into(),
1583 },
1584 )
1585 .await;
1586 check_ok::<SerializerV01>(
1587 &client,
1588 "version",
1589 AppVersion {
1590 app_version: None,
1591 disco_version: env!("CARGO_PKG_VERSION").parse().unwrap(),
1592 modules: [
1593 ("mod02".into(), vec![api_version.clone()]),
1594 ("mod03".into(), vec![api_version.clone()]),
1595 ]
1596 .into(),
1597 },
1598 )
1599 .await;
1600 check_ok::<SerializerV02>(&client, "mod02/ok", "ok".to_string()).await;
1601 check_ok::<SerializerV02>(&client, "mod02/healthcheck", HealthStatus::Available).await;
1602 check_ok::<SerializerV02>(&client, "mod02/version", api_version.clone()).await;
1603 check_ok::<SerializerV03>(&client, "mod03/ok", "ok".to_string()).await;
1604 check_ok::<SerializerV03>(&client, "mod03/healthcheck", HealthStatus::Available).await;
1605 check_ok::<SerializerV03>(&client, "mod03/version", api_version.clone()).await;
1606
1607 #[tracing::instrument(skip(client))]
1608 async fn check_wrong_version<S: BinarySerializer, T: Debug + DeserializeOwned>(
1609 client: &Client,
1610 endpoint: &str,
1611 ) {
1612 tracing::info!("checking deserialization fails with wrong version");
1613 get::<S, T>(client, endpoint, StatusCode::OK)
1614 .await
1615 .unwrap_err();
1616 }
1617
1618 check_wrong_version::<SerializerV02, AppHealth>(&client, "healthcheck").await;
1619 check_wrong_version::<SerializerV02, AppVersion>(&client, "version").await;
1620 check_wrong_version::<SerializerV03, String>(&client, "mod02/ok").await;
1621 check_wrong_version::<SerializerV03, HealthStatus>(&client, "mod02/healthcheck").await;
1622 check_wrong_version::<SerializerV03, ApiVersion>(&client, "mod02/version").await;
1623 check_wrong_version::<SerializerV01, String>(&client, "mod03/ok").await;
1624 check_wrong_version::<SerializerV01, HealthStatus>(&client, "mod03/healthcheck").await;
1625 check_wrong_version::<SerializerV01, ApiVersion>(&client, "mod03/version").await;
1626
1627 #[tracing::instrument(skip(client))]
1628 async fn check_err<S: BinarySerializer>(client: &Client, endpoint: &str) {
1629 tracing::info!("checking error deserialization");
1630 tracing::info!("checking successful deserialization");
1631 assert_eq!(
1632 get::<S, ServerError>(client, endpoint, StatusCode::INTERNAL_SERVER_ERROR)
1633 .await
1634 .unwrap(),
1635 ServerError::catch_all(StatusCode::INTERNAL_SERVER_ERROR, "err".into())
1636 );
1637 }
1638
1639 check_err::<SerializerV02>(&client, "mod02/err").await;
1640 check_err::<SerializerV03>(&client, "mod03/err").await;
1641 }
1642
1643 #[async_std::test]
1644 async fn test_api_prefix() {
1645 setup_test();
1646
1647 for (api1, api2) in [
1650 ("", "api"),
1651 ("api", ""),
1652 ("path", "path/sub"),
1653 ("path/sub", "path"),
1654 ] {
1655 tracing::info!(api1, api2, "test case");
1656 let (prefix, conflict) = if api1.len() < api2.len() {
1657 (api1.to_string(), api2.to_string())
1658 } else {
1659 (api2.to_string(), api1.to_string())
1660 };
1661
1662 let mut app = App::<_, ServerError>::with_state(());
1663 let toml = toml! {
1664 route = {}
1665 };
1666 app.module::<ServerError, StaticVer01>(api1, toml.clone())
1667 .unwrap()
1668 .register()
1669 .unwrap();
1670 assert_eq!(
1671 app.module::<ServerError, StaticVer01>(api2, toml)
1672 .unwrap()
1673 .register()
1674 .unwrap_err(),
1675 DispatchError::ConflictingModules { prefix, conflict }.into()
1676 );
1677 }
1678 }
1679
1680 #[async_std::test]
1681 async fn test_singleton_api() {
1682 setup_test();
1683
1684 let toml = toml! {
1686 [route.test]
1687 PATH = ["/test"]
1688 };
1689 let mut app = App::<_, ServerError>::with_state(());
1690 let mut api = app.module::<ServerError, StaticVer01>("", toml).unwrap();
1691 api.with_version("0.1.0".parse().unwrap())
1692 .get("test", |_, _| async move { Ok("response") }.boxed())
1693 .unwrap();
1694 api.register().unwrap();
1695
1696 let port = pick_unused_port().unwrap();
1697 spawn(app.serve(format!("0.0.0.0:{port}"), StaticVer01::instance()));
1698 let client = Client::new(format!("http://localhost:{port}").parse().unwrap()).await;
1699
1700 let res = client.get("/test").send().await.unwrap();
1702 assert_eq!(
1703 res.status(),
1704 StatusCode::OK,
1705 "{}",
1706 res.text().await.unwrap()
1707 );
1708 assert_eq!(res.json::<String>().await.unwrap(), "response");
1709
1710 let res = client.get("/healthcheck").send().await.unwrap();
1715 assert_eq!(res.status(), StatusCode::OK);
1716 assert_eq!(
1717 res.json::<HealthStatus>().await.unwrap(),
1718 HealthStatus::Available
1719 );
1720
1721 let res = client.get("/version").send().await.unwrap();
1722 assert_eq!(res.status(), StatusCode::OK);
1723 assert_eq!(
1724 res.json::<ApiVersion>().await.unwrap(),
1725 ApiVersion {
1726 api_version: Some("0.1.0".parse().unwrap()),
1727 spec_version: "0.1.0".parse().unwrap(),
1728 },
1729 );
1730 }
1731
1732 #[async_std::test]
1733 async fn test_multi_segment() {
1734 setup_test();
1735
1736 let toml = toml! {
1737 [route.test]
1738 PATH = ["/test"]
1739 };
1740 let mut app = App::<_, ServerError>::with_state(());
1741
1742 for name in ["a", "b"] {
1743 let path = format!("api/{name}");
1744 let mut api = app
1745 .module::<ServerError, StaticVer01>(&path, toml.clone())
1746 .unwrap();
1747 api.with_version("0.1.0".parse().unwrap())
1748 .get("test", move |_, _| async move { Ok(name) }.boxed())
1749 .unwrap();
1750 api.register().unwrap();
1751 }
1752
1753 let port = pick_unused_port().unwrap();
1754 spawn(app.serve(format!("0.0.0.0:{port}"), StaticVer01::instance()));
1755 let client = Client::new(format!("http://localhost:{port}").parse().unwrap()).await;
1756
1757 for api in ["a", "b"] {
1758 tracing::info!(api, "testing api");
1759
1760 let res = client.get(&format!("api/{api}/test")).send().await.unwrap();
1762 assert_eq!(res.status(), StatusCode::OK);
1763 assert_eq!(res.json::<String>().await.unwrap(), api);
1764
1765 let res = client
1767 .get(&format!("api/{api}/healthcheck"))
1768 .send()
1769 .await
1770 .unwrap();
1771 assert_eq!(res.status(), StatusCode::OK);
1772 assert_eq!(
1773 res.json::<HealthStatus>().await.unwrap(),
1774 HealthStatus::Available
1775 );
1776
1777 let res = client
1779 .get(&format!("api/{api}/version"))
1780 .send()
1781 .await
1782 .unwrap();
1783 assert_eq!(res.status(), StatusCode::OK);
1784 assert_eq!(
1785 res.json::<ApiVersion>().await.unwrap().api_version.unwrap(),
1786 "0.1.0".parse().unwrap()
1787 );
1788 }
1789
1790 let res = client.get("healthcheck").send().await.unwrap();
1792 assert_eq!(res.status(), StatusCode::OK);
1793 assert_eq!(
1794 res.json::<AppHealth>().await.unwrap(),
1795 AppHealth {
1796 status: HealthStatus::Available,
1797 modules: [
1798 ("api/a".into(), [(0, StatusCode::OK)].into()),
1799 ("api/b".into(), [(0, StatusCode::OK)].into()),
1800 ]
1801 .into()
1802 }
1803 );
1804
1805 let res = client.get("version").send().await.unwrap();
1807 assert_eq!(res.status(), StatusCode::OK);
1808 assert_eq!(
1809 res.json::<AppVersion>().await.unwrap().modules,
1810 [
1811 (
1812 "api/a".into(),
1813 vec![ApiVersion {
1814 api_version: Some("0.1.0".parse().unwrap()),
1815 spec_version: "0.1.0".parse().unwrap(),
1816 }]
1817 ),
1818 (
1819 "api/b".into(),
1820 vec![ApiVersion {
1821 api_version: Some("0.1.0".parse().unwrap()),
1822 spec_version: "0.1.0".parse().unwrap(),
1823 }]
1824 ),
1825 ]
1826 .into()
1827 );
1828 }
1829}