switchgear_testing/
credentials.rs

1use anyhow::Context;
2use secp256k1::PublicKey;
3use std::env;
4use std::fs;
5use std::net::SocketAddr;
6use std::path::{Path, PathBuf};
7use std::str::FromStr;
8
9const CREDENTIALS_PATH_ENV: &str = "LNURL_BALANCER_CREDENTIALS_PATH";
10const SKIP_INTEGRATION_TESTS_ENV: &str = "LNURL_SKIP_INTEGRATION_TESTS";
11
12#[derive(Debug, Clone, PartialEq, Eq, Hash)]
13pub struct ClnRegTestLnNode {
14    pub public_key: PublicKey,
15    pub address: RegTestLnNodeAddress,
16    pub ca_cert_path: PathBuf,
17    pub client_cert_path: PathBuf,
18    pub client_key_path: PathBuf,
19    pub sni: String,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq, Hash)]
23pub struct LndRegTestLnNode {
24    pub public_key: PublicKey,
25    pub address: RegTestLnNodeAddress,
26    pub tls_cert_path: PathBuf,
27    pub macaroon_path: PathBuf,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Hash)]
31pub enum RegTestLnNodeAddress {
32    Inet(SocketAddr),
33    Path(Vec<u8>),
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Hash)]
37pub enum RegTestLnNode {
38    Cln(ClnRegTestLnNode),
39    Lnd(LndRegTestLnNode),
40}
41
42impl RegTestLnNode {
43    pub fn public_key(&self) -> &PublicKey {
44        match self {
45            RegTestLnNode::Cln(cln) => &cln.public_key,
46            RegTestLnNode::Lnd(lnd) => &lnd.public_key,
47        }
48    }
49
50    pub fn address(&self) -> &RegTestLnNodeAddress {
51        match self {
52            RegTestLnNode::Cln(cln) => &cln.address,
53            RegTestLnNode::Lnd(lnd) => &lnd.address,
54        }
55    }
56
57    pub fn kind(&self) -> &'static str {
58        match self {
59            RegTestLnNode::Cln(_) => "cln",
60            RegTestLnNode::Lnd(_) => "lnd",
61        }
62    }
63}
64
65pub fn get_backends() -> anyhow::Result<Vec<RegTestLnNode>> {
66    if env::var(SKIP_INTEGRATION_TESTS_ENV).is_ok_and(|s| s.to_lowercase() == "true") {
67        eprintln!("⚠️ WARNING: {SKIP_INTEGRATION_TESTS_ENV} is true, skipping tests");
68        return Ok(Vec::new());
69    }
70
71    match get_backends_path() {
72        None => {
73            panic!(
74                "
75            
76❌❌❌ ERROR ❌❌❌
77
78{CREDENTIALS_PATH_ENV} is not set. Do one of:
79
801. configure test environment (see testing/README.md) and set {CREDENTIALS_PATH_ENV}
812. set env {SKIP_INTEGRATION_TESTS_ENV}=true to skip integration tests
82
83❌❌❌ ERROR ❌❌❌
84
85            "
86            );
87        }
88        Some(base_path) => get_backends_from_path(base_path),
89    }
90}
91
92#[derive(Copy, Clone, Debug)]
93pub enum RegTestLnNodeType {
94    Cln,
95    Lnd,
96}
97
98fn get_backends_from_path(base_path: PathBuf) -> anyhow::Result<Vec<RegTestLnNode>> {
99    let entries = fs::read_dir(&base_path)
100        .with_context(|| format!("reading directory {}", base_path.display()))?;
101
102    let mut backends = Vec::new();
103
104    for entry in entries {
105        let entry = entry
106            .with_context(|| format!("reading directory entry in {}", base_path.display(),))?;
107
108        let path = entry.path();
109
110        if !path.is_dir() {
111            continue;
112        }
113
114        let dir_name = match path.file_name() {
115            Some(name) => match name.to_str() {
116                Some(s) => s,
117                None => continue, // Skip non-UTF8 names
118            },
119            None => continue,
120        };
121
122        let node_type = if dir_name.starts_with("cln") {
123            RegTestLnNodeType::Cln
124        } else if dir_name.starts_with("lnd") {
125            RegTestLnNodeType::Lnd
126        } else {
127            continue;
128        };
129
130        // Read node ID
131        let node_id_path = path.join("node_id");
132        let node_id_str = fs::read_to_string(&node_id_path)
133            .with_context(|| format!("reading node ID from {}", node_id_path.display(),))?;
134
135        let node_id_hex = node_id_str.trim();
136        let node_id_bytes = hex::decode(node_id_hex)
137            .with_context(|| format!("decoding {} node ID to hex", node_id_path.display(),))?;
138
139        let public_key = PublicKey::from_slice(&node_id_bytes).with_context(|| {
140            format!("parsing {} public key from bytes", node_id_path.display(),)
141        })?;
142
143        // Read address from address.txt
144        let address = read_node_address(&path)?;
145
146        let node = match node_type {
147            RegTestLnNodeType::Cln => {
148                RegTestLnNode::Cln(build_cln_node(public_key, address, &path)?)
149            }
150            RegTestLnNodeType::Lnd => {
151                RegTestLnNode::Lnd(build_lnd_node(public_key, address, &path)?)
152            }
153        };
154
155        backends.push(node);
156    }
157
158    Ok(backends)
159}
160
161fn get_backends_path() -> Option<PathBuf> {
162    let _ = dotenvy::dotenv();
163    env::var(CREDENTIALS_PATH_ENV).ok().map(PathBuf::from)
164}
165
166fn read_node_address(node_path: &Path) -> anyhow::Result<RegTestLnNodeAddress> {
167    let address_file = node_path.join("address.txt");
168
169    let address_content = fs::read_to_string(&address_file)
170        .with_context(|| format!("reading {} address", address_file.display(),))?;
171
172    let address_str = address_content.trim();
173    let socket_addr = SocketAddr::from_str(address_str).with_context(|| {
174        format!(
175            "parsing {} address '{}'",
176            address_file.display(),
177            address_str
178        )
179    })?;
180
181    Ok(RegTestLnNodeAddress::Inet(socket_addr))
182}
183
184fn build_cln_node(
185    public_key: PublicKey,
186    address: RegTestLnNodeAddress,
187    node_path: &Path,
188) -> anyhow::Result<ClnRegTestLnNode> {
189    let ca_cert_path = node_path.join("ca.pem");
190    let ca_cert_path = ca_cert_path
191        .canonicalize()
192        .with_context(|| format!("canonicalizing CLN CA cert path {}", ca_cert_path.display(),))?;
193    let client_cert_path = node_path.join("client.pem");
194    let client_cert_path = client_cert_path.canonicalize().with_context(|| {
195        format!(
196            "canonicalizing CLN client cert path {}",
197            client_cert_path.display(),
198        )
199    })?;
200    let client_key_path = node_path.join("client-key.pem");
201    let client_key_path = client_key_path.canonicalize().with_context(|| {
202        format!(
203            "canonicalizing CLN client key path {}",
204            client_key_path.display(),
205        )
206    })?;
207
208    Ok(ClnRegTestLnNode {
209        public_key,
210        address,
211        ca_cert_path,
212        client_cert_path,
213        client_key_path,
214        sni: "localhost".to_string(),
215    })
216}
217
218fn build_lnd_node(
219    public_key: PublicKey,
220    address: RegTestLnNodeAddress,
221    node_path: &Path,
222) -> anyhow::Result<LndRegTestLnNode> {
223    let tls_cert_path = node_path.join("tls.cert");
224    let tls_cert_path = tls_cert_path.canonicalize().with_context(|| {
225        format!(
226            "canonicalizing LND TLS cert path {}",
227            tls_cert_path.display(),
228        )
229    })?;
230    let macaroon_path = node_path.join("admin.macaroon");
231    let macaroon_path = macaroon_path.canonicalize().with_context(|| {
232        format!(
233            "canonicalizing LND macaroon path {}",
234            macaroon_path.display(),
235        )
236    })?;
237
238    Ok(LndRegTestLnNode {
239        public_key,
240        address,
241        tls_cert_path,
242        macaroon_path,
243    })
244}