use assert_cmd::Command;
#[test]
fn test_cli_wasm_cache_info_default() {
let mut cmd = Command::cargo_bin("sdkt").unwrap();
let assert = cmd.arg("wasm").arg("cache").arg("info").assert();
assert
.success()
.stdout(predicates::str::contains("Cache Info for Network"));
}
#[test]
fn test_cli_wasm_cache_info_json() {
let mut cmd = Command::cargo_bin("sdkt").unwrap();
let assert = cmd
.arg("wasm")
.arg("cache")
.arg("info")
.arg("--format")
.arg("json")
.assert();
assert
.success()
.stdout(predicates::str::contains("\"network\":\"testnet\""));
}
#[test]
fn test_cli_wasm_cache_clear() {
let mut cmd = Command::cargo_bin("sdkt").unwrap();
let assert = cmd
.arg("wasm")
.arg("cache")
.arg("clear")
.arg("--network")
.arg("testnet")
.assert();
assert.success().stdout(predicates::str::contains(
"Cleared all cache entries for testnet.",
));
}
#[test]
fn test_cli_wasm_cache_remove_existing_entry_succeeds() {
let cache = tempfile::tempdir().unwrap();
let net_dir = cache.path().join("wasm").join("testnet");
std::fs::create_dir_all(&net_dir).unwrap();
std::fs::write(net_dir.join("fakehash123.json"), "{}").unwrap();
std::fs::write(net_dir.join("fakehash123.wasm"), b"wasm-bytes").unwrap();
let mut cmd = Command::cargo_bin("sdkt").unwrap();
let assert = cmd
.env("SDKT_CACHE_DIR", cache.path())
.arg("wasm")
.arg("cache")
.arg("remove")
.arg("fakehash123")
.assert();
assert.success().stdout(predicates::str::contains(
"Removed fakehash123 from testnet cache.",
));
assert!(!net_dir.join("fakehash123.json").exists());
assert!(!net_dir.join("fakehash123.wasm").exists());
}
#[test]
fn test_cli_wasm_cache_remove_missing_entry_is_not_a_success() {
let cache = tempfile::tempdir().unwrap();
let mut cmd = Command::cargo_bin("sdkt").unwrap();
let assert = cmd
.env("SDKT_CACHE_DIR", cache.path())
.arg("wasm")
.arg("cache")
.arg("remove")
.arg("definitelynotcached")
.assert();
assert
.failure()
.stderr(predicates::str::contains("Not found"))
.stderr(predicates::str::contains("'definitelynotcached'"))
.stderr(predicates::str::contains("not found in testnet cache"))
.stdout(predicates::str::contains("Removed").not());
}
#[test]
fn test_cli_wasm_cache_clear_reports_truthfully_on_empty_cache() {
let cache = tempfile::tempdir().unwrap();
let mut cmd = Command::cargo_bin("sdkt").unwrap();
let assert = cmd
.env("SDKT_CACHE_DIR", cache.path())
.arg("wasm")
.arg("cache")
.arg("clear")
.arg("--network")
.arg("testnet")
.assert();
assert.success().stdout(predicates::str::contains(
"Cleared all cache entries for testnet.",
));
}
#[test]
fn test_cli_wasm_cache_clear_removes_existing_entries() {
let cache = tempfile::tempdir().unwrap();
let net_dir = cache.path().join("wasm").join("testnet");
std::fs::create_dir_all(&net_dir).unwrap();
std::fs::write(net_dir.join("aaa.json"), "{}").unwrap();
std::fs::write(net_dir.join("aaa.wasm"), b"wasm-bytes").unwrap();
let mut cmd = Command::cargo_bin("sdkt").unwrap();
let assert = cmd
.env("SDKT_CACHE_DIR", cache.path())
.arg("wasm")
.arg("cache")
.arg("clear")
.arg("--network")
.arg("testnet")
.assert();
assert.success().stdout(predicates::str::contains(
"Cleared all cache entries for testnet.",
));
let mut info = Command::cargo_bin("sdkt").unwrap();
info.env("SDKT_CACHE_DIR", cache.path())
.arg("wasm")
.arg("cache")
.arg("info")
.assert()
.success()
.stdout(predicates::str::contains("Entries: 0"));
}
#[test]
fn test_cli_wasm_inspect_missing_file() {
let mut cmd = Command::cargo_bin("sdkt").unwrap();
let assert = cmd
.arg("wasm")
.arg("inspect")
.arg("non_existent_file.wasm")
.assert();
assert
.failure()
.stderr(predicates::str::contains("Error reading WASM file"));
}
#[test]
fn test_cli_wasm_inspect_invalid_wasm() {
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::write(tmp.path(), b"invalid wasm data").unwrap();
let mut cmd = Command::cargo_bin("sdkt").unwrap();
let assert = cmd.arg("wasm").arg("inspect").arg(tmp.path()).assert();
assert
.failure()
.stderr(predicates::str::contains("Error parsing WASM metadata"));
}
#[test]
fn test_cli_wasm_inspect_valid_empty_wasm() {
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::write(tmp.path(), [0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]).unwrap();
let mut cmd = Command::cargo_bin("sdkt").unwrap();
let assert = cmd.arg("wasm").arg("inspect").arg(tmp.path()).assert();
assert
.success()
.stdout(predicates::str::contains("WASM Inspection Report"))
.stdout(predicates::str::contains("Size: 8 bytes"))
.stdout(predicates::str::contains("Contract Spec Available: No"));
}
#[test]
fn test_cli_wasm_inspect_json() {
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::write(tmp.path(), [0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]).unwrap();
let mut cmd = Command::cargo_bin("sdkt").unwrap();
let assert = cmd
.arg("wasm")
.arg("inspect")
.arg(tmp.path())
.arg("--format")
.arg("json")
.assert();
assert
.success()
.stdout(predicates::str::contains("\"size_bytes\": 8"));
}
#[test]
fn test_cli_wasm_inspect_us_new_json_kind_strings() {
let wasm_path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/us_new.wasm");
let mut cmd = Command::cargo_bin("sdkt").unwrap();
let assert = cmd
.arg("wasm")
.arg("inspect")
.arg(wasm_path)
.arg("--format")
.arg("json")
.assert();
let output = assert.success().get_output().stdout.clone();
let stdout = String::from_utf8_lossy(&output);
assert!(!stdout.contains("Func("));
assert!(!stdout.contains("\"Func\""));
assert!(!stdout.contains("\"Memory\""));
assert!(stdout.contains("\"kind\": \"func\""));
assert!(stdout.contains("\"kind\": \"memory\""));
}
#[test]
fn test_cli_wasm_inspect_us_new_pretty_kind_strings() {
let wasm_path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/us_new.wasm");
let mut cmd = Command::cargo_bin("sdkt").unwrap();
let assert = cmd.arg("wasm").arg("inspect").arg(wasm_path).assert();
let output = assert.success().get_output().stdout.clone();
let stdout = String::from_utf8_lossy(&output);
assert!(!stdout.contains("[Func]"));
assert!(!stdout.contains("[Memory]"));
assert!(stdout.contains("[func]") || stdout.contains("[memory]"));
}
#[test]
fn test_cli_wasm_inspect_us_new_contract_metadata_pretty() {
let wasm_path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/us_new.wasm");
let mut cmd = Command::cargo_bin("sdkt").unwrap();
let assert = cmd.arg("wasm").arg("inspect").arg(wasm_path).assert();
let output = assert.success().get_output().stdout.clone();
let stdout = String::from_utf8_lossy(&output);
assert!(
stdout.contains("Contract Metadata (3):"),
"expected a Contract Metadata block, got:\n{stdout}"
);
assert!(stdout.contains("rsver = 1.97.1"));
assert!(stdout.contains("rssdkver = 22.0.11#"));
assert!(stdout.contains("cliver = 27.1.0#"));
}
#[test]
fn test_cli_wasm_inspect_us_old_contract_metadata_empty() {
let wasm_path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/us_old.wasm");
let mut cmd = Command::cargo_bin("sdkt").unwrap();
let assert = cmd.arg("wasm").arg("inspect").arg(wasm_path).assert();
let output = assert.success().get_output().stdout.clone();
let stdout = String::from_utf8_lossy(&output);
assert!(stdout.contains("Contract Metadata (0):"));
assert!(stdout.contains("(none)"));
}
#[test]
fn test_cli_wasm_metadata_missing_contract() {
let mut cmd = Command::cargo_bin("sdkt").unwrap();
let assert = cmd
.arg("wasm")
.arg("metadata")
.assert();
assert.failure();
}
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use predicates::prelude::*;
use sdkt_xdr::{encode_ledger_key, LedgerKeyParams};
use std::io::{Read, Write};
use std::net::TcpListener;
use std::thread;
use stellar_xdr::{
ContractCodeEntry, ContractCodeEntryExt, ContractDataDurability, ContractDataEntry,
ContractExecutable, ContractId, ExtensionPoint, Hash, LedgerEntry, LedgerEntryData,
LedgerEntryExt, Limited, Limits, ScAddress, ScContractInstance, ScVal, WriteXdr,
};
use tempfile::tempdir;
static WASM_FIXTURE: &[u8] = include_bytes!("fixtures/us_new.wasm");
const CONTRACT_ID: &str = "CAE3U7JKESRWZHPEQ72DVNGOQ6WPA7HSPQZL5YV46NPCE4TMUPAGYMEC";
const WASM_HASH_HEX: &str = "551c5a9c7fd4c4a71e57e9c8a0ece1e7bd506ea065993c63db2dc35516406775";
fn encode_ledger_entry(entry: &LedgerEntry) -> String {
let mut buf = Vec::new();
let mut l = Limited::new(&mut buf, Limits::none());
entry.write_xdr(&mut l).unwrap();
STANDARD.encode(&buf)
}
fn contract_data_entry_xdr(wasm_hash: [u8; 32]) -> String {
encode_ledger_entry(&LedgerEntry {
last_modified_ledger_seq: 1,
data: LedgerEntryData::ContractData(ContractDataEntry {
ext: ExtensionPoint::V0,
contract: ScAddress::Contract(ContractId(Hash([0; 32]))),
key: ScVal::LedgerKeyContractInstance,
durability: ContractDataDurability::Persistent,
val: ScVal::ContractInstance(ScContractInstance {
executable: ContractExecutable::Wasm(Hash(wasm_hash)),
storage: None,
}),
}),
ext: LedgerEntryExt::V0,
})
}
fn contract_code_entry_xdr(wasm_hash: [u8; 32], code: &[u8]) -> String {
encode_ledger_entry(&LedgerEntry {
last_modified_ledger_seq: 1,
data: LedgerEntryData::ContractCode(ContractCodeEntry {
ext: ContractCodeEntryExt::V0,
hash: Hash(wasm_hash),
code: code.to_vec().try_into().unwrap(),
}),
ext: LedgerEntryExt::V0,
})
}
fn spawn_mock_rpc_server(wasm_bytes: &'static [u8]) -> String {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
let url = format!("http://{}", addr);
let contract_id_hex = "09ba7d2a24a36c9de487f43ab4ce87acf07cf27c32bee2bcf35e22726ca3c06c";
let contract_data_key =
encode_ledger_key(&LedgerKeyParams::ContractData(contract_id_hex.to_string())).unwrap();
let contract_code_key =
encode_ledger_key(&LedgerKeyParams::ContractCode(WASM_HASH_HEX.to_string())).unwrap();
let mut wasm_hash = [0u8; 32];
hex::decode_to_slice(WASM_HASH_HEX, &mut wasm_hash).unwrap();
let inspect_xdr = contract_data_entry_xdr(wasm_hash);
let code_xdr = contract_code_entry_xdr(wasm_hash, wasm_bytes);
thread::spawn(move || {
for conn in listener.incoming() {
let mut sock = match conn {
Ok(s) => s,
Err(_) => break,
};
let mut buf = [0u8; 16384];
let n = sock.read(&mut buf).unwrap_or(0);
let req = String::from_utf8_lossy(&buf[..n]).to_string();
let body = if req.contains("\"getLatestLedger\"") {
r#"{"jsonrpc":"2.0","id":1,"result":{"id":"test","sequence":100,"protocolVersion":20}}"#.to_string()
} else if req.contains("\"getLedgerEntries\"") {
if req.contains(&contract_data_key) {
format!(
r#"{{"jsonrpc":"2.0","id":1,"result":{{"entries":[{{"key":"{contract_data_key}","xdr":"{inspect_xdr}","lastModifiedLedgerSeq":1}}],"latestLedger":100}}}}"#
)
} else if req.contains(&contract_code_key) {
format!(
r#"{{"jsonrpc":"2.0","id":1,"result":{{"entries":[{{"key":"{contract_code_key}","xdr":"{code_xdr}","lastModifiedLedgerSeq":1}}],"latestLedger":100}}}}"#
)
} else {
r#"{"jsonrpc":"2.0","id":1,"result":{"entries":[],"latestLedger":100}}"#
.to_string()
}
} else {
r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"method not found"}}"#
.to_string()
};
let resp = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
body.len(),
body
);
let _ = sock.write_all(resp.as_bytes());
let _ = sock.flush();
}
});
url
}
fn sdkt_isolated(cache_dir: &std::path::Path, network_dir: &std::path::Path) -> Command {
let mut cmd = Command::cargo_bin("sdkt").unwrap();
cmd.env("SDKT_CACHE_DIR", cache_dir);
cmd.env("SDKT_NETWORK_DIR", network_dir);
cmd
}
#[test]
fn test_cli_network_conflicts_rejected() {
let cache_dir = tempdir().unwrap();
let network_dir = tempdir().unwrap();
sdkt_isolated(cache_dir.path(), network_dir.path())
.args([
"wasm",
"metadata",
"--contract",
CONTRACT_ID,
"--network",
"mainnet",
"--rpc-url",
"http://127.0.0.1:1234",
])
.assert()
.failure()
.stderr(
predicates::str::contains("cannot be used with")
.or(predicates::str::contains("conflicts with")),
);
sdkt_isolated(cache_dir.path(), network_dir.path())
.args([
"wasm",
"metadata",
"--contract",
CONTRACT_ID,
"--network",
"testnet",
"--network-profile",
"mocknet",
])
.assert()
.failure()
.stderr(
predicates::str::contains("cannot be used with")
.or(predicates::str::contains("conflicts with")),
);
sdkt_isolated(cache_dir.path(), network_dir.path())
.args([
"verify",
"--contract",
CONTRACT_ID,
"--network",
"mainnet",
"--rpc-url",
"http://127.0.0.1:1234",
])
.assert()
.failure()
.stderr(
predicates::str::contains("cannot be used with")
.or(predicates::str::contains("conflicts with")),
);
sdkt_isolated(cache_dir.path(), network_dir.path())
.args([
"health",
"--contract",
CONTRACT_ID,
"--network",
"futurenet",
"--network-profile",
"mocknet",
])
.assert()
.failure()
.stderr(
predicates::str::contains("cannot be used with")
.or(predicates::str::contains("conflicts with")),
);
}
#[test]
fn test_cli_wasm_metadata_mock_rpc_cache_namespace() {
let cache_dir = tempdir().unwrap();
let network_dir = tempdir().unwrap();
let mock_url = spawn_mock_rpc_server(WASM_FIXTURE);
sdkt_isolated(cache_dir.path(), network_dir.path())
.args([
"network",
"add",
"profile_alpha",
"--rpc-url",
&mock_url,
"--passphrase",
"Test SDF Network ; September 2015",
])
.assert()
.success();
sdkt_isolated(cache_dir.path(), network_dir.path())
.args([
"wasm",
"metadata",
"--contract",
CONTRACT_ID,
"--network-profile",
"profile_alpha",
])
.assert()
.success()
.stdout(predicates::str::contains("Cache Status: Miss"))
.stdout(predicates::str::contains("Network: profile_alpha"));
let cached_file = cache_dir
.path()
.join("wasm")
.join("profile_alpha")
.join(format!("{}.json", WASM_HASH_HEX));
assert!(
cached_file.exists(),
"Cache file must exist at {:?}",
cached_file
);
let testnet_dir = cache_dir.path().join("wasm").join("testnet");
assert!(
!testnet_dir.exists(),
"testnet cache namespace must not be touched"
);
sdkt_isolated(cache_dir.path(), network_dir.path())
.args([
"wasm",
"metadata",
"--contract",
CONTRACT_ID,
"--network-profile",
"profile_alpha",
])
.assert()
.success()
.stdout(predicates::str::contains("Cache Status: Hit"))
.stdout(predicates::str::contains("Network: profile_alpha"));
}
#[test]
fn test_cli_wasm_metadata_mock_rpc_reports_contract_metadata() {
let cache_dir = tempdir().unwrap();
let network_dir = tempdir().unwrap();
let mock_url = spawn_mock_rpc_server(WASM_FIXTURE);
sdkt_isolated(cache_dir.path(), network_dir.path())
.args([
"network",
"add",
"profile_meta",
"--rpc-url",
&mock_url,
"--passphrase",
"Test SDF Network ; September 2015",
])
.assert()
.success();
let pretty = sdkt_isolated(cache_dir.path(), network_dir.path())
.args([
"wasm",
"metadata",
"--contract",
CONTRACT_ID,
"--network-profile",
"profile_meta",
])
.assert()
.success();
let stdout = String::from_utf8_lossy(&pretty.get_output().stdout).clone();
assert!(
stdout.contains("Contract Metadata (3):"),
"expected contractmetav0 entries on the on-chain path, got:\n{stdout}"
);
assert!(stdout.contains("rsver = 1.97.1"));
assert!(stdout.contains("rssdkver = 22.0.11#"));
assert!(stdout.contains("cliver = 27.1.0#"));
let json_out = sdkt_isolated(cache_dir.path(), network_dir.path())
.args([
"wasm",
"metadata",
"--contract",
CONTRACT_ID,
"--network-profile",
"profile_meta",
"--format",
"json",
])
.assert()
.success();
let json = String::from_utf8_lossy(&json_out.get_output().stdout).clone();
let v: serde_json::Value =
serde_json::from_str(&json).unwrap_or_else(|e| panic!("invalid JSON ({e}): {json}"));
for key in [
"contract_id",
"wasm_hash",
"wasm_size",
"abi",
"storage_summary",
"ttl_info",
"storage_keys",
"contract_meta",
] {
assert!(v.get(key).is_some(), "missing `{key}` in {v}");
}
let entries = v["contract_meta"]
.as_array()
.expect("contract_meta should be an array");
assert_eq!(entries.len(), 3, "expected 3 entries, got {entries:?}");
let keys: Vec<&str> = entries
.iter()
.filter_map(|e| e.get("key").and_then(|k| k.as_str()))
.collect();
assert_eq!(keys, vec!["rsver", "rssdkver", "cliver"]);
assert_eq!(entries[0]["value"].as_str(), Some("1.97.1"));
}
#[test]
fn test_cli_cache_namespace_isolation_regression() {
let cache_dir = tempdir().unwrap();
let network_dir = tempdir().unwrap();
let mock_url = spawn_mock_rpc_server(WASM_FIXTURE);
sdkt_isolated(cache_dir.path(), network_dir.path())
.args([
"network",
"add",
"profile_net1",
"--rpc-url",
&mock_url,
"--passphrase",
"Test SDF Network ; September 2015",
])
.assert()
.success();
sdkt_isolated(cache_dir.path(), network_dir.path())
.args([
"network",
"add",
"profile_net2",
"--rpc-url",
&mock_url,
"--passphrase",
"Test SDF Network ; September 2015",
])
.assert()
.success();
sdkt_isolated(cache_dir.path(), network_dir.path())
.args([
"wasm",
"metadata",
"--contract",
CONTRACT_ID,
"--network-profile",
"profile_net1",
])
.assert()
.success();
let net1_file = cache_dir
.path()
.join("wasm")
.join("profile_net1")
.join(format!("{}.json", WASM_HASH_HEX));
assert!(net1_file.exists());
let testnet_dir = cache_dir.path().join("wasm").join("testnet");
std::fs::create_dir_all(&testnet_dir).unwrap();
let testnet_poison_file = testnet_dir.join(format!("{}.json", WASM_HASH_HEX));
std::fs::write(&testnet_poison_file, b"{\"poisoned\":true}").unwrap();
sdkt_isolated(cache_dir.path(), network_dir.path())
.args([
"wasm",
"metadata",
"--contract",
CONTRACT_ID,
"--network-profile",
"profile_net2",
])
.assert()
.success()
.stdout(predicates::str::contains("Cache Status: Miss"));
let net2_file = cache_dir
.path()
.join("wasm")
.join("profile_net2")
.join(format!("{}.json", WASM_HASH_HEX));
assert!(net2_file.exists());
let testnet_content = std::fs::read(&testnet_poison_file).unwrap();
assert_eq!(testnet_content, b"{\"poisoned\":true}");
sdkt_isolated(cache_dir.path(), network_dir.path())
.args([
"wasm",
"metadata",
"--contract",
CONTRACT_ID,
"--network",
"mainnet",
])
.assert()
.failure();
let testnet_content_after = std::fs::read(&testnet_poison_file).unwrap();
assert_eq!(testnet_content_after, b"{\"poisoned\":true}");
}
#[test]
fn test_cli_verify_and_health_network_labels_against_mock() {
let cache_dir = tempdir().unwrap();
let network_dir = tempdir().unwrap();
let mock_url = spawn_mock_rpc_server(WASM_FIXTURE);
sdkt_isolated(cache_dir.path(), network_dir.path())
.args([
"network",
"add",
"custom_endpoint",
"--rpc-url",
&mock_url,
"--passphrase",
"Test SDF Network ; September 2015",
])
.assert()
.success();
sdkt_isolated(cache_dir.path(), network_dir.path())
.args([
"verify",
"--contract",
CONTRACT_ID,
"--network-profile",
"custom_endpoint",
])
.assert()
.success()
.stdout(predicates::str::contains("Network : custom_endpoint"));
let assert_verify_json = sdkt_isolated(cache_dir.path(), network_dir.path())
.args([
"verify",
"--contract",
CONTRACT_ID,
"--network-profile",
"custom_endpoint",
"--format",
"json",
])
.assert()
.success();
let verify_val: serde_json::Value =
serde_json::from_slice(&assert_verify_json.get_output().stdout).unwrap();
assert_eq!(verify_val["network"], "custom_endpoint");
sdkt_isolated(cache_dir.path(), network_dir.path())
.args([
"health",
"--contract",
CONTRACT_ID,
"--network-profile",
"custom_endpoint",
])
.assert()
.success()
.stdout(predicates::str::contains("Network : custom_endpoint"));
let assert_health_json = sdkt_isolated(cache_dir.path(), network_dir.path())
.args([
"health",
"--contract",
CONTRACT_ID,
"--network-profile",
"custom_endpoint",
"--format",
"json",
])
.assert()
.success();
let health_val: serde_json::Value =
serde_json::from_slice(&assert_health_json.get_output().stdout).unwrap();
assert_eq!(health_val["network"], "custom_endpoint");
sdkt_isolated(cache_dir.path(), network_dir.path())
.args([
"verify",
"--contract",
CONTRACT_ID,
"--rpc-url",
&mock_url,
"--network-passphrase",
"Public Global Stellar Network ; September 2015",
])
.assert()
.success()
.stdout(predicates::str::contains("Network : mainnet"));
sdkt_isolated(cache_dir.path(), network_dir.path())
.args([
"health",
"--contract",
CONTRACT_ID,
"--rpc-url",
&mock_url,
"--network-passphrase",
"Public Global Stellar Network ; September 2015",
])
.assert()
.success()
.stdout(predicates::str::contains("Network : mainnet"));
}