//! `lightwalletd`-specific shared code for the `zebrad` acceptance tests. //! //! # Warning //! //! Test functions in this file will not be run. //! This file is only for test library code. use std::{ env, net::SocketAddr, path::{Path, PathBuf}, }; use tempfile::TempDir; use zebra_chain::parameters::Network::{self, *}; use zebra_test::{ args, command::{Arguments, TestDirExt}, net::random_known_port, prelude::*, }; use super::{config::testdir, launch::ZebradTestDirExt, test_type::TestType}; #[cfg(feature = "lightwalletd-grpc-tests")] pub mod send_transaction_test; #[cfg(feature = "lightwalletd-grpc-tests")] pub mod sync; #[cfg(feature = "lightwalletd-grpc-tests")] pub mod wallet_grpc; #[cfg(feature = "lightwalletd-grpc-tests")] pub mod wallet_grpc_test; /// The name of the env var that enables Zebra lightwalletd integration tests. /// These tests need a `lightwalletd` binary in the test machine's path. /// /// We use a constant so that the compiler detects typos. /// /// # Note /// /// This environmental variable is used to enable the lightwalletd tests. /// But the network tests are *disabled* by their environmental variables. pub const ZEBRA_TEST_LIGHTWALLETD: &str = "ZEBRA_TEST_LIGHTWALLETD"; /// Optional environment variable with the cached state for lightwalletd. /// /// Required for [`TestType::UpdateCachedState`], /// so we can test lightwalletd RPC integration with a populated state. /// /// Can also be used to speed up the [`sending_transactions_using_lightwalletd`] test, /// by skipping the lightwalletd initial sync. pub const LIGHTWALLETD_DATA_DIR: &str = "LIGHTWALLETD_DATA_DIR"; /// Should we skip Zebra lightwalletd integration tests? #[allow(clippy::print_stderr)] pub fn zebra_skip_lightwalletd_tests() -> bool { // TODO: check if the lightwalletd binary is in the PATH? // (this doesn't seem to be implemented in the standard library) // // See is_command_available() in zebra-test/src/tests/command.rs for one way to do this. if env::var_os(ZEBRA_TEST_LIGHTWALLETD).is_none() { // This message is captured by the test runner, use // `cargo test -- --nocapture` to see it. eprintln!( "Skipped lightwalletd integration test, \ set the 'ZEBRA_TEST_LIGHTWALLETD' environmental variable to run the test", ); return true; } false } /// Spawns a lightwalletd instance on `network`, connected to `zebrad_rpc_address`, /// with its gRPC server functionality enabled. /// /// Expects cached state based on the `test_type`. Use the `LIGHTWALLETD_DATA_DIR` /// environmental variable to provide an initial state to the lightwalletd instance. /// /// Returns: /// - `Ok(Some(lightwalletd, lightwalletd_rpc_port))` on success, /// - `Ok(None)` if the test doesn't have the required network or cached state, and /// - `Err(_)` if spawning lightwalletd fails. #[tracing::instrument] pub fn spawn_lightwalletd_for_rpc + std::fmt::Debug>( network: Network, test_name: S, test_type: TestType, zebrad_rpc_address: SocketAddr, ) -> Result, u16)>> { assert_eq!(network, Mainnet, "this test only supports Mainnet for now"); let test_name = test_name.as_ref(); // Skip the test unless the user specifically asked for it if !can_spawn_lightwalletd_for_rpc(test_name, test_type) { return Ok(None); } let lightwalletd_state_path = test_type.lightwalletd_state_path(test_name); let lightwalletd_dir = testdir()?.with_lightwalletd_config(zebrad_rpc_address)?; let lightwalletd_rpc_port = random_known_port(); let lightwalletd_rpc_address = format!("127.0.0.1:{lightwalletd_rpc_port}"); let arguments = args!["--grpc-bind-addr": lightwalletd_rpc_address]; let (lightwalletd_failure_messages, lightwalletd_ignore_messages) = test_type.lightwalletd_failure_messages(); let mut lightwalletd = lightwalletd_dir .spawn_lightwalletd_child(lightwalletd_state_path, arguments)? .with_timeout(test_type.lightwalletd_timeout()) .with_failure_regex_iter(lightwalletd_failure_messages, lightwalletd_ignore_messages); // Wait until `lightwalletd` has launched. // This log happens very quickly, so it is ok to block for a short while here. lightwalletd.expect_stdout_line_matches(regex::escape("Starting gRPC server"))?; Ok(Some((lightwalletd, lightwalletd_rpc_port))) } /// Returns `true` if a lightwalletd test for `test_type` has everything it needs to run. #[tracing::instrument] pub fn can_spawn_lightwalletd_for_rpc + std::fmt::Debug>( test_name: S, test_type: TestType, ) -> bool { if zebra_test::net::zebra_skip_network_tests() { return false; } // Skip the test unless the user specifically asked for it // // TODO: pass test_type to zebra_skip_lightwalletd_tests() and check for lightwalletd launch in there if test_type.launches_lightwalletd() && zebra_skip_lightwalletd_tests() { return false; } let lightwalletd_state_path = test_type.lightwalletd_state_path(test_name); if test_type.needs_lightwalletd_cached_state() && lightwalletd_state_path.is_none() { return false; } true } /// Extension trait for methods on `tempfile::TempDir` for using it as a test /// directory for `lightwalletd`. pub trait LightWalletdTestDirExt: ZebradTestDirExt where Self: AsRef + Sized, { /// Spawn `lightwalletd` with `lightwalletd_state_path`, and `extra_args`, /// as a child process in this test directory, /// potentially taking ownership of the tempdir for the duration of the child process. /// /// By default, launch a working test instance with logging, and avoid port conflicts. /// /// # Panics /// /// If there is no lightwalletd config in the test directory. fn spawn_lightwalletd_child( self, lightwalletd_state_path: impl Into>, extra_args: Arguments, ) -> Result>; /// Create a config file and use it for all subsequently spawned `lightwalletd` processes. /// Returns an error if the config already exists. /// /// If needed: /// - recursively create directories for the config fn with_lightwalletd_config(self, zebra_rpc_listener: SocketAddr) -> Result; } impl LightWalletdTestDirExt for T where Self: TestDirExt + AsRef + Sized, { #[allow(clippy::unwrap_in_result)] fn spawn_lightwalletd_child( self, lightwalletd_state_path: impl Into>, extra_args: Arguments, ) -> Result> { let test_dir = self.as_ref().to_owned(); let default_config_path = test_dir.join("lightwalletd-zcash.conf"); assert!( default_config_path.exists(), "lightwalletd requires a config" ); // By default, launch a working test instance with logging, // and avoid port conflicts. let mut args = Arguments::new(); // the fake zcashd conf we just wrote let zcash_conf_path = default_config_path .as_path() .to_str() .expect("Path is valid Unicode"); args.set_parameter("--zcash-conf-path", zcash_conf_path); // the lightwalletd cache directory if let Some(lightwalletd_state_path) = lightwalletd_state_path.into() { args.set_parameter( "--data-dir", lightwalletd_state_path .to_str() .expect("path is valid Unicode"), ); } else { let empty_state_path = test_dir.join("lightwalletd_state"); std::fs::create_dir(&empty_state_path) .expect("unexpected failure creating lightwalletd state sub-directory"); args.set_parameter( "--data-dir", empty_state_path.to_str().expect("path is valid Unicode"), ); } // log to standard output // // TODO: if lightwalletd needs to run on Windows, // work out how to log to the terminal on all platforms args.set_parameter("--log-file", "/dev/stdout"); // let the OS choose a random available wallet client port args.set_parameter("--grpc-bind-addr", "127.0.0.1:0"); args.set_parameter("--http-bind-addr", "127.0.0.1:0"); // don't require a TLS certificate for the HTTP server args.set_argument("--no-tls-very-insecure"); // apply user provided arguments args.merge_with(extra_args); self.spawn_child_with_command("lightwalletd", args) } fn with_lightwalletd_config(self, zebra_rpc_listener: SocketAddr) -> Result { use std::fs; // zcash/lightwalletd requires rpcuser and rpcpassword, or a zcashd cookie file // But when a lightwalletd with this config is used by Zebra, // Zebra ignores any authentication and provides access regardless. let lightwalletd_config = format!( "\ rpcbind={}\n\ rpcport={}\n\ rpcuser=xxxxx rpcpassword=xxxxx ", zebra_rpc_listener.ip(), zebra_rpc_listener.port(), ); let dir = self.as_ref(); fs::create_dir_all(dir)?; let config_file = dir.join("lightwalletd-zcash.conf"); fs::write(config_file, lightwalletd_config.as_bytes())?; Ok(self) } }