use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use sha2::{Digest, Sha256};
const NPM_UUID: &str = "80630680-4da6-45f9-bba8-b888e0ffd58c";
const NPM_PURL: &str = "pkg:npm/minimist@1.2.2";
const BEFORE_HASH: &str = "311f1e893e6eac502693fad8617dcf5353a043ccc0f7b4ba9fe385e838b67a10";
const AFTER_HASH: &str = "043f04d19e884aa5f8371428718d2a3f27a0d231afe77a2620ac6312f80aaa28";
fn binary() -> PathBuf {
env!("CARGO_BIN_EXE_socket-patch").into()
}
fn has_command(cmd: &str) -> bool {
Command::new(cmd)
.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok()
}
fn git_sha256(content: &[u8]) -> String {
let header = format!("blob {}\0", content.len());
let mut hasher = Sha256::new();
hasher.update(header.as_bytes());
hasher.update(content);
hex::encode(hasher.finalize())
}
fn git_sha256_file(path: &Path) -> String {
let content = std::fs::read(path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
git_sha256(&content)
}
fn run(cwd: &Path, args: &[&str]) -> (i32, String, String) {
let out: Output = Command::new(binary())
.args(args)
.current_dir(cwd)
.env_remove("SOCKET_API_TOKEN") .output()
.expect("failed to execute socket-patch binary");
let code = out.status.code().unwrap_or(-1);
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
(code, stdout, stderr)
}
fn assert_run_ok(cwd: &Path, args: &[&str], context: &str) -> (String, String) {
let (code, stdout, stderr) = run(cwd, args);
assert_eq!(
code, 0,
"{context} failed (exit {code}).\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
(stdout, stderr)
}
fn npm_run(cwd: &Path, args: &[&str]) {
let out = Command::new("npm")
.args(args)
.current_dir(cwd)
.output()
.expect("failed to run npm");
assert!(
out.status.success(),
"npm {args:?} failed (exit {:?}).\nstdout:\n{}\nstderr:\n{}",
out.status.code(),
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
}
fn write_package_json(cwd: &Path) {
std::fs::write(
cwd.join("package.json"),
r#"{"name":"e2e-test","version":"0.0.0","private":true}"#,
)
.expect("write package.json");
}
#[test]
#[ignore]
fn test_npm_full_lifecycle() {
if !has_command("npm") {
eprintln!("SKIP: npm not found on PATH");
return;
}
let dir = tempfile::tempdir().unwrap();
let cwd = dir.path();
write_package_json(cwd);
npm_run(cwd, &["install", "minimist@1.2.2"]);
let index_js = cwd.join("node_modules/minimist/index.js");
assert!(index_js.exists(), "minimist/index.js must exist after npm install");
assert_eq!(
git_sha256_file(&index_js),
BEFORE_HASH,
"freshly installed index.js should have the expected beforeHash"
);
assert_run_ok(cwd, &["get", NPM_UUID], "get");
let manifest_path = cwd.join(".socket/manifest.json");
assert!(manifest_path.exists(), ".socket/manifest.json should exist after get");
let manifest: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&manifest_path).unwrap()).unwrap();
let patch = &manifest["patches"][NPM_PURL];
assert!(patch.is_object(), "manifest should contain {NPM_PURL}");
assert_eq!(patch["uuid"].as_str().unwrap(), NPM_UUID);
assert_eq!(
git_sha256_file(&index_js),
AFTER_HASH,
"index.js should match afterHash after get"
);
let (stdout, _) = assert_run_ok(cwd, &["list", "--json"], "list --json");
let list: serde_json::Value = serde_json::from_str(&stdout).unwrap();
let events = list["events"].as_array().expect("envelope events array");
let patches: Vec<&serde_json::Value> = events
.iter()
.filter(|e| e["action"] == "discovered")
.collect();
assert_eq!(patches.len(), 1);
assert_eq!(patches[0]["uuid"].as_str().unwrap(), NPM_UUID);
assert_eq!(patches[0]["purl"].as_str().unwrap(), NPM_PURL);
let vulns = patches[0]["details"]["vulnerabilities"]
.as_array()
.expect("vulnerabilities array");
assert!(!vulns.is_empty(), "patch should report at least one vulnerability");
let has_cve = vulns.iter().any(|v| {
v["cves"]
.as_array()
.map_or(false, |cves| cves.iter().any(|c| c == "CVE-2021-44906"))
});
assert!(has_cve, "vulnerability list should include CVE-2021-44906");
assert_run_ok(cwd, &["rollback"], "rollback");
assert_eq!(
git_sha256_file(&index_js),
BEFORE_HASH,
"index.js should match beforeHash after rollback"
);
assert_run_ok(cwd, &["apply"], "apply");
assert_eq!(
git_sha256_file(&index_js),
AFTER_HASH,
"index.js should match afterHash after re-apply"
);
assert_run_ok(cwd, &["remove", NPM_UUID], "remove");
assert_eq!(
git_sha256_file(&index_js),
BEFORE_HASH,
"index.js should match beforeHash after remove"
);
let manifest: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&manifest_path).unwrap()).unwrap();
assert!(
manifest["patches"].as_object().unwrap().is_empty(),
"manifest should be empty after remove"
);
}
#[test]
#[ignore]
fn test_npm_dry_run() {
if !has_command("npm") {
eprintln!("SKIP: npm not found on PATH");
return;
}
let dir = tempfile::tempdir().unwrap();
let cwd = dir.path();
write_package_json(cwd);
npm_run(cwd, &["install", "minimist@1.2.2"]);
let index_js = cwd.join("node_modules/minimist/index.js");
assert_eq!(git_sha256_file(&index_js), BEFORE_HASH);
assert_run_ok(cwd, &["get", NPM_UUID, "--no-apply"], "get --no-apply");
assert_eq!(
git_sha256_file(&index_js),
BEFORE_HASH,
"file should not change after get --no-apply"
);
assert_run_ok(cwd, &["apply", "--dry-run"], "apply --dry-run");
assert_eq!(
git_sha256_file(&index_js),
BEFORE_HASH,
"file should not change after apply --dry-run"
);
assert_run_ok(cwd, &["apply"], "apply");
assert_eq!(
git_sha256_file(&index_js),
AFTER_HASH,
"file should match afterHash after real apply"
);
}
#[test]
#[ignore]
fn test_npm_global_lifecycle() {
if !has_command("npm") {
eprintln!("SKIP: npm not found on PATH");
return;
}
let global_dir = tempfile::tempdir().unwrap();
let cwd_dir = tempfile::tempdir().unwrap();
let cwd = cwd_dir.path();
let out = Command::new("npm")
.args(["install", "-g", "--prefix", global_dir.path().to_str().unwrap(), "minimist@1.2.2"])
.output()
.expect("failed to run npm install -g");
assert!(
out.status.success(),
"npm install -g failed.\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
let nm_path = if cfg!(windows) {
global_dir.path().join("node_modules")
} else {
global_dir.path().join("lib/node_modules")
};
let index_js = nm_path.join("minimist/index.js");
assert!(
index_js.exists(),
"minimist/index.js must exist after global install at {}",
index_js.display()
);
assert_eq!(
git_sha256_file(&index_js),
BEFORE_HASH,
"globally installed index.js should have the expected beforeHash"
);
let nm_str = nm_path.to_str().unwrap();
let (stdout, _) = assert_run_ok(
cwd,
&["scan", "-g", "--global-prefix", nm_str, "--json"],
"scan -g --json",
);
let scan: serde_json::Value = serde_json::from_str(&stdout).unwrap();
let scanned = scan["scannedPackages"]
.as_u64()
.expect("scannedPackages should be a number");
assert!(scanned >= 1, "scan should find at least 1 package, got {scanned}");
assert_run_ok(
cwd,
&["get", NPM_UUID, "-g", "--global-prefix", nm_str],
"get -g",
);
let manifest_path = cwd.join(".socket/manifest.json");
assert!(manifest_path.exists(), "manifest should exist after get");
assert_eq!(
git_sha256_file(&index_js),
AFTER_HASH,
"index.js should match afterHash after global get"
);
let (stdout, _) = assert_run_ok(cwd, &["list", "--json"], "list --json");
let list: serde_json::Value = serde_json::from_str(&stdout).unwrap();
let events = list["events"].as_array().expect("envelope events array");
let patches: Vec<&serde_json::Value> = events
.iter()
.filter(|e| e["action"] == "discovered")
.collect();
assert_eq!(patches.len(), 1);
assert_eq!(patches[0]["uuid"].as_str().unwrap(), NPM_UUID);
assert_run_ok(
cwd,
&["rollback", "-g", "--global-prefix", nm_str],
"rollback -g",
);
assert_eq!(
git_sha256_file(&index_js),
BEFORE_HASH,
"index.js should match beforeHash after global rollback"
);
assert_run_ok(
cwd,
&["apply", "-g", "--global-prefix", nm_str],
"apply -g",
);
assert_eq!(
git_sha256_file(&index_js),
AFTER_HASH,
"index.js should match afterHash after global apply"
);
assert_run_ok(
cwd,
&["remove", NPM_UUID, "-g", "--global-prefix", nm_str],
"remove -g",
);
assert_eq!(
git_sha256_file(&index_js),
BEFORE_HASH,
"index.js should match beforeHash after global remove"
);
let manifest: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&manifest_path).unwrap()).unwrap();
assert!(
manifest["patches"].as_object().unwrap().is_empty(),
"manifest should be empty after global remove"
);
}
#[test]
#[ignore]
fn test_npm_save_only() {
if !has_command("npm") {
eprintln!("SKIP: npm not found on PATH");
return;
}
let dir = tempfile::tempdir().unwrap();
let cwd = dir.path();
write_package_json(cwd);
npm_run(cwd, &["install", "minimist@1.2.2"]);
let index_js = cwd.join("node_modules/minimist/index.js");
assert_eq!(git_sha256_file(&index_js), BEFORE_HASH);
assert_run_ok(cwd, &["get", NPM_UUID, "--save-only"], "get --save-only");
assert_eq!(
git_sha256_file(&index_js),
BEFORE_HASH,
"file should not change after get --save-only"
);
let manifest_path = cwd.join(".socket/manifest.json");
assert!(manifest_path.exists(), "manifest should exist after get --save-only");
let manifest: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&manifest_path).unwrap()).unwrap();
let patch = &manifest["patches"][NPM_PURL];
assert!(patch.is_object(), "manifest should contain {NPM_PURL}");
assert_eq!(patch["uuid"].as_str().unwrap(), NPM_UUID);
assert_run_ok(cwd, &["apply"], "apply");
assert_eq!(
git_sha256_file(&index_js),
AFTER_HASH,
"file should match afterHash after apply"
);
}
#[test]
#[ignore]
fn test_npm_apply_force() {
if !has_command("npm") {
eprintln!("SKIP: npm not found on PATH");
return;
}
let dir = tempfile::tempdir().unwrap();
let cwd = dir.path();
write_package_json(cwd);
npm_run(cwd, &["install", "minimist@1.2.2"]);
let index_js = cwd.join("node_modules/minimist/index.js");
assert_eq!(git_sha256_file(&index_js), BEFORE_HASH);
assert_run_ok(cwd, &["get", NPM_UUID, "--save-only"], "get --save-only");
std::fs::write(&index_js, b"// corrupted content\n").unwrap();
assert_ne!(
git_sha256_file(&index_js),
BEFORE_HASH,
"corrupted file should have a different hash"
);
let (code, _stdout, _stderr) = run(cwd, &["apply"]);
assert_ne!(code, 0, "apply without --force should fail on hash mismatch");
assert_run_ok(cwd, &["apply", "--force"], "apply --force");
assert_eq!(
git_sha256_file(&index_js),
AFTER_HASH,
"index.js should match afterHash after apply --force"
);
}
#[cfg(target_os = "macos")]
#[test]
#[ignore]
fn test_npm_macos_global_auto_discovery() {
if !has_command("npm") {
eprintln!("SKIP: npm not found on PATH");
return;
}
let cwd_dir = tempfile::tempdir().unwrap();
let cwd = cwd_dir.path();
let (code, stdout, stderr) = run(cwd, &["scan", "-g", "--json"]);
assert_eq!(
code, 0,
"scan -g --json failed (exit {code}).\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
let scan: serde_json::Value = serde_json::from_str(&stdout)
.unwrap_or_else(|e| panic!("invalid JSON from scan -g: {e}\nstdout:\n{stdout}"));
assert!(
scan["scannedPackages"].is_u64(),
"scannedPackages should be a number, got: {}",
scan["scannedPackages"]
);
}
#[test]
#[ignore]
fn test_npm_uuid_shortcut() {
if !has_command("npm") {
eprintln!("SKIP: npm not found on PATH");
return;
}
let dir = tempfile::tempdir().unwrap();
let cwd = dir.path();
write_package_json(cwd);
npm_run(cwd, &["install", "minimist@1.2.2"]);
let index_js = cwd.join("node_modules/minimist/index.js");
assert_eq!(git_sha256_file(&index_js), BEFORE_HASH);
assert_run_ok(cwd, &[NPM_UUID], "uuid shortcut");
assert_eq!(
git_sha256_file(&index_js),
AFTER_HASH,
"index.js should match afterHash after UUID shortcut"
);
let manifest_path = cwd.join(".socket/manifest.json");
assert!(manifest_path.exists(), "manifest should exist after UUID shortcut");
}