Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose test helpers #484

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 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,28 @@ dependencies = [
"tracing-subscriber",
]

[[package]]
name = "payjoin-test-utils"
version = "0.1.0"
dependencies = [
"bitcoin",
"bitcoincore-rpc",
"bitcoind",
"http",
"log",
"ohttp-relay",
"once_cell",
"payjoin-directory",
"rcgen",
"reqwest",
"testcontainers",
"testcontainers-modules",
"tokio",
"tracing",
"tracing-subscriber",
"url",
]

[[package]]
name = "pbkdf2"
version = "0.11.0"
Expand Down
24 changes: 24 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,28 @@ dependencies = [
"tracing-subscriber",
]

[[package]]
name = "payjoin-test-utils"
version = "0.1.0"
dependencies = [
"bitcoin",
"bitcoincore-rpc",
"bitcoind",
"http",
"log",
"ohttp-relay",
"once_cell",
"payjoin-directory",
"rcgen",
"reqwest",
"testcontainers",
"testcontainers-modules",
"tokio",
"tracing",
"tracing-subscriber",
"url",
]

[[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
2 changes: 2 additions & 0 deletions payjoin-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,5 @@ testcontainers-modules = { version = "0.1.3", features = ["redis"] }
tokio = { version = "1.12.0", features = ["full"] }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }

payjoin-test-utils = { path = "../payjoin-test-utils" }
166 changes: 11 additions & 155 deletions payjoin-cli/tests/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ mod e2e {
use std::env;
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::init_bitcoind_sender_receiver;
use tokio::fs;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::process::Command;
Expand All @@ -16,34 +13,7 @@ mod e2e {
#[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"
);

let (bitcoind, _sender, _receiver) = init_bitcoind_sender_receiver(None, None).unwrap();
let temp_dir = env::temp_dir();
let receiver_db_path = temp_dir.join("receiver_db");
let sender_db_path = temp_dir.join("sender_db");
Expand Down Expand Up @@ -162,49 +132,30 @@ mod e2e {
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn send_receive_payjoin() {
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;

use http::StatusCode;
use once_cell::sync::{Lazy, OnceCell};
use reqwest::{Client, ClientBuilder};
use payjoin_test_utils::{
http_agent, init_tracing, wait_for_service_ready, BoxError, TestServices,
};
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 BoxSendSyncError = Box<dyn std::error::Error + Send + Sync>;
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));
type Result<T> = std::result::Result<T, BoxError>;

init_tracing();
let (cert, key) = local_cert_key();
let docker: Cli = Cli::default();
let db = docker.run(Redis);
let db_host = format!("127.0.0.1:{}", db.get_host_port_ipv4(6379));
let (port, directory_handle) =
init_directory(db_host, (cert.clone(), key)).await.expect("Failed to init directory");
let directory = Url::parse(&format!("https://localhost:{}", port)).unwrap();

let gateway_origin = http::Uri::from_str(directory.as_str()).unwrap();
let (ohttp_relay_port, ohttp_relay_handle) =
ohttp_relay::listen_tcp_on_free_port(gateway_origin)
.await
.expect("Failed to init ohttp relay");
let ohttp_relay = Url::parse(&format!("http://localhost:{}", ohttp_relay_port)).unwrap();

let mut services = TestServices::initialize(db_host).await.unwrap();
let temp_dir = env::temp_dir();
let receiver_db_path = temp_dir.join("receiver_db");
let sender_db_path = temp_dir.join("sender_db");
let result: Result<()> = tokio::select! {
res = ohttp_relay_handle => Err(format!("Ohttp relay is long running: {:?}", res).into()),
res = directory_handle => 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()),
res = services.take_ohttp_relay_handle().unwrap() => Err(format!("Ohttp relay is long running: {:?}", res).into()),
res = services.take_directory_handle().unwrap() => Err(format!("Directory server is long running: {:?}", res).into()),
res = send_receive_cli_async(services.ohttp_relay_url(), services.directory_url(), services.cert(), receiver_db_path.clone(), sender_db_path.clone()) => res.map_err(|e| format!("send_receive failed: {:?}", e).into()),
};

cleanup_temp_file(&receiver_db_path).await;
Expand All @@ -218,33 +169,7 @@ 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"
);
let (bitcoind, _sender, _receiver) = init_bitcoind_sender_receiver(None, None)?;
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 @@ -471,75 +396,6 @@ mod e2e {
assert!(payjoin_sent.unwrap_or(false), "Payjoin send was not detected");
Ok(())
}

async fn wait_for_service_ready(service_url: Url, agent: Arc<Client>) -> Result<()> {
let health_url = service_url.join("/health").map_err(|_| "Invalid URL")?;
let start = std::time::Instant::now();

while start.elapsed() < *TESTS_TIMEOUT {
let request_result =
agent.get(health_url.as_str()).send().await.map_err(|_| "Bad request")?;

match request_result.status() {
StatusCode::OK => {
println!("READY {}", service_url);
return Ok(());
}
StatusCode::NOT_FOUND => return Err("Endpoint not found".into()),
_ => std::thread::sleep(*WAIT_SERVICE_INTERVAL),
}
}

Err("Timeout waiting for service to be ready".into())
}

async fn init_directory(
db_host: String,
local_cert_key: (Vec<u8>, Vec<u8>),
) -> std::result::Result<
(u16, tokio::task::JoinHandle<std::result::Result<(), BoxSendSyncError>>),
BoxSendSyncError,
> {
println!("Database running on {}", db_host);
let timeout = Duration::from_secs(2);
payjoin_directory::listen_tcp_with_tls_on_free_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()?)
}

fn http_agent_builder(cert_der: Vec<u8>) -> Result<ClientBuilder> {
Ok(ClientBuilder::new()
.danger_accept_invalid_certs(true)
.use_rustls_tls()
.add_root_certificate(reqwest::tls::Certificate::from_der(cert_der.as_slice())?))
}

fn init_tracing() {
INIT_TRACING.get_or_init(|| {
let subscriber = tracing_subscriber::FmtSubscriber::builder()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.with_test_writer()
.finish();

tracing::subscriber::set_global_default(subscriber)
.expect("failed to set global default subscriber");
});
}
}

async fn cleanup_temp_file(path: &std::path::Path) {
Expand Down
25 changes: 25 additions & 0 deletions payjoin-test-utils/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[package]
name = "payjoin-test-utils"
version = "0.1.0"
edition = "2021"
authors = ["Dan Gould <[email protected]>"]
rust-version = "1.63"
license = "MIT"

[dependencies]
bitcoin = { version = "0.32.5", features = ["base64"] }
bitcoincore-rpc = "0.19.0"
bitcoind = { version = "0.36.0", features = ["0_21_2"] }
http = "1"
log = "0.4.7"
ohttp-relay = "0.0.9"
once_cell = "1"
payjoin-directory = { path = "../payjoin-directory", features = ["_danger-local-https"] }
rcgen = "0.11"
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
testcontainers = "0.15.0"
testcontainers-modules = { version = "0.1.3", features = ["redis"] }
tokio = { version = "1.12.0", features = ["full"] }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
url = "2.2.2"
Loading
Loading