use assert_cmd::Command;
const MINIMAL_WASM: &[u8] = &[0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00];
#[test]
fn test_cli_verify_missing_contract_arg() {
let mut cmd = Command::cargo_bin("sdkt").unwrap();
let assert = cmd.arg("verify").assert();
assert.failure();
}
#[test]
fn test_cli_verify_invalid_format_arg() {
let mut cmd = Command::cargo_bin("sdkt").unwrap();
let assert = cmd
.arg("verify")
.arg("--contract")
.arg("CABCDEFG")
.arg("--format")
.arg("bogus")
.assert();
assert
.failure()
.stderr(predicates::str::contains("Invalid format"));
}
#[test]
fn test_cli_verify_missing_wasm_file() {
let mut cmd = Command::cargo_bin("sdkt").unwrap();
let assert = cmd
.arg("verify")
.arg("--contract")
.arg("CABCDEFG")
.arg("--wasm")
.arg("/nonexistent/path/contract.wasm")
.assert();
assert
.failure()
.stderr(predicates::str::contains("Error reading WASM"));
}
#[test]
fn test_cli_verify_invalid_wasm() {
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::write(tmp.path(), b"not a wasm file").unwrap();
let mut cmd = Command::cargo_bin("sdkt").unwrap();
let assert = cmd
.arg("verify")
.arg("--contract")
.arg("CABCDEFG")
.arg("--wasm")
.arg(tmp.path())
.assert();
assert
.failure()
.stderr(predicates::str::contains("not valid WASM"));
}
#[test]
fn test_cli_verify_json_format_accepted() {
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::write(tmp.path(), b"not a wasm file").unwrap();
let mut cmd = Command::cargo_bin("sdkt").unwrap();
let assert = cmd
.arg("verify")
.arg("--contract")
.arg("CABCDEFG")
.arg("--wasm")
.arg(tmp.path())
.arg("--format")
.arg("json")
.assert();
assert
.failure()
.stderr(predicates::str::contains("not valid WASM"));
}
#[test]
fn test_cli_verify_onchain_error_path() {
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::write(tmp.path(), MINIMAL_WASM).unwrap();
let mut cmd = Command::cargo_bin("sdkt").unwrap();
let assert = cmd
.arg("verify")
.arg("--contract")
.arg("CNotARealContractId")
.arg("--wasm")
.arg(tmp.path())
.assert();
assert.failure();
}
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use sdkt_xdr::{encode_ledger_key, LedgerKeyParams};
use sha2::{Digest, Sha256};
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::thread;
use stellar_xdr::{
ContractDataDurability, ContractDataEntry, ContractExecutable, ContractId, ExtensionPoint,
Hash, LedgerEntry, LedgerEntryData, LedgerEntryExt, Limited, Limits, ScAddress,
ScContractInstance, ScVal, WriteXdr,
};
const TEST_ID_HEX: &str = "7777777777777777777777777777777777777777777777777777777777777777";
fn test_contract_strkey() -> String {
let mut id = [0u8; 32];
hex::decode_to_slice(TEST_ID_HEX, &mut id).unwrap();
ScAddress::Contract(ContractId(Hash(id))).to_string()
}
fn sha256_raw(bytes: &[u8]) -> [u8; 32] {
Sha256::digest(bytes).into()
}
fn encode_ledger_entry(entry: &LedgerEntry) -> String {
let mut buf = Vec::new();
let mut cursor = std::io::Cursor::new(&mut buf);
let mut l = Limited::new(&mut cursor, Limits::none());
entry.write_xdr(&mut l).unwrap();
STANDARD.encode(&buf)
}
fn instance_xdr(on_chain_hash: &[u8; 32]) -> String {
let mut id = [0u8; 32];
hex::decode_to_slice(TEST_ID_HEX, &mut id).unwrap();
encode_ledger_entry(&LedgerEntry {
last_modified_ledger_seq: 1,
data: LedgerEntryData::ContractData(ContractDataEntry {
ext: ExtensionPoint::V0,
contract: ScAddress::Contract(ContractId(Hash(id))),
key: ScVal::LedgerKeyContractInstance,
durability: ContractDataDurability::Persistent,
val: ScVal::ContractInstance(ScContractInstance {
executable: ContractExecutable::Wasm(Hash(*on_chain_hash)),
storage: None,
}),
}),
ext: LedgerEntryExt::V0,
})
}
fn read_rpc_request(sock: &mut TcpStream) -> String {
let mut data = Vec::new();
let mut buf = [0u8; 4096];
loop {
let n = sock.read(&mut buf).unwrap_or(0);
if n == 0 {
break;
}
data.extend_from_slice(&buf[..n]);
let text = String::from_utf8_lossy(&data);
if let Some(end) = text.find("\r\n\r\n") {
let len = text[..end]
.lines()
.filter_map(|line| line.split_once(':'))
.find(|(key, _)| key.trim().eq_ignore_ascii_case("content-length"))
.and_then(|(_, value)| value.trim().parse::<usize>().ok())
.unwrap_or(0);
if data.len() >= end + 4 + len {
break;
}
}
}
String::from_utf8_lossy(&data).into_owned()
}
fn mock_verify_rpc(on_chain_hash: [u8; 32]) -> String {
let instance_key =
encode_ledger_key(&LedgerKeyParams::ContractData(TEST_ID_HEX.to_string())).unwrap();
let entry_xdr = instance_xdr(&on_chain_hash);
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let url = format!("http://{}", listener.local_addr().unwrap());
thread::spawn(move || {
for conn in listener.incoming() {
let Ok(mut sock) = conn else { break };
let request = read_rpc_request(&mut sock);
let body = request
.split_once("\r\n\r\n")
.map(|(_, body)| body)
.unwrap_or("");
let is_instance_lookup = body.contains(&instance_key);
let response = if body.contains("\"getLatestLedger\"") {
r#"{"jsonrpc":"2.0","id":1,"result":{"id":"mock","protocolVersion":28,"sequence":10000}}"#.to_string()
} else if is_instance_lookup {
serde_json::json!({
"jsonrpc": "2.0", "id": 1,
"result": { "entries": [{
"key": instance_key, "xdr": entry_xdr,
"lastModifiedLedgerSeq": 1, "liveUntilLedgerSeq": 12345
}], "latestLedger": 10000 }
})
.to_string()
} else {
serde_json::json!({
"jsonrpc": "2.0", "id": 1,
"result": { "entries": [], "latestLedger": 10000 }
})
.to_string()
};
let http_response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
response.len(),
response
);
let _ = sock.write_all(http_response.as_bytes());
}
});
url
}
fn write_wasm(bytes: &[u8]) -> tempfile::NamedTempFile {
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::write(tmp.path(), bytes).unwrap();
tmp
}
#[test]
fn verify_match_exits_zero() {
let local = MINIMAL_WASM;
let hash: [u8; 32] = sha2::Sha256::digest(local).as_slice().try_into().unwrap();
let url = mock_verify_rpc(hash);
let file = write_wasm(local);
let mut cmd = Command::cargo_bin("sdkt").unwrap();
cmd.arg("verify")
.arg("--contract")
.arg(test_contract_strkey())
.arg("--wasm")
.arg(file.path())
.arg("--format")
.arg("json")
.arg("--rpc-url")
.arg(url)
.assert()
.success()
.stdout(predicates::str::contains(
"\"verification_status\": \"Verified\"",
));
}
#[test]
fn verify_mismatch_exits_nonzero() {
let other: &[u8] = &[0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x0b];
let on_chain: [u8; 32] = sha256_raw(other);
assert_ne!(
on_chain,
sha256_raw(MINIMAL_WASM),
"fixture sanity: the two files must hash differently"
);
let url = mock_verify_rpc(on_chain);
let file = write_wasm(MINIMAL_WASM);
let mut cmd = Command::cargo_bin("sdkt").unwrap();
let assert = cmd
.arg("verify")
.arg("--contract")
.arg(test_contract_strkey())
.arg("--wasm")
.arg(file.path())
.arg("--format")
.arg("json")
.arg("--rpc-url")
.arg(url)
.assert()
.failure();
let output = assert.get_output();
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("\"verification_status\": \"Mismatch\"")
&& stdout.contains("\"match\": false"),
"mismatch report must still be emitted: {stdout}"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("does NOT match"),
"expected actionable mismatch error, got: {stderr}"
);
}
#[test]
fn verify_onchain_only_without_wasm_exits_zero() {
let on_chain: [u8; 32] = sha256_raw(MINIMAL_WASM);
let url = mock_verify_rpc(on_chain);
let mut cmd = Command::cargo_bin("sdkt").unwrap();
cmd.arg("verify")
.arg("--contract")
.arg(test_contract_strkey())
.arg("--format")
.arg("json")
.arg("--rpc-url")
.arg(url)
.assert()
.success()
.stdout(predicates::str::contains("OnChainOnly"));
}
#[test]
fn verify_rpc_failure_still_exits_nonzero() {
let file = write_wasm(MINIMAL_WASM);
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let url = format!("http://{}", listener.local_addr().unwrap());
drop(listener);
let mut cmd = Command::cargo_bin("sdkt").unwrap();
cmd.arg("verify")
.arg("--contract")
.arg(test_contract_strkey())
.arg("--wasm")
.arg(file.path())
.arg("--rpc-url")
.arg(url)
.assert()
.failure();
}