diff --git a/crates/trusted-server-cli/src/commands/dev/proxy/config.rs b/crates/trusted-server-cli/src/commands/dev/proxy/config.rs index d831735c2..42bae4023 100644 --- a/crates/trusted-server-cli/src/commands/dev/proxy/config.rs +++ b/crates/trusted-server-cli/src/commands/dev/proxy/config.rs @@ -349,6 +349,7 @@ fn resolve_basic_auth(args: &ProxyArgs) -> Result, ConfigError #[cfg(test)] mod tests { + use clap::Parser as _; use hyper::header::HeaderValue; use rustls::pki_types::ServerName; @@ -359,30 +360,26 @@ mod tests { }; fn base_args() -> crate::commands::dev::proxy::ProxyArgs { - // Construct via clap so defaults match the real surface. - use clap::Parser; - #[derive(clap::Parser)] - struct W { - #[command(flatten)] - a: crate::commands::dev::proxy::ProxyArgs, - } - W::parse_from(["ts"]).a + parse_args(&[ + "ts", + "--listen", + crate::commands::dev::proxy::DEFAULT_LISTEN, + ]) } fn parse_args(argv: &[&str]) -> crate::commands::dev::proxy::ProxyArgs { - use clap::Parser; #[derive(clap::Parser)] struct W { #[command(flatten)] a: crate::commands::dev::proxy::ProxyArgs, } - W::parse_from(argv).a + W::try_parse_from(argv).expect("should parse proxy args").a } #[test] fn clap_parses_rewrite_host_as_a_bool() { assert!( - !parse_args(&["ts"]).rewrite_host, + !parse_args(&["ts", "--insecure"]).rewrite_host, "absent --rewrite-host is false" ); assert!( diff --git a/crates/trusted-server-cli/src/commands/dev/proxy/mod.rs b/crates/trusted-server-cli/src/commands/dev/proxy/mod.rs index 1bebd872d..5cece9849 100644 --- a/crates/trusted-server-cli/src/commands/dev/proxy/mod.rs +++ b/crates/trusted-server-cli/src/commands/dev/proxy/mod.rs @@ -76,8 +76,13 @@ async fn finish_interrupted_run( let _ = tokio::time::timeout(std::time::Duration::from_secs(2), drain_manager).await; } +/// Default `--listen` address, shared with the `config` tests so they cannot +/// silently drift from the real default. +pub const DEFAULT_LISTEN: &str = "127.0.0.1:18080"; + /// `ts dev proxy [OPTIONS]` — see the design spec §4. #[derive(Debug, clap::Args)] +#[command(arg_required_else_help = true)] pub struct ProxyArgs { /// Rewrite rule `FROM=TO` (repeatable). #[arg(long = "map", value_name = "FROM=TO")] @@ -94,7 +99,7 @@ pub struct ProxyArgs { pub to: Option, /// Proxy listen address. Non-loopback requires `--allow-non-loopback`. - #[arg(long, value_name = "ADDR", default_value = "127.0.0.1:18080")] + #[arg(long, value_name = "ADDR", default_value = DEFAULT_LISTEN)] pub listen: String, /// Permit binding a non-loopback `--listen` (disables blind tunnel/forward). diff --git a/crates/trusted-server-cli/src/run.rs b/crates/trusted-server-cli/src/run.rs index 13009d448..d6f7c8f3b 100644 --- a/crates/trusted-server-cli/src/run.rs +++ b/crates/trusted-server-cli/src/run.rs @@ -678,4 +678,32 @@ mod tests { "error should explain unsupported option" ); } + + #[test] + #[cfg(target_os = "macos")] + fn dev_proxy_bare_invocation_shows_help_before_running() { + let error = Args::try_parse_from(["ts", "dev", "proxy"]) + .expect_err("a bare `ts dev proxy` should short-circuit to help, not run"); + assert_eq!( + error.kind(), + clap::error::ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand, + "should print help instead of touching system proxy state or attempting sudo" + ); + } + + #[test] + #[cfg(target_os = "macos")] + fn dev_proxy_ca_subcommands_still_parse_under_arg_required_else_help() { + for action in ["path", "install", "uninstall", "regenerate"] { + parse(&["ts", "dev", "proxy", "ca", action]); + } + } + + #[test] + #[cfg(target_os = "macos")] + fn dev_proxy_partial_rule_parses_instead_of_showing_help() { + // An explicit but incomplete rule (`--from` with no `--to`) must reach + // `run` and surface the concise no-rule error there, not clap help. + parse(&["ts", "dev", "proxy", "--from", "a.example.com"]); + } } diff --git a/crates/trusted-server-cli/tests/support/mod.rs b/crates/trusted-server-cli/tests/support/mod.rs index 8e8e3e66f..6c464b015 100644 --- a/crates/trusted-server-cli/tests/support/mod.rs +++ b/crates/trusted-server-cli/tests/support/mod.rs @@ -206,7 +206,7 @@ fn resolve(argv: &[&str]) -> config::ResolvedConfig { #[command(flatten)] args: trusted_server_cli::commands::dev::proxy::ProxyArgs, } - let parsed = Wrapper::parse_from(argv); + let parsed = Wrapper::try_parse_from(argv).expect("should parse proxy args"); config::resolve(&parsed.args).expect("should resolve test config") }