diff --git a/Cargo.toml b/Cargo.toml index 3250b70..cda27c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,6 @@ tokio = { version = "1.0.0", features = ["io-util", "rt-multi-thread", "macros", tokio-stream = "0.1" tokio-tungstenite = "0.21.0" zeromq = "0.4.1" + +[dev-dependencies] +mockall = "0.13.0" diff --git a/src/commit.rs b/src/commit.rs index a473fb3..07655b9 100644 --- a/src/commit.rs +++ b/src/commit.rs @@ -203,4 +203,305 @@ fn commit_new_transaction( Ok(commited_in) } +#[cfg(test)] +mod tests { + use super::*; + + use bitcoincore_rpc::bitcoin::hex::DisplayHex; + use sdk_common::pcd::Member; + use sdk_common::pcd::RoleDefinition; + use sdk_common::pcd::ValidationRule; + use sdk_common::sp_client::silentpayments::utils::SilentPaymentAddress; + use serde_json::json; + use mockall::predicate::*; + use mockall::mock; + use std::collections::HashMap; + use std::sync::{Mutex, Arc}; + use bitcoincore_rpc::bitcoin::*; + use crate::daemon::RpcCall; + use std::sync::OnceLock; + use sdk_common::sp_client::bitcoin::consensus::serialize; + use serde_json::{Map, Value}; + + const LOCAL_ADDRESS: &str = "sprt1qq222dhaxlzmjft2pa7qtspw2aw55vwfmtnjyllv5qrsqwm3nufxs6q7t88jf9asvd7rxhczt87de68du3jhem54xvqxy80wc6ep7lauxacsrq79v"; + const INIT_TRANSACTION: &str = "02000000000102b01b832bf34cf87583c628839c5316546646dcd4939e339c1d83e693216cdfa00100000000fdffffffdd1ca865b199accd4801634488fca87e0cf81b36ee7e9bec526a8f922539b8670000000000fdffffff0200e1f505000000001600140798fac9f310cefad436ea928f0bdacf03a11be544e0f5050000000016001468a66f38e7c2c9e367577d6fad8532ae2c728ed2014043764b77de5041f80d19e3d872f205635f87486af015c00d2a3b205c694a0ae1cbc60e70b18bcd4470abbd777de63ae52600aba8f5ad1334cdaa6bcd931ab78b0140b56dd8e7ac310d6dcbc3eff37f111ced470990d911b55cd6ff84b74b579c17d0bba051ec23b738eeeedba405a626d95f6bdccb94c626db74c57792254bfc5a7c00000000"; + + // Define the mock for Daemon with the required methods + mock! { + #[derive(Debug)] + pub Daemon {} + + impl RpcCall for Daemon { + fn connect( + rpcwallet: Option, + rpc_url: String, + network: bitcoincore_rpc::bitcoin::Network, + ) -> Result where Self: Sized; + + fn estimate_fee(&self, nblocks: u16) -> Result; + + fn get_relay_fee(&self) -> Result; + + fn get_current_height(&self) -> Result; + + fn get_block(&self, block_hash: BlockHash) -> Result; + + fn get_filters(&self, block_height: u32) -> Result<(u32, BlockHash, bip158::BlockFilter)>; + + fn list_unspent_from_to( + &self, + minamt: Option, + ) -> Result>; + + fn create_psbt( + &self, + unspents: &[bitcoincore_rpc::json::ListUnspentResultEntry], + spk: ScriptBuf, + network: Network, + ) -> Result; + + fn process_psbt(&self, psbt: String) -> Result; + + fn finalize_psbt(&self, psbt: String) -> Result; + + fn get_network(&self) -> Result; + + fn test_mempool_accept( + &self, + tx: &Transaction, + ) -> Result; + + fn broadcast(&self, tx: &Transaction) -> Result; + + fn get_transaction_info( + &self, + txid: &Txid, + blockhash: Option, + ) -> Result; + + fn get_transaction_hex( + &self, + txid: &Txid, + blockhash: Option, + ) -> Result; + + fn get_transaction( + &self, + txid: &Txid, + blockhash: Option, + ) -> Result; + + fn get_block_txids(&self, blockhash: BlockHash) -> Result>; + + fn get_mempool_txids(&self) -> Result>; + + fn get_mempool_entries( + &self, + txids: &[Txid], + ) -> Result>>; + + fn get_mempool_transactions( + &self, + txids: &[Txid], + ) -> Result>>; + } + } + + mock! { + #[derive(Debug)] + pub SpWallet { + fn get_receiving_address(&self) -> Result; + } + } + + mock! { + #[derive(Debug)] + pub SilentPaymentWallet { + fn get_sp_wallet(&self) -> Result; + } + } + + static WALLET: OnceLock = OnceLock::new(); + + pub fn initialize_static_variables() { + if DAEMON.get().is_none() { + let mut daemon = MockDaemon::new(); + daemon.expect_broadcast() + .withf(|tx: &Transaction| serialize(tx).to_lower_hex_string() == INIT_TRANSACTION) + .returning(|tx| Ok(tx.txid())); + DAEMON.set(Mutex::new(Box::new(daemon))).expect("DAEMON should only be initialized once"); + println!("Initialized DAEMON"); + } + + if WALLET.get().is_none() { + let mut wallet = MockSilentPaymentWallet::new(); + wallet.expect_get_sp_wallet().returning(|| Ok(MockSpWallet::new())); + WALLET.set(wallet).expect("WALLET should only be initialized once"); + println!("Initialized WALLET"); + } + + if CACHEDPROCESSES.get().is_none() { + CACHEDPROCESSES + .set(Mutex::new(HashMap::new())) + .expect("CACHEDPROCESSES should only be initialized once"); + + println!("Initialized CACHEDPROCESSES"); + } + } + + fn mock_commit_msg(init_tx: Transaction, first: bool) -> CommitMessage { + let field_name = "roles".to_owned(); + let member = Member::new(vec![SilentPaymentAddress::try_from(LOCAL_ADDRESS).unwrap()]).unwrap(); + let validation_rule = ValidationRule::new(1.0, vec![field_name.clone()], 1.0).unwrap(); + + let role_def = RoleDefinition { + members: vec![member], + validation_rules: vec![validation_rule] + }; + let roles = HashMap::from([(String::from("role_name"), role_def)]); + let pcd_commitment = json!({field_name: "b30212b9649054b71f938fbe0d1c08e72de95bdb12b8008082795c6e9c4ad26a"}); + + let init_tx = if first { serialize(&init_tx).to_lower_hex_string() } else { OutPoint::new(init_tx.txid(), 0).to_string() }; + + let commit_msg = CommitMessage { + init_tx, + roles: roles.clone(), + validation_tokens: vec![], + pcd_commitment: pcd_commitment.clone(), + error: None, + }; + + commit_msg + } + + #[test] + fn test_handle_commit_new_process() { + initialize_static_variables(); + let init_tx = deserialize::(&Vec::from_hex(INIT_TRANSACTION).unwrap()).unwrap(); + let init_txid = init_tx.txid(); + let init_commitment = OutPoint::new(init_txid, 0); + + let commit_msg = mock_commit_msg(init_tx, true); + + let roles = commit_msg.roles.clone(); + let pcd_commitment = commit_msg.pcd_commitment.clone(); + + let empty_state = ProcessState { + commited_in: init_commitment, + ..Default::default() + }; + + let result = handle_commit_request(commit_msg); + + assert_eq!(result.unwrap(), init_commitment); + + let cache = CACHEDPROCESSES.get().unwrap().lock().unwrap(); + let updated_process = cache.get(&init_commitment); + + assert!(updated_process.is_some()); + let concurrent_states = updated_process.unwrap().get_latest_concurrent_states().unwrap(); + + // Constructing the roles_map that was inserted in the process + let roles_object = serde_json::to_value(roles).unwrap(); + let mut roles_map = Map::new(); + roles_map.insert("roles".to_owned(), roles_object); + let new_state = ProcessState { + commited_in: init_commitment, + pcd_commitment, + encrypted_pcd: Value::Object(roles_map), + ..Default::default() + }; + let target = vec![&empty_state, &new_state]; + + assert_eq!(concurrent_states, target); + } + + #[test] + fn test_handle_commit_new_state() { + initialize_static_variables(); + let init_tx = deserialize::(&Vec::from_hex(INIT_TRANSACTION).unwrap()).unwrap(); + let init_txid = init_tx.txid(); + let init_commitment = OutPoint::new(init_txid, 0); + + let commit_msg = mock_commit_msg(init_tx, false); + + let roles = commit_msg.roles.clone(); + let pcd_commitment = commit_msg.pcd_commitment.clone(); + + let empty_state = ProcessState { + commited_in: init_commitment, + ..Default::default() + }; + let process = Process::new(vec![empty_state.clone()], vec![]); + CACHEDPROCESSES.get().unwrap().lock().unwrap().insert(init_commitment, process); + + let result = handle_commit_request(commit_msg); + + assert_eq!(result.unwrap(), init_commitment); + + let cache = CACHEDPROCESSES.get().unwrap().lock().unwrap(); + let updated_process = cache.get(&init_commitment); + + assert!(updated_process.is_some()); + let concurrent_states = updated_process.unwrap().get_latest_concurrent_states().unwrap(); + + let roles_object = serde_json::to_value(roles).unwrap(); + let mut roles_map = Map::new(); + roles_map.insert("roles".to_owned(), roles_object); + let new_state = ProcessState { + commited_in: init_commitment, + pcd_commitment, + encrypted_pcd: Value::Object(roles_map), + ..Default::default() + }; + let target = vec![&empty_state, &new_state]; + + assert_eq!(concurrent_states, target); + } + + // #[test] + // fn test_handle_commit_request_invalid_init_tx() { + // let commit_msg = CommitMessage { + // init_tx: "invalid_tx_hex".to_string(), + // roles: HashMap::new(), + // validation_tokens: vec![], + // pcd_commitment: json!({"roles": "expected_roles"}).as_object().unwrap().clone(), + // }; + + // // Call the function under test + // let result = handle_commit_request(commit_msg); + + // // Assertions for error + // assert!(result.is_err()); + // assert_eq!(result.unwrap_err().to_string(), "init_tx must be a valid transaction or txid"); + // } + + // // Example test for adding a new state to an existing commitment + // #[test] + // fn test_handle_commit_request_add_state() { + // // Set up data for adding a state to an existing commitment + // let commit_msg = CommitMessage { + // init_tx: "existing_outpoint_hex".to_string(), + // roles: HashMap::new(), + // validation_tokens: vec![], + // pcd_commitment: json!({"roles": "expected_roles"}).as_object().unwrap().clone(), + // }; + + // // Mock daemon and cache initialization + // let mut daemon = MockDaemon::new(); + // daemon.expect_broadcast().returning(|_| Ok(Txid::new())); + // DAEMON.set(Arc::new(Mutex::new(daemon))).unwrap(); + + // let process_state = Process::new(vec![], vec![]); + // CACHEDPROCESSES.lock().unwrap().insert(OutPoint::new("mock_txid", 0), process_state); + + // // Run the function + // let result = handle_commit_request(commit_msg); + + // // Assert success and that a new state was added + // assert!(result.is_ok()); + // assert_eq!(result.unwrap(), OutPoint::new("mock_txid", 0)); + // } + + // // Additional tests for errors and validation tokens would follow a similar setup }