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