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 PURL: &str = "pkg:npm/remove-network-test@1.0.0";
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_manifest(socket: &Path, before_hash: &str, after_hash: &str) {
std::fs::create_dir_all(socket).expect("create .socket");
let body = format!(
r#"{{
"patches": {{
"{PURL}": {{
"uuid": "{UUID}",
"exportedAt": "2024-01-01T00:00:00Z",
"files": {{
"package/index.js": {{
"beforeHash": "{before_hash}",
"afterHash": "{after_hash}"
}}
}},
"vulnerabilities": {{}},
"description": "remove network test patch",
"license": "MIT",
"tier": "free"
}}
}}
}}"#
);
std::fs::write(socket.join("manifest.json"), body).expect("write manifest");
}
fn manifest_has_entry(socket: &Path) -> bool {
let body = std::fs::read_to_string(socket.join("manifest.json")).expect("read manifest");
let v: serde_json::Value = serde_json::from_str(&body).expect("parse manifest");
v["patches"]
.as_object()
.map(|m| m.contains_key(PURL))
.unwrap_or(false)
}
async fn mount_before_blob(mock: &MockServer, before: &[u8], before_hash: &str) {
Mock::given(method("GET"))
.and(path(format!(
"/v0/orgs/{ORG_SLUG}/patches/blob/{before_hash}"
)))
.respond_with(ResponseTemplate::new(200).set_body_bytes(before.to_vec()))
.mount(mock)
.await;
}
fn run_remove(cwd: &Path, api_url: &str, extra: &[&str]) -> (i32, String) {
let mut argv: Vec<&str> = vec!["remove", PURL, "--json", "--yes"];
argv.extend_from_slice(extra);
let out = Command::new(binary())
.args(&argv)
.current_dir(cwd)
.env("SOCKET_API_URL", api_url)
.env("SOCKET_API_TOKEN", "fake-token-for-test")
.env("SOCKET_ORG_SLUG", ORG_SLUG)
.env("SOCKET_TELEMETRY_DISABLED", "1")
.output()
.expect("run socket-patch");
(
out.status.code().unwrap_or(-1),
String::from_utf8_lossy(&out.stdout).to_string(),
)
}
#[tokio::test]
async fn remove_online_downloads_missing_before_blob_then_removes() {
let before = b"before\n";
let after = b"after\n";
let before_hash = git_sha256(before);
let after_hash = git_sha256(after);
let mock = MockServer::start().await;
mount_before_blob(&mock, before, &before_hash).await;
let tmp = tempfile::tempdir().expect("tempdir");
let socket = tmp.path().join(".socket");
write_manifest(&socket, &before_hash, &after_hash);
let (code, stdout) = run_remove(tmp.path(), &mock.uri(), &[]);
assert_eq!(code, 0, "online remove must succeed; stdout=\n{stdout}");
assert!(
!manifest_has_entry(&socket),
"online remove must drop the manifest entry; stdout=\n{stdout}"
);
}
#[tokio::test]
async fn remove_offline_does_not_fetch_and_keeps_entry() {
let before = b"before\n";
let after = b"after\n";
let before_hash = git_sha256(before);
let after_hash = git_sha256(after);
let mock = MockServer::start().await;
mount_before_blob(&mock, before, &before_hash).await;
let tmp = tempfile::tempdir().expect("tempdir");
let socket = tmp.path().join(".socket");
write_manifest(&socket, &before_hash, &after_hash);
let (code, stdout) = run_remove(tmp.path(), &mock.uri(), &["--offline"]);
assert_eq!(
code, 1,
"remove --offline with a missing blob must fail rollback; stdout=\n{stdout}"
);
let v: serde_json::Value = serde_json::from_str(&stdout).expect("valid JSON");
assert_eq!(v["error"]["code"], "rollback_failed");
assert!(
manifest_has_entry(&socket),
"remove --offline must NOT delete the entry when rollback can't run; stdout=\n{stdout}"
);
}