use std::process::Command;
fn run_btc_keygen(args: &[&str]) -> std::process::Output {
let binary = env!("CARGO_BIN_EXE_btc-keygen");
Command::new(binary)
.args(args)
.output()
.expect("failed to execute btc-keygen binary")
}
#[test]
fn test_cli_generate_exit_code_zero() {
let output = run_btc_keygen(&["generate"]);
assert!(
output.status.success(),
"btc-keygen generate must exit 0, got: {:?}\nstderr: {}",
output.status.code(),
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn test_cli_generate_stdout_has_address_and_wif() {
let output = run_btc_keygen(&["generate"]);
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(
stdout.contains("bc1q"),
"stdout must contain a bc1q address, got:\n{stdout}"
);
let has_wif = stdout.lines().any(|line| {
let trimmed = line.trim();
(trimmed.starts_with('K') || trimmed.starts_with('L')) && trimmed.len() == 52
});
let has_wif_in_field = stdout.lines().any(|line| {
let parts: Vec<&str> = line.splitn(2, ':').collect();
if parts.len() == 2 {
let val = parts[1].trim();
(val.starts_with('K') || val.starts_with('L')) && val.len() == 52
} else {
false
}
});
assert!(
has_wif || has_wif_in_field,
"stdout must contain a WIF private key, got:\n{stdout}"
);
}
#[test]
fn test_cli_generate_stderr_has_warnings() {
let output = run_btc_keygen(&["generate"]);
let stderr = String::from_utf8(output.stderr).unwrap();
assert!(!stderr.is_empty(), "stderr must contain safety warnings");
}
#[test]
fn test_cli_json_flag() {
let output = run_btc_keygen(&["generate", "--json"]);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.starts_with('{'), "JSON must start with {{");
assert!(
stdout.contains("\"address\""),
"JSON must have address field"
);
assert!(stdout.contains("\"wif\""), "JSON must have wif field");
}
#[test]
fn test_cli_hex_flag() {
let output = run_btc_keygen(&["generate", "--hex"]);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
let has_hex = stdout
.lines()
.any(|line| line.chars().filter(|c| c.is_ascii_hexdigit()).count() >= 64);
assert!(
has_hex,
"stdout with --hex must contain 64-char hex string, got:\n{stdout}"
);
}
#[test]
fn test_cli_pubkey_flag() {
let output = run_btc_keygen(&["generate", "--pubkey"]);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
let has_pubkey = stdout.lines().any(|line| {
let trimmed = line.trim();
trimmed.contains("02") || trimmed.contains("03")
});
assert!(
has_pubkey,
"stdout with --pubkey must contain compressed public key hex, got:\n{stdout}"
);
}
#[test]
fn test_cli_all_flags_json() {
let output = run_btc_keygen(&["generate", "--hex", "--pubkey", "--json"]);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.starts_with('{'), "JSON must start with {{");
assert!(stdout.contains("\"address\""));
assert!(stdout.contains("\"wif\""));
assert!(stdout.contains("\"private_key_hex\""));
assert!(stdout.contains("\"pubkey_hex\""));
}
#[test]
fn test_cli_no_subcommand_shows_help() {
let output = run_btc_keygen(&[]);
assert!(
!output.status.success(),
"no subcommand should produce non-zero exit"
);
let stderr = String::from_utf8(output.stderr).unwrap();
assert!(
stderr.contains("Usage") || stderr.contains("usage"),
"should show usage info, got:\n{stderr}"
);
}
#[test]
fn test_cli_unknown_flag_errors() {
let output = run_btc_keygen(&["generate", "--unknown-flag"]);
assert!(
!output.status.success(),
"unknown flag must produce non-zero exit"
);
}
#[test]
fn test_cli_from_hex_produces_known_keypair() {
let output = run_btc_keygen(&[
"generate",
"--from-hex",
"0000000000000000000000000000000000000000000000000000000000000001",
]);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(
stdout.contains("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"),
"expected known address for scalar 1, got:\n{stdout}"
);
assert!(
stdout.contains("KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn"),
"expected known WIF for scalar 1, got:\n{stdout}"
);
}
#[test]
fn test_cli_from_hex_invalid_input_exits_non_zero() {
let output = run_btc_keygen(&["generate", "--from-hex", "notavalidhex"]);
assert!(
!output.status.success(),
"invalid hex must produce non-zero exit"
);
}
#[test]
fn test_two_cli_runs_produce_different_keys() {
let output1 = run_btc_keygen(&["generate", "--json"]);
let output2 = run_btc_keygen(&["generate", "--json"]);
assert!(output1.status.success());
assert!(output2.status.success());
let stdout1 = String::from_utf8(output1.stdout).unwrap();
let stdout2 = String::from_utf8(output2.stdout).unwrap();
assert_ne!(stdout1, stdout2, "two runs must produce different output");
}
#[test]
fn test_no_file_artifacts() {
let dir = std::env::temp_dir().join("btc_keygen_artifact_test");
let _ = std::fs::create_dir_all(&dir);
let before: Vec<_> = std::fs::read_dir(&dir)
.unwrap()
.filter_map(|e| e.ok())
.collect();
let binary = env!("CARGO_BIN_EXE_btc-keygen");
let output = Command::new(binary)
.args(["generate"])
.current_dir(&dir)
.output()
.unwrap();
assert!(output.status.success());
let after: Vec<_> = std::fs::read_dir(&dir)
.unwrap()
.filter_map(|e| e.ok())
.collect();
assert_eq!(
before.len(),
after.len(),
"generate must not create any files"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn test_no_env_mutation() {
let env_before: std::collections::HashMap<String, String> = std::env::vars().collect();
let output = run_btc_keygen(&["generate"]);
assert!(output.status.success());
let env_after: std::collections::HashMap<String, String> = std::env::vars().collect();
assert_eq!(
env_before, env_after,
"running btc-keygen must not mutate the parent process environment"
);
}
#[test]
fn test_no_network_deps() {
let output = Command::new("cargo")
.args(["tree", "--prefix", "none"])
.output()
.expect("cargo tree must succeed");
let tree = String::from_utf8(output.stdout).unwrap();
let banned = [
"reqwest",
"hyper",
"tokio",
"async-std",
"surf",
"ureq",
"curl",
];
for crate_name in &banned {
assert!(
!tree.lines().any(|line| line.starts_with(crate_name)),
"dependency tree must not contain networking crate '{}'\ntree:\n{}",
crate_name,
tree
);
}
}