use assert_cmd::Command;
use std::fs;
use std::net::TcpListener;
use std::path::Path;
use tempfile::tempdir;
fn sdkt() -> Command {
Command::cargo_bin("sdkt").unwrap()
}
const CONTRACT: &str = "CAE3U7JKESRWZHPEQ72DVNGOQ6WPA7HSPQZL5YV46NPCE4TMUPAGYMEC";
#[test]
fn abi_diff_labels_keys_without_changing_counts() {
use stellar_xdr::{
Limits, ScSpecEntry, ScSpecTypeDef, ScSpecUdtUnionCaseTupleV0, ScSpecUdtUnionCaseV0,
ScSpecUdtUnionV0, ScVal, WriteXdr,
};
let dir = tempdir().unwrap();
let wasm_path = dir.path().join("abi.wasm");
let union = ScSpecEntry::UdtUnionV0(ScSpecUdtUnionV0 {
doc: "".try_into().unwrap(),
lib: "test".try_into().unwrap(),
name: "DataKey".try_into().unwrap(),
cases: vec![ScSpecUdtUnionCaseV0::TupleV0(ScSpecUdtUnionCaseTupleV0 {
doc: "".try_into().unwrap(),
name: "Balance".try_into().unwrap(),
type_: vec![ScSpecTypeDef::U32].try_into().unwrap(),
})]
.try_into()
.unwrap(),
});
let mut section = vec!["contractspecv0".len() as u8];
section.extend_from_slice(b"contractspecv0");
section.extend_from_slice(&union.to_xdr(Limits::none()).unwrap());
let mut wasm = vec![0, 97, 115, 109, 1, 0, 0, 0, 0];
wasm.push(section.len() as u8);
wasm.extend_from_slice(§ion);
fs::write(&wasm_path, wasm).unwrap();
let arg = sdkt_xdr::scval_to_base64(&ScVal::U32(7)).unwrap();
let key = sdkt_xdr::build_map_key("Balance", &[arg]).unwrap();
let raw = sdkt_xdr::encode_ledger_key(&sdkt_xdr::LedgerKeyParams::ContractDataEntry {
contract: CONTRACT.to_string(),
key,
durability: stellar_xdr::ContractDataDurability::Persistent,
})
.unwrap();
let old_path = dir.path().join("old.json");
let new_path = dir.path().join("new.json");
create_snapshot_file(&old_path, CONTRACT, &[(&raw, 50_000)]);
create_snapshot_file(&new_path, CONTRACT, &[(&raw, 3_000)]);
let run = |with_abi: bool| {
let mut cmd = sdkt();
cmd.args(["storage", "diff", "--old"])
.arg(&old_path)
.arg("--new")
.arg(&new_path)
.args(["--format", "json"]);
if with_abi {
cmd.arg("--abi").arg(&wasm_path);
}
let output = cmd.assert().success().get_output().stdout.clone();
serde_json::from_slice::<serde_json::Value>(&output).unwrap()
};
let plain = run(false);
let labeled = run(true);
assert_eq!(plain["entries"][0]["key"], labeled["entries"][0]["key"]);
assert_eq!(
plain["entries"].as_array().unwrap().len(),
labeled["entries"].as_array().unwrap().len()
);
assert!(plain["entries"][0].get("label").is_none());
assert_eq!(labeled["entries"][0]["label"], "DataKey::Balance(u32)");
let mut pretty = sdkt();
pretty
.args(["storage", "diff", "--old"])
.arg(&old_path)
.arg("--new")
.arg(&new_path)
.arg("--abi")
.arg(&wasm_path);
let output = pretty.assert().success().get_output().stdout.clone();
assert!(String::from_utf8(output)
.unwrap()
.contains("label=DataKey::Balance(u32)"));
}
fn create_snapshot_file(path: &Path, contract_id: &str, entries: &[(&str, u32)]) {
let entry_objs: Vec<serde_json::Value> = entries
.iter()
.map(|(k, ttl)| {
serde_json::json!({
"key": k,
"class": "persistent",
"current_ttl": ttl,
"days_remaining": ttl / 17280,
"extension_cost_stroops": 0,
})
})
.collect();
let report = serde_json::json!({
"contract_id": contract_id,
"total_entries": entries.len(),
"instance_entries": 0,
"persistent_entries": entries.len(),
"temporary_entries": 0,
"other_entries": 0,
"entries": entry_objs,
});
fs::write(path, serde_json::to_string_pretty(&report).unwrap()).unwrap();
}
#[test]
fn extend_plan_cli_never_contacts_rpc_and_carries_network_flag() {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind ephemeral port");
let mock_url = format!("http://{}", listener.local_addr().unwrap());
listener
.set_nonblocking(true)
.expect("set nonblocking listener");
let dir = tempdir().unwrap();
let old_path = dir.path().join("old.json");
let new_path = dir.path().join("new.json");
create_snapshot_file(&old_path, CONTRACT, &[("keyA==", 50_000)]);
create_snapshot_file(&new_path, CONTRACT, &[("keyA==", 3_000)]);
let mut cmd = sdkt();
cmd.arg("storage")
.arg("storage-diff")
.arg("--old")
.arg(&old_path)
.arg("--new")
.arg(&new_path)
.arg("--extend-plan")
.arg("--rpc-url")
.arg(&mock_url);
let assert = cmd.assert().success();
let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap();
assert!(stdout.contains("Extension Plan"));
assert!(stdout.contains("Ready-to-run:"));
assert!(stdout.contains(&format!("--rpc-url '{}'", mock_url)));
assert!(stdout.contains(&format!("--contract '{}'", CONTRACT)));
assert!(stdout.contains("--key 'keyA=='"));
let accept_res = listener.accept();
assert!(
accept_res.is_err(),
"Expected no RPC connections, but server received a connection!"
);
}
#[test]
fn storage_diff_cli_shell_quoting_regression_test() {
let dir = tempdir().unwrap();
let old_path = dir.path().join("old.json");
let new_path = dir.path().join("new.json");
let pwn_file = "/tmp/sdkt_pwn_test_file";
let _ = fs::remove_file(pwn_file);
let injection_key = format!("BKEYB==; touch {pwn_file} space $(id)");
create_snapshot_file(&old_path, CONTRACT, &[(&injection_key, 50_000)]);
create_snapshot_file(&new_path, CONTRACT, &[]);
let mut cmd = sdkt();
cmd.arg("storage")
.arg("storage-diff")
.arg("--old")
.arg(&old_path)
.arg("--new")
.arg(&new_path)
.arg("--extend-plan");
let assert = cmd.assert().success();
let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap();
assert!(stdout.contains("Ready-to-run:"));
assert!(stdout.contains(&format!("--key '{}'", injection_key)));
assert!(
!Path::new(pwn_file).exists(),
"Injection file was created! Quoting failed."
);
}
#[test]
fn storage_diff_cli_rejects_empty_entries_when_total_entries_positive() {
let dir = tempdir().unwrap();
let legacy_old_path = dir.path().join("legacy_old.json");
let new_path = dir.path().join("new.json");
let legacy_json = serde_json::json!({
"contract_id": CONTRACT,
"total_entries": 3,
"instance_entries": 1,
"persistent_entries": 2,
"temporary_entries": 0,
"other_entries": 0,
});
fs::write(
&legacy_old_path,
serde_json::to_string(&legacy_json).unwrap(),
)
.unwrap();
create_snapshot_file(&new_path, CONTRACT, &[("keyA==", 10_000)]);
let mut cmd = sdkt();
cmd.arg("storage")
.arg("storage-diff")
.arg("--old")
.arg(&legacy_old_path)
.arg("--new")
.arg(&new_path)
.arg("--extend-plan");
let assert = cmd.assert().failure();
let stderr = String::from_utf8(assert.get_output().stderr.clone()).unwrap();
assert!(stderr.contains(&format!(
"Failed to load --old snapshot '{}'",
legacy_old_path.display()
)));
assert!(stderr.contains("total_entries (3) > 0"));
assert!(stderr.contains("entries array is empty"));
}
#[test]
fn storage_diff_cli_alias_diff_works() {
let dir = tempdir().unwrap();
let old_path = dir.path().join("old.json");
let new_path = dir.path().join("new.json");
create_snapshot_file(&old_path, CONTRACT, &[("keyA==", 50_000)]);
create_snapshot_file(&new_path, CONTRACT, &[("keyA==", 3_000)]);
let mut cmd = sdkt();
cmd.arg("storage")
.arg("diff")
.arg("--old")
.arg(&old_path)
.arg("--new")
.arg(&new_path)
.arg("--extend-plan");
let assert = cmd.assert().success();
let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap();
assert!(stdout.contains("Extension Plan"));
}