bitcoin_rpc_midas/
test_config.rs

1//! Test configuration for Bitcoin RPC testing
2//!
3//! This module provides configuration utilities for running Bitcoin nodes in test environments.
4
5use std::env;
6
7use bitcoin::Network;
8use crate::config::Config;
9
10/// TestConfig represents the configuration needed to run a Bitcoin node in a test environment.
11/// This struct is the single source of truth for test‑node settings: RPC port, username, and password.
12/// Defaults are:
13/// - `rpc_port = 0` (auto‑select a free port)
14/// - `rpc_username = "rpcuser"`
15/// - `rpc_password = "rpcpassword"`
16/// - `network = Network::Regtest` (for isolation and testability)
17///
18/// To override any of these, simply modify fields on `TestConfig::default()`
19/// (or assign directly in code). If you prefer not to recompile for every change,
20/// consider using `TestConfig::from_env()` to read overrides from environment variables.
21///
22/// # Examples
23///
24/// ```rust,ignore
25/// let mut cfg = TestConfig::default();
26/// cfg.rpc_port = 18545;
27/// cfg.rpc_username = "alice".into();
28/// cfg.network = Network::Testnet;
29/// ```
30///
31/// # Environment Overrides
32///
33/// Reads `RPC_PORT`, `RPC_USER`, `RPC_PASS`, and `RPC_NETWORK` environment variables to override defaults.
34#[derive(Debug, Clone)]
35pub struct TestConfig {
36    /// The port number for RPC communication with the Bitcoin node.
37    /// A value of 0 indicates that an available port should be automatically selected.
38    pub rpc_port: u16,
39    /// The username for RPC authentication.
40    /// Can be customized to match your `bitcoin.conf` `rpcuser` setting.
41    pub rpc_username: String,
42    /// The password for RPC authentication.
43    /// Can be customized to match your `bitcoin.conf` `rpcpassword` setting.
44    pub rpc_password: String,
45    /// Which Bitcoin network to run against.
46    pub network: Network,
47    /// Extra command-line arguments to pass to bitcoind
48    pub extra_args: Vec<String>,
49}
50
51impl TestConfig {
52    /// Return the value used with `-chain=<value>` for the configured network
53    pub fn as_chain_str(&self) -> &'static str {
54        #[allow(unreachable_patterns)]
55        match self.network {
56            Network::Bitcoin => "main",
57            Network::Regtest => "regtest",
58            Network::Signet => "signet",
59            Network::Testnet => "testnet",
60            Network::Testnet4 => "testnet4",
61            _ => panic!("Unsupported network variant"),
62        }
63    }
64
65    /// Parse network from common strings (case-insensitive). Accepts: regtest, testnet|test,
66    /// signet, mainnet|main|bitcoin, testnet4.
67    pub fn network_from_str(s: &str) -> Option<Network> {
68        match s.to_ascii_lowercase().as_str() {
69            "regtest" => Some(Network::Regtest),
70            "testnet" | "test" => Some(Network::Testnet),
71            "signet" => Some(Network::Signet),
72            "mainnet" | "main" | "bitcoin" => Some(Network::Bitcoin),
73            "testnet4" => Some(Network::Testnet4),
74            _ => None,
75        }
76    }
77
78    /// Create a `TestConfig`, overriding defaults with environment variables:
79    /// - `RPC_PORT`: overrides `rpc_port`
80    /// - `RPC_USER`: overrides `rpc_username`
81    /// - `RPC_PASS`: overrides `rpc_password`
82    /// - `RPC_NETWORK`: one of `regtest`, `testnet|test`, `signet`, `mainnet|main|bitcoin`, `testnet4`
83    #[allow(clippy::field_reassign_with_default)]
84    pub fn from_env() -> Self {
85        let mut cfg = Self::default();
86        if let Ok(port_str) = env::var("RPC_PORT") {
87            if let Ok(port) = port_str.parse() {
88                cfg.rpc_port = port;
89            }
90        }
91        if let Ok(user) = env::var("RPC_USER") {
92            cfg.rpc_username = user;
93        }
94        if let Ok(pass) = env::var("RPC_PASS") {
95            cfg.rpc_password = pass;
96        }
97        if let Ok(net) = env::var("RPC_NETWORK") {
98            if let Some(n) = Self::network_from_str(&net) {
99                cfg.network = n;
100            }
101        }
102        cfg
103    }
104
105    /// Convert this test configuration into a full Config instance
106    pub fn into_config(self) -> Config {
107        Config {
108            rpc_url: format!("http://127.0.0.1:{}", self.rpc_port),
109            rpc_user: self.rpc_username,
110            rpc_password: self.rpc_password,
111        }
112    }
113
114    /// Create a TestConfig from a full Config instance
115    pub fn from_config(config: &Config) -> Self {
116        // Extract port from URL, defaulting to 0 if parsing fails
117        let rpc_port = config.rpc_url
118            .split(':')
119            .next_back()
120            .and_then(|s| s.parse().ok())
121            .unwrap_or(0);
122            
123        Self {
124            rpc_port,
125            rpc_username: config.rpc_user.clone(),
126            rpc_password: config.rpc_password.clone(),
127            network: Network::Regtest, // Default to regtest for test environments
128            extra_args: vec![],
129        }
130    }
131}
132
133impl Default for TestConfig {
134    fn default() -> Self {
135        Self {
136            rpc_port: 0,
137            rpc_username: "rpcuser".to_string(),
138            rpc_password: "rpcpassword".to_string(),
139            network: Network::Regtest,
140            extra_args: vec![],
141        }
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn test_network_from_str() {
151        assert_eq!(TestConfig::network_from_str("regtest"), Some(Network::Regtest));
152        assert_eq!(TestConfig::network_from_str("testnet"), Some(Network::Testnet));
153        assert_eq!(TestConfig::network_from_str("test"), Some(Network::Testnet));
154        assert_eq!(TestConfig::network_from_str("signet"), Some(Network::Signet));
155        assert_eq!(TestConfig::network_from_str("mainnet"), Some(Network::Bitcoin));
156        assert_eq!(TestConfig::network_from_str("main"), Some(Network::Bitcoin));
157        assert_eq!(TestConfig::network_from_str("bitcoin"), Some(Network::Bitcoin));
158        assert_eq!(TestConfig::network_from_str("testnet4"), Some(Network::Testnet4));
159        assert_eq!(TestConfig::network_from_str("invalid"), None);
160    }
161
162    #[test]
163    fn test_as_chain_str() {
164        let config = TestConfig {
165            network: Network::Regtest,
166            ..TestConfig::default()
167        };
168        assert_eq!(config.as_chain_str(), "regtest");
169    }
170}