From: Vihiga Tyonum Date: Thu, 11 Jun 2026 12:38:52 +0000 (+0100) Subject: test: Add helper fns & integration tests for key X-Git-Url: http://internal-gitweb-vhost/blockdata/script/encode/consensus/struct.DisplayArray.html?a=commitdiff_plain;h=d8e2887ec41f3c182131e19e8f1f19f982516f3a;p=bdk-cli test: Add helper fns & integration tests for key - add BdkCli helper struct and impl methods - add integration tests for the key subcommand operations --- diff --git a/Cargo.lock b/Cargo.lock index a5826c4..82ef472 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -125,6 +125,21 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" +[[package]] +name = "assert_cmd" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aa3a22042e45de04255c7bf3626e239f450200fd0493c1e382263544b20aea6" +dependencies = [ + "anstyle", + "bstr", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -142,6 +157,12 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + [[package]] name = "aws-lc-rs" version = "1.17.1" @@ -196,6 +217,7 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" name = "bdk-cli" version = "3.0.0" dependencies = [ + "assert_cmd", "bdk_bip322", "bdk_bitcoind_rpc", "bdk_electrum", @@ -213,11 +235,13 @@ dependencies = [ "env_logger", "log", "payjoin", + "predicates", "reqwest 0.13.4", "serde", "serde_json", "shlex 1.3.0", "tap", + "tempfile", "thiserror 2.0.18", "tokio", "toml 1.1.2+spec-1.1.0", @@ -609,6 +633,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bstr" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cee35f73844aa3014bb606320a6c1f010249dbdf43342fe54b5a4f6a8ed4b79" +dependencies = [ + "memchr", + "regex-automata", + "serde_core", +] + [[package]] name = "bumpalo" version = "3.20.3" @@ -998,6 +1033,12 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "digest" version = "0.9.0" @@ -1162,6 +1203,15 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "float-cmp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits", +] + [[package]] name = "foreign-types" version = "0.3.2" @@ -1994,6 +2044,12 @@ dependencies = [ "tempfile", ] +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -2003,6 +2059,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -2213,6 +2278,36 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "predicates" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" +dependencies = [ + "anstyle", + "difflib", + "float-cmp", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" + +[[package]] +name = "predicates-tree" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -2975,6 +3070,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + [[package]] name = "thiserror" version = "1.0.69" @@ -3349,6 +3450,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index 0725488..7ffc611 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,3 +75,6 @@ silent-payments = ["dep:bdk_sp"] [dev-dependencies] claims = "0.8.0" +predicates = "3.0" +tempfile = "3.8" +assert_cmd = "2.2.2" diff --git a/tests/cli.rs b/tests/cli.rs new file mode 100644 index 0000000..db97d65 --- /dev/null +++ b/tests/cli.rs @@ -0,0 +1,115 @@ +// Copyright (c) 2020-2026 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! bdk-cli Integration Test Framework +//! +//! This modules performs the necessary integration test for bdk-cli +//! The tests can be run using `cargo test` + +mod common; + +// --- KEY COMMAND TESTS --- +mod test_key { + use crate::common::BdkCli; + use predicates::prelude::*; + use serde_json::Value; + + #[test] + fn test_cli_key_generate() { + // compile binary + let cli = BdkCli::new("testnet", None); + + cli.key_cmd(&["generate"]) + .assert() + .success() + .stdout(predicate::str::contains("\"xprv\":")) + .stdout(predicate::str::contains("\"mnemonic\":")) + .stdout(predicate::str::contains("\"fingerprint\":")); + } + + #[test] + fn test_cli_key_derive() { + let cli = BdkCli::new("testnet", None); + + let generate_output = cli + .key_cmd(&["generate"]) + .output() + .expect("Failed to execute generate command"); + assert!(generate_output.status.success(), "Generate command failed"); + + let generate_json: Value = + serde_json::from_slice(&generate_output.stdout).expect("Invalid JSON"); + let xprv = generate_json["xprv"].as_str().expect("Missing XPRV"); + + let mut cmd = cli.key_cmd(&[ + "derive", + "--xprv", + xprv, + "--derivation_path", + "m/84'/1'/0'/0", + ]); + + cmd.assert() + .success() + .stdout(predicate::str::contains("\"xprv\":")) + .stdout(predicate::str::contains("\"xpub\":")); + } + + #[test] + fn test_cli_key_restore() { + // Generate key + let cli = BdkCli::new("testnet", None); + + // Execute the command and capture the output + let generate_cmd = cli + .key_cmd(&["generate"]) + .output() + .expect("Failed to execute generate command"); + assert!(generate_cmd.status.success(), "Generate command failed"); + + // Parse the JSON to extract the mnemonic + let generate_json: Value = + serde_json::from_slice(&generate_cmd.stdout).expect("Failed to parse JSON"); + + let mnemonic = generate_json["mnemonic"] + .as_str() + .expect("Mnemonic missing"); + let xprv = generate_json["xprv"].as_str().expect("XPRV missing"); + let finger_print = generate_json["fingerprint"] + .as_str() + .expect("Fingerprint missing"); + + // Restore using the mnemonic + let output_restore = cli.key_cmd(&["restore", "--mnemonic", mnemonic]) + .output() + .expect("Failed to execute restore command"); + assert!(output_restore.status.success(), "Restore command failed"); + + // Parse the JSON from the restore command + let restore_json: Value = + serde_json::from_slice(&output_restore.stdout).expect("Failed to parse JSON"); + + let restored_xprv = restore_json["xprv"] + .as_str() + .expect("Restored XPRV missing"); + let restored_fingerprint = restore_json["fingerprint"] + .as_str() + .expect("Restored fingerprint missing"); + + // Assert that the restored data exactly matches the generated data + assert_eq!( + xprv, restored_xprv, + "The restored XPRV does not match the generated XPRV!" + ); + + assert_eq!( + finger_print, restored_fingerprint, + "The restored fingerprint does not match the generated fingerprint!" + ); + } +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..e957584 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,77 @@ +// Copyright (c) 2020-2026 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! bdk-cli Integration Test Framework +//! +//! This module provides the necessary helper methods for tests +//! +use assert_cmd::Command; +use std::path::PathBuf; + +/// The bdk-cli command struct +#[allow(dead_code)] +#[derive(Debug)] +pub struct BdkCli { + pub network: String, + pub datadir: Option, + pub verbosity: bool, + pub recv_desc: Option, + pub change_desc: Option, +} + +impl BdkCli { + /// Construct a new test environment configuration + pub fn new(network: &str, datadir: Option) -> Self { + Self { + network: network.to_string(), + datadir, + verbosity: false, + recv_desc: None, + change_desc: None, + } + } + + /// Creates the base assert_cmd::Command with the global flags pre-loaded + fn build_base_cmd(&self) -> Command { + let mut cmd = Command::cargo_bin("bdk-cli").expect("bdk-cli binary must compile"); + + cmd.arg("--network").arg(&self.network); + + if let Some(dir) = &self.datadir { + cmd.arg("--datadir").arg(dir); + } + + if self.verbosity { + cmd.arg("--verbose"); + } + + cmd + } + + /// Returns a pre-configured Command builder for `key` operations + pub fn key_cmd(&self, args: &[&str]) -> Command { + let mut cmd = self.build_base_cmd(); + cmd.arg("key"); + cmd.args(args); + cmd + } + + /// Returns a pre-configured Command builder for `wallet` operations + pub fn wallet_cmd(&self, args: &[&str]) -> Command { + let mut cmd = self.build_base_cmd(); + cmd.arg("wallet"); + + // Automatically inject descriptors if they are set in the helper state + if let Some(recv) = &self.recv_desc { + cmd.arg("--descriptor").arg(recv); + } + + cmd.args(args); + cmd + } +} diff --git a/tests/integration.rs b/tests/integration.rs deleted file mode 100644 index 79c887f..0000000 --- a/tests/integration.rs +++ /dev/null @@ -1,247 +0,0 @@ -// Copyright (c) 2020-2025 Bitcoin Dev Kit Developers -// -// This file is licensed under the Apache License, Version 2.0 or the MIT license -// , at your option. -// You may not use this file except in accordance with one or both of these -// licenses. - -//! bdk-cli Integration Test Framework -//! -//! This modules performs the necessary integration test for bdk-cli -//! The tests can be run using `cargo test` - -#[cfg(feature = "rpc")] -mod test { - use serde_json::{Value, json}; - use std::convert::From; - use std::env::temp_dir; - use std::path::PathBuf; - use std::process::Command; - - /// Testing errors for integration tests - #[allow(dead_code)] - #[derive(Debug)] - enum IntTestError { - // IO error - IO(std::io::Error), - // Command execution error - CmdExec(String), - // Json Data error - JsonData(String), - } - - impl From for IntTestError { - fn from(e: std::io::Error) -> Self { - IntTestError::IO(e) - } - } - - // Helper function - // Runs a system command with given args - #[allow(dead_code)] - fn run_cmd_with_args(cmd: &str, args: &[&str]) -> Result { - let output = Command::new(cmd).args(args).output().unwrap(); - let mut value = output.stdout; - let error = output.stderr; - if value.is_empty() { - return Err(IntTestError::CmdExec(String::from_utf8(error).unwrap())); - } - value.pop(); // remove `\n` at end - let output_string = std::str::from_utf8(&value).unwrap(); - let json_value: serde_json::Value = match serde_json::from_str(output_string) { - Ok(value) => value, - Err(_) => json!(output_string), // bitcoin-cli will sometime return raw string - }; - Ok(json_value) - } - - // Helper Function - // Transforms a json value to string - #[allow(dead_code)] - fn value_to_string(value: &Value) -> Result { - match value { - Value::Bool(bool) => match bool { - true => Ok("true".to_string()), - false => Ok("false".to_string()), - }, - Value::Number(n) => Ok(n.to_string()), - Value::String(s) => Ok(s.to_string()), - _ => Err(IntTestError::JsonData( - "Value parsing not implemented for this type".to_string(), - )), - } - } - - // Helper Function - // Extracts value from a given json object and key - #[allow(dead_code)] - fn get_value(json: &Value, key: &str) -> Result { - let map = json - .as_object() - .ok_or(IntTestError::JsonData("Json is not an object".to_string()))?; - let value = map - .get(key) - .ok_or(IntTestError::JsonData("Invalid key".to_string()))? - .to_owned(); - let string_value = value_to_string(&value)?; - Ok(string_value) - } - - /// The bdk-cli command struct - /// Use it to perform all bdk-cli operations - #[allow(dead_code)] - #[derive(Debug)] - struct BdkCli { - target: String, - network: String, - verbosity: bool, - recv_desc: Option, - chang_desc: Option, - node_datadir: Option, - } - - impl BdkCli { - /// Construct a new [`BdkCli`] struct - fn new( - network: &str, - node_datadir: Option, - verbosity: bool, - features: &[&str], - ) -> Result { - // Build bdk-cli with given features - let mut feat = "--features=".to_string(); - for item in features { - feat.push_str(item); - feat.push(','); - } - feat.pop(); // remove the last comma - let _build = Command::new("cargo").args(["build", &feat]).output()?; - - let mut bdk_cli = Self { - target: "./target/debug/bdk-cli".to_string(), - network: network.to_string(), - verbosity, - recv_desc: None, - chang_desc: None, - node_datadir, - }; - - println!("BDK-CLI Config : {bdk_cli:#?}"); - let bdk_master_key = bdk_cli.key_exec(&["generate"])?; - let bdk_xprv = get_value(&bdk_master_key, "xprv")?; - - let bdk_recv_desc = - bdk_cli.key_exec(&["derive", "--path", "m/84h/1h/0h/0", "--xprv", &bdk_xprv])?; - let bdk_recv_desc = get_value(&bdk_recv_desc, "xprv")?; - let bdk_recv_desc = format!("wpkh({bdk_recv_desc})"); - - let bdk_chng_desc = - bdk_cli.key_exec(&["derive", "--path", "m/84h/1h/0h/1", "--xprv", &bdk_xprv])?; - let bdk_chng_desc = get_value(&bdk_chng_desc, "xprv")?; - let bdk_chng_desc = format!("wpkh({bdk_chng_desc})"); - - bdk_cli.recv_desc = Some(bdk_recv_desc); - bdk_cli.chang_desc = Some(bdk_chng_desc); - - Ok(bdk_cli) - } - - /// Execute bdk-cli wallet commands with given args - fn wallet_exec(&self, args: &[&str]) -> Result { - // Check if data directory is specified - let mut wallet_args = if let Some(datadir) = &self.node_datadir { - let datadir = datadir.as_os_str().to_str().unwrap(); - ["--network", &self.network, "--datadir", datadir, "wallet"].to_vec() - } else { - ["--network", &self.network, "wallet"].to_vec() - }; - - if self.verbosity { - wallet_args.push("-v"); - } - - wallet_args.push("-d"); - wallet_args.push(self.recv_desc.as_ref().unwrap()); - wallet_args.push("-c"); - wallet_args.push(self.chang_desc.as_ref().unwrap()); - - for arg in args { - wallet_args.push(arg); - } - run_cmd_with_args(&self.target, &wallet_args) - } - - /// Execute bdk-cli key commands with given args - fn key_exec(&self, args: &[&str]) -> Result { - let mut key_args = ["key"].to_vec(); - for arg in args { - key_args.push(arg); - } - run_cmd_with_args(&self.target, &key_args) - } - - /// Execute bdk-cli node command - fn node_exec(&self, args: &[&str]) -> Result { - // Check if data directory is specified - let mut node_args = if let Some(datadir) = &self.node_datadir { - let datadir = datadir.as_os_str().to_str().unwrap(); - ["--network", &self.network, "--datadir", datadir, "node"].to_vec() - } else { - ["--network", &self.network, "node"].to_vec() - }; - - for arg in args { - node_args.push(arg); - } - run_cmd_with_args(&self.target, &node_args) - } - } - - // Run A Basic wallet operation test, with given feature - #[cfg(test)] - #[allow(dead_code)] - fn basic_wallet_ops(feature: &str) { - // Create a temporary directory for testing env - let mut test_dir = std::env::current_dir().unwrap(); - test_dir.push("bdk-testing"); - - let test_dir = temp_dir(); - // let test_dir = test_temp_dir.into_path().to_path_buf(); - - // Create bdk-cli instance - let bdk_cli = BdkCli::new("regtest", Some(test_dir), false, &[feature]).unwrap(); - - // Generate 101 blocks - bdk_cli.node_exec(&["generate", "101"]).unwrap(); - - // Get a bdk address - let bdk_addr_json = bdk_cli.wallet_exec(&["get_new_address"]).unwrap(); - let bdk_addr = get_value(&bdk_addr_json, "address").unwrap(); - - // Send coins from core to bdk - bdk_cli - .node_exec(&["sendtoaddress", &bdk_addr, "1000000000"]) - .unwrap(); - - bdk_cli.node_exec(&["generate", "1"]).unwrap(); - - // Sync the bdk wallet - bdk_cli.wallet_exec(&["sync"]).unwrap(); - - // Get the balance - let balance_json = bdk_cli.wallet_exec(&["get_balance"]).unwrap(); - let confirmed_balance = balance_json - .as_object() - .unwrap() - .get("satoshi") - .unwrap() - .as_object() - .unwrap() - .get("confirmed") - .unwrap() - .as_u64() - .unwrap(); - assert_eq!(confirmed_balance, 1000000000u64); - } -}