diff --git a/Cargo-minimal.lock b/Cargo-minimal.lock index cc0da7a5..d8e9abd9 100644 --- a/Cargo-minimal.lock +++ b/Cargo-minimal.lock @@ -1590,6 +1590,7 @@ dependencies = [ "ohttp-relay", "once_cell", "payjoin-directory", + "payjoin-test-utils", "rcgen", "reqwest", "rustls 0.22.4", @@ -1624,6 +1625,7 @@ dependencies = [ "once_cell", "payjoin", "payjoin-directory", + "payjoin-test-utils", "rcgen", "reqwest", "rustls 0.22.4", @@ -1659,6 +1661,21 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "payjoin-test-utils" +version = "0.1.0" +dependencies = [ + "bitcoincore-rpc", + "bitcoind", + "http", + "log", + "ohttp-relay", + "payjoin-directory", + "rcgen", + "testcontainers", + "testcontainers-modules", +] + [[package]] name = "pbkdf2" version = "0.11.0" diff --git a/Cargo-recent.lock b/Cargo-recent.lock index cc0da7a5..d8e9abd9 100644 --- a/Cargo-recent.lock +++ b/Cargo-recent.lock @@ -1590,6 +1590,7 @@ dependencies = [ "ohttp-relay", "once_cell", "payjoin-directory", + "payjoin-test-utils", "rcgen", "reqwest", "rustls 0.22.4", @@ -1624,6 +1625,7 @@ dependencies = [ "once_cell", "payjoin", "payjoin-directory", + "payjoin-test-utils", "rcgen", "reqwest", "rustls 0.22.4", @@ -1659,6 +1661,21 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "payjoin-test-utils" +version = "0.1.0" +dependencies = [ + "bitcoincore-rpc", + "bitcoind", + "http", + "log", + "ohttp-relay", + "payjoin-directory", + "rcgen", + "testcontainers", + "testcontainers-modules", +] + [[package]] name = "pbkdf2" version = "0.11.0" diff --git a/Cargo.toml b/Cargo.toml index 532c7cda..0efa8fd2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["payjoin", "payjoin-cli", "payjoin-directory"] +members = ["payjoin", "payjoin-cli", "payjoin-directory", "payjoin-test-utils"] resolver = "2" [patch.crates-io.payjoin] diff --git a/payjoin-cli/Cargo.toml b/payjoin-cli/Cargo.toml index 3e1937cc..543d0d93 100644 --- a/payjoin-cli/Cargo.toml +++ b/payjoin-cli/Cargo.toml @@ -53,6 +53,7 @@ http = "1" ohttp-relay = "0.0.8" once_cell = "1" payjoin-directory = { path = "../payjoin-directory", features = ["_danger-local-https"] } +payjoin-test-utils = { path = "../payjoin-test-utils" } testcontainers = "0.15.0" testcontainers-modules = { version = "0.1.3", features = ["redis"] } tokio = { version = "1.12.0", features = ["full"] } diff --git a/payjoin-cli/tests/e2e.rs b/payjoin-cli/tests/e2e.rs index e9caa23c..91a7bfe6 100644 --- a/payjoin-cli/tests/e2e.rs +++ b/payjoin-cli/tests/e2e.rs @@ -4,45 +4,24 @@ mod e2e { use std::process::Stdio; use bitcoincore_rpc::json::AddressType; - use bitcoind::bitcoincore_rpc::RpcApi; - use log::{log_enabled, Level}; - use payjoin::bitcoin::Amount; + use payjoin_test_utils::*; use tokio::fs; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::process::Command; const RECEIVE_SATS: &str = "54321"; + type Error = Box; + type Result = std::result::Result; + #[cfg(not(feature = "v2"))] #[tokio::test(flavor = "multi_thread", worker_threads = 4)] - async fn send_receive_payjoin() { - let bitcoind_exe = env::var("BITCOIND_EXE") - .ok() - .or_else(|| bitcoind::downloaded_exe_path().ok()) - .expect("version feature or env BITCOIND_EXE is required for tests"); - let mut conf = bitcoind::Conf::default(); - conf.view_stdout = log_enabled!(Level::Debug); - let bitcoind = bitcoind::BitcoinD::with_conf(bitcoind_exe, &conf).unwrap(); - let receiver = bitcoind.create_wallet("receiver").unwrap(); - let receiver_address = - receiver.get_new_address(None, Some(AddressType::Bech32)).unwrap().assume_checked(); - let sender = bitcoind.create_wallet("sender").unwrap(); - let sender_address = - sender.get_new_address(None, Some(AddressType::Bech32)).unwrap().assume_checked(); - bitcoind.client.generate_to_address(1, &receiver_address).unwrap(); - bitcoind.client.generate_to_address(101, &sender_address).unwrap(); - - assert_eq!( - Amount::from_btc(50.0).unwrap(), - receiver.get_balances().unwrap().mine.trusted, - "receiver doesn't own bitcoin" - ); - - assert_eq!( - Amount::from_btc(50.0).unwrap(), - sender.get_balances().unwrap().mine.trusted, - "sender doesn't own bitcoin" - ); + async fn send_receive_payjoin() -> Result<()> { + // _sender and _receiver are called by the payjoin-cli using RPC directly + let (bitcoind, _sender, _receiver) = payjoin_test_utils::init_bitcoind_sender_receiver( + Some(AddressType::Bech32), + Some(AddressType::Bech32), + )?; let temp_dir = env::temp_dir(); let receiver_db_path = temp_dir.join("receiver_db"); @@ -151,6 +130,7 @@ mod e2e { payjoin_sent.unwrap().unwrap_or(Some(false)).unwrap(), "Payjoin send was not detected" ); + Ok(()) } #[cfg(feature = "v2")] @@ -164,20 +144,15 @@ mod e2e { use http::StatusCode; use once_cell::sync::{Lazy, OnceCell}; use reqwest::{Client, ClientBuilder}; - use testcontainers::clients::Cli; - use testcontainers_modules::redis::Redis; use tokio::process::Child; use url::Url; - type Error = Box; - type Result = std::result::Result; - static INIT_TRACING: OnceCell<()> = OnceCell::new(); static TESTS_TIMEOUT: Lazy = Lazy::new(|| Duration::from_secs(20)); static WAIT_SERVICE_INTERVAL: Lazy = Lazy::new(|| Duration::from_secs(3)); init_tracing(); - let (cert, key) = local_cert_key(); + let (cert, key) = payjoin_test_utils::local_cert_key(); let ohttp_relay_port = find_free_port(); let ohttp_relay = Url::parse(&format!("http://localhost:{}", ohttp_relay_port)).unwrap(); let directory_port = find_free_port(); @@ -189,7 +164,7 @@ mod e2e { let sender_db_path = temp_dir.join("sender_db"); let result: Result<()> = tokio::select! { res = ohttp_relay::listen_tcp(ohttp_relay_port, gateway_origin) => Err(format!("Ohttp relay is long running: {:?}", res).into()), - res = init_directory(directory_port, (cert.clone(), key)) => Err(format!("Directory server is long running: {:?}", res).into()), + res = payjoin_test_utils::init_directory(directory_port, (cert.clone(), key)) => Err(format!("Directory server is long running: {:?}", res).into()), res = send_receive_cli_async(ohttp_relay, directory, cert, receiver_db_path.clone(), sender_db_path.clone()) => res.map_err(|e| format!("send_receive failed: {:?}", e).into()), }; @@ -204,33 +179,13 @@ mod e2e { receiver_db_path: PathBuf, sender_db_path: PathBuf, ) -> Result<()> { - let bitcoind_exe = env::var("BITCOIND_EXE") - .ok() - .or_else(|| bitcoind::downloaded_exe_path().ok()) - .expect("version feature or env BITCOIND_EXE is required for tests"); - let mut conf = bitcoind::Conf::default(); - conf.view_stdout = log_enabled!(Level::Debug); - let bitcoind = bitcoind::BitcoinD::with_conf(bitcoind_exe, &conf)?; - let receiver = bitcoind.create_wallet("receiver")?; - let receiver_address = - receiver.get_new_address(None, Some(AddressType::Bech32))?.assume_checked(); - let sender = bitcoind.create_wallet("sender")?; - let sender_address = - sender.get_new_address(None, Some(AddressType::Bech32))?.assume_checked(); - bitcoind.client.generate_to_address(1, &receiver_address)?; - bitcoind.client.generate_to_address(101, &sender_address)?; - - assert_eq!( - Amount::from_btc(50.0)?, - receiver.get_balances()?.mine.trusted, - "receiver doesn't own bitcoin" - ); - - assert_eq!( - Amount::from_btc(50.0)?, - sender.get_balances()?.mine.trusted, - "sender doesn't own bitcoin" - ); + // _sender and _receiver are called by the payjoin-cli using RPC directly + let (bitcoind, _sender, _receiver) = payjoin_test_utils::init_bitcoind_sender_receiver( + Some(AddressType::Bech32), + Some(AddressType::Bech32), + ) + .unwrap(); + let temp_dir = env::temp_dir(); let cert_path = temp_dir.join("localhost.der"); tokio::fs::write(&cert_path, cert.clone()).await?; @@ -476,27 +431,6 @@ mod e2e { Err("Timeout waiting for service to be ready".into()) } - async fn init_directory(port: u16, local_cert_key: (Vec, Vec)) -> Result<()> { - let docker: Cli = Cli::default(); - let timeout = Duration::from_secs(2); - let db = docker.run(Redis); - let db_host = format!("127.0.0.1:{}", db.get_host_port_ipv4(6379)); - println!("Database running on {}", db.get_host_port_ipv4(6379)); - payjoin_directory::listen_tcp_with_tls(port, db_host, timeout, local_cert_key).await - } - - // generates or gets a DER encoded localhost cert and key. - fn local_cert_key() -> (Vec, Vec) { - let cert = rcgen::generate_simple_self_signed(vec![ - "0.0.0.0".to_string(), - "localhost".to_string(), - ]) - .expect("Failed to generate cert"); - let cert_der = cert.serialize_der().expect("Failed to serialize cert"); - let key_der = cert.serialize_private_key_der(); - (cert_der, key_der) - } - fn http_agent(cert_der: Vec) -> Result { Ok(http_agent_builder(cert_der)?.build()?) } @@ -521,11 +455,6 @@ mod e2e { } } - fn find_free_port() -> u16 { - let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); - listener.local_addr().unwrap().port() - } - async fn cleanup_temp_file(path: &std::path::Path) { if let Err(e) = fs::remove_dir_all(path).await { eprintln!("Failed to remove {:?}: {}", path, e); diff --git a/payjoin-test-utils/Cargo.toml b/payjoin-test-utils/Cargo.toml new file mode 100644 index 00000000..c8f0c0ea --- /dev/null +++ b/payjoin-test-utils/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "payjoin-test-utils" +version = "0.1.0" +edition = "2021" +authors = ["Dan Gould "] +rust-version = "1.63" +license = "MIT" + +[dependencies] +bitcoincore-rpc = "0.19.0" +bitcoind = { version = "0.36.0", features = ["0_21_2"] } +http = "1" +log = "0.4.7" +ohttp-relay = "0.0.8" +payjoin-directory = { path = "../payjoin-directory", features = ["_danger-local-https"] } +rcgen = "0.11" +testcontainers = "0.15.0" +testcontainers-modules = { version = "0.1.3", features = ["redis"] } diff --git a/payjoin-test-utils/src/lib.rs b/payjoin-test-utils/src/lib.rs new file mode 100644 index 00000000..984de1df --- /dev/null +++ b/payjoin-test-utils/src/lib.rs @@ -0,0 +1,77 @@ +use std::time::Duration; + +use bitcoincore_rpc::bitcoin::Amount; +use bitcoincore_rpc::json::AddressType; +use bitcoincore_rpc::RpcApi; +use testcontainers::clients::Cli; +use testcontainers_modules::redis::Redis; + +type Error = Box; + +pub fn init_bitcoind() -> Result { + let bitcoind_exe = std::env::var("BITCOIND_EXE") + .ok() + .or_else(|| bitcoind::downloaded_exe_path().ok()) + .unwrap(); + let mut conf = bitcoind::Conf::default(); + conf.view_stdout = log::log_enabled!(log::Level::Debug); + let bitcoind = bitcoind::BitcoinD::with_conf(bitcoind_exe, &conf)?; + Ok(bitcoind) +} + +pub fn init_bitcoind_sender_receiver( + sender_address_type: Option, + receiver_address_type: Option, +) -> Result<(bitcoind::BitcoinD, bitcoincore_rpc::Client, bitcoincore_rpc::Client), Error> { + let bitcoind = init_bitcoind()?; + let receiver = bitcoind.create_wallet("receiver")?; + let receiver_address = receiver.get_new_address(None, receiver_address_type)?.assume_checked(); + let sender = bitcoind.create_wallet("sender")?; + let sender_address = sender.get_new_address(None, sender_address_type)?.assume_checked(); + bitcoind.client.generate_to_address(1, &receiver_address)?; + bitcoind.client.generate_to_address(101, &sender_address)?; + + assert_eq!( + Amount::from_btc(50.0)?, + receiver.get_balances()?.mine.trusted, + "receiver doesn't own bitcoin" + ); + + assert_eq!( + Amount::from_btc(50.0)?, + sender.get_balances()?.mine.trusted, + "sender doesn't own bitcoin" + ); + Ok((bitcoind, sender, receiver)) +} + +pub async fn init_directory(port: u16, local_cert_key: (Vec, Vec)) -> Result<(), Error> { + let docker: Cli = Cli::default(); + let timeout = Duration::from_secs(2); + let db = docker.run(Redis); + let db_host = format!("127.0.0.1:{}", db.get_host_port_ipv4(6379)); + println!("Database running on {}", db.get_host_port_ipv4(6379)); + payjoin_directory::listen_tcp_with_tls(port, db_host, timeout, local_cert_key).await +} + +pub async fn init_ohttp_relay( + port: u16, + gateway_origin: http::Uri, +) -> Result<(), Box> { + ohttp_relay::listen_tcp(port, gateway_origin).await +} + +// generates or gets a DER encoded localhost cert and key. +pub fn local_cert_key() -> (Vec, Vec) { + let cert = + rcgen::generate_simple_self_signed(vec!["0.0.0.0".to_string(), "localhost".to_string()]) + .expect("Failed to generate cert"); + let cert_der = cert.serialize_der().expect("Failed to serialize cert"); + let key_der = cert.serialize_private_key_der(); + (cert_der, key_der) +} + +pub fn find_free_port() -> u16 { + let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + listener.local_addr().unwrap().port() +} diff --git a/payjoin/Cargo.toml b/payjoin/Cargo.toml index 2fd573fd..3a973759 100644 --- a/payjoin/Cargo.toml +++ b/payjoin/Cargo.toml @@ -41,6 +41,7 @@ serde_json = "1.0.108" bitcoind = { version = "0.36.0", features = ["0_21_2"] } http = "1" payjoin-directory = { path = "../payjoin-directory", features = ["_danger-local-https"] } +payjoin-test-utils = { path = "../payjoin-test-utils" } ohttp-relay = "0.0.8" once_cell = "1" rcgen = { version = "0.11" } diff --git a/payjoin/tests/integration.rs b/payjoin/tests/integration.rs index a82e581a..317d0aa3 100644 --- a/payjoin/tests/integration.rs +++ b/payjoin/tests/integration.rs @@ -1,7 +1,6 @@ #[cfg(all(feature = "send", feature = "receive"))] mod integration { use std::collections::HashMap; - use std::env; use std::str::FromStr; use bitcoin::policy::DEFAULT_MIN_RELAY_TX_FEE; @@ -10,11 +9,11 @@ mod integration { use bitcoin::{Amount, FeeRate, OutPoint, TxIn, TxOut, Weight}; use bitcoind::bitcoincore_rpc::json::{AddressType, WalletProcessPsbtResult}; use bitcoind::bitcoincore_rpc::{self, RpcApi}; - use log::{log_enabled, Level}; use once_cell::sync::{Lazy, OnceCell}; use payjoin::receive::InputPair; use payjoin::send::SenderBuilder; use payjoin::{PjUri, PjUriBuilder, Request, Uri}; + use payjoin_test_utils::*; use tracing_subscriber::{EnvFilter, FmtSubscriber}; use url::Url; @@ -61,16 +60,16 @@ mod integration { do_v1_to_v1(sender, receiver, false) } - // TODO: Not supported by bitcoind 0_21_2. Later versions fail for unknown reasons - //#[test] - //fn v1_to_v1_taproot() -> Result<(), BoxError> { - // init_tracing(); - // let (_bitcoind, sender, receiver) = init_bitcoind_sender_receiver( - // Some(AddressType::Bech32m), - // Some(AddressType::Bech32m), - // )?; - // do_v1_to_v1(sender, receiver, false) - //} + #[ignore] // TODO: Not supported by bitcoind 0_21_2. Later versions fail for unknown reasons + #[test] + fn v1_to_v1_taproot() -> Result<(), BoxError> { + init_tracing(); + let (_bitcoind, sender, receiver) = init_bitcoind_sender_receiver( + Some(AddressType::Bech32m), + Some(AddressType::Bech32m), + )?; + do_v1_to_v1(sender, receiver, false) + } fn do_v1_to_v1( sender: bitcoincore_rpc::Client, @@ -182,8 +181,6 @@ mod integration { use payjoin::receive::v2::{PayjoinProposal, Receiver, UncheckedProposal}; use payjoin::{HpkeKeyPair, OhttpKeys, PjUri, UriExt}; use reqwest::{Client, ClientBuilder, Error, Response}; - use testcontainers_modules::redis::Redis; - use testcontainers_modules::testcontainers::clients::Cli; use super::*; @@ -307,8 +304,8 @@ mod integration { let directory = Url::parse(&format!("https://localhost:{}", directory_port)).unwrap(); let gateway_origin = http::Uri::from_str(directory.as_str()).unwrap(); tokio::select!( - _ = ohttp_relay::listen_tcp(ohttp_relay_port, gateway_origin) => panic!("Ohttp relay is long running"), - _ = init_directory(directory_port, (cert.clone(), key)) => panic!("Directory server is long running"), + _ = payjoin_test_utils::init_ohttp_relay(ohttp_relay_port, gateway_origin) => panic!("Ohttp relay is long running"), + _ = payjoin_test_utils::init_directory(directory_port, (cert.clone(), key)) => panic!("Directory server is long running"), res = do_v2_send_receive(ohttp_relay, directory, cert) => assert!(res.is_ok(), "v2 send receive failed: {:#?}", res) ); @@ -770,30 +767,6 @@ mod integration { } } - async fn init_directory( - port: u16, - local_cert_key: (Vec, Vec), - ) -> Result<(), BoxError> { - let docker: Cli = Cli::default(); - let timeout = Duration::from_secs(2); - let db = docker.run(Redis); - let db_host = format!("127.0.0.1:{}", db.get_host_port_ipv4(6379)); - println!("Database running on {}", db.get_host_port_ipv4(6379)); - payjoin_directory::listen_tcp_with_tls(port, db_host, timeout, local_cert_key).await - } - - // generates or gets a DER encoded localhost cert and key. - fn local_cert_key() -> (Vec, Vec) { - let cert = rcgen::generate_simple_self_signed(vec![ - "0.0.0.0".to_string(), - "localhost".to_string(), - ]) - .expect("Failed to generate cert"); - let cert_der = cert.serialize_der().expect("Failed to serialize cert"); - let key_der = cert.serialize_private_key_der(); - (cert_der, key_der) - } - fn initialize_session( address: Address, directory: Url, @@ -905,11 +878,6 @@ mod integration { )) } - fn find_free_port() -> u16 { - let listener = std::net::TcpListener::bind("0.0.0.0:0").unwrap(); - listener.local_addr().unwrap().port() - } - async fn wait_for_service_ready( service_url: Url, agent: Arc, @@ -1152,38 +1120,6 @@ mod integration { }); } - fn init_bitcoind_sender_receiver( - sender_address_type: Option, - receiver_address_type: Option, - ) -> Result<(bitcoind::BitcoinD, bitcoincore_rpc::Client, bitcoincore_rpc::Client), BoxError> - { - let bitcoind_exe = - env::var("BITCOIND_EXE").ok().or_else(|| bitcoind::downloaded_exe_path().ok()).unwrap(); - let mut conf = bitcoind::Conf::default(); - conf.view_stdout = log_enabled!(Level::Debug); - let bitcoind = bitcoind::BitcoinD::with_conf(bitcoind_exe, &conf)?; - let receiver = bitcoind.create_wallet("receiver")?; - let receiver_address = - receiver.get_new_address(None, receiver_address_type)?.assume_checked(); - let sender = bitcoind.create_wallet("sender")?; - let sender_address = sender.get_new_address(None, sender_address_type)?.assume_checked(); - bitcoind.client.generate_to_address(1, &receiver_address)?; - bitcoind.client.generate_to_address(101, &sender_address)?; - - assert_eq!( - Amount::from_btc(50.0)?, - receiver.get_balances()?.mine.trusted, - "receiver doesn't own bitcoin" - ); - - assert_eq!( - Amount::from_btc(50.0)?, - sender.get_balances()?.mine.trusted, - "sender doesn't own bitcoin" - ); - Ok((bitcoind, sender, receiver)) - } - fn build_original_psbt( sender: &bitcoincore_rpc::Client, pj_uri: &PjUri,