[keygen] refactor argument parsing logic into separate `app(...)` function and add tests (#31015)

* refactor argument parsing and processing for testing

* add tests for command verify

* add tests for command pubkey

* add tests for command new

* add tests for command grind

* clippy

* be explicit about types

* use `try_get_matches` and tempfile

* clippy

* call `Error::exit` on error from `try_get_matches`
This commit is contained in:
samkim-crypto 2023-04-06 07:43:52 +09:00 committed by GitHub
parent d67fa6c470
commit 0ff8a09041
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 395 additions and 18 deletions

1
Cargo.lock generated
View File

@ -5876,6 +5876,7 @@ dependencies = [
"solana-remote-wallet",
"solana-sdk 1.16.0",
"solana-version",
"tempfile",
"tiny-bip39",
]

View File

@ -21,6 +21,9 @@ solana-sdk = { workspace = true }
solana-version = { workspace = true }
tiny-bip39 = { workspace = true }
[dev-dependencies]
tempfile = "3.4.0"
[[bin]]
name = "solana-keygen"
path = "src/keygen.rs"

View File

@ -27,7 +27,6 @@ use {
collections::HashSet,
error,
path::Path,
process::exit,
sync::{
atomic::{AtomicBool, AtomicU64, Ordering},
Arc,
@ -124,12 +123,13 @@ impl KeyGenerationCommonArgs for Command<'_> {
}
}
fn check_for_overwrite(outfile: &str, matches: &ArgMatches) {
fn check_for_overwrite(outfile: &str, matches: &ArgMatches) -> Result<(), Box<dyn error::Error>> {
let force = matches.is_present("force");
if !force && Path::new(outfile).exists() {
eprintln!("Refusing to overwrite {outfile} without --force flag");
exit(1);
let err_msg = format!("Refusing to overwrite {outfile} without --force flag");
return Err(err_msg.into());
}
Ok(())
}
fn get_keypair_from_matches(
@ -357,11 +357,10 @@ fn acquire_derivation_path(
}
}
fn main() -> Result<(), Box<dyn error::Error>> {
let default_num_threads = num_cpus::get().to_string();
let matches = Command::new(crate_name!())
fn app<'a>(num_threads: &'a str, crate_version: &'a str) -> Command<'a> {
Command::new(crate_name!())
.about(crate_description!())
.version(solana_version::version!())
.version(crate_version)
.subcommand_required(true)
.arg_required_else_help(true)
.arg({
@ -477,7 +476,7 @@ fn main() -> Result<(), Box<dyn error::Error>> {
.value_name("NUMBER")
.takes_value(true)
.validator(is_parsable::<usize>)
.default_value(&default_num_threads)
.default_value(num_threads)
.help("Specify the number of grind threads"),
)
.arg(
@ -561,8 +560,13 @@ fn main() -> Result<(), Box<dyn error::Error>> {
),
)
.get_matches();
}
fn main() -> Result<(), Box<dyn error::Error>> {
let default_num_threads = num_cpus::get().to_string();
let matches = app(&default_num_threads, solana_version::version!())
.try_get_matches()
.unwrap_or_else(|e| e.exit());
do_main(&matches).map_err(|err| DisplayError::new_as_boxed(err).into())
}
@ -584,7 +588,7 @@ fn do_main(matches: &ArgMatches) -> Result<(), Box<dyn error::Error>> {
if matches.is_present("outfile") {
let outfile = matches.value_of("outfile").unwrap();
check_for_overwrite(outfile, matches);
check_for_overwrite(outfile, matches)?;
write_pubkey_file(outfile, pubkey)?;
} else {
println!("{pubkey}");
@ -603,7 +607,7 @@ fn do_main(matches: &ArgMatches) -> Result<(), Box<dyn error::Error>> {
match outfile {
Some(STDOUT_OUTFILE_TOKEN) => (),
Some(outfile) => check_for_overwrite(outfile, matches),
Some(outfile) => check_for_overwrite(outfile, matches)?,
None => (),
}
@ -651,7 +655,7 @@ fn do_main(matches: &ArgMatches) -> Result<(), Box<dyn error::Error>> {
};
if outfile != STDOUT_OUTFILE_TOKEN {
check_for_overwrite(outfile, matches);
check_for_overwrite(outfile, matches)?;
}
let keypair_name = "recover";
@ -698,10 +702,9 @@ fn do_main(matches: &ArgMatches) -> Result<(), Box<dyn error::Error>> {
&& ends_with_args.is_empty()
&& starts_and_ends_with_args.is_empty()
{
eprintln!(
"Error: No keypair search criteria provided (--starts-with or --ends-with or --starts-and-ends-with)"
return Err(
"Error: No keypair search criteria provided (--starts-with or --ends-with or --starts-and-ends-with)".into()
);
exit(1);
}
let num_threads: usize = matches.value_of_t_or_exit("num_threads");
@ -842,8 +845,8 @@ fn do_main(matches: &ArgMatches) -> Result<(), Box<dyn error::Error>> {
if signature.verify(&pubkey, &simple_message) {
println!("Verification for public key: {pubkey_bs58}: Success");
} else {
println!("Verification for public key: {pubkey_bs58}: Failed");
exit(1);
let err_msg = format!("Verification for public key: {pubkey_bs58}: Failed");
return Err(err_msg.into());
}
}
_ => unreachable!(),
@ -851,3 +854,373 @@ fn do_main(matches: &ArgMatches) -> Result<(), Box<dyn error::Error>> {
Ok(())
}
#[cfg(test)]
mod tests {
use {
super::*,
tempfile::{tempdir, TempDir},
};
fn process_test_command(args: &[&str]) -> Result<(), Box<dyn error::Error>> {
let default_num_threads = num_cpus::get().to_string();
let solana_version = solana_version::version!();
let app_matches = app(&default_num_threads, solana_version).get_matches_from(args);
do_main(&app_matches)
}
fn create_tmp_keypair_and_config_file(
keypair_out_dir: &TempDir,
config_out_dir: &TempDir,
) -> (Pubkey, String, String) {
let keypair = Keypair::new();
let keypair_path = keypair_out_dir
.path()
.join(format!("{}-keypair", keypair.pubkey()));
let keypair_outfile = keypair_path.into_os_string().into_string().unwrap();
write_keypair_file(&keypair, &keypair_outfile).unwrap();
let config = Config {
keypair_path: keypair_outfile.clone(),
..Config::default()
};
let config_path = config_out_dir
.path()
.join(format!("{}-config", keypair.pubkey()));
let config_outfile = config_path.into_os_string().into_string().unwrap();
config.save(&config_outfile).unwrap();
(keypair.pubkey(), keypair_outfile, config_outfile)
}
fn tmp_outfile_path(out_dir: &TempDir, name: &str) -> String {
let path = out_dir.path().join(name);
path.into_os_string().into_string().unwrap()
}
#[test]
fn test_arguments() {
let default_num_threads = num_cpus::get().to_string();
let solana_version = solana_version::version!();
// run clap internal assert statements
app(&default_num_threads, solana_version).debug_assert();
}
#[test]
fn test_verify() {
let keypair_out_dir = tempdir().unwrap();
let config_out_dir = tempdir().unwrap();
let (correct_pubkey, keypair_path, config_path) =
create_tmp_keypair_and_config_file(&keypair_out_dir, &config_out_dir);
// success case using a keypair file
process_test_command(&[
"solana-keygen",
"verify",
&correct_pubkey.to_string(),
&keypair_path,
])
.unwrap();
// success case using a config file
process_test_command(&[
"solana-keygen",
"verify",
&correct_pubkey.to_string(),
"--config",
&config_path,
])
.unwrap();
// fail case using a keypair file
let incorrect_pubkey = Pubkey::new_unique();
let result = process_test_command(&[
"solana-keygen",
"verify",
&incorrect_pubkey.to_string(),
&keypair_path,
])
.unwrap_err()
.to_string();
let expected = format!("Verification for public key: {incorrect_pubkey}: Failed");
assert_eq!(result, expected);
// fail case using a config file
process_test_command(&[
"solana-keygen",
"verify",
&incorrect_pubkey.to_string(),
"--config",
&config_path,
])
.unwrap_err()
.to_string();
let expected = format!("Verification for public key: {incorrect_pubkey}: Failed");
assert_eq!(result, expected);
// keypair file takes precedence over config file
let alt_keypair_out_dir = tempdir().unwrap();
let alt_config_out_dir = tempdir().unwrap();
let (_, alt_keypair_path, alt_config_path) =
create_tmp_keypair_and_config_file(&alt_keypair_out_dir, &alt_config_out_dir);
process_test_command(&[
"solana-keygen",
"verify",
&correct_pubkey.to_string(),
&keypair_path,
"--config",
&alt_config_path,
])
.unwrap();
process_test_command(&[
"solana-keygen",
"verify",
&correct_pubkey.to_string(),
&alt_keypair_path,
"--config",
&config_path,
])
.unwrap_err()
.to_string();
let expected = format!("Verification for public key: {incorrect_pubkey}: Failed");
assert_eq!(result, expected);
}
#[test]
fn test_pubkey() {
let keypair_out_dir = tempdir().unwrap();
let config_out_dir = tempdir().unwrap();
let (expected_pubkey, keypair_path, config_path) =
create_tmp_keypair_and_config_file(&keypair_out_dir, &config_out_dir);
// success case using a keypair file
{
let outfile_dir = tempdir().unwrap();
let outfile_path = tmp_outfile_path(&outfile_dir, &expected_pubkey.to_string());
process_test_command(&[
"solana-keygen",
"pubkey",
&keypair_path,
"--outfile",
&outfile_path,
])
.unwrap();
let result_pubkey = solana_sdk::pubkey::read_pubkey_file(&outfile_path).unwrap();
assert_eq!(result_pubkey, expected_pubkey);
}
// success case using a config file
{
let outfile_dir = tempdir().unwrap();
let outfile_path = tmp_outfile_path(&outfile_dir, &expected_pubkey.to_string());
process_test_command(&[
"solana-keygen",
"pubkey",
"--config",
&config_path,
"--outfile",
&outfile_path,
])
.unwrap();
let result_pubkey = solana_sdk::pubkey::read_pubkey_file(&outfile_path).unwrap();
assert_eq!(result_pubkey, expected_pubkey);
}
// keypair file takes precedence over config file
{
let alt_keypair_out_dir = tempdir().unwrap();
let alt_config_out_dir = tempdir().unwrap();
let (_, _, alt_config_path) =
create_tmp_keypair_and_config_file(&alt_keypair_out_dir, &alt_config_out_dir);
let outfile_dir = tempdir().unwrap();
let outfile_path = tmp_outfile_path(&outfile_dir, &expected_pubkey.to_string());
process_test_command(&[
"solana-keygen",
"pubkey",
&keypair_path,
"--config",
&alt_config_path,
"--outfile",
&outfile_path,
])
.unwrap();
let result_pubkey = solana_sdk::pubkey::read_pubkey_file(&outfile_path).unwrap();
assert_eq!(result_pubkey, expected_pubkey);
}
// refuse to overwrite file
{
let outfile_dir = tempdir().unwrap();
let outfile_path = tmp_outfile_path(&outfile_dir, &expected_pubkey.to_string());
process_test_command(&[
"solana-keygen",
"pubkey",
&keypair_path,
"--outfile",
&outfile_path,
])
.unwrap();
let result = process_test_command(&[
"solana-keygen",
"pubkey",
"--config",
&config_path,
"--outfile",
&outfile_path,
])
.unwrap_err()
.to_string();
let expected = format!("Refusing to overwrite {outfile_path} without --force flag");
assert_eq!(result, expected);
}
}
#[test]
fn test_new() {
let keypair_out_dir = tempdir().unwrap();
let config_out_dir = tempdir().unwrap();
let (expected_pubkey, _, _) =
create_tmp_keypair_and_config_file(&keypair_out_dir, &config_out_dir);
let outfile_dir = tempdir().unwrap();
let outfile_path = tmp_outfile_path(&outfile_dir, &expected_pubkey.to_string());
// general success case
process_test_command(&[
"solana-keygen",
"new",
"--outfile",
&outfile_path,
"--no-bip39-passphrase",
])
.unwrap();
// refuse to overwrite file
let result = process_test_command(&[
"solana-keygen",
"new",
"--outfile",
&outfile_path,
"--no-bip39-passphrase",
])
.unwrap_err()
.to_string();
let expected = format!("Refusing to overwrite {outfile_path} without --force flag");
assert_eq!(result, expected);
// no outfile
process_test_command(&[
"solana-keygen",
"new",
"--no-bip39-passphrase",
"--no-outfile",
])
.unwrap();
// sanity check on languages and word count combinations
let languages = [
"english",
"chinese-simplified",
"chinese-traditional",
"japanese",
"spanish",
"korean",
"french",
"italian",
];
let word_counts = ["12", "15", "18", "21", "24"];
for language in languages {
for word_count in word_counts {
process_test_command(&[
"solana-keygen",
"new",
"--no-outfile",
"--no-bip39-passphrase",
"--language",
language,
"--word-count",
word_count,
])
.unwrap();
}
}
// sanity check derivation path
process_test_command(&[
"solana-keygen",
"new",
"--no-bip39-passphrase",
"--no-outfile",
"--derivation-path",
// empty derivation path
])
.unwrap();
process_test_command(&[
"solana-keygen",
"new",
"--no-bip39-passphrase",
"--no-outfile",
"--derivation-path",
"m/44'/501'/0'/0'", // default derivation path
])
.unwrap();
let result = process_test_command(&[
"solana-keygen",
"new",
"--no-bip39-passphrase",
"--no-outfile",
"--derivation-path",
"-", // invalid derivation path
])
.unwrap_err()
.to_string();
let expected = "invalid derivation path: invalid prefix: -";
assert_eq!(result, expected);
}
#[test]
fn test_grind() {
// simple sanity checks
process_test_command(&[
"solana-keygen",
"grind",
"--no-outfile",
"--no-bip39-passphrase",
"--use-mnemonic",
"--starts-with",
"a:1",
])
.unwrap();
process_test_command(&[
"solana-keygen",
"grind",
"--no-outfile",
"--no-bip39-passphrase",
"--use-mnemonic",
"--ends-with",
"b:1",
])
.unwrap();
}
}