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),
("--break-lock", None),
("--lock-timeout", Some("30")),
]
}
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]
#[serial_test::serial]
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_LOCK_TIMEOUT", "30"),
("SOCKET_BREAK_LOCK", "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_eq!(args.common.lock_timeout, Some(30));
assert!(args.common.break_lock);
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),
}
}
}
#[test]
#[serial_test::serial]
fn bool_env_vars_accept_one_and_yes() {
let cases: &[(&str, &str)] = &[
("SOCKET_OFFLINE", "1"),
("SOCKET_GLOBAL", "yes"),
("SOCKET_JSON", "on"),
("SOCKET_VERBOSE", "1"),
("SOCKET_SILENT", "y"),
("SOCKET_DRY_RUN", "1"),
("SOCKET_YES", "yes"),
("SOCKET_BREAK_LOCK", "1"),
("SOCKET_DEBUG", "1"),
("SOCKET_TELEMETRY_DISABLED", "1"),
];
let saved: Vec<(String, Option<String>)> = cases
.iter()
.map(|(k, _)| (k.to_string(), std::env::var(k).ok()))
.collect();
for (k, v) in cases {
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!(args.common.offline, "SOCKET_OFFLINE=1 must parse as true");
assert!(args.common.global, "SOCKET_GLOBAL=yes must parse as true");
assert!(args.common.json, "SOCKET_JSON=on must parse as true");
assert!(args.common.verbose, "SOCKET_VERBOSE=1 must parse as true");
assert!(args.common.silent, "SOCKET_SILENT=y must parse as true");
assert!(args.common.dry_run, "SOCKET_DRY_RUN=1 must parse as true");
assert!(args.common.yes, "SOCKET_YES=yes must parse as true");
assert!(args.common.break_lock, "SOCKET_BREAK_LOCK=1 must parse as true");
assert!(args.common.debug, "SOCKET_DEBUG=1 must parse as true");
assert!(
args.common.no_telemetry,
"SOCKET_TELEMETRY_DISABLED=1 must parse as true"
);
} 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),
}
}
}
#[test]
#[serial_test::serial]
fn bool_env_vars_reject_zero_and_falsey() {
let cases: &[(&str, &str)] = &[
("SOCKET_OFFLINE", "0"),
("SOCKET_DEBUG", "false"),
("SOCKET_TELEMETRY_DISABLED", "no"),
("SOCKET_JSON", "off"),
];
let saved: Vec<(String, Option<String>)> = cases
.iter()
.map(|(k, _)| (k.to_string(), std::env::var(k).ok()))
.collect();
for (k, v) in cases {
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!(!args.common.offline);
assert!(!args.common.debug);
assert!(!args.common.no_telemetry);
assert!(!args.common.json);
} 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),
}
}
}
const GLOBAL_ENV_VARS: &[&str] = &[
"SOCKET_CWD",
"SOCKET_MANIFEST_PATH",
"SOCKET_API_URL",
"SOCKET_API_TOKEN",
"SOCKET_ORG_SLUG",
"SOCKET_PROXY_URL",
"SOCKET_ECOSYSTEMS",
"SOCKET_DOWNLOAD_MODE",
"SOCKET_OFFLINE",
"SOCKET_GLOBAL",
"SOCKET_GLOBAL_PREFIX",
"SOCKET_JSON",
"SOCKET_VERBOSE",
"SOCKET_SILENT",
"SOCKET_DRY_RUN",
"SOCKET_YES",
"SOCKET_LOCK_TIMEOUT",
"SOCKET_BREAK_LOCK",
"SOCKET_DEBUG",
"SOCKET_TELEMETRY_DISABLED",
];
fn save_and_clear_global_env() -> Vec<(&'static str, Option<String>)> {
let saved: Vec<(&'static str, Option<String>)> = GLOBAL_ENV_VARS
.iter()
.map(|&k| (k, std::env::var(k).ok()))
.collect();
for &k in GLOBAL_ENV_VARS {
std::env::remove_var(k);
}
saved
}
fn restore_global_env(saved: Vec<(&'static str, Option<String>)>) {
for (k, orig) in saved {
match orig {
Some(v) => std::env::set_var(k, v),
None => std::env::remove_var(k),
}
}
}
#[test]
#[serial_test::serial]
fn cli_arg_overrides_env_var() {
let saved = save_and_clear_global_env();
std::env::set_var("SOCKET_API_URL", "https://env-api.example.com");
let cli = Cli::try_parse_from([
"socket-patch",
"list",
"--api-url",
"https://cli-api.example.com",
])
.expect("parse");
let socket_patch_cli::Commands::List(args) = cli.command else {
panic!("expected List");
};
assert_eq!(
args.common.api_url, "https://cli-api.example.com",
"CLI --api-url must override SOCKET_API_URL"
);
let cli = Cli::try_parse_from(["socket-patch", "list"]).expect("parse");
let socket_patch_cli::Commands::List(args) = cli.command else {
panic!("expected List");
};
assert_eq!(
args.common.api_url, "https://env-api.example.com",
"with no CLI flag the env var must resolve through"
);
std::env::set_var("SOCKET_OFFLINE", "0");
let cli = Cli::try_parse_from(["socket-patch", "list", "--offline"]).expect("parse");
let socket_patch_cli::Commands::List(args) = cli.command else {
panic!("expected List");
};
assert!(
args.common.offline,
"CLI --offline must win over SOCKET_OFFLINE=0"
);
restore_global_env(saved);
}
#[test]
#[serial_test::serial]
fn production_defaults_populate_when_unset() {
let saved = save_and_clear_global_env();
let cli = Cli::try_parse_from(["socket-patch", "list"]).expect("parse");
let socket_patch_cli::Commands::List(args) = cli.command else {
panic!("expected List");
};
let c = &args.common;
assert_eq!(c.cwd, std::path::PathBuf::from("."));
assert_eq!(c.manifest_path, ".socket/manifest.json");
assert_eq!(c.api_url, "https://api.socket.dev");
assert_eq!(c.proxy_url, "https://patches-api.socket.dev");
assert_eq!(c.download_mode, "diff");
assert!(c.api_token.is_none());
assert!(c.org.is_none());
assert!(c.ecosystems.is_none());
assert!(!c.offline && !c.global && !c.json && !c.verbose && !c.silent);
assert!(!c.dry_run && !c.yes && !c.break_lock && !c.debug && !c.no_telemetry);
assert!(c.lock_timeout.is_none());
assert!(c.global_prefix.is_none());
let o = c.api_client_overrides();
assert_eq!(o.api_url.as_deref(), Some("https://api.socket.dev"));
assert_eq!(o.proxy_url.as_deref(), Some("https://patches-api.socket.dev"));
assert!(o.api_token.is_none());
assert!(o.org_slug.is_none());
restore_global_env(saved);
}