Skip to content

Commit

Permalink
Expose test fixtures and fns at payjoin-test-utils
Browse files Browse the repository at this point in the history
This new crate also provides an opportunity to create downstream test
fixtures in payjoin-ffi and language bindings downstream of it.
  • Loading branch information
DanGould committed Dec 3, 2024
1 parent a1fbac5 commit ae470e8
Show file tree
Hide file tree
Showing 9 changed files with 155 additions and 159 deletions.
17 changes: 17 additions & 0 deletions Cargo-minimal.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1590,6 +1590,7 @@ dependencies = [
"ohttp-relay",
"once_cell",
"payjoin-directory",
"payjoin-test-utils",
"rcgen",
"reqwest",
"rustls 0.22.4",
Expand Down Expand Up @@ -1624,6 +1625,7 @@ dependencies = [
"once_cell",
"payjoin",
"payjoin-directory",
"payjoin-test-utils",
"rcgen",
"reqwest",
"rustls 0.22.4",
Expand Down Expand Up @@ -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"
Expand Down
17 changes: 17 additions & 0 deletions Cargo-recent.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1590,6 +1590,7 @@ dependencies = [
"ohttp-relay",
"once_cell",
"payjoin-directory",
"payjoin-test-utils",
"rcgen",
"reqwest",
"rustls 0.22.4",
Expand Down Expand Up @@ -1624,6 +1625,7 @@ dependencies = [
"once_cell",
"payjoin",
"payjoin-directory",
"payjoin-test-utils",
"rcgen",
"reqwest",
"rustls 0.22.4",
Expand Down Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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]
Expand Down
1 change: 1 addition & 0 deletions payjoin-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
111 changes: 20 additions & 91 deletions payjoin-cli/tests/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn std::error::Error + 'static>;
type Result<T> = std::result::Result<T, Error>;

#[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");
Expand Down Expand Up @@ -151,6 +130,7 @@ mod e2e {
payjoin_sent.unwrap().unwrap_or(Some(false)).unwrap(),
"Payjoin send was not detected"
);
Ok(())
}

#[cfg(feature = "v2")]
Expand All @@ -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<dyn std::error::Error + 'static>;
type Result<T> = std::result::Result<T, Error>;

static INIT_TRACING: OnceCell<()> = OnceCell::new();
static TESTS_TIMEOUT: Lazy<Duration> = Lazy::new(|| Duration::from_secs(20));
static WAIT_SERVICE_INTERVAL: Lazy<Duration> = 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();
Expand All @@ -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()),
};

Expand All @@ -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?;
Expand Down Expand Up @@ -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<u8>, Vec<u8>)) -> 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<u8>, Vec<u8>) {
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<u8>) -> Result<Client> {
Ok(http_agent_builder(cert_der)?.build()?)
}
Expand All @@ -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);
Expand Down
18 changes: 18 additions & 0 deletions payjoin-test-utils/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "payjoin-test-utils"
version = "0.1.0"
edition = "2021"
authors = ["Dan Gould <[email protected]>"]
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"] }
77 changes: 77 additions & 0 deletions payjoin-test-utils/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<dyn std::error::Error + 'static>;

pub fn init_bitcoind() -> Result<bitcoind::BitcoinD, Error> {
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<AddressType>,
receiver_address_type: Option<AddressType>,
) -> 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<u8>, Vec<u8>)) -> 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<dyn std::error::Error + Send + Sync>> {
ohttp_relay::listen_tcp(port, gateway_origin).await
}

// generates or gets a DER encoded localhost cert and key.
pub fn local_cert_key() -> (Vec<u8>, Vec<u8>) {
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()
}
1 change: 1 addition & 0 deletions payjoin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
Loading

0 comments on commit ae470e8

Please sign in to comment.