use clap::Parser;
use socket_patch_cli::Cli;
const SUBCOMMANDS_NO_POSITIONAL: &[&str] = &[
"apply", "list", "scan", "setup", "repair", "rollback",
];
const SUBCOMMANDS_WITH_IDENTIFIER: &[&str] = &["get", "remove"];
const DUMMY_IDENTIFIER: &str = "80630680-4da6-45f9-bba8-b888e0ffd58c";
fn global_flag_cases() -> Vec<(&'static str, Option<&'static str>)> {
vec![
("--cwd", Some("/tmp")),
("--manifest-path", Some("custom.json")),
("--api-url", Some("https://example.com")),
("--api-token", Some("tok123")),
("--org", Some("acme")),
("--proxy-url", Some("https://proxy.example.com")),
("--ecosystems", Some("npm,pypi")),
("--download-mode", Some("diff")),
("--offline", None),
("--global", None),
("--global-prefix", Some("/opt/global")),
("--json", None),
("--verbose", None),
("--silent", None),
("--dry-run", None),
("--yes", None),
("--debug", None),
("--no-telemetry", None),
]
}
fn try_parse(subcommand: &str, extra: &[&str]) -> Result<Cli, clap::Error> {
let mut argv: Vec<String> = vec!["socket-patch".into(), subcommand.into()];
if SUBCOMMANDS_WITH_IDENTIFIER.contains(&subcommand) {
argv.push(DUMMY_IDENTIFIER.into());
}
for &arg in extra {
argv.push(arg.into());
}
Cli::try_parse_from(&argv)
}
#[test]
fn every_global_flag_parses_on_every_subcommand() {
let cases = global_flag_cases();
let all_subcommands: Vec<&str> = SUBCOMMANDS_NO_POSITIONAL
.iter()
.chain(SUBCOMMANDS_WITH_IDENTIFIER.iter())
.copied()
.collect();
for &subcommand in &all_subcommands {
for &(flag, value) in &cases {
let extra: Vec<&str> = if let Some(v) = value {
vec![flag, v]
} else {
vec![flag]
};
let result = try_parse(subcommand, &extra);
assert!(
result.is_ok(),
"subcommand `{}` failed to parse global flag `{}`: {}",
subcommand,
flag,
result.err().map(|e| e.to_string()).unwrap_or_default(),
);
}
}
}
#[test]
fn every_global_short_form_parses_on_every_subcommand() {
let shorts: &[(&str, bool)] = &[
("-o", true), ("-e", true), ("-g", false), ("-j", false), ("-v", false), ("-s", false), ("-y", false), ];
let all_subcommands: Vec<&str> = SUBCOMMANDS_NO_POSITIONAL
.iter()
.chain(SUBCOMMANDS_WITH_IDENTIFIER.iter())
.copied()
.collect();
for &subcommand in &all_subcommands {
for &(short, needs_value) in shorts {
let extra: Vec<&str> = if needs_value {
vec![short, "value"]
} else {
vec![short]
};
let result = try_parse(subcommand, &extra);
assert!(
result.is_ok(),
"subcommand `{}` failed to parse short flag `{}`: {}",
subcommand,
short,
result.err().map(|e| e.to_string()).unwrap_or_default(),
);
}
}
}
#[test]
fn reserved_short_forms_are_not_assigned() {
let all_subcommands: Vec<&str> = SUBCOMMANDS_NO_POSITIONAL
.iter()
.chain(SUBCOMMANDS_WITH_IDENTIFIER.iter())
.copied()
.collect();
for &subcommand in &all_subcommands {
for short in ["-d", "-m"] {
let result = try_parse(subcommand, &[short]);
assert!(
result.is_err(),
"`{}` should NOT accept the reserved short `{}` — \
if you bound it intentionally, update this test and \
the corresponding `--help` docs.",
subcommand,
short,
);
let err = result.err().unwrap();
assert_eq!(
err.kind(),
clap::error::ErrorKind::UnknownArgument,
"expected UnknownArgument when `{}` is passed to `{}`; got {:?}",
short,
subcommand,
err.kind(),
);
}
}
}
#[test]
fn env_vars_populate_global_args() {
let pairs = [
("SOCKET_CWD", "/env/cwd"),
("SOCKET_MANIFEST_PATH", "env-manifest.json"),
("SOCKET_API_URL", "https://env-api.example.com"),
("SOCKET_API_TOKEN", "env-token"),
("SOCKET_ORG_SLUG", "env-org"),
("SOCKET_PROXY_URL", "https://env-proxy.example.com"),
("SOCKET_ECOSYSTEMS", "npm,maven"),
("SOCKET_DOWNLOAD_MODE", "package"),
("SOCKET_OFFLINE", "true"),
("SOCKET_GLOBAL", "true"),
("SOCKET_GLOBAL_PREFIX", "/env/global"),
("SOCKET_JSON", "true"),
("SOCKET_VERBOSE", "true"),
("SOCKET_SILENT", "true"),
("SOCKET_DRY_RUN", "true"),
("SOCKET_YES", "true"),
("SOCKET_DEBUG", "true"),
("SOCKET_TELEMETRY_DISABLED", "true"),
];
let saved: Vec<(String, Option<String>)> = pairs
.iter()
.map(|(k, _)| (k.to_string(), std::env::var(k).ok()))
.collect();
for (k, v) in &pairs {
std::env::set_var(k, v);
}
let cli = Cli::try_parse_from(["socket-patch", "list"]).expect("parse");
if let socket_patch_cli::Commands::List(args) = cli.command {
assert_eq!(args.common.cwd, std::path::PathBuf::from("/env/cwd"));
assert_eq!(args.common.manifest_path, "env-manifest.json");
assert_eq!(args.common.api_url, "https://env-api.example.com");
assert_eq!(args.common.api_token.as_deref(), Some("env-token"));
assert_eq!(args.common.org.as_deref(), Some("env-org"));
assert_eq!(args.common.proxy_url, "https://env-proxy.example.com");
assert_eq!(
args.common.ecosystems.as_deref(),
Some(&["npm".to_string(), "maven".to_string()][..])
);
assert_eq!(args.common.download_mode, "package");
assert!(args.common.offline);
assert!(args.common.global);
assert_eq!(
args.common.global_prefix,
Some(std::path::PathBuf::from("/env/global"))
);
assert!(args.common.json);
assert!(args.common.verbose);
assert!(args.common.silent);
assert!(args.common.dry_run);
assert!(args.common.yes);
assert!(args.common.debug);
assert!(args.common.no_telemetry);
} else {
panic!("expected List");
}
for (k, orig) in saved {
match orig {
Some(v) => std::env::set_var(&k, v),
None => std::env::remove_var(&k),
}
}
}