-
Notifications
You must be signed in to change notification settings - Fork 13
Show help for a bare ts dev proxy invocation
#1176
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
1476137
ae1d7c6
85ac1ff
afba58b
adfad9a
40b65c6
ffd5bf8
fe35b68
057a647
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -349,6 +349,7 @@ fn resolve_basic_auth(args: &ProxyArgs) -> Result<Option<BasicAuth>, ConfigError | |
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use clap::Parser as _; | ||
| use hyper::header::HeaderValue; | ||
| use rustls::pki_types::ServerName; | ||
|
|
||
|
|
@@ -358,31 +359,19 @@ mod tests { | |
| AddressPolicy, OriginKey, ReferenceIdentity, Transport, VerifyMode, | ||
| }; | ||
|
|
||
| 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 | ||
| } | ||
|
|
||
| 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", "--from", "a.example.com", "--to", "b.example.com"]).rewrite_host, | ||
| "absent --rewrite-host is false" | ||
| ); | ||
| assert!( | ||
|
|
@@ -391,11 +380,25 @@ mod tests { | |
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn clap_applies_the_real_listen_default() { | ||
| let args = parse_args(&["ts", "--rewrite-host"]); | ||
| assert_eq!( | ||
| args.listen, | ||
| crate::commands::dev::proxy::DEFAULT_LISTEN, | ||
| "should apply the real clap --listen default" | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn single_rule_from_to_keeps_from_host_by_default() { | ||
| let mut args = base_args(); | ||
| args.from = Some("www.example-publisher.com".into()); | ||
| args.to = Some("to.edgecompute.app".into()); | ||
| let args = parse_args(&[ | ||
| "ts", | ||
| "--from", | ||
| "www.example-publisher.com", | ||
| "--to", | ||
| "to.edgecompute.app", | ||
| ]); | ||
| let cfg = resolve(&args).expect("should resolve"); | ||
| let rule = cfg | ||
| .rules | ||
|
|
@@ -411,9 +414,12 @@ mod tests { | |
|
|
||
| #[test] | ||
| fn rewrite_host_uses_to() { | ||
| let mut args = base_args(); | ||
| args.map = vec!["www.example-publisher.com=to.edgecompute.app".into()]; | ||
| args.rewrite_host = true; | ||
| let args = parse_args(&[ | ||
| "ts", | ||
| "--map", | ||
| "www.example-publisher.com=to.edgecompute.app", | ||
| "--rewrite-host", | ||
| ]); | ||
| let cfg = resolve(&args).expect("should resolve"); | ||
| assert_eq!( | ||
| rewrite_for( | ||
|
|
@@ -429,10 +435,13 @@ mod tests { | |
|
|
||
| #[test] | ||
| fn resolve_pins_host_to_ip() { | ||
| let mut args = base_args(); | ||
| args.map = vec!["www.example-publisher.com=ts.edgecompute.app".into()]; | ||
| // Mixed case to confirm the host key is lowercased. | ||
| args.resolve = vec!["TS.EdgeCompute.app:192.0.2.10".into()]; | ||
| let args = parse_args(&[ | ||
| "ts", | ||
| "--map", | ||
| "www.example-publisher.com=ts.edgecompute.app", | ||
| "--resolve", | ||
| "TS.EdgeCompute.app:192.0.2.10", // Mixed case to confirm the host key is lowercased. | ||
| ]); | ||
| let cfg = resolve(&args).expect("should resolve"); | ||
| assert_eq!( | ||
| cfg.resolve.get("ts.edgecompute.app"), | ||
|
|
@@ -443,10 +452,13 @@ mod tests { | |
|
|
||
| #[test] | ||
| fn resolve_accepts_ipv6_target() { | ||
| let mut args = base_args(); | ||
| args.map = vec!["a.example.com=b.edgecompute.app".into()]; | ||
| // Split-on-first-colon must keep the colon-bearing IPv6 address intact. | ||
| args.resolve = vec!["b.edgecompute.app:::1".into()]; | ||
| let args = parse_args(&[ | ||
| "ts", | ||
| "--map", | ||
| "a.example.com=b.edgecompute.app", | ||
| "--resolve", | ||
| "b.edgecompute.app:::1", // Split-on-first-colon must keep the colon-bearing IPv6 address intact. | ||
| ]); | ||
| let cfg = resolve(&args).expect("should resolve"); | ||
| assert_eq!( | ||
| cfg.resolve.get("b.edgecompute.app"), | ||
|
|
@@ -457,11 +469,15 @@ mod tests { | |
|
|
||
| #[test] | ||
| fn resolve_host_not_matching_any_rule_warns_but_succeeds() { | ||
| let mut args = base_args(); | ||
| args.map = vec!["a.example.com=b.edgecompute.app".into()]; | ||
| // A pin for a host that is no rule's TO is a likely typo: it should warn | ||
| // (not error) and still be recorded. | ||
| args.resolve = vec!["typo.edgecompute.app:192.0.2.10".into()]; | ||
| let args = parse_args(&[ | ||
| "ts", | ||
| "--map", | ||
| "a.example.com=b.edgecompute.app", | ||
| // A pin for a host that is no rule's TO is a likely typo: it should warn | ||
| // (not error) and still be recorded. | ||
| "--resolve", | ||
| "typo.edgecompute.app:192.0.2.10", | ||
| ]); | ||
| let cfg = resolve(&args).expect("an unmatched --resolve host should warn, not error"); | ||
| assert!( | ||
| cfg.resolve.contains_key("typo.edgecompute.app"), | ||
|
|
@@ -471,9 +487,13 @@ mod tests { | |
|
|
||
| #[test] | ||
| fn resolve_rejects_malformed_value() { | ||
| let mut args = base_args(); | ||
| args.map = vec!["a.example.com=b.edgecompute.app".into()]; | ||
| args.resolve = vec!["b.edgecompute.app:not-an-ip".into()]; | ||
| let args = parse_args(&[ | ||
| "ts", | ||
| "--map", | ||
| "a.example.com=b.edgecompute.app", | ||
| "--resolve", | ||
| "b.edgecompute.app:not-an-ip", | ||
| ]); | ||
| let err = resolve(&args).expect_err("a non-IP --resolve target should error"); | ||
| assert!( | ||
| matches!(err.current_context(), ConfigError::Resolve { .. }), | ||
|
|
@@ -483,20 +503,24 @@ mod tests { | |
|
|
||
| #[test] | ||
| fn map_value_must_be_from_equals_to() { | ||
| let mut args = base_args(); | ||
| args.map = vec!["not-a-map".into()]; | ||
| let args = parse_args(&["ts", "--map", "not-a-map"]); | ||
| assert!(resolve(&args).is_err(), "malformed --map errors"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn basic_auth_on_non_loopback_listen_is_rejected() { | ||
| // Injected Basic auth on a non-loopback bind would expose the upstream | ||
| // credentials to any reachable network client. | ||
| let mut args = base_args(); | ||
| args.map = vec!["a.example.com=b.edgecompute.app".into()]; | ||
| args.listen = "0.0.0.0:18080".into(); | ||
| args.allow_non_loopback = true; | ||
| args.basic_auth = Some("dev:secret".into()); | ||
| let args = parse_args(&[ | ||
| "ts", | ||
| "--map", | ||
| "a.example.com=b.edgecompute.app", | ||
| "--listen", | ||
| "0.0.0.0:18080", | ||
| "--allow-non-loopback", | ||
| "--basic-auth", | ||
| "dev:secret", | ||
| ]); | ||
| let err = | ||
| resolve(&args).expect_err("non-loopback listen with --basic-auth should be rejected"); | ||
| assert!( | ||
|
|
@@ -508,7 +532,14 @@ mod tests { | |
| ); | ||
|
|
||
| // The same non-loopback bind without credentials is allowed. | ||
| args.basic_auth = None; | ||
| let args = parse_args(&[ | ||
| "ts", | ||
| "--map", | ||
| "a.example.com=b.edgecompute.app", | ||
| "--listen", | ||
| "0.0.0.0:18080", | ||
| "--allow-non-loopback", | ||
| ]); | ||
| assert!( | ||
| resolve(&args).is_ok(), | ||
| "non-loopback without --basic-auth is allowed" | ||
|
|
@@ -518,8 +549,7 @@ mod tests { | |
| #[test] | ||
| fn invalid_from_host_is_rejected() { | ||
| // A FROM with characters that would break the PAC JS / Host header. | ||
| let mut args = base_args(); | ||
| args.map = vec!["bad\"host=to.edgecompute.app".into()]; | ||
| let args = parse_args(&["ts", "--map", "bad\"host=to.edgecompute.app"]); | ||
| let err = resolve(&args).expect_err("a malformed FROM host should error"); | ||
| assert!( | ||
| matches!(err.current_context(), ConfigError::InvalidFrom { .. }), | ||
|
|
@@ -529,14 +559,25 @@ mod tests { | |
|
|
||
| #[test] | ||
| fn non_loopback_listen_requires_flag() { | ||
| let mut args = base_args(); | ||
| args.map = vec!["a.example.com=b.edgecompute.app".into()]; | ||
| args.listen = "0.0.0.0:18080".into(); | ||
| let args = parse_args(&[ | ||
| "ts", | ||
| "--map", | ||
| "a.example.com=b.edgecompute.app", | ||
| "--listen", | ||
| "0.0.0.0:18080", | ||
| ]); | ||
| assert!( | ||
| resolve(&args).is_err(), | ||
| "non-loopback without flag is rejected" | ||
| ); | ||
| args.allow_non_loopback = true; | ||
| let args = parse_args(&[ | ||
| "ts", | ||
| "--map", | ||
| "a.example.com=b.edgecompute.app", | ||
| "--listen", | ||
| "0.0.0.0:18080", | ||
| "--allow-non-loopback", | ||
| ]); | ||
| assert!(resolve(&args).is_ok(), "non-loopback allowed with flag"); | ||
| } | ||
|
|
||
|
|
@@ -559,11 +600,15 @@ mod tests { | |
|
|
||
| #[test] | ||
| fn resolve_precomputes_typed_rule_identity_and_headers() { | ||
| let mut args = base_args(); | ||
| args.map = vec!["www.example.com=TO.Example.com:8443".into()]; | ||
| args.rewrite_host = true; | ||
| args.insecure = true; | ||
| args.resolve = vec!["to.example.com:192.0.2.10".into()]; | ||
| let args = parse_args(&[ | ||
| "ts", | ||
| "--map", | ||
| "www.example.com=TO.Example.com:8443", | ||
| "--rewrite-host", | ||
| "--insecure", | ||
| "--resolve", | ||
| "to.example.com:192.0.2.10", | ||
| ]); | ||
|
|
||
| let cfg = resolve(&args).expect("should resolve"); | ||
| let rule = cfg | ||
|
|
@@ -602,10 +647,7 @@ mod tests { | |
|
|
||
| #[test] | ||
| fn resolve_keeps_ip_reference_identities_http1_only() { | ||
| let mut args = base_args(); | ||
| args.map = vec!["www.example.com=127.0.0.1".into()]; | ||
| args.rewrite_host = true; | ||
|
|
||
| let args = parse_args(&["ts", "--map", "www.example.com=127.0.0.1", "--rewrite-host"]); | ||
| let cfg = resolve(&args).expect("should resolve"); | ||
| let rule = cfg | ||
| .rules | ||
|
|
@@ -655,9 +697,13 @@ mod tests { | |
| let dir = tempfile::tempdir().expect("should create temp dir"); | ||
| let missing = dir.path().join("no-such-file.txt"); | ||
|
|
||
| let mut args = base_args(); | ||
| args.map = vec!["a.example.com=b.edgecompute.app".into()]; | ||
| args.basic_auth_file = Some(missing.to_string_lossy().into_owned()); | ||
| let args = parse_args(&[ | ||
| "ts", | ||
| "--map", | ||
| "a.example.com=b.edgecompute.app", | ||
| "--basic-auth-file", | ||
| &missing.to_string_lossy(), | ||
| ]); | ||
|
|
||
| let err = resolve(&args).expect_err("should fail when file is missing"); | ||
| assert!( | ||
|
|
@@ -666,9 +712,19 @@ mod tests { | |
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| #[should_panic(expected = "DisplayHelpOnMissingArgumentOrSubcommand")] | ||
| fn bare_invocation_is_rejected_at_parse_time() { | ||
| // `arg_required_else_help` makes a fully-bare `ts` fail to parse at all, | ||
| // before `resolve` (and its `NoRule` check) ever runs. | ||
| parse_args(&["ts"]); | ||
| } | ||
|
Comment on lines
+715
to
+721
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔧 wrench — This assertion is pinned to clap's private internal representation, not its public API.
Two consequences:
Credit where due: the substring is specific enough to discriminate the error kind — I verified that mutating the call to This PR already does the same assertion the right way, with clap's public typed API, in 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"
);Applying that pattern here needs a fallible sibling to fn parse_args(argv: &[&str]) -> crate::commands::dev::proxy::ProxyArgs {
try_parse_args(argv).expect("should parse proxy args")
}
fn try_parse_args(
argv: &[&str],
) -> Result<crate::commands::dev::proxy::ProxyArgs, clap::Error> {
#[derive(clap::Parser)]
struct W {
#[command(flatten)]
a: crate::commands::dev::proxy::ProxyArgs,
}
W::try_parse_from(argv).map(|w| w.a)
}
#[test]
fn bare_invocation_is_rejected_at_parse_time() {
// `arg_required_else_help` makes a fully-bare `ts` fail to parse at all,
// before `resolve` (and its `NoRule` check) ever runs.
let error = try_parse_args(&["ts"])
.expect_err("a fully-bare invocation should short-circuit to help");
assert_eq!(
error.kind(),
clap::error::ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand,
"should short-circuit to help rather than reaching resolve"
);
}Note I verified this in a scratch worktree: Apply manually — the change spans two hunks in this file (the |
||
|
|
||
| #[test] | ||
| fn no_rule_passed_is_a_no_rule_error() { | ||
| let args = base_args(); | ||
| // An invocation with some other flag but no rule still reaches | ||
| // `resolve`: `arg_required_else_help` only rejects a fully-bare `ts`. | ||
| let args = parse_args(&["ts", "--insecure"]); | ||
| let err = resolve(&args).expect_err("should error when no rule is passed"); | ||
| assert!( | ||
| matches!(err.current_context(), ConfigError::NoRule), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -77,7 +77,8 @@ shorthand, or one or more `--map FROM=TO` rules: | |
| ts dev proxy -f www.example-publisher.com -t trusted-server-example.edgecompute.app | ||
| ``` | ||
|
|
||
| With no `--map`/`-f`/`-t`, the proxy exits with | ||
| A bare `ts dev proxy` prints help and exits before proxy startup. An | ||
| invocation with explicit options but no complete rewrite rule reports | ||
| `no rewrite rule: pass --map FROM=TO (or -f/--from with -t/--to)`. | ||
|
Comment on lines
+80
to
82
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤔 thinking — Correcting this paragraph was the right call, and the bare-invocation half is now accurate. The second half is where I'd push slightly. As shipped, an invocation with explicit options but no complete rule does more than "report So this text describes the intended end state rather than current behaviour. If the two blocking findings land in this PR, it becomes accurate exactly as written and needs no edit. If either is deferred, I'd soften it here so the guide doesn't promise output the binary doesn't produce yet. No change requested on its own — it just resolves differently depending on how you decide the blocking findings. |
||
|
|
||
| Connection options — `--rewrite-host`, `--basic-auth`/`--basic-auth-file`, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 note —
ffd5bf8ff's commit message says it "fixes two invalid--rewrite-host true/falseassertions (that flag takes no value)", but I can't find anything in the diff that corresponds to that.I checked every commit on the branch plus the whole tree at head for
--rewrite-hostfollowed by a value, and there are no matches anywhere — not in the base, not in any intermediate commit, not now. What actually changed in this test is the absent case:!base_args().rewrite_hostbecame!parse_args(&["ts", "--from", ..., "--to", ...]).rewrite_host. Both forms pass--rewrite-hostas the bare flag it is.No code problem here, and the test itself is fine. Flagging it only because it is the same class of drift as the
{report:#}finding: a message claiming a change the diff does not contain. Since this message is what lands inmain's history, it is worth amending so a future bisector isn't hunting for a fix that was never made.