/ fedimint-testing / src / fixtures.rs
fixtures.rs
  1  use std::path::PathBuf;
  2  use std::sync::Arc;
  3  use std::time::Duration;
  4  use std::{env, fs};
  5  
  6  use fedimint_bitcoind::{create_bitcoind, DynBitcoindRpc};
  7  use fedimint_client::module::init::{
  8      ClientModuleInitRegistry, DynClientModuleInit, IClientModuleInit,
  9  };
 10  use fedimint_core::config::{
 11      ModuleInitParams, ServerModuleConfigGenParamsRegistry, ServerModuleInitRegistry,
 12  };
 13  use fedimint_core::core::{ModuleInstanceId, ModuleKind};
 14  use fedimint_core::envs::BitcoinRpcConfig;
 15  use fedimint_core::module::{DynServerModuleInit, IServerModuleInit};
 16  use fedimint_core::runtime::block_in_place;
 17  use fedimint_core::task::{MaybeSend, MaybeSync, TaskGroup};
 18  use fedimint_core::util::SafeUrl;
 19  use fedimint_logging::TracingSetup;
 20  use tempfile::TempDir;
 21  
 22  use crate::btc::mock::FakeBitcoinFactory;
 23  use crate::btc::real::RealBitcoinTest;
 24  use crate::btc::BitcoinTest;
 25  use crate::envs::{
 26      FM_PORT_ESPLORA_ENV, FM_TEST_BITCOIND_RPC_ENV, FM_TEST_DIR_ENV, FM_TEST_USE_REAL_DAEMONS_ENV,
 27  };
 28  use crate::federation::{FederationTest, FederationTestBuilder};
 29  use crate::gateway::GatewayTest;
 30  use crate::ln::FakeLightningTest;
 31  
 32  /// A default timeout for things happening in tests
 33  pub const TIMEOUT: Duration = Duration::from_secs(10);
 34  
 35  /// A tool for easily writing fedimint integration tests
 36  pub struct Fixtures {
 37      clients: Vec<DynClientModuleInit>,
 38      servers: Vec<DynServerModuleInit>,
 39      params: ServerModuleConfigGenParamsRegistry,
 40      bitcoin_rpc: BitcoinRpcConfig,
 41      bitcoin: Arc<dyn BitcoinTest>,
 42      dyn_bitcoin_rpc: DynBitcoindRpc,
 43      id: ModuleInstanceId,
 44  }
 45  
 46  impl Fixtures {
 47      pub fn new_primary(
 48          client: impl IClientModuleInit + 'static,
 49          server: impl IServerModuleInit + MaybeSend + MaybeSync + 'static,
 50          params: impl ModuleInitParams,
 51      ) -> Self {
 52          // Ensure tracing has been set once
 53          let _ = TracingSetup::default().init();
 54          let real_testing = Fixtures::is_real_test();
 55          let task_group = TaskGroup::new();
 56          let (dyn_bitcoin_rpc, bitcoin, config): (
 57              DynBitcoindRpc,
 58              Arc<dyn BitcoinTest>,
 59              BitcoinRpcConfig,
 60          ) = if real_testing {
 61              let rpc_config = BitcoinRpcConfig::get_defaults_from_env_vars().unwrap();
 62              let dyn_bitcoin_rpc = create_bitcoind(&rpc_config, task_group.make_handle()).unwrap();
 63              let bitcoincore_url = env::var(FM_TEST_BITCOIND_RPC_ENV)
 64                  .expect("Must have bitcoind RPC defined for real tests")
 65                  .parse()
 66                  .expect("Invalid bitcoind RPC URL");
 67              let bitcoin = RealBitcoinTest::new(&bitcoincore_url, dyn_bitcoin_rpc.clone());
 68              (dyn_bitcoin_rpc, Arc::new(bitcoin), rpc_config)
 69          } else {
 70              let FakeBitcoinFactory { bitcoin, config } = FakeBitcoinFactory::register_new();
 71              let dyn_bitcoin_rpc = DynBitcoindRpc::from(bitcoin.clone());
 72              let bitcoin = Arc::new(bitcoin);
 73              (dyn_bitcoin_rpc, bitcoin, config)
 74          };
 75  
 76          Self {
 77              clients: vec![],
 78              servers: vec![],
 79              params: Default::default(),
 80              bitcoin_rpc: config,
 81              bitcoin,
 82              dyn_bitcoin_rpc,
 83              id: 0,
 84          }
 85          .with_module(client, server, params)
 86      }
 87  
 88      pub fn is_real_test() -> bool {
 89          env::var(FM_TEST_USE_REAL_DAEMONS_ENV) == Ok("1".to_string())
 90      }
 91  
 92      // TODO: Auto-assign instance ids after removing legacy id order
 93      /// Add a module to the fed
 94      pub fn with_module(
 95          mut self,
 96          client: impl IClientModuleInit + 'static,
 97          server: impl IServerModuleInit + MaybeSend + MaybeSync + 'static,
 98          params: impl ModuleInitParams,
 99      ) -> Self {
100          self.params
101              .attach_config_gen_params_by_id(self.id, server.module_kind(), params);
102          self.clients.push(DynClientModuleInit::from(client));
103          self.servers.push(DynServerModuleInit::from(server));
104          self.id += 1;
105  
106          self
107      }
108  
109      pub fn with_server_only_module(
110          mut self,
111          server: impl IServerModuleInit + MaybeSend + MaybeSync + 'static,
112          params: impl ModuleInitParams,
113      ) -> Self {
114          self.params
115              .attach_config_gen_params_by_id(self.id, server.module_kind(), params);
116          self.servers.push(DynServerModuleInit::from(server));
117          self.id += 1;
118  
119          self
120      }
121  
122      /// Starts a new federation with default number of peers for testing
123      pub async fn new_default_fed(&self) -> FederationTest {
124          let federation_builder = FederationTestBuilder::new(
125              self.params.clone(),
126              ServerModuleInitRegistry::from(self.servers.clone()),
127              ClientModuleInitRegistry::from(self.clients.clone()),
128          )
129          .await;
130          federation_builder.build().await
131      }
132  
133      pub async fn new_fed_builder(&self) -> FederationTestBuilder {
134          FederationTestBuilder::new(
135              self.params.clone(),
136              ServerModuleInitRegistry::from(self.servers.clone()),
137              ClientModuleInitRegistry::from(self.clients.clone()),
138          )
139          .await
140      }
141  
142      /// Starts a new gateway with a given lightning node
143      pub async fn new_gateway(
144          &self,
145          num_route_hints: u32,
146          cli_password: Option<String>,
147      ) -> GatewayTest {
148          // TODO: Make construction easier
149          let server_gens = ServerModuleInitRegistry::from(self.servers.clone());
150          let module_kinds = self.params.iter_modules().map(|(id, kind, _)| (id, kind));
151          let decoders = server_gens.available_decoders(module_kinds).unwrap();
152          let clients = self.clients.clone().into_iter();
153  
154          GatewayTest::new(
155              block_in_place(|| fedimint_portalloc::port_alloc(1))
156                  .expect("Failed to allocate a port range"),
157              cli_password,
158              FakeLightningTest::new(),
159              decoders,
160              ClientModuleInitRegistry::from_iter(
161                  clients
162                      .filter(|client| {
163                          // Remove LN module because the gateway adds one
164                          client.to_dyn_common().module_kind() != ModuleKind::from_static_str("ln")
165                      })
166                      .filter(|client| {
167                          // Remove LN NG module because the gateway adds one
168                          client.to_dyn_common().module_kind() != ModuleKind::from_static_str("lnv2")
169                      }),
170              ),
171              num_route_hints,
172          )
173          .await
174      }
175  
176      /// Get a server bitcoin RPC config
177      pub fn bitcoin_server(&self) -> BitcoinRpcConfig {
178          self.bitcoin_rpc.clone()
179      }
180  
181      /// Get a client bitcoin RPC config
182      // TODO: Right now we only support mocks or esplora, we should support others in
183      // the future
184      pub fn bitcoin_client(&self) -> BitcoinRpcConfig {
185          match Fixtures::is_real_test() {
186              true => BitcoinRpcConfig {
187                  kind: "esplora".to_string(),
188                  url: SafeUrl::parse(&format!(
189                      "http://127.0.0.1:{}/",
190                      env::var(FM_PORT_ESPLORA_ENV).unwrap_or(String::from("50002"))
191                  ))
192                  .expect("Failed to parse default esplora server"),
193              },
194              false => self.bitcoin_rpc.clone(),
195          }
196      }
197  
198      /// Get a test bitcoin fixture
199      pub fn bitcoin(&self) -> Arc<dyn BitcoinTest> {
200          self.bitcoin.clone()
201      }
202  
203      pub fn dyn_bitcoin_rpc(&self) -> DynBitcoindRpc {
204          self.dyn_bitcoin_rpc.clone()
205      }
206  }
207  
208  /// If `FM_TEST_DIR` is set, use it as a base, otherwise use a tempdir
209  ///
210  /// Callers must hold onto the tempdir until it is no longer needed
211  pub fn test_dir(pathname: &str) -> (PathBuf, Option<TempDir>) {
212      let (parent, maybe_tmp_dir_guard) = match env::var(FM_TEST_DIR_ENV) {
213          Ok(directory) => (directory, None),
214          Err(_) => {
215              let random = format!("test-{}", rand::random::<u64>());
216              let guard = tempfile::Builder::new().prefix(&random).tempdir().unwrap();
217              let directory = guard.path().to_str().unwrap().to_owned();
218              (directory, Some(guard))
219          }
220      };
221      let fullpath = PathBuf::from(parent).join(pathname);
222      fs::create_dir_all(fullpath.clone()).expect("Can make dirs");
223      (fullpath, maybe_tmp_dir_guard)
224  }