use std::path::{Path, PathBuf};
use std::process::Command;
use wiremock::matchers::{method, path, path_regex};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn binary() -> PathBuf {
env!("CARGO_BIN_EXE_socket-patch").into()
}
const ORG_SLUG: &str = "test-org";
const UUID_A: &str = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa";
const UUID_B: &str = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb";
fn run_get_auth(cwd: &Path, api_url: &str, identifier: &str, extra: &[&str]) -> (i32, String, String) {
let mut args = vec![
"get",
identifier,
"--json",
"--save-only",
"--yes",
"--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)
.env_remove("SOCKET_API_TOKEN")
.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 get_by_purl_with_multiple_patches_emits_selection_required() {
let mock = MockServer::start().await;
let purl = "pkg:npm/multipatch@1.0.0";
let encoded = "pkg%3Anpm%2Fmultipatch%401.0.0";
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_A, "purl": purl,
"publishedAt": "2024-01-01T00:00:00Z",
"description": "Patch A", "license": "MIT", "tier": "free",
"vulnerabilities": {}
},
{
"uuid": UUID_B, "purl": purl,
"publishedAt": "2024-02-01T00:00:00Z",
"description": "Patch B", "license": "MIT", "tier": "free",
"vulnerabilities": {}
}
],
"canAccessPaidPatches": true,
})))
.mount(&mock)
.await;
let tmp = tempfile::tempdir().expect("tempdir");
let (code, stdout, _stderr) = run_get_auth(tmp.path(), &mock.uri(), purl, &[]);
assert_ne!(code, 0, "multi-patch without --id should not exit 0");
let v: serde_json::Value =
serde_json::from_str(stdout.trim()).expect("valid JSON envelope");
let status = v["status"].as_str().unwrap_or("");
assert!(
status == "selection_required" || status == "partial_failure" || status == "error",
"multi-patch must surface as selection_required / partial_failure / error; got {status}"
);
}
#[tokio::test]
async fn get_by_purl_with_id_filter_no_match_emits_error() {
let mock = MockServer::start().await;
let purl = "pkg:npm/idmiss@1.0.0";
let encoded = "pkg%3Anpm%2Fidmiss%401.0.0";
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_A, "purl": purl,
"publishedAt": "2024-01-01T00:00:00Z",
"description": "Patch A", "license": "MIT", "tier": "free",
"vulnerabilities": {}
}
],
"canAccessPaidPatches": true,
})))
.mount(&mock)
.await;
let tmp = tempfile::tempdir().expect("tempdir");
let (code, stdout, _stderr) = run_get_auth(
tmp.path(),
&mock.uri(),
purl,
&["--id", UUID_B],
);
assert_ne!(code, 0, "non-matching --id must fail");
let _ = serde_json::from_str::<serde_json::Value>(stdout.trim());
}
#[tokio::test]
async fn get_uuid_returning_404_emits_not_found() {
let mock = MockServer::start().await;
Mock::given(method("GET"))
.and(path(format!("/v0/orgs/{ORG_SLUG}/patches/view/{UUID_A}")))
.respond_with(ResponseTemplate::new(404))
.mount(&mock)
.await;
let tmp = tempfile::tempdir().expect("tempdir");
let (_code, stdout, _stderr) = run_get_auth(tmp.path(), &mock.uri(), UUID_A, &[]);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("valid JSON");
let status = v["status"].as_str().unwrap_or("");
assert!(
status == "not_found" || status == "error",
"404 must surface as not_found or error; got {status}"
);
}
#[tokio::test]
async fn get_uuid_returning_500_emits_error() {
let mock = MockServer::start().await;
Mock::given(method("GET"))
.and(path(format!("/v0/orgs/{ORG_SLUG}/patches/view/{UUID_A}")))
.respond_with(ResponseTemplate::new(500).set_body_string("server exploded"))
.mount(&mock)
.await;
let tmp = tempfile::tempdir().expect("tempdir");
let (code, stdout, _stderr) = run_get_auth(tmp.path(), &mock.uri(), UUID_A, &[]);
assert_ne!(code, 0);
if let Ok(v) = serde_json::from_str::<serde_json::Value>(stdout.trim()) {
assert_eq!(v["status"], "error");
}
}
#[tokio::test]
async fn get_uuid_returning_malformed_json_emits_error() {
let mock = MockServer::start().await;
Mock::given(method("GET"))
.and(path(format!("/v0/orgs/{ORG_SLUG}/patches/view/{UUID_A}")))
.respond_with(
ResponseTemplate::new(200).set_body_string("{ this is not json"),
)
.mount(&mock)
.await;
let tmp = tempfile::tempdir().expect("tempdir");
let (code, stdout, _stderr) = run_get_auth(tmp.path(), &mock.uri(), UUID_A, &[]);
assert_ne!(code, 0);
let _ = serde_json::from_str::<serde_json::Value>(stdout.trim());
}
#[tokio::test]
async fn get_by_cve_with_no_patches_emits_no_match() {
let mock = MockServer::start().await;
Mock::given(method("GET"))
.and(path_regex(format!(
r"^/v0/orgs/{ORG_SLUG}/patches/by-cve/CVE-2099-9999$"
)))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"patches": [],
"canAccessPaidPatches": true,
})))
.mount(&mock)
.await;
let tmp = tempfile::tempdir().expect("tempdir");
let (_code, stdout, _stderr) =
run_get_auth(tmp.path(), &mock.uri(), "CVE-2099-9999", &[]);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("valid JSON");
let status = v["status"].as_str().unwrap_or("");
assert!(
status == "no_match" || status == "not_found",
"CVE empty result must emit no_match/not_found; got {status}"
);
}
#[tokio::test]
async fn get_by_ghsa_with_no_patches_emits_no_match() {
let mock = MockServer::start().await;
Mock::given(method("GET"))
.and(path_regex(format!(
r"^/v0/orgs/{ORG_SLUG}/patches/by-ghsa/GHSA-xxxx-xxxx-xxxx$"
)))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"patches": [],
"canAccessPaidPatches": true,
})))
.mount(&mock)
.await;
let tmp = tempfile::tempdir().expect("tempdir");
let (_code, stdout, _stderr) =
run_get_auth(tmp.path(), &mock.uri(), "GHSA-xxxx-xxxx-xxxx", &[]);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("valid JSON");
let status = v["status"].as_str().unwrap_or("");
assert!(
status == "no_match" || status == "not_found",
"GHSA empty result must emit no_match/not_found; got {status}"
);
}