use std::io::Write;
use std::process::{Command, Stdio};
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")
}
fn run_btc_keygen_with_stdin(args: &[&str], input: &[u8]) -> std::process::Output {
let binary = env!("CARGO_BIN_EXE_btc-keygen");
let mut child = Command::new(binary)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("failed to spawn btc-keygen binary");
child
.stdin
.as_mut()
.expect("stdin was piped")
.write_all(input)
.expect("failed to write to stdin");
child.wait_with_output().expect("failed to collect output")
}
const SCALAR_ONE_HEX: &str = "0000000000000000000000000000000000000000000000000000000000000001";
const SCALAR_ONE_ADDRESS: &str = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4";
const SCALAR_ONE_WIF: &str = "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn";
#[test]
fn test_from_hex_stdin_matches_argument_form() {
let piped = run_btc_keygen_with_stdin(
&["generate", "--from-hex", "-", "--hex", "--pubkey", "--json"],
format!("{SCALAR_ONE_HEX}\n").as_bytes(),
);
assert!(piped.status.success(), "piping the key must succeed");
let on_argv = run_btc_keygen(&[
"generate",
"--from-hex",
SCALAR_ONE_HEX,
"--hex",
"--pubkey",
"--json",
]);
assert_eq!(
piped.stdout, on_argv.stdout,
"stdin and argument forms must produce identical output"
);
let stdout = String::from_utf8(piped.stdout).unwrap();
assert!(stdout.contains(SCALAR_ONE_ADDRESS));
assert!(stdout.contains(SCALAR_ONE_WIF));
}
#[test]
fn test_from_hex_stdin_tolerates_no_trailing_newline() {
let output =
run_btc_keygen_with_stdin(&["generate", "--from-hex", "-"], SCALAR_ONE_HEX.as_bytes());
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains(SCALAR_ONE_ADDRESS));
}
#[test]
fn test_from_hex_stdin_tolerates_crlf_and_whitespace() {
let output = run_btc_keygen_with_stdin(
&["generate", "--from-hex", "-"],
format!(" {SCALAR_ONE_HEX} \r\n").as_bytes(),
);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains(SCALAR_ONE_ADDRESS));
}
#[test]
fn test_from_hex_stdin_accepts_uppercase() {
let output = run_btc_keygen_with_stdin(
&["generate", "--from-hex", "-"],
b"0C28FCA386C7A227600B2FE50B7CAE11EC86D3BF1FBE471BE89827E19D72AA1D\n",
);
assert!(output.status.success(), "uppercase hex must be accepted");
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("KwdMAjGmerYanjeui5SHS7JkmpZvVipYvB2LJGU1ZxJwYvP98617"));
}
#[test]
fn test_from_hex_stdin_empty_fails_loudly() {
let output = run_btc_keygen_with_stdin(&["generate", "--from-hex", "-"], b"");
assert!(
!output.status.success(),
"empty stdin must not silently generate a random key"
);
let stderr = String::from_utf8(output.stderr).unwrap();
assert!(
stderr.contains("failed to read private key"),
"must say why, got:\n{stderr}"
);
}
#[test]
fn test_from_hex_stdin_rejects_overlong_input() {
let junk = vec![b'0'; 4096];
let output = run_btc_keygen_with_stdin(&["generate", "--from-hex", "-"], &junk);
assert!(
!output.status.success(),
"over-long input must be rejected, never truncated to a different key"
);
}
#[test]
fn test_stdin_form_emits_no_argv_warning() {
let piped = run_btc_keygen_with_stdin(
&["generate", "--from-hex", "-"],
format!("{SCALAR_ONE_HEX}\n").as_bytes(),
);
let stderr = String::from_utf8(piped.stderr).unwrap();
assert!(
!stderr.contains("command-line argument"),
"the safe channel must not be scolded, got:\n{stderr}"
);
}
#[test]
fn test_argv_form_still_works_but_warns() {
let output = run_btc_keygen(&["generate", "--from-hex", SCALAR_ONE_HEX]);
assert!(
output.status.success(),
"the argument form must keep working for existing scripts"
);
let stderr = String::from_utf8(output.stderr).unwrap();
assert!(
stderr.contains("command-line argument") && stderr.contains("history"),
"passing a key on argv must warn about history and ps, got:\n{stderr}"
);
assert!(
stderr.contains("--from-hex -"),
"the warning must point at the safe alternative"
);
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(!stdout.to_lowercase().contains("warning"));
}
#[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
);
}
}