use clap::Parser;
use socket_patch_cli::commands::scan::ScanArgs;
use socket_patch_cli::{Cli, Commands};
fn parse_scan(extra: &[&str]) -> ScanArgs {
let mut argv = vec!["socket-patch", "scan"];
argv.extend_from_slice(extra);
let cli = Cli::try_parse_from(&argv).expect("parse");
match cli.command {
Commands::Scan(a) => a,
_ => panic!("expected Scan"),
}
}
fn try_parse_scan(extra: &[&str]) -> Result<ScanArgs, clap::Error> {
let mut argv = vec!["socket-patch", "scan"];
argv.extend_from_slice(extra);
let cli = Cli::try_parse_from(&argv)?;
match cli.command {
Commands::Scan(a) => Ok(a),
_ => panic!("expected Scan"),
}
}
#[test]
fn defaults_match_contract() {
let args = parse_scan(&[]);
assert_eq!(args.batch_size, 100, "--batch-size default is 100");
assert_eq!(
args.common.download_mode, "diff",
"--download-mode default is \"diff\""
);
assert_eq!(args.common.cwd, std::path::PathBuf::from("."));
assert_eq!(args.common.org, None);
assert!(!args.common.json);
assert!(!args.common.yes);
assert!(!args.common.global);
assert_eq!(args.common.global_prefix, None);
assert_eq!(args.common.api_url, "https://api.socket.dev");
assert_eq!(args.common.api_token, None);
assert_eq!(args.common.ecosystems, None);
assert!(!args.apply, "--apply default is false (scan --json stays read-only)");
assert!(!args.prune, "--prune default is false (GC is opt-in in v3.0)");
assert!(!args.sync, "--sync default is false");
assert!(!args.common.dry_run, "--dry-run default is false");
assert!(
!args.all_releases,
"--all-releases default is false (narrow — installed-dist variant only)"
);
assert_eq!(args.vex.vex, None);
assert_eq!(args.vex.vex_product, None);
assert!(!args.vex.vex_no_verify);
assert_eq!(args.vex.vex_doc_id, None);
assert!(!args.vex.vex_compact);
}
#[test]
fn vex_path_sets_output() {
assert_eq!(
parse_scan(&["--vex", "out.vex.json"]).vex.vex,
Some(std::path::PathBuf::from("out.vex.json"))
);
}
#[test]
fn vex_passthrough_flags() {
let args = parse_scan(&[
"--vex",
"out.vex.json",
"--vex-product",
"pkg:npm/app@1.0.0",
"--vex-no-verify",
"--vex-doc-id",
"urn:uuid:fixed",
"--vex-compact",
]);
assert_eq!(args.vex.vex, Some(std::path::PathBuf::from("out.vex.json")));
assert_eq!(args.vex.vex_product.as_deref(), Some("pkg:npm/app@1.0.0"));
assert!(args.vex.vex_no_verify);
assert_eq!(args.vex.vex_doc_id.as_deref(), Some("urn:uuid:fixed"));
assert!(args.vex.vex_compact);
}
#[test]
fn all_releases_flag_long_form() {
let args = parse_scan(&["--all-releases"]);
assert!(args.all_releases);
}
#[test]
fn yes_short_flag() {
let args = parse_scan(&["-y"]);
assert!(args.common.yes);
}
#[test]
fn yes_long_flag() {
let args = parse_scan(&["--yes"]);
assert!(args.common.yes);
}
#[test]
fn global_short_flag() {
let args = parse_scan(&["-g"]);
assert!(args.common.global);
}
#[test]
fn global_long_flag() {
let args = parse_scan(&["--global"]);
assert!(args.common.global);
}
#[test]
fn cwd_flag() {
let args = parse_scan(&["--cwd", "/tmp/x"]);
assert_eq!(args.common.cwd, std::path::PathBuf::from("/tmp/x"));
}
#[test]
fn org_flag() {
let args = parse_scan(&["--org", "myorg"]);
assert_eq!(args.common.org.as_deref(), Some("myorg"));
}
#[test]
fn json_flag() {
let args = parse_scan(&["--json"]);
assert!(args.common.json);
}
#[test]
fn global_prefix_flag() {
let args = parse_scan(&["--global-prefix", "/foo"]);
assert_eq!(args.common.global_prefix, Some(std::path::PathBuf::from("/foo")));
}
#[test]
fn api_url_flag() {
let args = parse_scan(&["--api-url", "https://api"]);
assert_eq!(args.common.api_url, "https://api");
}
#[test]
fn api_token_flag() {
let args = parse_scan(&["--api-token", "tok"]);
assert_eq!(args.common.api_token.as_deref(), Some("tok"));
}
#[test]
fn batch_size_500() {
let args = parse_scan(&["--batch-size", "500"]);
assert_eq!(args.batch_size, 500);
}
#[test]
fn batch_size_1() {
let args = parse_scan(&["--batch-size", "1"]);
assert_eq!(args.batch_size, 1);
}
#[test]
fn batch_size_0_parses() {
let args = parse_scan(&["--batch-size", "0"]);
assert_eq!(args.batch_size, 0);
}
#[test]
fn batch_size_negative_fails() {
let err = match try_parse_scan(&["--batch-size=-1"]) {
Ok(_) => panic!("negative batch-size should fail to parse"),
Err(e) => e,
};
let kind = err.kind();
assert!(
matches!(
kind,
clap::error::ErrorKind::ValueValidation | clap::error::ErrorKind::InvalidValue
),
"expected ValueValidation or InvalidValue, got {:?}",
kind
);
}
#[test]
fn ecosystems_csv_multi() {
let args = parse_scan(&["--ecosystems", "npm,pypi,cargo,maven"]);
assert_eq!(
args.common.ecosystems,
Some(vec![
"npm".to_string(),
"pypi".to_string(),
"cargo".to_string(),
"maven".to_string(),
])
);
}
#[test]
fn ecosystems_csv_single() {
let args = parse_scan(&["--ecosystems", "npm"]);
assert_eq!(args.common.ecosystems, Some(vec!["npm".to_string()]));
}
#[test]
fn download_mode_diff() {
let args = parse_scan(&["--download-mode", "diff"]);
assert_eq!(args.common.download_mode, "diff");
}
#[test]
fn download_mode_package() {
let args = parse_scan(&["--download-mode", "package"]);
assert_eq!(args.common.download_mode, "package");
}
#[test]
fn download_mode_file() {
let args = parse_scan(&["--download-mode", "file"]);
assert_eq!(args.common.download_mode, "file");
}
#[test]
fn unknown_flag_fails() {
let err = match try_parse_scan(&["--not-a-real-flag"]) {
Ok(_) => panic!("unknown flag should fail to parse"),
Err(e) => e,
};
assert_eq!(err.kind(), clap::error::ErrorKind::UnknownArgument);
}
#[test]
fn apply_flag_long_form() {
let args = parse_scan(&["--apply"]);
assert!(args.apply);
}
#[test]
fn apply_flag_combines_with_json_and_yes() {
let args = parse_scan(&["--apply", "--json", "--yes"]);
assert!(args.apply);
assert!(args.common.json);
assert!(args.common.yes);
}
#[test]
fn prune_flag_long_form() {
let args = parse_scan(&["--prune"]);
assert!(args.prune);
}
#[test]
fn prune_combines_with_apply_and_json() {
let args = parse_scan(&["--apply", "--json", "--yes", "--prune"]);
assert!(args.apply);
assert!(args.common.json);
assert!(args.common.yes);
assert!(args.prune);
}
#[test]
fn sync_flag_long_form() {
let args = parse_scan(&["--sync"]);
assert!(args.sync);
assert!(!args.apply);
assert!(!args.prune);
}
#[test]
fn sync_combines_with_json_and_yes() {
let args = parse_scan(&["--json", "--sync", "--yes"]);
assert!(args.common.json);
assert!(args.sync);
assert!(args.common.yes);
}
#[test]
fn dry_run_long_form() {
let args = parse_scan(&["--dry-run"]);
assert!(args.common.dry_run);
}
#[test]
fn scan_json_empty_cwd_emits_updates_key() {
let bin = env!("CARGO_BIN_EXE_socket-patch");
let tmp = tempfile::tempdir().expect("tempdir");
let out = std::process::Command::new(bin)
.args(["scan", "--json", "--cwd"])
.arg(tmp.path())
.env_remove("SOCKET_API_TOKEN")
.env_remove("SOCKET_API_URL")
.output()
.expect("spawn socket-patch");
assert_eq!(
out.status.code(),
Some(0),
"stdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
let v: serde_json::Value =
serde_json::from_slice(&out.stdout).expect("scan emitted valid JSON");
assert_eq!(v["status"], "success");
assert_eq!(v["scannedPackages"], 0);
assert_eq!(v["packagesWithPatches"], 0);
assert_eq!(v["totalPatches"], 0);
assert!(
v["packages"].is_array(),
"packages must be an array, got {}",
v["packages"]
);
assert!(
v["updates"].is_array(),
"updates key must be present and an array — locks contract",
);
assert_eq!(
v["updates"].as_array().unwrap().len(),
0,
"updates is empty when no packages were scanned"
);
}