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