diff --git a/Cargo.toml b/Cargo.toml index 2fd2cc2..0f8b7d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,8 +27,6 @@ http = "0.2.12" async-trait = "0.1.76" thiserror = "1.0.40" percent-encoding = "2.1.0" -[dev-dependencies] -once_cell = "1.18.0" [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "0.2.9", features = ["js"] } @@ -40,6 +38,11 @@ wasm-bindgen-futures = "0.4.21" web-sys = { version = "0.3.55", features = ["Request", "RequestInit", "RequestMode", "Window"] } # web-sys = { version = "0.3.55", features = ["console", "Headers", "Request", "RequestInit", "RequestMode", "Response", "Window"] } +[dev-dependencies] +once_cell = "1.18.0" +testcontainers = "0.15.0" +lazy_static = "1.4" + [target.'cfg(target_arch = "wasm32")'.dev-dependencies] wasm-bindgen-test = { version = "0.3.2" } diff --git a/tests/docker.rs b/tests/docker.rs new file mode 100644 index 0000000..03f440d --- /dev/null +++ b/tests/docker.rs @@ -0,0 +1,83 @@ +#![feature(test)] +#![feature(custom_test_frameworks)] +#![test_runner(docker_tests_runner)] + +#[path = "docker/mod.rs"] mod docker; + +#[macro_use] extern crate lazy_static; +extern crate test; + +use docker::utils::{SIA_DOCKER_IMAGE, SIA_DOCKER_IMAGE_WITH_TAG, SIA_WALLATD_RPC_PORT}; + +use std::env; +use std::io::{BufRead, BufReader}; +use std::process::Command; +use test::{test_main, StaticBenchFn, StaticTestFn, TestDescAndFn}; +use testcontainers::clients::Cli; +use testcontainers::{Container, GenericImage, RunnableImage}; + +/// Custom test runner intended to initialize the SIA coin daemon in a Docker container. +pub fn docker_tests_runner(tests: &[&TestDescAndFn]) { + let docker = Cli::default(); + + pull_docker_image(SIA_DOCKER_IMAGE_WITH_TAG); + remove_docker_containers(SIA_DOCKER_IMAGE_WITH_TAG); + let _sia_node = sia_docker_node(&docker, SIA_WALLATD_RPC_PORT); + + let owned_tests: Vec<_> = tests + .iter() + .map(|t| match t.testfn { + StaticTestFn(f) => TestDescAndFn { + testfn: StaticTestFn(f), + desc: t.desc.clone(), + }, + StaticBenchFn(f) => TestDescAndFn { + testfn: StaticBenchFn(f), + desc: t.desc.clone(), + }, + _ => panic!("non-static tests passed to the test runner"), + }) + .collect(); + + let args: Vec<_> = env::args().collect(); + test_main(&args, owned_tests, None); +} + +fn pull_docker_image(name: &str) { + Command::new("docker") + .arg("pull") + .arg(name) + .status() + .expect("Failed to execute docker command"); +} + +fn remove_docker_containers(name: &str) { + let stdout = Command::new("docker") + .arg("ps") + .arg("-f") + .arg(format!("ancestor={}", name)) + .arg("-q") + .output() + .expect("Failed to execute docker command"); + + let reader = BufReader::new(stdout.stdout.as_slice()); + let ids: Vec<_> = reader.lines().map(|line| line.unwrap()).collect(); + if !ids.is_empty() { + Command::new("docker") + .arg("rm") + .arg("-f") + .args(ids) + .status() + .expect("Failed to execute docker command"); + } +} + +fn sia_docker_node(docker: &Cli, port: u16) -> Container<'_, GenericImage> { + let image = + GenericImage::new(SIA_DOCKER_IMAGE, "latest").with_env_var("WALLETD_API_PASSWORD", "password".to_string()); + let args = vec![]; + let image = RunnableImage::from((image, args)) + .with_mapped_port((port, port)) + .with_container_name("sia-docker"); + docker.run(image) +} diff --git a/tests/docker/mod.rs b/tests/docker/mod.rs new file mode 100644 index 0000000..eca0d7c --- /dev/null +++ b/tests/docker/mod.rs @@ -0,0 +1,2 @@ +mod tests; +pub mod utils; diff --git a/tests/docker/tests.rs b/tests/docker/tests.rs new file mode 100644 index 0000000..e860487 --- /dev/null +++ b/tests/docker/tests.rs @@ -0,0 +1,113 @@ +use super::utils::{block_on, mine_blocks, SIA_WALLETD_RPC_URL}; + +use http::StatusCode; +use sia_rust::transport::client::native::{Conf, NativeClient}; +use sia_rust::transport::client::{ApiClient, ApiClientError}; +use sia_rust::transport::endpoints::{AddressBalanceRequest, ConsensusTipRequest, GetAddressUtxosRequest, + TxpoolBroadcastRequest}; +use sia_rust::types::{Address, Currency, Keypair, SiacoinOutput, SpendPolicy, V2TransactionBuilder}; +use std::str::FromStr; +use url::Url; + +#[test] +fn test_sia_new_client() { + let conf = Conf { + server_url: Url::parse(SIA_WALLETD_RPC_URL).unwrap(), + password: Some("password".to_string()), + timeout: None, + }; + let _api_client = block_on(NativeClient::new(conf)).unwrap(); +} + +#[test] +fn test_sia_client_bad_auth() { + let conf = Conf { + server_url: Url::parse(SIA_WALLETD_RPC_URL).unwrap(), + password: Some("foo".to_string()), + timeout: None, + }; + let result = block_on(NativeClient::new(conf)); + assert!(matches!( + result, + Err(ApiClientError::UnexpectedHttpStatus { + status: StatusCode::UNAUTHORIZED, + .. + }) + )); +} + +#[test] +fn test_sia_client_consensus_tip() { + let conf = Conf { + server_url: Url::parse(SIA_WALLETD_RPC_URL).unwrap(), + password: Some("password".to_string()), + timeout: None, + }; + let api_client = block_on(NativeClient::new(conf)).unwrap(); + let _response = block_on(api_client.dispatcher(ConsensusTipRequest)).unwrap(); +} + +// This test likely needs to be removed because mine_blocks has possibility of interfering with other async tests +// related to block height +#[test] +fn test_sia_client_address_balance() { + let conf = Conf { + server_url: Url::parse(SIA_WALLETD_RPC_URL).unwrap(), + password: Some("password".to_string()), + timeout: None, + }; + let api_client = block_on(NativeClient::new(conf)).unwrap(); + + let address = + Address::from_str("addr:591fcf237f8854b5653d1ac84ae4c107b37f148c3c7b413f292d48db0c25a8840be0653e411f").unwrap(); + mine_blocks(10, &address); + + let request = AddressBalanceRequest { address }; + let response = block_on(api_client.dispatcher(request)).unwrap(); + + assert_eq!(response.siacoins, Currency(1000000000000000000000000000000000000)); +} + +#[test] +fn test_sia_client_build_tx() { + let conf = Conf { + server_url: Url::parse(SIA_WALLETD_RPC_URL).unwrap(), + password: Some("password".to_string()), + timeout: None, + }; + let api_client = block_on(NativeClient::new(conf)).unwrap(); + let keypair = Keypair::from_private_bytes( + &hex::decode("0100000000000000000000000000000000000000000000000000000000000000").unwrap(), + ) + .unwrap(); + let spend_policy = SpendPolicy::PublicKey(keypair.public()); + + let address = spend_policy.address(); + + mine_blocks(201, &address); + + let utxos = block_on(api_client.dispatcher(GetAddressUtxosRequest { + address: address.clone(), + limit: None, + offset: None, + })) + .unwrap(); + let spend_this = utxos[0].clone(); + let vin = spend_this.clone(); + println!("utxo[0]: {:?}", spend_this); + let vout = SiacoinOutput { + value: spend_this.siacoin_output.value, + address, + }; + let tx = V2TransactionBuilder::new() + .add_siacoin_input(vin, spend_policy) + .add_siacoin_output(vout) + .sign_simple(vec![&keypair]) + .build(); + + let req = TxpoolBroadcastRequest { + transactions: vec![], + v2transactions: vec![tx], + }; + let _response = block_on(api_client.dispatcher(req)).unwrap(); +} diff --git a/tests/docker/utils.rs b/tests/docker/utils.rs new file mode 100644 index 0000000..429e061 --- /dev/null +++ b/tests/docker/utils.rs @@ -0,0 +1,41 @@ +use sia_rust::types::Address; +use std::future::Future; +use std::process::Command; + +pub const SIA_DOCKER_IMAGE: &str = "docker.io/alrighttt/walletd-komodo"; +pub const SIA_DOCKER_IMAGE_WITH_TAG: &str = "docker.io/alrighttt/walletd-komodo:latest"; +pub const SIA_WALLATD_RPC_PORT: u16 = 9980; +pub const SIA_WALLETD_RPC_URL: &str = "http://localhost:9980/"; + +pub fn mine_blocks(n: u64, addr: &Address) { + Command::new("docker") + .arg("exec") + .arg("sia-docker") + .arg("walletd") + .arg("mine") + .arg(format!("-addr={}", addr)) + .arg(format!("-n={}", n)) + .status() + .expect("Failed to execute docker command"); +} + +pub fn block_on(fut: F) -> F::Output +where + F: Future, +{ + #[cfg(not(target = "wasm32"))] + { + lazy_static! { + pub static ref RUNTIME: tokio::runtime::Runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + } + RUNTIME.block_on(fut) + } + // Not actually needed since we don't run end-to-end tests for wasm. + // TODO: Generalize over the construction of platform-specific objects and use + // #[wasm_bindgen_test(unsupported = test)] macro to test both wasm and non-wasm targets + #[cfg(target = "wasm32")] + futures::executor::block_on(fut) +}