fedimint_testing/
fixtures.rs

1use std::env;
2use std::net::SocketAddr;
3use std::str::FromStr;
4use std::sync::Arc;
5use std::time::Duration;
6
7use fedimint_bitcoind::{DynBitcoindRpc, create_bitcoind};
8use fedimint_client::module_init::{
9    ClientModuleInitRegistry, DynClientModuleInit, IClientModuleInit,
10};
11use fedimint_core::config::{ModuleInitParams, ServerModuleConfigGenParamsRegistry};
12use fedimint_core::core::{ModuleInstanceId, ModuleKind};
13use fedimint_core::db::Database;
14use fedimint_core::db::mem_impl::MemDatabase;
15use fedimint_core::envs::BitcoinRpcConfig;
16use fedimint_core::module::registry::ModuleRegistry;
17use fedimint_core::task::{MaybeSend, MaybeSync};
18use fedimint_core::util::SafeUrl;
19use fedimint_gateway_common::LightningMode;
20use fedimint_gateway_server::Gateway;
21use fedimint_gateway_server::client::GatewayClientBuilder;
22use fedimint_gateway_server::config::LightningModuleMode;
23use fedimint_lightning::{ILnRpcClient, LightningContext};
24use fedimint_logging::TracingSetup;
25use fedimint_server::core::{DynServerModuleInit, IServerModuleInit, ServerModuleInitRegistry};
26use fedimint_testing_core::test_dir;
27
28use crate::btc::BitcoinTest;
29use crate::btc::mock::FakeBitcoinFactory;
30use crate::btc::real::RealBitcoinTest;
31use crate::envs::{
32    FM_PORT_ESPLORA_ENV, FM_TEST_BACKEND_BITCOIN_RPC_KIND_ENV, FM_TEST_BACKEND_BITCOIN_RPC_URL_ENV,
33    FM_TEST_BITCOIND_RPC_ENV, FM_TEST_USE_REAL_DAEMONS_ENV,
34};
35use crate::federation::{FederationTest, FederationTestBuilder};
36use crate::ln::FakeLightningTest;
37
38/// A default timeout for things happening in tests
39pub const TIMEOUT: Duration = Duration::from_secs(10);
40
41pub const DEFAULT_GATEWAY_PASSWORD: &str = "thereisnosecondbest";
42
43/// A tool for easily writing fedimint integration tests
44pub struct Fixtures {
45    clients: Vec<DynClientModuleInit>,
46    servers: Vec<DynServerModuleInit>,
47    params: ServerModuleConfigGenParamsRegistry,
48    bitcoin_rpc: BitcoinRpcConfig,
49    bitcoin: Arc<dyn BitcoinTest>,
50    dyn_bitcoin_rpc: DynBitcoindRpc,
51    primary_module_kind: ModuleKind,
52    id: ModuleInstanceId,
53}
54
55impl Fixtures {
56    pub fn new_primary(
57        client: impl IClientModuleInit + 'static,
58        server: impl IServerModuleInit + MaybeSend + MaybeSync + 'static,
59        params: impl ModuleInitParams,
60    ) -> Self {
61        // Ensure tracing has been set once
62        let _ = TracingSetup::default().init();
63        let real_testing = Fixtures::is_real_test();
64        let (dyn_bitcoin_rpc, bitcoin, config): (
65            DynBitcoindRpc,
66            Arc<dyn BitcoinTest>,
67            BitcoinRpcConfig,
68        ) = if real_testing {
69            // `backend-test.sh` overrides which Bitcoin RPC to use for electrs and esplora
70            // backend tests
71            let override_bitcoin_rpc_kind = env::var(FM_TEST_BACKEND_BITCOIN_RPC_KIND_ENV);
72            let override_bitcoin_rpc_url = env::var(FM_TEST_BACKEND_BITCOIN_RPC_URL_ENV);
73
74            let rpc_config = match (override_bitcoin_rpc_kind, override_bitcoin_rpc_url) {
75                (Ok(kind), Ok(url)) => BitcoinRpcConfig {
76                    kind: kind.parse().expect("must provide valid kind"),
77                    url: url.parse().expect("must provide valid url"),
78                },
79                _ => BitcoinRpcConfig::get_defaults_from_env_vars()
80                    .expect("must provide valid default env vars"),
81            };
82
83            let dyn_bitcoin_rpc = create_bitcoind(&rpc_config).unwrap();
84            let bitcoincore_url = env::var(FM_TEST_BITCOIND_RPC_ENV)
85                .expect("Must have bitcoind RPC defined for real tests")
86                .parse()
87                .expect("Invalid bitcoind RPC URL");
88            let bitcoin = RealBitcoinTest::new(&bitcoincore_url, dyn_bitcoin_rpc.clone());
89
90            (dyn_bitcoin_rpc, Arc::new(bitcoin), rpc_config)
91        } else {
92            let FakeBitcoinFactory { bitcoin, config } = FakeBitcoinFactory::register_new();
93            let dyn_bitcoin_rpc = DynBitcoindRpc::from(bitcoin.clone());
94            let bitcoin = Arc::new(bitcoin);
95            (dyn_bitcoin_rpc, bitcoin, config)
96        };
97
98        Self {
99            clients: vec![],
100            servers: vec![],
101            params: ModuleRegistry::default(),
102            bitcoin_rpc: config,
103            bitcoin,
104            dyn_bitcoin_rpc,
105            primary_module_kind: IClientModuleInit::module_kind(&client),
106            id: 0,
107        }
108        .with_module(client, server, params)
109    }
110
111    pub fn is_real_test() -> bool {
112        env::var(FM_TEST_USE_REAL_DAEMONS_ENV) == Ok("1".to_string())
113    }
114
115    // TODO: Auto-assign instance ids after removing legacy id order
116    /// Add a module to the fed
117    pub fn with_module(
118        mut self,
119        client: impl IClientModuleInit + 'static,
120        server: impl IServerModuleInit + MaybeSend + MaybeSync + 'static,
121        params: impl ModuleInitParams,
122    ) -> Self {
123        self.params
124            .attach_config_gen_params_by_id(self.id, server.module_kind(), params);
125        self.clients.push(DynClientModuleInit::from(client));
126        self.servers.push(DynServerModuleInit::from(server));
127        self.id += 1;
128
129        self
130    }
131
132    pub fn with_server_only_module(
133        mut self,
134        server: impl IServerModuleInit + MaybeSend + MaybeSync + 'static,
135        params: impl ModuleInitParams,
136    ) -> Self {
137        self.params
138            .attach_config_gen_params_by_id(self.id, server.module_kind(), params);
139        self.servers.push(DynServerModuleInit::from(server));
140        self.id += 1;
141
142        self
143    }
144
145    /// Starts a new federation with 3/4 peers online
146    pub async fn new_fed_degraded(&self) -> FederationTest {
147        self.new_fed_builder(1).build().await
148    }
149
150    /// Starts a new federation with 4/4 peers online
151    pub async fn new_fed_not_degraded(&self) -> FederationTest {
152        self.new_fed_builder(0).build().await
153    }
154
155    /// Creates a new `FederationTestBuilder` that can be used to build up a
156    /// `FederationTest` for module tests.
157    pub fn new_fed_builder(&self, num_offline: u16) -> FederationTestBuilder {
158        FederationTestBuilder::new(
159            self.params.clone(),
160            ServerModuleInitRegistry::from(self.servers.clone()),
161            ClientModuleInitRegistry::from(self.clients.clone()),
162            self.primary_module_kind.clone(),
163            num_offline,
164        )
165    }
166
167    /// Creates a new Gateway that can be used for module tests.
168    pub async fn new_gateway(&self, lightning_module_mode: LightningModuleMode) -> Gateway {
169        let server_gens = ServerModuleInitRegistry::from(self.servers.clone());
170        let module_kinds = self.params.iter_modules().map(|(id, kind, _)| (id, kind));
171        let decoders = server_gens.available_decoders(module_kinds).unwrap();
172        let gateway_db = Database::new(MemDatabase::new(), decoders.clone());
173        let clients = self.clients.clone().into_iter();
174
175        let registry = clients
176            .filter(|client| {
177                // Remove LN module because the gateway adds one
178                client.to_dyn_common().module_kind() != ModuleKind::from_static_str("ln")
179            })
180            .filter(|client| {
181                // Remove LN NG module because the gateway adds one
182                client.to_dyn_common().module_kind() != ModuleKind::from_static_str("lnv2")
183            })
184            .collect();
185
186        let (path, _config_dir) = test_dir(&format!("gateway-{}", rand::random::<u64>()));
187
188        // Create federation client builder for the gateway
189        let client_builder: GatewayClientBuilder =
190            GatewayClientBuilder::new(path.clone(), registry, ModuleKind::from_static_str("dummy"));
191
192        let ln_client: Arc<dyn ILnRpcClient> = Arc::new(FakeLightningTest::new());
193
194        let (lightning_public_key, lightning_alias, lightning_network, _, _) = ln_client
195            .parsed_node_info()
196            .await
197            .expect("Could not get Lightning info");
198        let lightning_context = LightningContext {
199            lnrpc: ln_client.clone(),
200            lightning_public_key,
201            lightning_alias,
202            lightning_network,
203        };
204
205        // Module tests do not use the webserver, so any port is ok
206        let listen: SocketAddr = "127.0.0.1:9000".parse().unwrap();
207        let address: SafeUrl = format!("http://{listen}").parse().unwrap();
208
209        Gateway::new_with_custom_registry(
210            // Fixtures does not use real lightning connection, so just fake the connection
211            // parameters
212            LightningMode::Lnd {
213                lnd_rpc_addr: "FakeRpcAddr".to_string(),
214                lnd_tls_cert: "FakeTlsCert".to_string(),
215                lnd_macaroon: "FakeMacaroon".to_string(),
216            },
217            client_builder,
218            listen,
219            address.clone(),
220            bcrypt::HashParts::from_str(
221                &bcrypt::hash(DEFAULT_GATEWAY_PASSWORD, bcrypt::DEFAULT_COST).unwrap(),
222            )
223            .unwrap(),
224            bitcoin::Network::Regtest,
225            0,
226            gateway_db,
227            // Manually set the gateway's state to `Running`. In tests, we do don't run the
228            // webserver or intercept HTLCs, so this is necessary for instructing the
229            // gateway that it is connected to the mock Lightning node.
230            fedimint_gateway_server::GatewayState::Running { lightning_context },
231            lightning_module_mode,
232        )
233        .await
234        .expect("Failed to create gateway")
235    }
236
237    /// Get a server bitcoin RPC config
238    pub fn bitcoin_server(&self) -> BitcoinRpcConfig {
239        self.bitcoin_rpc.clone()
240    }
241
242    /// Get a client bitcoin RPC config
243    // TODO: Right now we only support mocks or esplora, we should support others in
244    // the future
245    pub fn bitcoin_client(&self) -> BitcoinRpcConfig {
246        if Fixtures::is_real_test() {
247            BitcoinRpcConfig {
248                kind: "esplora".to_string(),
249                url: SafeUrl::parse(&format!(
250                    "http://127.0.0.1:{}/",
251                    env::var(FM_PORT_ESPLORA_ENV).unwrap_or(String::from("50002"))
252                ))
253                .expect("Failed to parse default esplora server"),
254            }
255        } else {
256            self.bitcoin_rpc.clone()
257        }
258    }
259
260    /// Get a test bitcoin fixture
261    pub fn bitcoin(&self) -> Arc<dyn BitcoinTest> {
262        self.bitcoin.clone()
263    }
264
265    pub fn dyn_bitcoin_rpc(&self) -> DynBitcoindRpc {
266        self.dyn_bitcoin_rpc.clone()
267    }
268}