use std::path::{Path, PathBuf};
use std::process::Command;
use sha2::{Digest, Sha256};
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn binary() -> PathBuf {
env!("CARGO_BIN_EXE_socket-patch").into()
}
const ORG_SLUG: &str = "test-org";
const UUID: &str = "11111111-1111-4111-8111-111111111111";
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 write_npm_package(root: &Path, name: &str, version: &str, content: &[u8]) {
let pkg_dir = root.join("node_modules").join(name);
std::fs::create_dir_all(&pkg_dir).unwrap();
std::fs::write(
pkg_dir.join("package.json"),
format!(r#"{{ "name": "{name}", "version": "{version}" }}"#),
)
.unwrap();
std::fs::write(pkg_dir.join("index.js"), content).unwrap();
}
fn write_root(root: &Path) {
std::fs::write(
root.join("package.json"),
r#"{ "name": "scan-sync-test", "version": "0.0.0" }"#,
)
.unwrap();
}
#[tokio::test]
async fn scan_sync_against_clean_project_adds_and_applies_patch() {
let before = b"before\n";
let after = b"after\n";
let before_hash = git_sha256(before);
let after_hash = git_sha256(after);
let purl = "pkg:npm/sync-target@1.0.0";
let encoded = "pkg%3Anpm%2Fsync-target%401.0.0";
let mock = MockServer::start().await;
Mock::given(method("POST"))
.and(path(format!("/v0/orgs/{ORG_SLUG}/patches/batch")))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"packages": [{
"purl": purl,
"patches": [{
"uuid": UUID,
"purl": purl,
"tier": "free",
"cveIds": [],
"ghsaIds": [],
"severity": "high",
"title": "sync patch"
}]
}],
"canAccessPaidPatches": false,
})))
.mount(&mock)
.await;
Mock::given(method("GET"))
.and(path(format!("/v0/orgs/{ORG_SLUG}/patches/by-package/{encoded}")))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"patches": [{
"uuid": UUID,
"purl": purl,
"publishedAt": "2024-01-01T00:00:00Z",
"description": "Sync patch",
"license": "MIT",
"tier": "free",
"vulnerabilities": {}
}],
"canAccessPaidPatches": false,
})))
.mount(&mock)
.await;
let blob_b64 = "YWZ0ZXIK";
Mock::given(method("GET"))
.and(path(format!("/v0/orgs/{ORG_SLUG}/patches/view/{UUID}")))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"uuid": UUID,
"purl": purl,
"publishedAt": "2024-01-01T00:00:00Z",
"files": {
"package/index.js": {
"beforeHash": before_hash,
"afterHash": after_hash,
"blobContent": blob_b64,
}
},
"vulnerabilities": {},
"description": "Sync test patch",
"license": "MIT",
"tier": "free",
})))
.mount(&mock)
.await;
let tmp = tempfile::tempdir().unwrap();
write_root(tmp.path());
write_npm_package(tmp.path(), "sync-target", "1.0.0", before);
let out = Command::new(binary())
.args([
"scan",
"--json",
"--sync",
"--yes",
"--api-url",
&mock.uri(),
"--api-token",
"fake-token",
"--org",
ORG_SLUG,
])
.current_dir(tmp.path())
.output()
.expect("run");
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();
assert_eq!(
code, 0,
"scan --sync must succeed; stdout={stdout}; stderr={stderr}"
);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("valid JSON");
let status = v["status"].as_str().expect("status string");
assert!(
status == "success" || status == "partial_failure",
"unexpected status: {status}; envelope={v}"
);
let manifest_path = tmp.path().join(".socket/manifest.json");
assert!(
manifest_path.exists(),
"scan --sync must write the manifest"
);
let apply_obj = v["apply"].as_object();
if let Some(apply) = apply_obj {
assert!(
apply.contains_key("patches") || apply.contains_key("applied"),
"apply sub-object should have outcomes; got: {apply:?}"
);
}
}
#[tokio::test]
async fn scan_apply_with_existing_blob_uses_local_cache() {
let before = b"before\n";
let after = b"after\n";
let before_hash = git_sha256(before);
let after_hash = git_sha256(after);
let purl = "pkg:npm/cached-sync@1.0.0";
let encoded = "pkg%3Anpm%2Fcached-sync%401.0.0";
let mock = MockServer::start().await;
Mock::given(method("POST"))
.and(path(format!("/v0/orgs/{ORG_SLUG}/patches/batch")))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"packages": [{
"purl": purl,
"patches": [{
"uuid": UUID,
"purl": purl,
"tier": "free",
"cveIds": [], "ghsaIds": [], "severity": "low",
"title": "x"
}]
}],
"canAccessPaidPatches": false,
})))
.mount(&mock)
.await;
Mock::given(method("GET"))
.and(path(format!("/v0/orgs/{ORG_SLUG}/patches/by-package/{encoded}")))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"patches": [{
"uuid": UUID,
"purl": purl,
"publishedAt": "2024-01-01T00:00:00Z",
"description": "x",
"license": "MIT",
"tier": "free",
"vulnerabilities": {}
}],
"canAccessPaidPatches": false,
})))
.mount(&mock)
.await;
let blob_b64 = "YWZ0ZXIK";
Mock::given(method("GET"))
.and(path(format!("/v0/orgs/{ORG_SLUG}/patches/view/{UUID}")))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"uuid": UUID,
"purl": purl,
"publishedAt": "2024-01-01T00:00:00Z",
"files": {
"package/index.js": {
"beforeHash": before_hash,
"afterHash": after_hash,
"blobContent": blob_b64,
}
},
"vulnerabilities": {},
"description": "x", "license": "MIT", "tier": "free",
})))
.mount(&mock)
.await;
let tmp = tempfile::tempdir().unwrap();
write_root(tmp.path());
write_npm_package(tmp.path(), "cached-sync", "1.0.0", before);
let socket = tmp.path().join(".socket");
std::fs::create_dir_all(&socket).unwrap();
std::fs::write(
socket.join("manifest.json"),
format!(
r#"{{
"patches": {{
"{purl}": {{
"uuid": "{UUID}",
"exportedAt": "2024-01-01T00:00:00Z",
"files": {{
"package/index.js": {{
"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), after).unwrap();
let out = Command::new(binary())
.args([
"scan",
"--json",
"--apply",
"--yes",
"--api-url",
&mock.uri(),
"--api-token",
"fake-token",
"--org",
ORG_SLUG,
])
.current_dir(tmp.path())
.output()
.expect("run");
let code = out.status.code().unwrap_or(-1);
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
assert_eq!(code, 0, "scan --apply with cached UUID must succeed; stdout={stdout}");
}
#[tokio::test]
async fn scan_apply_with_no_patches_emits_empty_apply_object() {
let mock = MockServer::start().await;
Mock::given(method("POST"))
.and(path(format!("/v0/orgs/{ORG_SLUG}/patches/batch")))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"packages": [],
"canAccessPaidPatches": false,
})))
.mount(&mock)
.await;
let tmp = tempfile::tempdir().unwrap();
write_root(tmp.path());
write_npm_package(tmp.path(), "empty-target", "1.0.0", b"x");
let out = Command::new(binary())
.args([
"scan",
"--json",
"--apply",
"--yes",
"--api-url",
&mock.uri(),
"--api-token",
"fake-token",
"--org",
ORG_SLUG,
])
.current_dir(tmp.path())
.output()
.expect("run");
let code = out.status.code().unwrap_or(-1);
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
assert_eq!(code, 0);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap();
let apply = v["apply"].as_object().unwrap();
assert_eq!(apply["found"], 0);
assert_eq!(apply["applied"], 0);
}