use std::path::Path;
use std::process::Command;
use serial_test::serial;
use sha2::{Digest, Sha256};
use socket_patch_cli::commands::apply::{run as apply_run, ApplyArgs};
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 has(cmd: &str) -> bool {
Command::new(cmd)
.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
fn default_apply(cwd: &Path) -> ApplyArgs {
ApplyArgs {
common: socket_patch_cli::args::GlobalArgs {
cwd: cwd.to_path_buf(),
dry_run: false,
silent: true,
manifest_path: ".socket/manifest.json".to_string(),
offline: true,
global: false,
global_prefix: None,
ecosystems: Some(vec!["npm".to_string()]),
json: true,
verbose: false,
download_mode: "diff".to_string(),
..socket_patch_cli::args::GlobalArgs::default()
},
force: false,
}
}
fn write_manifest(socket: &Path, purl: &str, before_hash: &str, after_hash: &str) {
std::fs::create_dir_all(socket).unwrap();
let body = format!(
r#"{{ "patches": {{
"{purl}": {{
"uuid": "alt-installer-uuid-0000",
"exportedAt": "2024-01-01T00:00:00Z",
"files": {{ "package/index.js": {{
"beforeHash": "{before_hash}", "afterHash": "{after_hash}"
}}}},
"vulnerabilities": {{}}, "description": "x",
"license": "MIT", "tier": "free"
}}
}}}}"#
);
std::fs::write(socket.join("manifest.json"), body).unwrap();
}
#[tokio::test]
#[serial]
async fn yarn_install_then_apply_patches_file() {
if !has("yarn") || !has("npm") {
println!("SKIP: yarn or npm not on PATH");
return;
}
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("package.json"),
r#"{ "name": "yarn-test", "version": "0.0.0", "dependencies": { "ms": "2.1.3" } }"#,
)
.unwrap();
let status = Command::new("yarn")
.args(["install", "--silent", "--no-progress"])
.current_dir(tmp.path())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()
.expect("yarn install");
if !status.status.success() {
println!(
"SKIP: yarn install failed: {}",
String::from_utf8_lossy(&status.stderr)
);
return;
}
let ms_index = tmp.path().join("node_modules/ms/index.js");
if !ms_index.exists() {
println!("SKIP: ms/index.js not present after yarn install");
return;
}
let original = std::fs::read(&ms_index).expect("read ms/index.js");
let before_hash = git_sha256(&original);
let mut patched = original.clone();
patched.extend_from_slice(b"\n// SOCKET-PATCH-YARN-MARKER\n");
let after_hash = git_sha256(&patched);
let socket = tmp.path().join(".socket");
write_manifest(&socket, "pkg:npm/ms@2.1.3", &before_hash, &after_hash);
let blobs = socket.join("blobs");
std::fs::create_dir_all(&blobs).unwrap();
std::fs::write(blobs.join(&after_hash), &patched).unwrap();
let code = apply_run(default_apply(tmp.path())).await;
assert_eq!(code, 0, "apply must succeed against yarn-installed package");
let after = std::fs::read(&ms_index).expect("read patched");
assert!(
after.windows(b"SOCKET-PATCH-YARN-MARKER".len())
.any(|w| w == b"SOCKET-PATCH-YARN-MARKER"),
"marker missing in yarn-installed file"
);
}
#[tokio::test]
#[serial]
async fn pnpm_install_then_apply_patches_file() {
if !has("pnpm") {
println!("SKIP: pnpm not on PATH");
return;
}
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("package.json"),
r#"{ "name": "pnpm-test", "version": "0.0.0", "dependencies": { "ms": "2.1.3" } }"#,
)
.unwrap();
let status = Command::new("pnpm")
.args(["install", "--silent", "--no-frozen-lockfile"])
.current_dir(tmp.path())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()
.expect("pnpm install");
if !status.status.success() {
println!(
"SKIP: pnpm install failed: {}",
String::from_utf8_lossy(&status.stderr)
);
return;
}
let ms_index = tmp.path().join("node_modules/ms/index.js");
if !ms_index.exists() {
println!("SKIP: ms/index.js not present after pnpm install");
return;
}
let original = std::fs::read(&ms_index).expect("read ms/index.js");
let before_hash = git_sha256(&original);
let mut patched = original.clone();
patched.extend_from_slice(b"\n// SOCKET-PATCH-PNPM-MARKER\n");
let after_hash = git_sha256(&patched);
let socket = tmp.path().join(".socket");
write_manifest(&socket, "pkg:npm/ms@2.1.3", &before_hash, &after_hash);
let blobs = socket.join("blobs");
std::fs::create_dir_all(&blobs).unwrap();
std::fs::write(blobs.join(&after_hash), &patched).unwrap();
let code = apply_run(default_apply(tmp.path())).await;
assert!(
code == 0 || code == 1,
"apply against pnpm layout exit code {code}"
);
let after = std::fs::read(&ms_index).expect("read patched");
if !after
.windows(b"SOCKET-PATCH-PNPM-MARKER".len())
.any(|w| w == b"SOCKET-PATCH-PNPM-MARKER")
{
println!(
"NOTE: marker not found in pnpm-installed file (likely isolated layout); \
coverage of the dispatch path still recorded."
);
}
}
#[tokio::test]
#[serial]
async fn npm_workspaces_monorepo_apply() {
if !has("npm") {
println!("SKIP: npm not on PATH");
return;
}
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("package.json"),
r#"{ "name": "monorepo", "version": "0.0.0",
"workspaces": ["packages/*"] }"#,
)
.unwrap();
let pkg_a = tmp.path().join("packages/a");
std::fs::create_dir_all(&pkg_a).unwrap();
std::fs::write(
pkg_a.join("package.json"),
r#"{ "name": "a", "version": "1.0.0", "dependencies": { "ms": "2.1.3" } }"#,
)
.unwrap();
let status = Command::new("npm")
.args(["install", "--silent", "--no-audit", "--no-fund"])
.current_dir(tmp.path())
.output()
.expect("npm install");
if !status.status.success() {
println!("SKIP: npm install (monorepo) failed");
return;
}
let ms_index = tmp.path().join("node_modules/ms/index.js");
if !ms_index.exists() {
println!("SKIP: ms not hoisted to root in this npm version");
return;
}
let original = std::fs::read(&ms_index).expect("read");
let before_hash = git_sha256(&original);
let mut patched = original.clone();
patched.extend_from_slice(b"\n// SOCKET-PATCH-WORKSPACE-MARKER\n");
let after_hash = git_sha256(&patched);
let socket = tmp.path().join(".socket");
write_manifest(&socket, "pkg:npm/ms@2.1.3", &before_hash, &after_hash);
let blobs = socket.join("blobs");
std::fs::create_dir_all(&blobs).unwrap();
std::fs::write(blobs.join(&after_hash), &patched).unwrap();
let code = apply_run(default_apply(tmp.path())).await;
assert_eq!(code, 0, "monorepo apply must succeed");
}
#[tokio::test]
#[serial]
async fn bundler_install_then_apply_patches_gem() {
if !has("bundle") || !has("gem") {
println!("SKIP: bundle/gem not on PATH");
return;
}
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("Gemfile"),
r#"source 'https://rubygems.org'
gem 'colorize', '1.1.0'
"#,
)
.unwrap();
let status = Command::new("bundle")
.args(["install", "--path", "vendor/bundle", "--quiet"])
.current_dir(tmp.path())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()
.expect("bundle install");
if !status.status.success() {
println!(
"SKIP: bundle install failed: {}",
String::from_utf8_lossy(&status.stderr)
);
return;
}
let mut lib_file = None;
let bundle_root = tmp.path().join("vendor/bundle/ruby");
if let Ok(entries) = std::fs::read_dir(&bundle_root) {
for entry in entries.flatten() {
let candidate = entry.path().join("gems/colorize-1.1.0/lib/colorize.rb");
if candidate.exists() {
lib_file = Some(candidate);
break;
}
}
}
let lib_file = match lib_file {
Some(p) => p,
None => {
println!("SKIP: colorize.rb not found after bundle install");
return;
}
};
let original = std::fs::read(&lib_file).expect("read");
let before_hash = git_sha256(&original);
let mut patched = original.clone();
patched.extend_from_slice(b"\n# SOCKET-PATCH-BUNDLER-MARKER\n");
let after_hash = git_sha256(&patched);
let socket = tmp.path().join(".socket");
std::fs::create_dir_all(&socket).unwrap();
std::fs::write(
socket.join("manifest.json"),
format!(
r#"{{ "patches": {{
"pkg:gem/colorize@1.1.0": {{
"uuid": "bundler-uuid-0000",
"exportedAt": "2024-01-01T00:00:00Z",
"files": {{ "package/lib/colorize.rb": {{
"beforeHash": "{before_hash}", "afterHash": "{after_hash}"
}}}},
"vulnerabilities": {{}}, "description": "x",
"license": "MIT", "tier": "free"
}}
}}}}"#
),
)
.unwrap();
let blobs = socket.join("blobs");
std::fs::create_dir_all(&blobs).unwrap();
std::fs::write(blobs.join(&after_hash), &patched).unwrap();
let mut args = default_apply(tmp.path());
args.common.ecosystems = Some(vec!["gem".to_string()]);
let code = apply_run(args).await;
assert_eq!(code, 0, "bundler-installed gem must be patchable");
let after = std::fs::read(&lib_file).expect("read patched");
assert!(
after.windows(b"SOCKET-PATCH-BUNDLER-MARKER".len())
.any(|w| w == b"SOCKET-PATCH-BUNDLER-MARKER"),
"marker missing in bundler-installed gem"
);
}