Skip to content

Commit 2e3ad78

Browse files
Add integration test framework
1 parent c20eb5a commit 2e3ad78

File tree

1 file changed

+280
-0
lines changed

1 file changed

+280
-0
lines changed

tests/integration.rs

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
// Magical Bitcoin Library
2+
// Written in 2022 by
3+
// Rajarshi Maitra <rajarshi149@gmail.com>
4+
//
5+
// Copyright (c) 2022 Magical Bitcoin
6+
//
7+
// Permission is hereby granted, free of charge, to any person obtaining a copy
8+
// of this software and associated documentation files (the "Software"), to deal
9+
// in the Software without restriction, including without limitation the rights
10+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
// copies of the Software, and to permit persons to whom the Software is
12+
// furnished to do so, subject to the following conditions:
13+
//
14+
// The above copyright notice and this permission notice shall be included in all
15+
// copies or substantial portions of the Software.
16+
//
17+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
// SOFTWARE.
24+
25+
//! bdk-cli Integration Test Framework
26+
//!
27+
//! This modules performs the necessary integration test for bdk-cli
28+
//! The tests can be run using `cargo test`
29+
30+
use serde_json::{json, Value};
31+
use std::convert::From;
32+
use std::process::Command;
33+
34+
/// Testing errors for integration tests
35+
#[derive(Debug)]
36+
enum IntTestError {
37+
// IO error
38+
IO(std::io::Error),
39+
// Command execution error
40+
CmdExec(String),
41+
// Json Data error
42+
JsonData(String),
43+
}
44+
45+
impl From<std::io::Error> for IntTestError {
46+
fn from(e: std::io::Error) -> Self {
47+
IntTestError::IO(e)
48+
}
49+
}
50+
51+
// Helper function
52+
// Runs a system command with given args
53+
fn run_cmd_with_args(cmd: &str, args: &[&str]) -> Result<serde_json::Value, IntTestError> {
54+
let output = Command::new(cmd).args(args).output().unwrap();
55+
let mut value = output.stdout;
56+
let error = output.stderr;
57+
if value.len() == 0 {
58+
return Err(IntTestError::CmdExec(String::from_utf8(error).unwrap()));
59+
}
60+
value.pop(); // remove `\n` at end
61+
let output_string = std::str::from_utf8(&value).unwrap();
62+
let json_value: serde_json::Value = match serde_json::from_str(output_string) {
63+
Ok(value) => value,
64+
Err(_) => json!(output_string), // bitcoin-cli will sometime return raw string
65+
};
66+
Ok(json_value)
67+
}
68+
69+
// Helper Function
70+
// Transforms a json value to string
71+
fn value_to_string(value: &Value) -> Result<String, IntTestError> {
72+
match value {
73+
Value::Bool(bool) => match bool {
74+
true => Ok("true".to_string()),
75+
false => Ok("false".to_string()),
76+
},
77+
Value::Number(n) => Ok(n.to_string()),
78+
Value::String(s) => Ok(s.to_string()),
79+
_ => Err(IntTestError::JsonData(
80+
"Value parsing not implemented for this type".to_string(),
81+
)),
82+
}
83+
}
84+
85+
// Helper Function
86+
// Extracts value from a given json object and key
87+
fn get_value(json: &Value, key: &str) -> Result<String, IntTestError> {
88+
let map = json
89+
.as_object()
90+
.ok_or(IntTestError::JsonData("Json is not an object".to_string()))?;
91+
let value = map
92+
.get(key)
93+
.ok_or(IntTestError::JsonData("Invalid key".to_string()))?
94+
.to_owned();
95+
let string_value = value_to_string(&value)?;
96+
Ok(string_value)
97+
}
98+
99+
/// The Bitcoin Cli process structure
100+
/// Use this to spawn and manage the bitcoin regtest backend
101+
struct BitcoinCli {
102+
/// bitcoin-cli execution target
103+
target: String,
104+
/// bitcoin-cli test wallet name
105+
wallet: String,
106+
}
107+
108+
impl BitcoinCli {
109+
/// Create a new [`BitcoinCli`] struct
110+
fn new(target: Option<&str>, wallet: Option<&str>) -> Self {
111+
let target = target.unwrap_or("bitcoin-cli").to_owned();
112+
let wallet = wallet.unwrap_or("test").to_owned();
113+
Command::new(&target)
114+
.args(&["createwallet", &wallet])
115+
.output()
116+
.unwrap();
117+
Command::new(&target)
118+
.args(&["loadwallet", &wallet])
119+
.output()
120+
.unwrap();
121+
122+
Self { target, wallet }
123+
}
124+
125+
/// Execute a bitcoin-cli command with given args
126+
fn exec(&mut self, args: &[&str]) -> Result<Value, IntTestError> {
127+
let cli_wallet = format!("-rpcwallet={}", &self.wallet);
128+
let mut cli_args = [cli_wallet.as_str()].to_vec();
129+
for arg in args {
130+
cli_args.push(arg);
131+
}
132+
run_cmd_with_args(&self.target, &cli_args)
133+
}
134+
}
135+
136+
/// The bdk-cli command struct
137+
/// Use it to perform all bdk-cli operations
138+
struct BdkCli {
139+
target: String,
140+
network: String,
141+
verbosity: bool,
142+
recv_desc: Option<String>,
143+
chang_desc: Option<String>,
144+
}
145+
146+
impl BdkCli {
147+
/// Construct a new [`BdkCli`] struct
148+
fn new(network: &str, verbosity: bool, features: &[&str]) -> Result<Self, IntTestError> {
149+
// Build bdk-cli with given features
150+
let mut feat = "--features=".to_string();
151+
for item in features {
152+
feat.push_str(item);
153+
feat.push_str(",");
154+
}
155+
feat.pop(); // remove the last comma
156+
let _build = Command::new("cargo").args(&["build", &feat]).output()?;
157+
158+
let mut bdk_cli = Self {
159+
target: "./target/debug/bdk-cli".to_string(),
160+
network: network.to_string(),
161+
verbosity,
162+
recv_desc: None,
163+
chang_desc: None,
164+
};
165+
166+
let bdk_master_key = bdk_cli.key_exec(&["generate"])?;
167+
let bdk_xprv = get_value(&bdk_master_key, "xprv")?;
168+
169+
let bdk_recv_desc =
170+
bdk_cli.key_exec(&["derive", "--path", "m/84h/1h/0h/0", "--xprv", &bdk_xprv])?;
171+
let bdk_recv_desc = get_value(&bdk_recv_desc, "xprv")?;
172+
let bdk_recv_desc = format!("wpkh({})", bdk_recv_desc);
173+
174+
let bdk_chng_desc =
175+
bdk_cli.key_exec(&["derive", "--path", "m/84h/1h/0h/1", "--xprv", &bdk_xprv])?;
176+
let bdk_chng_desc = get_value(&bdk_chng_desc, "xprv")?;
177+
let bdk_chng_desc = format!("wpkh({})", bdk_chng_desc);
178+
179+
bdk_cli.recv_desc = Some(bdk_recv_desc);
180+
bdk_cli.chang_desc = Some(bdk_chng_desc);
181+
182+
Ok(bdk_cli)
183+
}
184+
185+
/// Execute bdk-cli wallet commands with given args
186+
fn wallet_exec(&mut self, args: &[&str]) -> Result<Value, IntTestError> {
187+
let mut wallet_args = ["--network", &self.network, "wallet"].to_vec();
188+
if self.verbosity {
189+
wallet_args.push("-v");
190+
}
191+
192+
wallet_args.push("-d");
193+
wallet_args.push(self.recv_desc.as_ref().unwrap());
194+
wallet_args.push("-c");
195+
wallet_args.push(&self.chang_desc.as_ref().unwrap());
196+
197+
for arg in args {
198+
wallet_args.push(arg);
199+
}
200+
run_cmd_with_args(&self.target, &wallet_args)
201+
}
202+
203+
/// Execute bdk-cli key commands with given args
204+
fn key_exec(&mut self, args: &[&str]) -> Result<Value, IntTestError> {
205+
let mut key_args = ["key"].to_vec();
206+
for arg in args {
207+
key_args.push(arg);
208+
}
209+
run_cmd_with_args(&self.target, &key_args)
210+
}
211+
}
212+
213+
#[test]
214+
fn test_basic() {
215+
// Test basic building, fmt and unit tests
216+
let features = [
217+
"default",
218+
"electrum",
219+
"esplora-ureq",
220+
"esplora-reqwest",
221+
"compiler",
222+
"compact_filters",
223+
"reserves",
224+
"reserves,electrum",
225+
"reserves,esplora-ureq",
226+
"reserves,compact_filters",
227+
"rpc",
228+
];
229+
230+
// Test for build and fmt
231+
for feature in features {
232+
Command::new("cargo")
233+
.args(["build", "--features", feature, "--locked"])
234+
.output()
235+
.unwrap();
236+
Command::new("cargo")
237+
.args(["fmt", "--features", feature, "--locked"])
238+
.output()
239+
.unwrap();
240+
println!("Building with {} feature completed", feature);
241+
}
242+
}
243+
244+
#[test]
245+
fn test_wallet_ops() {
246+
// Create a bitcoin-cli instance
247+
let mut bitcoin_cli = BitcoinCli::new(None, None);
248+
249+
// Get a address from core
250+
let core_addr_json = bitcoin_cli.exec(&["getnewaddress"]).unwrap();
251+
let core_addr = value_to_string(&core_addr_json).unwrap();
252+
253+
// Generate few blocks to sync the chain
254+
bitcoin_cli
255+
.exec(&["generatetoaddress", "101", &core_addr])
256+
.unwrap();
257+
258+
// Create bdk-cli instance
259+
let mut bdk_cli = BdkCli::new("regtest", false, &["rpc"]).unwrap();
260+
261+
// Get a bdk address
262+
let bdk_addr_json = bdk_cli.wallet_exec(&["get_new_address"]).unwrap();
263+
let bdk_addr = get_value(&bdk_addr_json, "address").unwrap();
264+
265+
// Send coins from core to bdk
266+
bitcoin_cli
267+
.exec(&["sendtoaddress", &bdk_addr, "10"])
268+
.unwrap();
269+
bitcoin_cli
270+
.exec(&["generatetoaddress", "1", &core_addr])
271+
.unwrap();
272+
273+
// Sync the bdk wallet
274+
bdk_cli.wallet_exec(&["sync"]).unwrap();
275+
276+
// Get the balance
277+
let balance_json = bdk_cli.wallet_exec(&["get_balance"]).unwrap();
278+
let balance = get_value(&balance_json, "satoshi").unwrap();
279+
assert_eq!(balance, "1000000000");
280+
}

0 commit comments

Comments
 (0)