diff --git a/rust/src/hosting/app/create.rs b/rust/src/hosting/app/create.rs index 71203535..5d39c17a 100644 --- a/rust/src/hosting/app/create.rs +++ b/rust/src/hosting/app/create.rs @@ -11,7 +11,7 @@ use crate::scopes::HOSTING_APPLICATION_CREATE as APP_CREATE; #[derive(Debug, Clone, clap::Args)] struct AppCreateArgs { - /// Hosting product. + /// Application type (required). #[arg(long = "app-type", value_name = "TYPE", ignore_case = true)] app_type: HostingAppType, @@ -122,9 +122,10 @@ fn create_err(e: ClientError, app_type: HostingAppType) -> cli_engine::CliCoreEr #[cfg(test)] mod tests { - use cli_engine::{Cli, CliConfig, Stage}; + use cli_engine::Cli; use super::*; + use crate::hosting::test_support::{cli, run_hosting}; fn fix_for(status: u16, issue: &str, app_type: HostingAppType) -> Option { let err = create_err( @@ -216,22 +217,10 @@ mod tests { assert!(poll_description(HostingAppType::Mhwp).contains("Coming Soon")); } - fn cli(mhwp: bool) -> Cli { - let mut config = CliConfig::new("gddy", "GoDaddy developer CLI", "gddy") - .with_min_stage(Stage::Beta) - // Unregistered provider: commands that pass parsing stop at auth. - .with_default_auth_provider("godaddy") - .with_module(crate::hosting::module()); - if mhwp { - config = config.with_feature_override("hosting-mhwp", Stage::Ga); - } - Cli::new(config) - } - async fn run(cli: &Cli, args: &[&str]) -> String { - let mut argv = vec!["gddy", "hosting", "app"]; + let mut argv = vec!["app"]; argv.extend_from_slice(args); - cli.run(argv).await.rendered + run_hosting(cli, &argv).await } const REJECTED: &str = "invalid value 'MHWP'"; diff --git a/rust/src/hosting/app/list.rs b/rust/src/hosting/app/list.rs index 1fae8c6b..f2306faf 100644 --- a/rust/src/hosting/app/list.rs +++ b/rust/src/hosting/app/list.rs @@ -3,14 +3,13 @@ use serde_json::{Value, json}; use crate::hosting::common::{ HostingAppSummary, HostingAppType, app_type_command, client_err, make_client, next_page_token, - supported_app_types, }; use crate::next_action::next_action; use crate::scopes::HOSTING_APPLICATION_READ as APP_READ; #[derive(Debug, Clone, clap::Args)] struct AppListArgs { - /// Hosting product. + /// Application type (required). #[arg(long = "app-type", value_name = "TYPE", ignore_case = true)] app_type: HostingAppType, @@ -22,13 +21,10 @@ struct AppListArgs { pub(super) fn command(mhwp: bool) -> RuntimeCommandSpec { RuntimeCommandSpec::new_typed_with_context::( app_type_command::("list", "List hosting applications", mhwp) - .with_long(format!( + .with_long( "List all hosting applications of a given type. Results are autopaginated — \ - all pages are fetched and combined. Use --limit to cap the total returned.\n\ - \n\ - --app-type is required. Currently supported: {}.", - supported_app_types(mhwp) - )) + all pages are fetched and combined. Use --limit to cap the total returned.", + ) .with_system("hosting") .with_tier(Tier::Read) .with_scopes(&[APP_READ]) diff --git a/rust/src/hosting/app/mod.rs b/rust/src/hosting/app/mod.rs index 1d7d5b6e..836370d3 100644 --- a/rust/src/hosting/app/mod.rs +++ b/rust/src/hosting/app/mod.rs @@ -19,7 +19,7 @@ pub(super) fn group(mhwp: bool) -> RuntimeGroupSpec { .with_alias("apps") .with_long(format!( "Work with hosting applications. Use --app-type on list and create to \ - specify the product type (currently {}).", + specify the application type (currently {}).", supported_app_types(mhwp) )), ) diff --git a/rust/src/hosting/mod.rs b/rust/src/hosting/mod.rs index ed445766..914636b0 100644 --- a/rust/src/hosting/mod.rs +++ b/rust/src/hosting/mod.rs @@ -9,6 +9,8 @@ mod runtime; mod secrets; mod source; mod subscription; +#[cfg(test)] +mod test_support; use cli_engine::{GroupSpec, Module, RuntimeGroupSpec}; diff --git a/rust/src/hosting/subscription/list.rs b/rust/src/hosting/subscription/list.rs index 003fe67b..03e995cf 100644 --- a/rust/src/hosting/subscription/list.rs +++ b/rust/src/hosting/subscription/list.rs @@ -11,7 +11,7 @@ use crate::scopes::HOSTING_SUBSCRIPTION_READ as SUB_READ; #[derive(Debug, Clone, clap::Args)] struct SubscriptionListArgs { - /// Hosting product to list (WEB_HOSTING or MANAGED_WORDPRESS). + /// Hosting product (required). #[arg(long, value_name = "PRODUCT", ignore_case = true)] hosting_product: HostingProduct, @@ -28,7 +28,7 @@ pub(super) fn command() -> RuntimeCommandSpec { ) .with_long( "List hosting plan subscriptions available to attach to applications. \ - --hosting-product is required. Results are autopaginated.", + Results are autopaginated.", ) .with_system("hosting") .with_tier(Tier::Read) diff --git a/rust/src/hosting/test_support.rs b/rust/src/hosting/test_support.rs new file mode 100644 index 00000000..e91eaa74 --- /dev/null +++ b/rust/src/hosting/test_support.rs @@ -0,0 +1,57 @@ +use cli_engine::{Cli, CliConfig, Stage}; + +pub(crate) fn cli(mhwp: bool) -> Cli { + let mut config = CliConfig::new("gddy", "GoDaddy developer CLI", "gddy") + .with_min_stage(Stage::Beta) + // Unregistered provider: commands that pass parsing stop at auth. + .with_default_auth_provider("godaddy") + .with_module(crate::hosting::module()); + if mhwp { + config = config.with_feature_override("hosting-mhwp", Stage::Ga); + } + Cli::new(config) +} + +/// Runs `gddy hosting ` and returns the rendered output. +pub(crate) async fn run_hosting(cli: &Cli, args: &[&str]) -> String { + let mut argv = vec!["gddy", "hosting"]; + argv.extend_from_slice(args); + cli.run(argv).await.rendered +} + +#[tokio::test] +async fn required_flags_are_marked_in_help_and_enforced() { + let cli = cli(false); + let cases: [(&[&str], &str); 3] = [ + (&["app", "list"], "--app-type"), + (&["app", "create", "--name", "x"], "--app-type"), + (&["subscription", "list"], "--hosting-product"), + ]; + for (command, flag) in cases { + let missing = run_hosting(&cli, command).await; + assert!( + missing.contains("required arguments were not provided") && missing.contains(flag), + "{command:?}: {missing}" + ); + + let mut help_args = command.to_vec(); + help_args.push("--help"); + let help = run_hosting(&cli, &help_args).await; + // The entry is the flag line plus its description, up to the next flag. + let entry: Vec<&str> = help + .lines() + .skip_while(|line| !line.trim_start().starts_with(flag)) + .enumerate() + .take_while(|(i, line)| *i == 0 || !line.trim_start().starts_with("-")) + .map(|(_, line)| line) + .collect(); + assert!( + !entry.is_empty(), + "{command:?}: no entry for {flag}: {help}" + ); + assert!( + entry.iter().any(|line| line.contains("(required)")), + "{command:?}: {help}" + ); + } +}