use std::path::{Path, PathBuf};
use std::process::Command;
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";
fn write_npm_package(root: &Path, name: &str, version: &str) {
let pkg_dir = root.join("node_modules").join(name);
std::fs::create_dir_all(&pkg_dir).expect("create pkg dir");
let pkg_json = format!(
r#"{{ "name": "{name}", "version": "{version}" }}"#
);
std::fs::write(pkg_dir.join("package.json"), pkg_json).expect("write pkg json");
}
fn write_root_package_json(root: &Path) {
std::fs::write(
root.join("package.json"),
r#"{ "name": "scan-test-root", "version": "0.0.0" }"#,
)
.expect("write root package.json");
}
fn run_scan(cwd: &Path, api_url: &str, extra: &[&str]) -> (i32, String, String) {
let mut args = vec![
"scan",
"--json",
"--api-url",
api_url,
"--api-token",
"fake-token-for-test",
"--org",
ORG_SLUG,
];
args.extend_from_slice(extra);
let out = Command::new(binary())
.args(&args)
.current_dir(cwd)
.output()
.expect("run socket-patch");
(
out.status.code().unwrap_or(-1),
String::from_utf8_lossy(&out.stdout).to_string(),
String::from_utf8_lossy(&out.stderr).to_string(),
)
}
#[tokio::test]
async fn scan_with_no_installed_packages_reports_zero() {
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().expect("tempdir");
write_root_package_json(tmp.path());
let (code, stdout, stderr) = run_scan(tmp.path(), &mock.uri(), &[]);
assert_eq!(
code, 0,
"scan with no packages must succeed; stdout={stdout}; stderr={stderr}"
);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("valid JSON");
assert_eq!(v["status"], "success");
assert_eq!(v["scannedPackages"], 0);
assert_eq!(v["packagesWithPatches"], 0);
assert_eq!(v["totalPatches"], 0);
}
#[tokio::test]
async fn scan_reports_available_patch_for_installed_package() {
let mock = MockServer::start().await;
let purl = "pkg:npm/minimist@1.2.2";
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": "11111111-1111-4111-8111-111111111111",
"purl": purl,
"tier": "free",
"cveIds": ["CVE-2021-44906"],
"ghsaIds": ["GHSA-xvch-5gv4-984h"],
"severity": "high",
"title": "Prototype Pollution"
}]
}],
"canAccessPaidPatches": false,
})))
.mount(&mock)
.await;
let tmp = tempfile::tempdir().expect("tempdir");
write_root_package_json(tmp.path());
write_npm_package(tmp.path(), "minimist", "1.2.2");
let (code, stdout, stderr) = run_scan(tmp.path(), &mock.uri(), &[]);
assert_eq!(
code, 0,
"scan must succeed; stdout={stdout}; stderr={stderr}"
);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("valid JSON");
assert_eq!(v["status"], "success");
assert_eq!(v["packagesWithPatches"], 1);
assert_eq!(v["totalPatches"], 1);
assert_eq!(v["freePatches"], 1);
assert_eq!(v["paidPatches"], 0);
let packages = v["packages"].as_array().expect("packages array");
assert_eq!(packages.len(), 1);
assert_eq!(packages[0]["purl"], purl);
let patches = packages[0]["patches"].as_array().unwrap();
assert_eq!(patches.len(), 1);
assert_eq!(patches[0]["uuid"], "11111111-1111-4111-8111-111111111111");
assert_eq!(patches[0]["severity"], "high");
}
#[tokio::test]
async fn scan_emits_updates_entry_when_newer_uuid_available() {
let mock = MockServer::start().await;
let purl = "pkg:npm/minimist@1.2.2";
let new_uuid = "99999999-9999-4999-8999-999999999999";
let old_uuid = "11111111-1111-4111-8111-111111111111";
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": new_uuid,
"purl": purl,
"tier": "free",
"cveIds": [],
"ghsaIds": [],
"severity": "high",
"title": "Newer patch"
}]
}],
"canAccessPaidPatches": false,
})))
.mount(&mock)
.await;
let tmp = tempfile::tempdir().expect("tempdir");
write_root_package_json(tmp.path());
write_npm_package(tmp.path(), "minimist", "1.2.2");
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": "{old_uuid}",
"exportedAt": "2024-01-01T00:00:00Z",
"files": {{}},
"vulnerabilities": {{}},
"description": "old",
"license": "MIT",
"tier": "free"
}}
}}
}}"#
),
)
.unwrap();
let (code, stdout, _) = run_scan(tmp.path(), &mock.uri(), &[]);
assert_eq!(code, 0);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("valid JSON");
let updates = v["updates"].as_array().expect("updates array");
assert_eq!(updates.len(), 1, "one PURL changed UUID");
assert_eq!(updates[0]["purl"], purl);
assert_eq!(updates[0]["oldUuid"], old_uuid);
assert_eq!(updates[0]["newUuid"], new_uuid);
}
#[tokio::test]
async fn scan_with_no_manifest_emits_empty_updates() {
let mock = MockServer::start().await;
let purl = "pkg:npm/minimist@1.2.2";
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": "22222222-2222-4222-8222-222222222222",
"purl": purl,
"tier": "free",
"cveIds": [],
"ghsaIds": [],
"severity": "low",
"title": "Some patch"
}]
}],
"canAccessPaidPatches": false,
})))
.mount(&mock)
.await;
let tmp = tempfile::tempdir().expect("tempdir");
write_root_package_json(tmp.path());
write_npm_package(tmp.path(), "minimist", "1.2.2");
let (code, stdout, _) = run_scan(tmp.path(), &mock.uri(), &[]);
assert_eq!(code, 0);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("valid JSON");
assert_eq!(
v["updates"].as_array().map(|a| a.len()),
Some(0),
"updates should be empty when no manifest exists; got: {v}"
);
assert_eq!(v["packagesWithPatches"], 1);
}
#[tokio::test]
async fn scan_without_prune_omits_gc_field() {
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().expect("tempdir");
write_root_package_json(tmp.path());
let (_, stdout, _) = run_scan(tmp.path(), &mock.uri(), &[]);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("valid JSON");
assert!(
v.as_object().unwrap().get("gc").is_none(),
"scan without --prune/--sync must NOT emit `gc`; got: {v}"
);
}
#[tokio::test]
async fn scan_apply_dry_run_with_empty_manifest_emits_added_action() {
let mock = MockServer::start().await;
let purl = "pkg:npm/minimist@1.2.2";
let new_uuid = "11111111-1111-4111-8111-111111111111";
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": new_uuid,
"purl": purl,
"tier": "free",
"cveIds": [],
"ghsaIds": [],
"severity": "high",
"title": "Prototype Pollution"
}]
}],
"canAccessPaidPatches": false,
})))
.mount(&mock)
.await;
Mock::given(method("GET"))
.and(path(format!(
"/v0/orgs/{ORG_SLUG}/patches/by-package/pkg%3Anpm%2Fminimist%401.2.2"
)))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"patches": [{
"uuid": new_uuid,
"purl": purl,
"publishedAt": "2024-01-01T00:00:00Z",
"description": "Fixes prototype pollution",
"license": "MIT",
"tier": "free",
"vulnerabilities": {}
}],
"canAccessPaidPatches": false,
})))
.mount(&mock)
.await;
let tmp = tempfile::tempdir().expect("tempdir");
write_root_package_json(tmp.path());
write_npm_package(tmp.path(), "minimist", "1.2.2");
let (code, stdout, stderr) = run_scan(
tmp.path(),
&mock.uri(),
&["--apply", "--dry-run", "--yes"],
);
assert_eq!(
code, 0,
"scan --apply --dry-run must succeed; stdout={stdout}; stderr={stderr}"
);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("valid JSON");
assert_eq!(v["status"], "success");
let apply = v["apply"]
.as_object()
.expect("apply object present in --apply mode");
assert_eq!(apply["dryRun"], true);
assert_eq!(apply["found"], 1);
assert_eq!(apply["added"], 1);
assert_eq!(apply["updated"], 0);
assert_eq!(apply["skipped"], 0);
let patches = apply["patches"].as_array().expect("patches array");
assert_eq!(patches.len(), 1);
assert_eq!(patches[0]["action"], "added");
assert_eq!(patches[0]["uuid"], new_uuid);
assert_eq!(patches[0]["purl"], purl);
assert!(
!tmp.path().join(".socket/manifest.json").exists(),
"scan --apply --dry-run must not write .socket/manifest.json"
);
}
#[tokio::test]
async fn scan_apply_dry_run_with_existing_uuid_emits_skipped_action() {
let mock = MockServer::start().await;
let purl = "pkg:npm/minimist@1.2.2";
let same_uuid = "11111111-1111-4111-8111-111111111111";
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": same_uuid,
"purl": purl,
"tier": "free",
"cveIds": [],
"ghsaIds": [],
"severity": "low",
"title": "Some patch"
}]
}],
"canAccessPaidPatches": false,
})))
.mount(&mock)
.await;
Mock::given(method("GET"))
.and(path(format!(
"/v0/orgs/{ORG_SLUG}/patches/by-package/pkg%3Anpm%2Fminimist%401.2.2"
)))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"patches": [{
"uuid": same_uuid,
"purl": purl,
"publishedAt": "2024-01-01T00:00:00Z",
"description": "x",
"license": "MIT",
"tier": "free",
"vulnerabilities": {}
}],
"canAccessPaidPatches": false,
})))
.mount(&mock)
.await;
let tmp = tempfile::tempdir().expect("tempdir");
write_root_package_json(tmp.path());
write_npm_package(tmp.path(), "minimist", "1.2.2");
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": "{same_uuid}",
"exportedAt": "2024-01-01T00:00:00Z",
"files": {{}},
"vulnerabilities": {{}},
"description": "existing",
"license": "MIT",
"tier": "free"
}}
}}
}}"#
),
)
.unwrap();
let (code, stdout, _) = run_scan(
tmp.path(),
&mock.uri(),
&["--apply", "--dry-run", "--yes"],
);
assert_eq!(code, 0);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("valid JSON");
let apply = &v["apply"];
assert_eq!(apply["skipped"], 1);
assert_eq!(apply["added"], 0);
assert_eq!(apply["updated"], 0);
let patches = apply["patches"].as_array().unwrap();
assert_eq!(patches[0]["action"], "skipped");
}
#[tokio::test]
async fn scan_apply_dry_run_with_different_uuid_emits_updated_action() {
let mock = MockServer::start().await;
let purl = "pkg:npm/minimist@1.2.2";
let new_uuid = "99999999-9999-4999-8999-999999999999";
let old_uuid = "11111111-1111-4111-8111-111111111111";
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": new_uuid,
"purl": purl,
"tier": "free",
"cveIds": [],
"ghsaIds": [],
"severity": "high",
"title": "Newer patch"
}]
}],
"canAccessPaidPatches": false,
})))
.mount(&mock)
.await;
Mock::given(method("GET"))
.and(path(format!(
"/v0/orgs/{ORG_SLUG}/patches/by-package/pkg%3Anpm%2Fminimist%401.2.2"
)))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"patches": [{
"uuid": new_uuid,
"purl": purl,
"publishedAt": "2024-02-01T00:00:00Z",
"description": "newer",
"license": "MIT",
"tier": "free",
"vulnerabilities": {}
}],
"canAccessPaidPatches": false,
})))
.mount(&mock)
.await;
let tmp = tempfile::tempdir().expect("tempdir");
write_root_package_json(tmp.path());
write_npm_package(tmp.path(), "minimist", "1.2.2");
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": "{old_uuid}",
"exportedAt": "2024-01-01T00:00:00Z",
"files": {{}},
"vulnerabilities": {{}},
"description": "older",
"license": "MIT",
"tier": "free"
}}
}}
}}"#
),
)
.unwrap();
let (code, stdout, _) = run_scan(
tmp.path(),
&mock.uri(),
&["--apply", "--dry-run", "--yes"],
);
assert_eq!(code, 0);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("valid JSON");
let apply = &v["apply"];
assert_eq!(apply["updated"], 1);
assert_eq!(apply["added"], 0);
assert_eq!(apply["skipped"], 0);
let patches = apply["patches"].as_array().unwrap();
assert_eq!(patches[0]["action"], "updated");
assert_eq!(patches[0]["oldUuid"], old_uuid);
assert_eq!(patches[0]["uuid"], new_uuid);
}
#[tokio::test]
async fn scan_prune_dry_run_reports_prunable_manifest_entries() {
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().expect("tempdir");
write_root_package_json(tmp.path());
write_npm_package(tmp.path(), "fresh-pkg", "1.0.0");
let socket = tmp.path().join(".socket");
std::fs::create_dir_all(&socket).unwrap();
std::fs::write(
socket.join("manifest.json"),
r#"{
"patches": {
"pkg:npm/uninstalled@1.0.0": {
"uuid": "11111111-1111-4111-8111-111111111111",
"exportedAt": "2024-01-01T00:00:00Z",
"files": {},
"vulnerabilities": {},
"description": "stranded entry",
"license": "MIT",
"tier": "free"
}
}
}"#,
)
.unwrap();
let (code, stdout, stderr) = run_scan(
tmp.path(),
&mock.uri(),
&["--prune", "--dry-run", "--yes"],
);
assert_eq!(code, 0, "expected exit 0; stdout={stdout}; stderr={stderr}");
let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("valid JSON");
let gc = v["gc"].as_object().unwrap_or_else(|| {
panic!("--prune must emit gc field; full envelope was: {v}")
});
let prunable = gc["prunableManifestEntries"]
.as_array()
.expect("prunableManifestEntries present in dry-run gc");
assert_eq!(prunable.len(), 1);
assert_eq!(prunable[0], "pkg:npm/uninstalled@1.0.0");
let body = std::fs::read_to_string(socket.join("manifest.json")).unwrap();
let manifest: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(manifest["patches"].as_object().unwrap().len(), 1);
}
#[tokio::test]
async fn scan_prune_removes_stale_manifest_entries() {
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().expect("tempdir");
write_root_package_json(tmp.path());
write_npm_package(tmp.path(), "fresh-pkg", "1.0.0");
let socket = tmp.path().join(".socket");
std::fs::create_dir_all(&socket).unwrap();
std::fs::write(
socket.join("manifest.json"),
r#"{
"patches": {
"pkg:npm/uninstalled@1.0.0": {
"uuid": "11111111-1111-4111-8111-111111111111",
"exportedAt": "2024-01-01T00:00:00Z",
"files": {},
"vulnerabilities": {},
"description": "stranded",
"license": "MIT",
"tier": "free"
}
}
}"#,
)
.unwrap();
let (code, stdout, _) = run_scan(tmp.path(), &mock.uri(), &["--prune", "--yes"]);
assert_eq!(code, 0);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("valid JSON");
let gc = &v["gc"];
let pruned = gc["prunedManifestEntries"]
.as_array()
.expect("prunedManifestEntries present in apply-mode gc");
assert_eq!(pruned.len(), 1);
let body = std::fs::read_to_string(socket.join("manifest.json")).unwrap();
let manifest: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(
manifest["patches"].as_object().unwrap().len(),
0,
"stale entry must be pruned from manifest"
);
}
#[tokio::test]
async fn scan_handles_api_500_error_gracefully() {
let mock = MockServer::start().await;
Mock::given(method("POST"))
.and(path(format!("/v0/orgs/{ORG_SLUG}/patches/batch")))
.respond_with(ResponseTemplate::new(500).set_body_string("internal server error"))
.mount(&mock)
.await;
let tmp = tempfile::tempdir().expect("tempdir");
write_root_package_json(tmp.path());
write_npm_package(tmp.path(), "minimist", "1.2.2");
let (code, _stdout, _stderr) = run_scan(tmp.path(), &mock.uri(), &[]);
assert!(
code == 0 || code == 1,
"scan must not crash on 500; got exit code {code}"
);
}
#[tokio::test]
async fn scan_prune_keeps_entry_when_package_installed_but_api_silent() {
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().expect("tempdir");
write_root_package_json(tmp.path());
write_npm_package(tmp.path(), "still-installed", "1.0.0");
let socket = tmp.path().join(".socket");
std::fs::create_dir_all(&socket).unwrap();
let original_manifest = r#"{
"patches": {
"pkg:npm/still-installed@1.0.0": {
"uuid": "22222222-2222-4222-8222-222222222222",
"exportedAt": "2024-01-01T00:00:00Z",
"files": {},
"vulnerabilities": {},
"description": "still here, just no patch this scan",
"license": "MIT",
"tier": "free"
}
}
}"#;
std::fs::write(socket.join("manifest.json"), original_manifest).unwrap();
let (code, _stdout, _stderr) = run_scan(tmp.path(), &mock.uri(), &["--prune", "--yes"]);
assert_eq!(code, 0);
let body = std::fs::read_to_string(socket.join("manifest.json")).unwrap();
let manifest: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(
manifest["patches"].as_object().unwrap().len(),
1,
"entry for still-installed package must survive prune when API is silent"
);
assert!(
manifest["patches"]["pkg:npm/still-installed@1.0.0"]
.as_object()
.is_some(),
"the original PURL/UUID record must remain intact"
);
}
#[tokio::test]
async fn scan_prune_removes_withdrawn_patch_entry() {
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().expect("tempdir");
write_root_package_json(tmp.path());
write_npm_package(tmp.path(), "unrelated", "1.0.0");
let socket = tmp.path().join(".socket");
std::fs::create_dir_all(socket.join("blobs")).unwrap();
std::fs::write(
socket.join("manifest.json"),
r#"{
"patches": {
"pkg:npm/withdrawn-pkg@1.0.0": {
"uuid": "33333333-3333-4333-8333-333333333333",
"exportedAt": "2024-01-01T00:00:00Z",
"files": {},
"vulnerabilities": {},
"description": "withdrawn from upstream",
"license": "MIT",
"tier": "free"
}
}
}"#,
)
.unwrap();
std::fs::write(
socket.join("blobs").join("stub-blob"),
b"placeholder bytes for withdrawn patch",
)
.unwrap();
let (code, _stdout, _stderr) = run_scan(tmp.path(), &mock.uri(), &["--prune", "--yes"]);
assert_eq!(code, 0);
let manifest: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(socket.join("manifest.json")).unwrap(),
)
.unwrap();
assert_eq!(
manifest["patches"].as_object().unwrap().len(),
0,
"withdrawn entry must be removed"
);
}
#[tokio::test]
async fn scan_detects_update_without_touching_existing_blobs() {
const OLD_UUID: &str = "44444444-4444-4444-8444-444444444444";
const NEW_UUID: &str = "55555555-5555-4555-8555-555555555555";
let purl = "pkg:npm/lodash@4.17.20";
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": NEW_UUID,
"purl": purl,
"tier": "free",
"cveIds": [],
"ghsaIds": [],
"severity": "high",
"title": "Updated lodash patch",
}]
}],
"canAccessPaidPatches": false,
})))
.mount(&mock)
.await;
let tmp = tempfile::tempdir().expect("tempdir");
write_root_package_json(tmp.path());
write_npm_package(tmp.path(), "lodash", "4.17.20");
let socket = tmp.path().join(".socket");
std::fs::create_dir_all(socket.join("blobs")).unwrap();
std::fs::write(
socket.join("manifest.json"),
format!(
r#"{{
"patches": {{
"pkg:npm/lodash@4.17.20": {{
"uuid": "{OLD_UUID}",
"exportedAt": "2024-01-01T00:00:00Z",
"files": {{}},
"vulnerabilities": {{}},
"description": "Original lodash patch",
"license": "MIT",
"tier": "free"
}}
}}
}}"#
),
)
.unwrap();
let marker = socket.join("blobs").join("untouched-by-scan");
std::fs::write(&marker, b"original contents").unwrap();
let (code, stdout, _stderr) = run_scan(tmp.path(), &mock.uri(), &[]);
assert_eq!(code, 0);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("valid JSON");
let updates = v["updates"].as_array().expect("updates array present");
assert_eq!(updates.len(), 1, "exactly one update detected");
assert_eq!(updates[0]["purl"], "pkg:npm/lodash@4.17.20");
assert_eq!(updates[0]["oldUuid"], OLD_UUID);
assert_eq!(updates[0]["newUuid"], NEW_UUID);
let manifest: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(socket.join("manifest.json")).unwrap(),
)
.unwrap();
assert_eq!(
manifest["patches"]["pkg:npm/lodash@4.17.20"]["uuid"], OLD_UUID,
"scan without --apply must not rewrite the manifest"
);
assert_eq!(
std::fs::read(&marker).unwrap(),
b"original contents",
"scan without --apply must not touch existing blobs"
);
}