use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use sha2::{Digest, Sha256};
const NPM_PURL: &str = "pkg:npm/minimist@1.2.2";
const BEFORE_HASH: &str = "311f1e893e6eac502693fad8617dcf5353a043ccc0f7b4ba9fe385e838b67a10";
const FAKE_ORPHAN_HASH: &str =
"0000000000000000000000000000000000000000000000000000000000000000";
const FAKE_OLD_UUID: &str = "11111111-1111-4111-8111-111111111111";
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")
.env_remove("SOCKET_API_URL")
.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-scan-test","version":"0.0.0","private":true}"#,
)
.expect("write package.json");
}
fn parse_scan_json(stdout: &str) -> serde_json::Value {
serde_json::from_str(stdout)
.unwrap_or_else(|e| panic!("scan emitted invalid JSON: {e}\nstdout:\n{stdout}"))
}
fn read_manifest_file(cwd: &Path) -> serde_json::Value {
let path = cwd.join(".socket/manifest.json");
let content = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
serde_json::from_str(&content)
.unwrap_or_else(|e| panic!("manifest is not valid JSON: {e}\n{content}"))
}
fn write_seed_manifest(cwd: &Path, purl: &str, uuid: &str) {
let socket_dir = cwd.join(".socket");
std::fs::create_dir_all(&socket_dir).expect("create .socket");
let manifest = serde_json::json!({
"version": 1,
"patches": {
purl: {
"uuid": uuid,
"exportedAt": "2024-01-01T00:00:00Z",
"files": {},
"vulnerabilities": {},
"description": "",
"license": "",
"tier": "free",
}
}
});
std::fs::write(
socket_dir.join("manifest.json"),
serde_json::to_string_pretty(&manifest).expect("serialize manifest"),
)
.expect("write seed manifest");
}
#[test]
#[ignore]
fn test_scan_apply_json_adds_new_patch() {
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);
let (stdout, _) = assert_run_ok(
cwd,
&["scan", "--json", "--apply", "--yes"],
"scan --json --apply --yes (fresh)",
);
let v = parse_scan_json(&stdout);
assert_eq!(v["status"], "success");
let patches = v["apply"]["patches"].as_array().expect("apply.patches array");
let minimist = patches
.iter()
.find(|p| p["purl"] == NPM_PURL)
.expect("apply.patches should include minimist");
assert_eq!(minimist["action"], "added");
assert!(minimist["uuid"].is_string(), "uuid must be present");
assert_ne!(
git_sha256_file(&index_js),
BEFORE_HASH,
"file should have been patched (no longer BEFORE_HASH)",
);
let manifest = read_manifest_file(cwd);
assert!(
manifest["patches"][NPM_PURL].is_object(),
"manifest must record an entry for {NPM_PURL}"
);
}
#[test]
#[ignore]
fn test_scan_apply_json_skips_existing() {
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"]);
assert_run_ok(cwd, &["scan", "--json", "--apply", "--yes"], "first run");
let (stdout, _) = assert_run_ok(
cwd,
&["scan", "--json", "--apply", "--yes"],
"second run",
);
let v = parse_scan_json(&stdout);
let patches = v["apply"]["patches"].as_array().expect("apply.patches array");
let minimist = patches
.iter()
.find(|p| p["purl"] == NPM_PURL)
.expect("apply.patches should include minimist on re-run");
assert_eq!(minimist["action"], "skipped");
assert_ne!(
git_sha256_file(&cwd.join("node_modules/minimist/index.js")),
BEFORE_HASH,
"file should still be patched after a no-op re-run",
);
}
#[test]
#[ignore]
fn test_scan_apply_json_updates_existing() {
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"]);
write_seed_manifest(cwd, NPM_PURL, FAKE_OLD_UUID);
let (stdout, _) = assert_run_ok(
cwd,
&["scan", "--json", "--apply", "--yes"],
"scan with seeded fake UUID",
);
let v = parse_scan_json(&stdout);
let patches = v["apply"]["patches"].as_array().expect("apply.patches array");
let minimist = patches
.iter()
.find(|p| p["purl"] == NPM_PURL)
.expect("apply.patches should include minimist");
assert_eq!(minimist["action"], "updated");
assert_eq!(minimist["oldUuid"], FAKE_OLD_UUID);
assert!(
minimist["uuid"].is_string(),
"uuid must be present (specific value can drift as API serves multiple patches)",
);
assert_ne!(
minimist["uuid"], FAKE_OLD_UUID,
"new uuid must differ from the seeded fake oldUuid",
);
let manifest = read_manifest_file(cwd);
let new_uuid = manifest["patches"][NPM_PURL]["uuid"]
.as_str()
.expect("manifest must record a new uuid");
assert_ne!(new_uuid, FAKE_OLD_UUID, "manifest must reflect the update");
}
#[test]
#[ignore]
fn test_scan_json_read_only_emits_updates_array() {
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"]);
write_seed_manifest(cwd, NPM_PURL, FAKE_OLD_UUID);
let index_js = cwd.join("node_modules/minimist/index.js");
assert_eq!(git_sha256_file(&index_js), BEFORE_HASH);
let (stdout, _) = assert_run_ok(cwd, &["scan", "--json"], "scan --json (read-only)");
let v = parse_scan_json(&stdout);
let updates = v["updates"].as_array().expect("updates array");
assert_eq!(updates.len(), 1, "expected exactly one update for minimist");
assert_eq!(updates[0]["purl"], NPM_PURL);
assert_eq!(updates[0]["oldUuid"], FAKE_OLD_UUID);
assert!(updates[0]["newUuid"].is_string(), "newUuid must be present");
assert_ne!(
updates[0]["newUuid"], FAKE_OLD_UUID,
"newUuid must differ from the seeded oldUuid",
);
let manifest = read_manifest_file(cwd);
assert_eq!(manifest["patches"][NPM_PURL]["uuid"], FAKE_OLD_UUID);
assert_eq!(git_sha256_file(&index_js), BEFORE_HASH);
}
#[test]
#[ignore]
fn test_scan_json_read_only_no_mutation() {
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");
let (_, _) = assert_run_ok(cwd, &["scan", "--json"], "scan --json (no manifest)");
assert!(
!cwd.join(".socket/manifest.json").exists(),
"scan --json must not create a manifest"
);
assert_eq!(
git_sha256_file(&index_js),
BEFORE_HASH,
"scan --json must not patch files"
);
}
#[test]
#[ignore]
fn test_scan_apply_prune_prunes_uninstalled_package() {
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"]);
assert_run_ok(cwd, &["scan", "--json", "--apply", "--yes"], "initial apply");
assert!(cwd.join(".socket/manifest.json").exists());
npm_run(cwd, &["uninstall", "minimist"]);
npm_run(cwd, &["install", "left-pad@1.3.0"]);
let (stdout, _) = assert_run_ok(
cwd,
&["scan", "--json", "--apply", "--yes", "--prune"],
"scan with --prune after uninstall",
);
let v = parse_scan_json(&stdout);
let pruned = v["gc"]["prunedManifestEntries"]
.as_array()
.expect("gc.prunedManifestEntries array");
assert!(
pruned.iter().any(|p| p == NPM_PURL),
"minimist should be pruned from manifest after uninstall; got {pruned:?}"
);
let manifest = read_manifest_file(cwd);
assert!(
manifest["patches"][NPM_PURL].is_null(),
"minimist entry should be removed from manifest"
);
}
#[test]
#[ignore]
fn test_scan_apply_default_keeps_uninstalled_entries() {
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"]);
assert_run_ok(cwd, &["scan", "--json", "--apply", "--yes"], "initial apply");
npm_run(cwd, &["uninstall", "minimist"]);
npm_run(cwd, &["install", "left-pad@1.3.0"]);
let (stdout, _) = assert_run_ok(
cwd,
&["scan", "--json", "--apply", "--yes"],
"scan without --prune",
);
let v = parse_scan_json(&stdout);
assert!(
v.get("gc").is_none() || v["gc"].is_null(),
"gc field must be omitted when --prune is not set; got {}",
v["gc"]
);
let manifest = read_manifest_file(cwd);
assert!(
!manifest["patches"][NPM_PURL].is_null(),
"minimist entry must survive when --prune is not set"
);
}
#[test]
#[ignore]
fn test_scan_apply_prune_cleans_orphan_blobs() {
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"]);
assert_run_ok(cwd, &["scan", "--json", "--apply", "--yes"], "initial apply");
let blobs_dir = cwd.join(".socket/blobs");
std::fs::create_dir_all(&blobs_dir).expect("create blobs dir");
let orphan = blobs_dir.join(FAKE_ORPHAN_HASH);
std::fs::write(&orphan, b"junk").expect("plant orphan");
assert!(orphan.exists());
let (stdout, _) = assert_run_ok(
cwd,
&["scan", "--json", "--apply", "--yes", "--prune"],
"scan --prune with orphan blob present",
);
let v = parse_scan_json(&stdout);
let removed = v["gc"]["removedBlobs"]
.as_u64()
.expect("gc.removedBlobs should be a number");
assert!(
removed >= 1,
"gc should report at least 1 removed blob, got {removed}"
);
assert!(!orphan.exists(), "orphan blob should be deleted");
}
#[test]
#[ignore]
fn test_scan_dry_run_sync_previews_apply_and_gc() {
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"]);
assert_run_ok(cwd, &["scan", "--json", "--apply", "--yes"], "initial apply");
npm_run(cwd, &["uninstall", "minimist"]);
npm_run(cwd, &["install", "left-pad@1.3.0"]);
let blobs_dir = cwd.join(".socket/blobs");
let orphan = blobs_dir.join(FAKE_ORPHAN_HASH);
std::fs::write(&orphan, b"junk").expect("plant orphan");
let pre_manifest = read_manifest_file(cwd);
let (stdout, _) = assert_run_ok(
cwd,
&["scan", "--json", "--dry-run", "--sync", "--yes"],
"scan --dry-run --sync",
);
let v = parse_scan_json(&stdout);
let prunable = v["gc"]["prunableManifestEntries"]
.as_array()
.expect("gc.prunableManifestEntries array");
assert!(
prunable.iter().any(|p| p == NPM_PURL),
"preview should list minimist as prunable; got {prunable:?}"
);
assert!(
v["gc"]["orphanBlobs"].as_u64().unwrap_or(0) >= 1,
"preview should count at least 1 orphan blob"
);
assert_eq!(v["apply"]["dryRun"], true);
assert!(orphan.exists(), "dry-run must not delete orphan blob");
let post_manifest = read_manifest_file(cwd);
assert_eq!(
pre_manifest, post_manifest,
"dry-run must leave manifest exactly as it was"
);
}
#[test]
#[ignore]
fn test_scan_json_no_gc_field_without_prune() {
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"]);
assert_run_ok(cwd, &["scan", "--json", "--apply", "--yes"], "initial apply");
npm_run(cwd, &["uninstall", "minimist"]);
npm_run(cwd, &["install", "left-pad@1.3.0"]);
let blobs_dir = cwd.join(".socket/blobs");
let orphan = blobs_dir.join(FAKE_ORPHAN_HASH);
std::fs::write(&orphan, b"junk").expect("plant orphan");
let (stdout, _) = assert_run_ok(cwd, &["scan", "--json"], "scan --json (no prune)");
let v = parse_scan_json(&stdout);
assert!(
v.get("gc").is_none() || v["gc"].is_null(),
"scan --json must NOT emit gc when --prune is not set; got {}",
v["gc"]
);
}
#[test]
#[ignore]
fn test_scan_sync_yes_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 (stdout1, _) = assert_run_ok(
cwd,
&["scan", "--json", "--sync", "--yes"],
"first --sync apply",
);
let v1 = parse_scan_json(&stdout1);
let patches = v1["apply"]["patches"]
.as_array()
.expect("first sync should populate apply.patches");
assert!(
patches.iter().any(|p| p["purl"] == NPM_PURL && p["action"] == "added"),
"first sync should add the minimist patch"
);
assert!(v1["gc"].is_object(), "gc must be emitted under --sync");
npm_run(cwd, &["uninstall", "minimist"]);
npm_run(cwd, &["install", "left-pad@1.3.0"]);
let blobs_dir = cwd.join(".socket/blobs");
let orphan = blobs_dir.join(FAKE_ORPHAN_HASH);
std::fs::write(&orphan, b"junk").expect("plant orphan");
let (stdout2, _) = assert_run_ok(
cwd,
&["scan", "--json", "--sync", "--yes"],
"second --sync after uninstall",
);
let v2 = parse_scan_json(&stdout2);
let pruned = v2["gc"]["prunedManifestEntries"]
.as_array()
.expect("gc.prunedManifestEntries array");
assert!(
pruned.iter().any(|p| p == NPM_PURL),
"minimist should be pruned by --sync after uninstall; got {pruned:?}"
);
assert!(!orphan.exists(), "orphan should be reaped");
let manifest = read_manifest_file(cwd);
assert!(
manifest["patches"][NPM_PURL].is_null(),
"manifest must not retain minimist after --sync prune"
);
}