use assert_cmd::Command;
use std::io::{Read, Write};
use std::net::TcpListener;
use std::thread;
use std::time::Duration;
use tempfile::tempdir;
fn sdkt_isolated(dir: &std::path::Path) -> Command {
let mut cmd = Command::cargo_bin("sdkt").unwrap();
cmd.env("SDKT_IDENTITY_DIR", dir.join("identity"));
cmd.env("SDKT_NETWORK_DIR", dir.join("network"));
cmd
}
fn mock_http_server(status: &str, body: &str) -> String {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
let url = format!("http://{}", addr);
let response_body = body.to_string();
let status_line = status.to_string();
thread::spawn(move || {
if let Ok((mut sock, _)) = listener.accept() {
let mut buf = [0u8; 8192];
let _ = sock.read(&mut buf);
let header = format!(
"{}Content-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
status_line,
response_body.len(),
response_body
);
let _ = sock.write_all(header.as_bytes());
}
});
thread::sleep(Duration::from_millis(50));
url
}
#[test]
fn fund_help_exposes_command() {
let dir = tempdir().unwrap();
sdkt_isolated(dir.path())
.args(["identity", "fund", "--help"])
.assert()
.success()
.stdout(predicates::str::contains("Fund an identity"))
.stdout(predicates::str::contains("--network-profile"))
.stdout(predicates::str::contains("--format"));
}
#[test]
fn fund_missing_identity_fails() {
let dir = tempdir().unwrap();
sdkt_isolated(dir.path())
.args([
"network",
"add",
"testnet",
"--rpc-url",
"https://soroban-testnet.stellar.org",
"--passphrase",
"Test SDF Network ; September 2015",
"--friendbot",
"https://friendbot.stellar.org",
])
.assert()
.success();
sdkt_isolated(dir.path())
.args(["identity", "fund", "ghost", "--network-profile", "testnet"])
.assert()
.failure()
.stderr(predicates::str::contains("not found"));
}
#[test]
fn fund_missing_friendbot_url_fails() {
let dir = tempdir().unwrap();
sdkt_isolated(dir.path())
.args(["identity", "generate", "bob"])
.assert()
.success();
sdkt_isolated(dir.path())
.args([
"network",
"add",
"mainnet",
"--rpc-url",
"https://soroban-mainnet.stellar.org",
"--passphrase",
"Public Global Stellar Network ; September 2015",
])
.assert()
.success();
sdkt_isolated(dir.path())
.args(["identity", "fund", "bob", "--network-profile", "mainnet"])
.assert()
.failure()
.stderr(predicates::str::contains("no Friendbot URL"));
}
#[test]
fn fund_missing_profile_fails() {
let dir = tempdir().unwrap();
sdkt_isolated(dir.path())
.args(["identity", "generate", "carol"])
.assert()
.success();
sdkt_isolated(dir.path())
.args([
"identity",
"fund",
"carol",
"--network-profile",
"ghost-profile",
])
.assert()
.failure()
.stderr(predicates::str::contains("not found"));
}
#[test]
fn fund_success_pretty_output() {
let dir = tempdir().unwrap();
sdkt_isolated(dir.path())
.args(["identity", "generate", "dave"])
.assert()
.success();
let friendbot_url = mock_http_server(
"HTTP/1.1 200 OK\r\n",
r#"{"hash":"abc123def456","ledger":12345}"#,
);
sdkt_isolated(dir.path())
.args([
"network",
"add",
"mocknet",
"--rpc-url",
"https://soroban-testnet.stellar.org",
"--passphrase",
"Test SDF Network ; September 2015",
"--friendbot",
&friendbot_url,
])
.assert()
.success();
sdkt_isolated(dir.path())
.args(["identity", "fund", "dave", "--network-profile", "mocknet"])
.assert()
.success()
.stdout(predicates::str::contains("Identity Funded via Friendbot"))
.stdout(predicates::str::contains("dave"))
.stdout(predicates::str::contains("mocknet"));
}
#[test]
fn fund_success_json_output() {
let dir = tempdir().unwrap();
sdkt_isolated(dir.path())
.args(["identity", "generate", "eve"])
.assert()
.success();
let friendbot_url =
mock_http_server("HTTP/1.1 200 OK\r\n", r#"{"hash":"xyz789","ledger":67890}"#);
sdkt_isolated(dir.path())
.args([
"network",
"add",
"mocknet",
"--rpc-url",
"https://soroban-testnet.stellar.org",
"--passphrase",
"Test SDF Network ; September 2015",
"--friendbot",
&friendbot_url,
])
.assert()
.success();
let output = sdkt_isolated(dir.path())
.args([
"identity",
"fund",
"eve",
"--network-profile",
"mocknet",
"--format",
"json",
])
.output()
.expect("Failed to execute fund command");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
output.status.success(),
"Command failed: stdout={} stderr={}",
stdout,
String::from_utf8_lossy(&output.stderr)
);
let parsed: serde_json::Value = serde_json::from_str(&stdout)
.unwrap_or_else(|e| panic!("Output is not valid JSON: {}\nOutput was: {}", e, stdout));
assert!(
parsed.get("address").is_some(),
"Missing 'address' field in JSON output"
);
assert!(
parsed.get("status").is_some(),
"Missing 'status' field in JSON output"
);
}
#[test]
fn fund_http_500_error_propagates() {
let dir = tempdir().unwrap();
sdkt_isolated(dir.path())
.args(["identity", "generate", "frank"])
.assert()
.success();
let friendbot_url = mock_http_server(
"HTTP/1.1 500 Internal Server Error\r\n",
r#"{"error":"internal server error"}"#,
);
sdkt_isolated(dir.path())
.args([
"network",
"add",
"mocknet",
"--rpc-url",
"https://soroban-testnet.stellar.org",
"--passphrase",
"Test SDF Network ; September 2015",
"--friendbot",
&friendbot_url,
])
.assert()
.success();
sdkt_isolated(dir.path())
.args(["identity", "fund", "frank", "--network-profile", "mocknet"])
.assert()
.failure()
.stderr(predicates::str::contains("500"));
}
#[test]
fn fund_http_429_rate_limit() {
let dir = tempdir().unwrap();
sdkt_isolated(dir.path())
.args(["identity", "generate", "grace"])
.assert()
.success();
let friendbot_url = mock_http_server(
"HTTP/1.1 429 Too Many Requests\r\n",
r#"{"error":"rate limit exceeded"}"#,
);
sdkt_isolated(dir.path())
.args([
"network",
"add",
"mocknet",
"--rpc-url",
"https://soroban-testnet.stellar.org",
"--passphrase",
"Test SDF Network ; September 2015",
"--friendbot",
&friendbot_url,
])
.assert()
.success();
sdkt_isolated(dir.path())
.args(["identity", "fund", "grace", "--network-profile", "mocknet"])
.assert()
.failure()
.stderr(predicates::str::contains("rate limit"));
}