From 3327a8a7f7496936ae112b550ddac597d3874f9a Mon Sep 17 00:00:00 2001 From: Spencer Ferris <3319370+spencewenski@users.noreply.github.com> Date: Tue, 21 May 2024 23:37:09 -0700 Subject: [PATCH] Add tests for controller config methods --- src/config/service/http/mod.rs | 63 ++------------- src/controller/http/docs.rs | 143 ++++++++++++++++++++++++++++++--- src/controller/http/health.rs | 56 +++++++++++-- src/controller/http/ping.rs | 56 +++++++++++-- 4 files changed, 241 insertions(+), 77 deletions(-) diff --git a/src/config/service/http/mod.rs b/src/config/service/http/mod.rs index bc40fb8f..d815e999 100644 --- a/src/config/service/http/mod.rs +++ b/src/config/service/http/mod.rs @@ -2,7 +2,6 @@ use crate::app_context::AppContext; use crate::config::service::http::initializer::Initializer; use crate::config::service::http::middleware::Middleware; -use crate::controller::http::build_path; use crate::util::serde_util::default_true; use serde_derive::{Deserialize, Serialize}; use validator::{Validate, ValidationError}; @@ -40,43 +39,27 @@ impl Address { } } -#[derive(Debug, Clone, Validate, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Validate, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] #[validate(schema(function = "validate_default_routes"))] pub struct DefaultRoutes { #[serde(default = "default_true")] pub default_enable: bool, - #[serde(default = "DefaultRouteConfig::default_ping")] + #[serde(default)] pub ping: DefaultRouteConfig, - #[serde(default = "DefaultRouteConfig::default_health")] + #[serde(default)] pub health: DefaultRouteConfig, #[cfg(feature = "open-api")] - #[serde(default = "DefaultRouteConfig::default_api_schema")] + #[serde(default)] pub api_schema: DefaultRouteConfig, #[cfg(feature = "open-api")] - #[serde(default = "DefaultRouteConfig::default_scalar")] + #[serde(default)] pub scalar: DefaultRouteConfig, #[cfg(feature = "open-api")] - #[serde(default = "DefaultRouteConfig::default_redoc")] + #[serde(default)] pub redoc: DefaultRouteConfig, } -impl Default for DefaultRoutes { - fn default() -> Self { - Self { - default_enable: default_true(), - ping: DefaultRouteConfig::default_ping(), - health: DefaultRouteConfig::default_health(), - #[cfg(feature = "open-api")] - api_schema: DefaultRouteConfig::default_api_schema(), - #[cfg(feature = "open-api")] - scalar: DefaultRouteConfig::default_scalar(), - #[cfg(feature = "open-api")] - redoc: DefaultRouteConfig::default_redoc(), - } - } -} - fn validate_default_routes( // This parameter isn't used for some feature flag combinations #[allow(unused)] default_routes: &DefaultRoutes, @@ -103,44 +86,14 @@ fn validate_default_routes( Ok(()) } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct DefaultRouteConfig { pub enable: Option, - pub route: String, + pub route: Option, } impl DefaultRouteConfig { - fn new(route: &str) -> Self { - Self { - enable: None, - route: build_path("", route), - } - } - - fn default_ping() -> Self { - DefaultRouteConfig::new("_ping") - } - - fn default_health() -> Self { - DefaultRouteConfig::new("_health") - } - - #[cfg(feature = "open-api")] - fn default_api_schema() -> Self { - DefaultRouteConfig::new("_docs/api.json") - } - - #[cfg(feature = "open-api")] - fn default_scalar() -> Self { - DefaultRouteConfig::new("_docs") - } - - #[cfg(feature = "open-api")] - fn default_redoc() -> Self { - DefaultRouteConfig::new("_docs/redoc") - } - pub fn enabled(&self, context: &AppContext) -> bool { self.enable.unwrap_or( context diff --git a/src/controller/http/docs.rs b/src/controller/http/docs.rs index e18aed41..c53d50a3 100644 --- a/src/controller/http/docs.rs +++ b/src/controller/http/docs.rs @@ -3,6 +3,7 @@ use crate::app_context::AppContext; use std::ops::Deref; use std::sync::Arc; +use crate::config::app_config::AppConfig; use crate::controller::http::build_path; use aide::axum::routing::get_with; use aide::axum::{ApiRouter, IntoApiResponse}; @@ -12,6 +13,7 @@ use aide::scalar::Scalar; use axum::response::IntoResponse; use axum::{Extension, Json}; +const BASE: &str = "_docs"; const TAG: &str = "Docs"; /// This API is only available when using Aide. @@ -19,7 +21,8 @@ pub fn routes(parent: &str, context: &AppContext) -> ApiRouter(context: &AppContext) -> bool { .enabled(context) } -fn scalar_route(context: &AppContext) -> &str { - &context - .config() +fn scalar_route(context: &AppContext) -> String { + let config: &AppConfig = context.config(); + config .service .http .custom .default_routes .scalar .route + .clone() + .unwrap_or_else(|| "/".to_string()) } fn redoc_enabled(context: &AppContext) -> bool { @@ -101,15 +106,17 @@ fn redoc_enabled(context: &AppContext) -> bool { .enabled(context) } -fn redoc_route(context: &AppContext) -> &str { - &context - .config() +fn redoc_route(context: &AppContext) -> String { + let config: &AppConfig = context.config(); + config .service .http .custom .default_routes .redoc .route + .clone() + .unwrap_or_else(|| "redoc".to_string()) } fn api_schema_enabled(context: &AppContext) -> bool { @@ -123,13 +130,125 @@ fn api_schema_enabled(context: &AppContext) -> bool { .enabled(context) } -fn api_schema_route(context: &AppContext) -> &str { - &context - .config() +fn api_schema_route(context: &AppContext) -> String { + let config: &AppConfig = context.config(); + config .service .http .custom .default_routes .api_schema .route + .clone() + .unwrap_or_else(|| "api.json".to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::app::MockTestApp; + use crate::app_context::MockAppContext; + use crate::config::app_config::AppConfig; + use rstest::rstest; + + // Todo: Is there a better way to structure these tests (and the ones in `health` and `ping`) + // to reduce duplication? + #[rstest] + #[case(false, None, None, false)] + #[case(false, Some(false), None, false)] + #[case(true, None, Some("/foo".to_string()), true)] + #[case(false, Some(true), None, true)] + #[cfg_attr(coverage_nightly, coverage(off))] + fn scalar( + #[case] default_enable: bool, + #[case] enable: Option, + #[case] route: Option, + #[case] enabled: bool, + ) { + let mut config = AppConfig::empty(None).unwrap(); + config.service.http.custom.default_routes.default_enable = default_enable; + config.service.http.custom.default_routes.scalar.enable = enable; + config + .service + .http + .custom + .default_routes + .scalar + .route + .clone_from(&route); + let mut context = MockAppContext::::default(); + context.expect_config().return_const(config); + + assert_eq!(scalar_enabled(&context), enabled); + assert_eq!( + scalar_route(&context), + route.unwrap_or_else(|| "/".to_string()) + ); + } + + #[rstest] + #[case(false, None, None, false)] + #[case(false, Some(false), None, false)] + #[case(true, None, Some("/foo".to_string()), true)] + #[case(false, Some(true), None, true)] + #[cfg_attr(coverage_nightly, coverage(off))] + fn redoc( + #[case] default_enable: bool, + #[case] enable: Option, + #[case] route: Option, + #[case] enabled: bool, + ) { + let mut config = AppConfig::empty(None).unwrap(); + config.service.http.custom.default_routes.default_enable = default_enable; + config.service.http.custom.default_routes.redoc.enable = enable; + config + .service + .http + .custom + .default_routes + .redoc + .route + .clone_from(&route); + let mut context = MockAppContext::::default(); + context.expect_config().return_const(config); + + assert_eq!(redoc_enabled(&context), enabled); + assert_eq!( + redoc_route(&context), + route.unwrap_or_else(|| "redoc".to_string()) + ); + } + + #[rstest] + #[case(false, None, None, false)] + #[case(false, Some(false), None, false)] + #[case(true, None, Some("/foo".to_string()), true)] + #[case(false, Some(true), None, true)] + #[cfg_attr(coverage_nightly, coverage(off))] + fn api_schema( + #[case] default_enable: bool, + #[case] enable: Option, + #[case] route: Option, + #[case] enabled: bool, + ) { + let mut config = AppConfig::empty(None).unwrap(); + config.service.http.custom.default_routes.default_enable = default_enable; + config.service.http.custom.default_routes.api_schema.enable = enable; + config + .service + .http + .custom + .default_routes + .api_schema + .route + .clone_from(&route); + let mut context = MockAppContext::::default(); + context.expect_config().return_const(config); + + assert_eq!(api_schema_enabled(&context), enabled); + assert_eq!( + api_schema_route(&context), + route.unwrap_or_else(|| "api.json".to_string()) + ); + } } diff --git a/src/controller/http/health.rs b/src/controller/http/health.rs index 3e772c3c..21708f17 100644 --- a/src/controller/http/health.rs +++ b/src/controller/http/health.rs @@ -1,5 +1,6 @@ #[mockall_double::double] use crate::app_context::AppContext; +use crate::config::app_config::AppConfig; use crate::controller::http::build_path; use crate::view::http::app_error::AppError; #[cfg(feature = "open-api")] @@ -40,7 +41,7 @@ where if !enabled(context) { return router; } - let root = build_path(parent, route(context)); + let root = build_path(parent, &route(context)); router.route(&root, get(health_get::)) } @@ -53,7 +54,7 @@ where if !enabled(context) { return router; } - let root = build_path(parent, route(context)); + let root = build_path(parent, &route(context)); router.api_route(&root, get_with(health_get::, health_get_docs)) } @@ -68,15 +69,17 @@ fn enabled(context: &AppContext) -> bool { .enabled(context) } -fn route(context: &AppContext) -> &str { - &context - .config() +fn route(context: &AppContext) -> String { + let config: &AppConfig = context.config(); + config .service .http .custom .default_routes .health .route + .clone() + .unwrap_or_else(|| "_health".to_string()) } #[serde_as] @@ -243,3 +246,46 @@ fn health_get_docs(op: TransformOperation) -> TransformOperation { }) }) } + +#[cfg(test)] +mod tests { + use crate::app::MockTestApp; + use crate::app_context::MockAppContext; + use crate::config::app_config::AppConfig; + use rstest::rstest; + + // Todo: Is there a better way to structure this test (and the ones in `docs` and `ping`) + // to reduce duplication? + #[rstest] + #[case(false, None, None, false)] + #[case(false, Some(false), None, false)] + #[case(true, None, Some("/foo".to_string()), true)] + #[case(false, Some(true), None, true)] + #[cfg_attr(coverage_nightly, coverage(off))] + fn health( + #[case] default_enable: bool, + #[case] enable: Option, + #[case] route: Option, + #[case] enabled: bool, + ) { + let mut config = AppConfig::empty(None).unwrap(); + config.service.http.custom.default_routes.default_enable = default_enable; + config.service.http.custom.default_routes.health.enable = enable; + config + .service + .http + .custom + .default_routes + .health + .route + .clone_from(&route); + let mut context = MockAppContext::::default(); + context.expect_config().return_const(config); + + assert_eq!(super::enabled(&context), enabled); + assert_eq!( + super::route(&context), + route.unwrap_or_else(|| "_health".to_string()) + ); + } +} diff --git a/src/controller/http/ping.rs b/src/controller/http/ping.rs index d82e954d..fd14ead6 100644 --- a/src/controller/http/ping.rs +++ b/src/controller/http/ping.rs @@ -1,5 +1,6 @@ #[mockall_double::double] use crate::app_context::AppContext; +use crate::config::app_config::AppConfig; use crate::controller::http::build_path; use crate::view::http::app_error::AppError; #[cfg(feature = "open-api")] @@ -27,7 +28,7 @@ where if !enabled(context) { return router; } - let root = build_path(parent, route(context)); + let root = build_path(parent, &route(context)); router.route(&root, get(ping_get)) } @@ -40,7 +41,7 @@ where if !enabled(context) { return router; } - let root = build_path(parent, route(context)); + let root = build_path(parent, &route(context)); router.api_route(&root, get_with(ping_get, ping_get_docs)) } @@ -55,15 +56,17 @@ fn enabled(context: &AppContext) -> bool { .enabled(context) } -fn route(context: &AppContext) -> &str { - &context - .config() +fn route(context: &AppContext) -> String { + let config: &AppConfig = context.config(); + config .service .http .custom .default_routes .ping .route + .clone() + .unwrap_or_else(|| "_ping".to_string()) } #[derive(Debug, Clone, Default, Serialize, Deserialize)] @@ -82,3 +85,46 @@ fn ping_get_docs(op: TransformOperation) -> TransformOperation { .tag(TAG) .response_with::<200, Json, _>(|res| res.example(PingResponse::default())) } + +#[cfg(test)] +mod tests { + use crate::app::MockTestApp; + use crate::app_context::MockAppContext; + use crate::config::app_config::AppConfig; + use rstest::rstest; + + // Todo: Is there a better way to structure this test (and the ones in `health` and `ping`) + // to reduce duplication? + #[rstest] + #[case(false, None, None, false)] + #[case(false, Some(false), None, false)] + #[case(true, None, Some("/foo".to_string()), true)] + #[case(false, Some(true), None, true)] + #[cfg_attr(coverage_nightly, coverage(off))] + fn ping( + #[case] default_enable: bool, + #[case] enable: Option, + #[case] route: Option, + #[case] enabled: bool, + ) { + let mut config = AppConfig::empty(None).unwrap(); + config.service.http.custom.default_routes.default_enable = default_enable; + config.service.http.custom.default_routes.ping.enable = enable; + config + .service + .http + .custom + .default_routes + .ping + .route + .clone_from(&route); + let mut context = MockAppContext::::default(); + context.expect_config().return_const(config); + + assert_eq!(super::enabled(&context), enabled); + assert_eq!( + super::route(&context), + route.unwrap_or_else(|| "_ping".to_string()) + ); + } +}