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::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 =
217            GatewayClientBuilder::new(path.clone(), registry, ModuleKind::from_static_str("dummy"));
218
219        let ln_client: Arc<dyn ILnRpcClient> = Arc::new(FakeLightningTest::new());
220
221        let (lightning_public_key, lightning_alias, lightning_network, _, _) = ln_client
222            .parsed_node_info()
223            .await
224            .expect("Could not get Lightning info");
225        let lightning_context = LightningContext {
226            lnrpc: ln_client.clone(),
227            lightning_public_key,
228            lightning_alias,
229            lightning_network,
230        };
231
232        // Module tests do not use the webserver, so any port is ok
233        let listen: SocketAddr = "127.0.0.1:9000".parse().unwrap();
234        let address: SafeUrl = format!("http://{listen}").parse().unwrap();
235
236        Gateway::new_with_custom_registry(
237            // Fixtures does not use real lightning connection, so just fake the connection
238            // parameters
239            LightningMode::Lnd {
240                lnd_rpc_addr: "FakeRpcAddr".to_string(),
241                lnd_tls_cert: "FakeTlsCert".to_string(),
242                lnd_macaroon: "FakeMacaroon".to_string(),
243            },
244            client_builder,
245            listen,
246            address.clone(),
247            bcrypt::HashParts::from_str(
248                &bcrypt::hash(DEFAULT_GATEWAY_PASSWORD, bcrypt::DEFAULT_COST).unwrap(),
249            )
250            .unwrap(),
251            bitcoin::Network::Regtest,
252            0,
253            gateway_db,
254            // Manually set the gateway's state to `Running`. In tests, we do don't run the
255            // webserver or intercept HTLCs, so this is necessary for instructing the
256            // gateway that it is connected to the mock Lightning node.
257            fedimint_gateway_server::GatewayState::Running { lightning_context },
258            lightning_module_mode,
259        )
260        .await
261        .expect("Failed to create gateway")
262    }
263
264    /// Get a server bitcoin RPC config
265    pub fn bitcoin_server(&self) -> BitcoinRpcConfig {
266        self.bitcoin_rpc.clone()
267    }
268
269    pub fn client_esplora_rpc(&self) -> DynBitcoindRpc {
270        if Fixtures::is_real_test() {
271            create_esplora_rpc(
272                &SafeUrl::parse(&format!(
273                    "http://127.0.0.1:{}/",
274                    env::var(FM_PORT_ESPLORA_ENV).unwrap_or(String::from("50002"))
275                ))
276                .expect("Failed to parse default esplora server"),
277            )
278            .unwrap()
279        } else {
280            self.fake_bitcoin_rpc.clone().unwrap()
281        }
282    }
283
284    /// Get a test bitcoin fixture
285    pub fn bitcoin(&self) -> Arc<dyn BitcoinTest> {
286        self.bitcoin.clone()
287    }
288
289    pub fn server_bitcoin_rpc(&self) -> DynServerBitcoinRpc {
290        self.server_bitcoin_rpc.clone()
291    }
292}