]> Untitled Git - bdk-cli/commitdiff
test: Add helper fns & integration tests for key
authorVihiga Tyonum <withtvpeter@gmail.com>
Thu, 11 Jun 2026 12:38:52 +0000 (13:38 +0100)
committerVihiga Tyonum <withtvpeter@gmail.com>
Wed, 8 Jul 2026 21:49:17 +0000 (22:49 +0100)
- add BdkCli helper struct and impl methods
- add integration tests for the key subcommand
operations

Cargo.lock
Cargo.toml
tests/cli.rs [new file with mode: 0644]
tests/common/mod.rs [new file with mode: 0644]
tests/integration.rs [deleted file]

index a5826c4ba0dd92b4bc107b9c42b7d4a335b4245b..82ef4721c260ff090b79b5647d245c4ff94dcea0 100644 (file)
@@ -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"
index 072548803377a42537170a613b1309958d159b23..7ffc611f86a63b3e21d1b9c4c58d52f6f4845340 100644 (file)
@@ -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 (file)
index 0000000..db97d65
--- /dev/null
@@ -0,0 +1,115 @@
+// Copyright (c) 2020-2026 Bitcoin Dev Kit Developers
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, 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 (file)
index 0000000..e957584
--- /dev/null
@@ -0,0 +1,77 @@
+// Copyright (c) 2020-2026 Bitcoin Dev Kit Developers
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, 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<PathBuf>,
+    pub verbosity: bool,
+    pub recv_desc: Option<String>,
+    pub change_desc: Option<String>,
+}
+
+impl BdkCli {
+    /// Construct a new test environment configuration
+    pub fn new(network: &str, datadir: Option<PathBuf>) -> 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 (file)
index 79c887f..0000000
+++ /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 <LICENSE-APACHE
-// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
-// <LICENSE-MIT or http://opensource.org/licenses/MIT>, 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<std::io::Error> 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<serde_json::Value, IntTestError> {
-        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<String, IntTestError> {
-        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<String, IntTestError> {
-        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<String>,
-        chang_desc: Option<String>,
-        node_datadir: Option<PathBuf>,
-    }
-
-    impl BdkCli {
-        /// Construct a new [`BdkCli`] struct
-        fn new(
-            network: &str,
-            node_datadir: Option<PathBuf>,
-            verbosity: bool,
-            features: &[&str],
-        ) -> Result<Self, IntTestError> {
-            // 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<Value, IntTestError> {
-            // 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<Value, IntTestError> {
-            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<Value, IntTestError> {
-            // 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);
-    }
-}