use std::path::{Path, PathBuf};
use std::process::Command;
fn binary() -> PathBuf {
env!("CARGO_BIN_EXE_socket-patch").into()
}
fn run_setup(cwd: &Path, extra: &[&str]) -> (i32, String) {
let mut args = vec!["setup", "--json"];
args.extend_from_slice(extra);
let out = Command::new(binary())
.args(&args)
.current_dir(cwd)
.env_remove("SOCKET_API_TOKEN")
.output()
.expect("run socket-patch");
(
out.status.code().unwrap_or(-1),
String::from_utf8_lossy(&out.stdout).to_string(),
)
}
fn write(path: &Path, content: &str) {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).expect("create parent");
}
std::fs::write(path, content).expect("write file");
}
#[test]
fn setup_no_package_json_emits_no_files_status() {
let tmp = tempfile::tempdir().expect("tempdir");
let (code, stdout) = run_setup(tmp.path(), &[]);
assert_eq!(code, 0, "no files should still exit 0; stdout=\n{stdout}");
let v: serde_json::Value = serde_json::from_str(&stdout).expect("valid JSON");
assert_eq!(v["status"], "no_files");
assert_eq!(v["updated"], 0);
assert_eq!(v["alreadyConfigured"], 0);
assert_eq!(v["errors"], 0);
}
#[test]
fn setup_dry_run_does_not_modify_package_json() {
let tmp = tempfile::tempdir().expect("tempdir");
let pkg = tmp.path().join("package.json");
let original = r#"{
"name": "test-proj",
"version": "1.0.0"
}
"#;
write(&pkg, original);
let (code, stdout) = run_setup(tmp.path(), &["--dry-run"]);
assert_eq!(code, 0, "dry-run should succeed; stdout=\n{stdout}");
let v: serde_json::Value = serde_json::from_str(&stdout).expect("valid JSON");
assert_eq!(v["status"], "dry_run");
assert_eq!(v["dryRun"], true);
assert_eq!(v["wouldUpdate"], 1);
let after = std::fs::read_to_string(&pkg).expect("read package.json");
assert_eq!(after, original, "dry-run must not modify package.json");
}
#[test]
fn setup_yes_writes_postinstall_script() {
let tmp = tempfile::tempdir().expect("tempdir");
let pkg = tmp.path().join("package.json");
write(
&pkg,
r#"{ "name": "test-proj", "version": "1.0.0" }
"#,
);
let (code, stdout) = run_setup(tmp.path(), &["--yes"]);
assert_eq!(code, 0, "setup should succeed; stdout=\n{stdout}");
let v: serde_json::Value = serde_json::from_str(&stdout).expect("valid JSON");
assert_eq!(v["status"], "success");
assert_eq!(v["updated"], 1);
let after = std::fs::read_to_string(&pkg).expect("read package.json");
let parsed: serde_json::Value = serde_json::from_str(&after).expect("valid package.json");
let postinstall = parsed["scripts"]["postinstall"]
.as_str()
.expect("postinstall script must be set");
assert!(
postinstall.contains("socket-patch"),
"postinstall must invoke socket-patch; got: {postinstall}"
);
}
#[test]
fn setup_already_configured_returns_idempotent_status() {
let tmp = tempfile::tempdir().expect("tempdir");
let pkg = tmp.path().join("package.json");
write(
&pkg,
r#"{ "name": "test-proj", "version": "1.0.0" }
"#,
);
let (code1, _) = run_setup(tmp.path(), &["--yes"]);
assert_eq!(code1, 0);
let (code2, stdout2) = run_setup(tmp.path(), &["--yes"]);
assert_eq!(code2, 0, "second run should succeed; stdout=\n{stdout2}");
let v: serde_json::Value = serde_json::from_str(&stdout2).expect("valid JSON");
assert_eq!(v["status"], "already_configured");
assert_eq!(v["updated"], 0);
assert_eq!(v["alreadyConfigured"], 1);
}
#[test]
fn setup_detects_pnpm_from_lockfile() {
let tmp = tempfile::tempdir().expect("tempdir");
write(
&tmp.path().join("package.json"),
r#"{ "name": "test-proj", "version": "1.0.0" }
"#,
);
write(&tmp.path().join("pnpm-lock.yaml"), "lockfileVersion: '9.0'\n");
let (code, stdout) = run_setup(tmp.path(), &["--yes"]);
assert_eq!(code, 0, "setup should succeed; stdout=\n{stdout}");
let v: serde_json::Value = serde_json::from_str(&stdout).expect("valid JSON");
assert_eq!(v["packageManager"], "pnpm");
let after = std::fs::read_to_string(tmp.path().join("package.json")).unwrap();
assert!(
after.contains("pnpm dlx"),
"pnpm projects should use `pnpm dlx`; got: {after}"
);
}
#[test]
fn setup_defaults_to_npm_when_no_lockfile() {
let tmp = tempfile::tempdir().expect("tempdir");
write(
&tmp.path().join("package.json"),
r#"{ "name": "test-proj", "version": "1.0.0" }
"#,
);
let (_, stdout) = run_setup(tmp.path(), &["--yes"]);
let v: serde_json::Value = serde_json::from_str(&stdout).expect("valid JSON");
assert_eq!(v["packageManager"], "npm");
}
#[test]
fn setup_pnpm_monorepo_only_updates_root() {
let tmp = tempfile::tempdir().expect("tempdir");
write(
&tmp.path().join("package.json"),
r#"{ "name": "monorepo-root", "version": "1.0.0" }
"#,
);
write(
&tmp.path().join("pnpm-lock.yaml"),
"lockfileVersion: '9.0'\n",
);
write(
&tmp.path().join("pnpm-workspace.yaml"),
"packages:\n - 'packages/*'\n",
);
write(
&tmp.path().join("packages/a/package.json"),
r#"{ "name": "a", "version": "1.0.0" }
"#,
);
write(
&tmp.path().join("packages/b/package.json"),
r#"{ "name": "b", "version": "1.0.0" }
"#,
);
let (code, stdout) = run_setup(tmp.path(), &["--yes"]);
assert_eq!(code, 0, "monorepo setup should succeed; stdout=\n{stdout}");
let v: serde_json::Value = serde_json::from_str(&stdout).expect("valid JSON");
assert_eq!(
v["updated"], 1,
"only the root package.json should be touched in a pnpm monorepo"
);
let a = std::fs::read_to_string(tmp.path().join("packages/a/package.json")).unwrap();
assert!(
!a.contains("socket-patch"),
"workspace package.json must not be touched"
);
}
#[test]
fn setup_yes_json_files_entry_has_expected_keys() {
let tmp = tempfile::tempdir().expect("tempdir");
write(
&tmp.path().join("package.json"),
r#"{ "name": "test-proj", "version": "1.0.0" }
"#,
);
let (_, stdout) = run_setup(tmp.path(), &["--yes"]);
let v: serde_json::Value = serde_json::from_str(&stdout).expect("valid JSON");
let files = v["files"].as_array().expect("files array");
assert_eq!(files.len(), 1);
let entry = &files[0];
assert!(entry["path"].is_string());
assert!(entry["status"].is_string());
}
#[test]
fn setup_malformed_package_json_reports_error_and_exits_nonzero() {
let tmp = tempfile::tempdir().expect("tempdir");
write(&tmp.path().join("package.json"), "not valid json!!!");
let (code, stdout) = run_setup(tmp.path(), &["--yes"]);
assert_eq!(code, 1, "a malformed package.json must exit non-zero; stdout=\n{stdout}");
let v: serde_json::Value = serde_json::from_str(&stdout).expect("valid JSON");
assert_eq!(
v["status"], "error",
"must not be reported as already_configured"
);
assert_eq!(v["updated"], 0);
assert_eq!(v["alreadyConfigured"], 0);
assert_eq!(v["errors"], 1);
let files = v["files"].as_array().expect("files array");
assert_eq!(files[0]["status"], "error");
assert!(files[0]["error"].is_string());
}
#[test]
fn setup_malformed_does_not_claim_already_configured_in_human_mode() {
let tmp = tempfile::tempdir().expect("tempdir");
write(&tmp.path().join("package.json"), "not valid json!!!");
let out = Command::new(binary())
.args(["setup", "--yes"])
.current_dir(tmp.path())
.env_remove("SOCKET_API_TOKEN")
.output()
.expect("run socket-patch");
let stdout = String::from_utf8_lossy(&out.stdout);
assert_eq!(out.status.code(), Some(1), "human mode must exit 1; stdout=\n{stdout}");
assert!(
!stdout.contains("already configured with socket-patch"),
"must not falsely claim everything is already configured; stdout=\n{stdout}"
);
}
#[test]
fn setup_dry_run_with_error_exits_nonzero() {
let tmp = tempfile::tempdir().expect("tempdir");
write(
&tmp.path().join("package.json"),
r#"{ "name": "root", "workspaces": ["packages/*"] }
"#,
);
write(&tmp.path().join("packages/a/package.json"), "{bad json");
let (code, stdout) = run_setup(tmp.path(), &["--dry-run"]);
assert_eq!(code, 1, "dry-run with an error must exit non-zero; stdout=\n{stdout}");
let v: serde_json::Value = serde_json::from_str(&stdout).expect("valid JSON");
assert_eq!(v["status"], "dry_run");
assert_eq!(v["errors"], 1);
assert_eq!(v["wouldUpdate"], 1);
let root = std::fs::read_to_string(tmp.path().join("package.json")).unwrap();
assert!(!root.contains("socket-patch"), "dry-run must not modify files");
}
#[test]
fn setup_partial_failure_exits_nonzero_when_applying() {
let tmp = tempfile::tempdir().expect("tempdir");
write(
&tmp.path().join("package.json"),
r#"{ "name": "root", "workspaces": ["packages/*"] }
"#,
);
write(&tmp.path().join("packages/a/package.json"), "{bad json");
let (code, stdout) = run_setup(tmp.path(), &["--yes"]);
assert_eq!(code, 1, "partial failure must exit non-zero; stdout=\n{stdout}");
let v: serde_json::Value = serde_json::from_str(&stdout).expect("valid JSON");
assert_eq!(v["status"], "partial_failure");
assert_eq!(v["updated"], 1);
assert_eq!(v["errors"], 1);
let root = std::fs::read_to_string(tmp.path().join("package.json")).unwrap();
assert!(root.contains("socket-patch"), "valid file should still be updated");
}