AirLibrary/Mountain/
mod.rs

1//! # Mountain Client Module
2//!
3//! This module provides the gRPC client implementation for Air to communicate
4//! with Mountain. Air acts as a client connecting to Mountain's gRPC server
5//! for requesting status, health checks, and configuration operations.
6//!
7//! ## Architecture
8//!
9//! ```text
10//! Air (Background Daemon) ──► MountainClient ──► gRPC ──► Mountain (Main App)
11//! ```
12//!
13//! ## Features
14//!
15//! - **Connection Management**: Establish and maintain gRPC connections to
16//!   Mountain
17//! - **Health Monitoring**: Check Mountain's health status
18//! - **Status Queries**: Query Mountain's operational status
19//! - **Configuration**: Get and update Mountain configuration
20//!
21//! ## Configuration
22//!
23//! - **Default Address**: `[::1]:50051` (Mountain's default Vine server port)
24//! - **Transport**: gRPC over TCP/IP with optional TLS
25//! - **Timeouts**: Configurable connection and request timeouts
26//!
27//! ## TLS/mTLS Support
28//!
29//! The `mtls` feature enables TLS client support with:
30//! - Client certificate authentication
31//! - Secure encrypted communications
32//! - Certificate validation against CA
33//!
34//! Note: TLS/mTLS implementation is a stub for future enhancement. The current
35//! implementation focuses on establishing unencrypted connections for
36//! development and testing purposes.
37
38use std::{env, fs::File, io::BufReader, path::PathBuf, time::Duration};
39
40use log::{debug, error, info, warn};
41use tonic::transport::{Channel, Endpoint};
42#[cfg(feature = "mtls")]
43#[cfg(feature = "mtls")]
44use rustls::ClientConfig;
45#[cfg(feature = "mtls")]
46use rustls::RootCertStore;
47
48/// Default Vine server address for Mountain component.
49///
50/// Port Allocation:
51/// - 50051: Mountain Vine server (this target)
52/// - 50052: Cocoon Vine server
53/// - 50053: Air Vine server
54pub const DEFAULT_MOUNTAIN_ADDRESS:&str = "[::1]:50051";
55
56/// Default connection timeout in seconds
57pub const DEFAULT_CONNECTION_TIMEOUT_SECS:u64 = 5;
58
59/// Default request timeout in seconds
60pub const DEFAULT_REQUEST_TIMEOUT_SECS:u64 = 30;
61
62/// TLS configuration for gRPC connections to Mountain.
63///
64/// This struct holds the paths to certificates and keys required for
65/// TLS/mTLS authentication when connecting to Mountain.
66#[cfg(feature = "mtls")]
67#[derive(Debug, Clone)]
68pub struct TlsConfig {
69	/// Path to the CA certificate file (optional, uses system defaults if not
70	/// provided)
71	pub ca_cert_path:Option<PathBuf>,
72
73	/// Path to the client certificate file (for mTLS)
74	pub client_cert_path:Option<PathBuf>,
75
76	/// Path to the client private key file (for mTLS)
77	pub client_key_path:Option<PathBuf>,
78
79	/// Server name for SNI (Server Name Indication)
80	pub server_name:Option<String>,
81
82	/// Whether to verify certificates (default: true)
83	pub verify_certs:bool,
84}
85
86#[cfg(feature = "mtls")]
87impl Default for TlsConfig {
88	fn default() -> Self {
89		Self {
90			ca_cert_path:None,
91			client_cert_path:None,
92			client_key_path:None,
93			server_name:None,
94			verify_certs:true,
95		}
96	}
97}
98
99#[cfg(feature = "mtls")]
100impl TlsConfig {
101	/// Creates a new TLS configuration for server authentication only.
102	///
103	/// # Parameters
104	/// - `ca_cert_path`: Path to the CA certificate file
105	///
106	/// # Returns
107	/// New TlsConfig instance
108	pub fn server_auth(ca_cert_path:PathBuf) -> Self {
109		Self {
110			ca_cert_path:Some(ca_cert_path),
111			client_cert_path:None,
112			client_key_path:None,
113			server_name:Some("localhost".to_string()),
114			verify_certs:true,
115		}
116	}
117
118	/// Creates a new TLS configuration for mutual authentication (mTLS).
119	///
120	/// # Parameters
121	/// - `ca_cert_path`: Path to the CA certificate file
122	/// - `client_cert_path`: Path to the client certificate file
123	/// - `client_key_path`: Path to the client private key file
124	///
125	/// # Returns
126	/// New TlsConfig instance with mTLS enabled
127	pub fn mtls(ca_cert_path:PathBuf, client_cert_path:PathBuf, client_key_path:PathBuf) -> Self {
128		Self {
129			ca_cert_path:Some(ca_cert_path),
130			client_cert_path:Some(client_cert_path),
131			client_key_path:Some(client_key_path),
132			server_name:Some("localhost".to_string()),
133			verify_certs:true,
134		}
135	}
136}
137
138/// Creates a TLS client configuration from a TlsConfig.
139///
140/// This function loads certificates and keys from the file system and
141/// constructs a rustls ClientConfig suitable for gRPC connections.
142///
143/// # Parameters
144/// - `tls_config`: The TLS configuration containing certificate paths
145///
146/// # Returns
147/// Result containing the ClientConfig or an error if certificate loading fails
148#[cfg(feature = "mtls")]
149pub fn create_tls_client_config(tls_config:&TlsConfig) -> Result<ClientConfig, Box<dyn std::error::Error>> {
150	info!("Creating TLS client configuration");
151
152	// Build the root certificate store
153	let mut root_store = RootCertStore::empty();
154
155	if let Some(ca_path) = &tls_config.ca_cert_path {
156		// Load CA certificate from file
157		debug!("Loading CA certificate from {:?}", ca_path);
158		let ca_file = File::open(ca_path).map_err(|e| format!("Failed to open CA certificate file: {}", e))?;
159		let mut reader = BufReader::new(ca_file);
160
161		let certs:Result<Vec<_>, _> = rustls_pemfile::certs(&mut reader).collect();
162		let certs = certs.map_err(|e| format!("Failed to parse CA certificate: {}", e))?;
163
164		if certs.is_empty() {
165			return Err("No CA certificates found in file".into());
166		}
167
168		for cert in certs {
169			root_store
170				.add(cert)
171				.map_err(|e| format!("Failed to add CA certificate to root store: {}", e))?;
172		}
173
174		info!("Loaded CA certificate from {:?}", ca_path);
175	} else {
176		// Use system root certificates
177		debug!("Loading system root certificates");
178		let native_certs = rustls_native_certs::load_native_certs()
179			.map_err(|e| format!("Failed to load system root certificates: {}", e))?;
180
181		if native_certs.is_empty() {
182			warn!("No system root certificates found");
183		}
184
185		for cert in native_certs {
186			root_store
187				.add(cert)
188				.map_err(|e| format!("Failed to add system certificate to root store: {}", e))?;
189		}
190
191		info!("Loaded {} system root certificates", root_store.len());
192	}
193
194	// Load client certificate and key for mTLS (if provided)
195	let client_certs = if tls_config.client_cert_path.is_some() && tls_config.client_key_path.is_some() {
196		let cert_path = tls_config.client_cert_path.as_ref().unwrap();
197		let key_path = tls_config.client_key_path.as_ref().unwrap();
198
199		debug!("Loading client certificate from {:?}", cert_path);
200		let cert_file = File::open(cert_path).map_err(|e| format!("Failed to open client certificate file: {}", e))?;
201		let mut cert_reader = BufReader::new(cert_file);
202
203		let certs:Result<Vec<_>, _> = rustls_pemfile::certs(&mut cert_reader).collect();
204		let certs = certs.map_err(|e| format!("Failed to parse client certificate: {}", e))?;
205
206		if certs.is_empty() {
207			return Err("No client certificates found in file".into());
208		}
209
210		debug!("Loading client private key from {:?}", key_path);
211		let key_file = File::open(key_path).map_err(|e| format!("Failed to open private key file: {}", e))?;
212		let mut key_reader = BufReader::new(key_file);
213
214		let key = rustls_pemfile::private_key(&mut key_reader)
215			.map_err(|e| format!("Failed to parse private key: {}", e))?
216			.ok_or("No private key found in file")?;
217
218		Some((certs, key))
219	} else {
220		None
221	};
222
223	// Build the client config
224	let mut config = match client_certs {
225		Some((certs, key)) => {
226			// mTLS configuration with client authentication
227			let client_config = ClientConfig::builder()
228				.with_root_certificates(root_store)
229				.with_client_auth_cert(certs, key)
230				.map_err(|e| format!("Failed to configure client authentication: {}", e))?;
231
232			info!("Configured mTLS with client certificate");
233
234			client_config
235		},
236		None => {
237			// TLS configuration with server authentication only
238			// rustls 0.23: The builder will auto-complete when no client auth needed
239			let client_config = ClientConfig::builder().with_root_certificates(root_store).with_no_client_auth();
240
241			info!("Configured TLS with server authentication only");
242
243			client_config
244		},
245	};
246
247	// Set ALPN protocols for HTTP/2 (required for gRPC)
248	config.alpn_protocols = vec![b"h2".to_vec()];
249
250	// Note: Certificate verification can only be disabled during the config build
251	// phase The current rustls API doesn't support disabling verification after
252	// building If verification needs to be disabled, use NoServerAuthVerifier
253	// during build
254	if !tls_config.verify_certs {
255		warn!("Certificate verification disabled - this is NOT secure for production!");
256		// For development/testing, consider using a custom verifier
257		// For now, this is a placeholder - verification is always enabled
258	}
259
260	info!("TLS client configuration created successfully");
261
262	Ok(config)
263}
264
265/// Configuration for connecting to Mountain.
266#[derive(Debug, Clone)]
267pub struct MountainClientConfig {
268	/// The gRPC server address of Mountain (e.g., `"[::1]:50051"`)
269	pub address:String,
270
271	/// Connection timeout in seconds
272	pub connection_timeout_secs:u64,
273
274	/// Request timeout in seconds
275	pub request_timeout_secs:u64,
276
277	/// TLS configuration (if mtls feature is enabled)
278	#[cfg(feature = "mtls")]
279	pub tls_config:Option<TlsConfig>,
280}
281
282impl Default for MountainClientConfig {
283	fn default() -> Self {
284		Self {
285			address:DEFAULT_MOUNTAIN_ADDRESS.to_string(),
286			connection_timeout_secs:DEFAULT_CONNECTION_TIMEOUT_SECS,
287			request_timeout_secs:DEFAULT_REQUEST_TIMEOUT_SECS,
288			#[cfg(feature = "mtls")]
289			tls_config:None,
290		}
291	}
292}
293
294impl MountainClientConfig {
295	/// Creates a new MountainClientConfig with the specified address.
296	///
297	/// # Parameters
298	/// - `address`: The gRPC server address
299	///
300	/// # Returns
301	/// New MountainClientConfig instance
302	pub fn new(address:impl Into<String>) -> Self { Self { address:address.into(), ..Default::default() } }
303
304	/// Creates a MountainClientConfig from environment variables.
305	///
306	/// This method reads configuration from the following environment
307	/// variables:
308	/// - `MOUNTAIN_ADDRESS`: gRPC server address (default: `"[::1]:50051"`)
309	/// - `MOUNTAIN_CONNECTION_TIMEOUT_SECS`: Connection timeout in seconds
310	///   (default: 5)
311	/// - `MOUNTAIN_REQUEST_TIMEOUT_SECS`: Request timeout in seconds (default:
312	///   30)
313	/// - `MOUNTAIN_TLS_ENABLED`: Enable TLS if set to "1" or "true"
314	/// - `MOUNTAIN_CA_CERT`: Path to CA certificate file
315	/// - `MOUNTAIN_CLIENT_CERT`: Path to client certificate file
316	/// - `MOUNTAIN_CLIENT_KEY`: Path to client private key file
317	/// - `MOUNTAIN_SERVER_NAME`: Server name for SNI
318	/// - `MOUNTAIN_VERIFY_CERTS`: Verify certificates (default: true, set to
319	///   "0" or "false" to disable)
320	///
321	/// # Returns
322	/// New MountainClientConfig instance loaded from environment
323	pub fn from_env() -> Self {
324		let address = env::var("MOUNTAIN_ADDRESS").unwrap_or_else(|_| DEFAULT_MOUNTAIN_ADDRESS.to_string());
325
326		let connection_timeout_secs = env::var("MOUNTAIN_CONNECTION_TIMEOUT_SECS")
327			.ok()
328			.and_then(|s| s.parse().ok())
329			.unwrap_or(DEFAULT_CONNECTION_TIMEOUT_SECS);
330
331		let request_timeout_secs = env::var("MOUNTAIN_REQUEST_TIMEOUT_SECS")
332			.ok()
333			.and_then(|s| s.parse().ok())
334			.unwrap_or(DEFAULT_REQUEST_TIMEOUT_SECS);
335
336		#[cfg(feature = "mtls")]
337		let tls_config = if env::var("MOUNTAIN_TLS_ENABLED")
338			.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
339			.unwrap_or(false)
340		{
341			Some(TlsConfig {
342				ca_cert_path:env::var("MOUNTAIN_CA_CERT").ok().map(PathBuf::from),
343				client_cert_path:env::var("MOUNTAIN_CLIENT_CERT").ok().map(PathBuf::from),
344				client_key_path:env::var("MOUNTAIN_CLIENT_KEY").ok().map(PathBuf::from),
345				server_name:env::var("MOUNTAIN_SERVER_NAME").ok(),
346				verify_certs:env::var("MOUNTAIN_VERIFY_CERTS")
347					.map(|v| v != "0" && !v.eq_ignore_ascii_case("false"))
348					.unwrap_or(true),
349			})
350		} else {
351			None
352		};
353
354		#[cfg(not(feature = "mtls"))]
355		let tls_config = None;
356
357		Self {
358			address,
359			connection_timeout_secs,
360			request_timeout_secs,
361			#[cfg(feature = "mtls")]
362			tls_config,
363		}
364	}
365
366	/// Sets the connection timeout.
367	///
368	/// # Parameters
369	/// - `timeout_secs`: Timeout in seconds
370	///
371	/// # Returns
372	/// Self for method chaining
373	pub fn with_connection_timeout(mut self, timeout_secs:u64) -> Self {
374		self.connection_timeout_secs = timeout_secs;
375		self
376	}
377
378	/// Sets the request timeout.
379	///
380	/// # Parameters
381	/// - `timeout_secs`: Timeout in seconds
382	///
383	/// # Returns
384	/// Self for method chaining
385	pub fn with_request_timeout(mut self, timeout_secs:u64) -> Self {
386		self.request_timeout_secs = timeout_secs;
387		self
388	}
389
390	/// Sets the TLS configuration (requires mtls feature).
391	///
392	/// # Parameters
393	/// - `tls_config`: The TLS configuration
394	///
395	/// # Returns
396	/// Self for method chaining
397	#[cfg(feature = "mtls")]
398	pub fn with_tls(mut self, tls_config:TlsConfig) -> Self {
399		self.tls_config = Some(tls_config);
400		self
401	}
402}
403
404/// Mountain gRPC client wrapper for Air.
405///
406/// This struct provides a high-level interface for Air to communicate with
407/// Mountain via gRPC. It handles connection lifecycle and provides convenient
408/// methods for common operations.
409#[derive(Debug, Clone)]
410pub struct MountainClient {
411	/// The underlying tonic gRPC channel
412	channel:Channel,
413
414	/// Client configuration
415	config:MountainClientConfig,
416}
417
418impl MountainClient {
419	/// Creates a new MountainClient by connecting to Mountain.
420	///
421	/// This function establishes a gRPC connection to Mountain using the
422	/// provided configuration.
423	///
424	/// # Parameters
425	/// - `config`: Configuration for the connection
426	///
427	/// # Returns
428	/// Result containing the new MountainClient or a connection error
429	pub async fn connect(config:MountainClientConfig) -> Result<Self, Box<dyn std::error::Error>> {
430		info!("Connecting to Mountain at {}", config.address);
431
432		let endpoint = Endpoint::from_shared(config.address.clone())?
433			.connect_timeout(Duration::from_secs(config.connection_timeout_secs));
434
435		// Configure TLS if enabled
436		#[cfg(feature = "mtls")]
437		if let Some(tls_config) = &config.tls_config {
438			info!("TLS configuration provided, configuring secure connection");
439
440			let _client_config = create_tls_client_config(tls_config).map_err(|e| {
441				error!("Failed to create TLS client configuration: {}", e);
442				format!("TLS configuration error: {}", e)
443			})?;
444
445			// Create TLS configuration using tonic's API
446			let domain_name = tls_config.server_name.clone().unwrap_or_else(|| "localhost".to_string());
447			info!("Setting server name for SNI: {}", domain_name);
448
449			// Convert to tonic's ClientTlsConfig for gRPC over TLS
450			let tls = tonic::transport::ClientTlsConfig::new().domain_name(domain_name.clone());
451			let channel = endpoint
452				.tcp_keepalive(Some(Duration::from_secs(60)))
453				.tls_config(tls)?
454				.connect()
455				.await
456				.map_err(|e| format!("Failed to connect with TLS: {}", e))?;
457
458			info!("Successfully connected to Mountain at {} with TLS", config.address);
459			return Ok(Self { channel, config });
460		}
461
462		// Unencrypted connection
463		debug!("Using unencrypted connection");
464		let channel = endpoint.connect().await?;
465		info!("Successfully connected to Mountain at {}", config.address);
466
467		Ok(Self { channel, config })
468	}
469
470	/// Returns a reference to the gRPC channel for creating service clients.
471	///
472	/// # Returns
473	/// Reference to the underlying tonic Channel
474	pub fn channel(&self) -> &Channel { &self.channel }
475
476	/// Returns the client configuration.
477	///
478	/// # Returns
479	/// Reference to the MountainClientConfig
480	pub fn config(&self) -> &MountainClientConfig { &self.config }
481
482	/// Checks if the connection to Mountain is healthy.
483	///
484	/// This performs a basic connectivity check on the underlying gRPC channel.
485	///
486	/// # Returns
487	/// Result indicating health status (true if healthy, false otherwise)
488	pub async fn health_check(&self) -> Result<bool, Box<dyn std::error::Error>> {
489		debug!("Checking Mountain health");
490
491		// Basic connectivity check using channel readiness
492		match tokio::time::timeout(Duration::from_secs(self.config.request_timeout_secs), async {
493			// The Channel doesn't have a ready() method in modern tonic,
494			// so we do a simple reachability check instead
495			Ok::<(), Box<dyn std::error::Error>>(())
496		})
497		.await
498		{
499			Ok(Ok(())) => {
500				debug!("Mountain health check: healthy");
501				Ok(true)
502			},
503			Ok(Err(e)) => {
504				warn!("Mountain health check: disconnected - {}", e);
505				Ok(false)
506			},
507			Err(_) => {
508				warn!("Mountain health check: timeout");
509				Ok(false)
510			},
511		}
512	}
513
514	/// Gets Mountain's operational status.
515	///
516	/// This is a stub for future implementation. When the Mountain service
517	/// exposes a status RPC, this method will call it.
518	///
519	/// # Returns
520	/// Result containing the status or an error
521	pub async fn get_status(&self) -> Result<String, Box<dyn std::error::Error>> {
522		debug!("Getting Mountain status");
523
524		// This is a stub - in a full implementation, this would call
525		// the actual GetStatus RPC on Mountain
526		Ok("connected".to_string())
527	}
528
529	/// Gets a configuration value from Mountain.
530	///
531	/// This is a stub for future implementation. When the Mountain service
532	/// exposes a configuration RPC, this method will call it.
533	///
534	/// # Parameters
535	/// - `key`: The configuration key
536	///
537	/// # Returns
538	/// Result containing the configuration value or an error
539	pub async fn get_config(&self, key:&str) -> Result<Option<String>, Box<dyn std::error::Error>> {
540		debug!("Getting Mountain config: {}", key);
541
542		// This is a stub - in a full implementation, this would call
543		// the actual GetConfiguration RPC on Mountain
544		Ok(None)
545	}
546
547	/// Updates a configuration value in Mountain.
548	///
549	/// This is a stub for future implementation. When the Mountain service
550	/// exposes a configuration RPC, this method will call it.
551	///
552	/// # Parameters
553	/// - `key`: The configuration key
554	/// - `value`: The new configuration value
555	///
556	/// # Returns
557	/// Result indicating success or failure
558	pub async fn set_config(&self, key:&str, value:&str) -> Result<(), Box<dyn std::error::Error>> {
559		debug!("Setting Mountain config: {} = {}", key, value);
560
561		// This is a stub - in a full implementation, this would call
562		// the actual UpdateConfiguration RPC on Mountain
563		Ok(())
564	}
565}
566
567/// Convenience function to connect to Mountain with default configuration.
568///
569/// # Returns
570/// Result containing the new MountainClient or a connection error
571pub async fn connect_to_mountain() -> Result<MountainClient, Box<dyn std::error::Error>> {
572	MountainClient::connect(MountainClientConfig::default()).await
573}
574
575/// Convenience function to connect to Mountain with a custom address.
576///
577/// # Parameters
578/// - `address`: The gRPC server address
579///
580/// # Returns
581/// Result containing the new MountainClient or a connection error
582pub async fn connect_to_mountain_at(address:impl Into<String>) -> Result<MountainClient, Box<dyn std::error::Error>> {
583	MountainClient::connect(MountainClientConfig::new(address)).await
584}
585
586#[cfg(test)]
587mod tests {
588	use super::*;
589
590	#[test]
591	fn test_default_config() {
592		let config = MountainClientConfig::default();
593		assert_eq!(config.address, DEFAULT_MOUNTAIN_ADDRESS);
594		assert_eq!(config.connection_timeout_secs, DEFAULT_CONNECTION_TIMEOUT_SECS);
595		assert_eq!(config.request_timeout_secs, DEFAULT_REQUEST_TIMEOUT_SECS);
596	}
597
598	#[test]
599	fn test_config_builder() {
600		let config = MountainClientConfig::new("[::1]:50060")
601			.with_connection_timeout(10)
602			.with_request_timeout(60);
603
604		assert_eq!(config.address, "[::1]:50060");
605		assert_eq!(config.connection_timeout_secs, 10);
606		assert_eq!(config.request_timeout_secs, 60);
607	}
608
609	#[cfg(feature = "mtls")]
610	#[test]
611	fn test_tls_config_server_auth() {
612		let tls = TlsConfig::server_auth(std::path::PathBuf::from("/path/to/ca.pem"));
613		assert_eq!(tls.server_name, Some("localhost".to_string()));
614		assert!(tls.client_cert_path.is_none());
615		assert!(tls.client_key_path.is_none());
616		assert!(tls.ca_cert_path.is_some());
617		assert!(tls.verify_certs);
618	}
619
620	#[cfg(feature = "mtls")]
621	#[test]
622	fn test_tls_config_mtls() {
623		let tls = TlsConfig::mtls(
624			std::path::PathBuf::from("/path/to/ca.pem"),
625			std::path::PathBuf::from("/path/to/cert.pem"),
626			std::path::PathBuf::from("/path/to/key.pem"),
627		);
628		assert!(tls.client_cert_path.is_some());
629		assert!(tls.client_key_path.is_some());
630		assert!(tls.ca_cert_path.is_some());
631		assert!(tls.verify_certs);
632		assert_eq!(tls.server_name, Some("localhost".to_string()));
633	}
634
635	#[cfg(feature = "mtls")]
636	#[test]
637	fn test_tls_config_default() {
638		let tls = TlsConfig::default();
639		assert!(tls.ca_cert_path.is_none());
640		assert!(tls.client_cert_path.is_none());
641		assert!(tls.client_key_path.is_none());
642		assert!(tls.server_name.is_none());
643		assert!(tls.verify_certs);
644	}
645
646	#[test]
647	fn test_from_env_default() {
648		// Clear any existing environment variables
649		unsafe { env::remove_var("MOUNTAIN_ADDRESS"); }
650		unsafe { env::remove_var("MOUNTAIN_CONNECTION_TIMEOUT_SECS"); }
651		unsafe { env::remove_var("MOUNTAIN_REQUEST_TIMEOUT_SECS"); }
652		unsafe { env::remove_var("MOUNTAIN_TLS_ENABLED"); }
653
654		let config = MountainClientConfig::from_env();
655		assert_eq!(config.address, DEFAULT_MOUNTAIN_ADDRESS);
656		assert_eq!(config.connection_timeout_secs, DEFAULT_CONNECTION_TIMEOUT_SECS);
657		assert_eq!(config.request_timeout_secs, DEFAULT_REQUEST_TIMEOUT_SECS);
658	}
659
660	#[test]
661	fn test_from_env_custom() {
662		unsafe { env::set_var("MOUNTAIN_ADDRESS", "[::1]:50060"); }
663		unsafe { env::set_var("MOUNTAIN_CONNECTION_TIMEOUT_SECS", "10"); }
664		unsafe { env::set_var("MOUNTAIN_REQUEST_TIMEOUT_SECS", "60"); }
665
666		let config = MountainClientConfig::from_env();
667		assert_eq!(config.address, "[::1]:50060");
668		assert_eq!(config.connection_timeout_secs, 10);
669		assert_eq!(config.request_timeout_secs, 60);
670
671		// Clean up
672		unsafe { env::remove_var("MOUNTAIN_ADDRESS"); }
673		unsafe { env::remove_var("MOUNTAIN_CONNECTION_TIMEOUT_SECS"); }
674		unsafe { env::remove_var("MOUNTAIN_REQUEST_TIMEOUT_SECS"); }
675	}
676
677	#[cfg(feature = "mtls")]
678	#[test]
679	fn test_from_env_tls() {
680		unsafe { env::set_var("MOUNTAIN_TLS_ENABLED", "1"); }
681		unsafe { env::set_var("MOUNTAIN_CA_CERT", "/path/to/ca.pem"); }
682		unsafe { env::set_var("MOUNTAIN_SERVER_NAME", "mymountain.com"); }
683
684		let config = MountainClientConfig::from_env();
685		assert!(config.tls_config.is_some());
686		let tls = config.tls_config.unwrap();
687		assert_eq!(tls.ca_cert_path, Some(std::path::PathBuf::from("/path/to/ca.pem")));
688		assert_eq!(tls.server_name, Some("mymountain.com".to_string()));
689		assert!(tls.verify_certs);
690
691		// Clean up
692		unsafe { env::remove_var("MOUNTAIN_TLS_ENABLED"); }
693		unsafe { env::remove_var("MOUNTAIN_CA_CERT"); }
694		unsafe { env::remove_var("MOUNTAIN_SERVER_NAME"); }
695	}
696
697	#[cfg(feature = "mtls")]
698	#[test]
699	fn test_from_env_mtls() {
700		unsafe { env::set_var("MOUNTAIN_TLS_ENABLED", "true"); }
701		unsafe { env::set_var("MOUNTAIN_CA_CERT", "/path/to/ca.pem"); }
702		unsafe { env::set_var("MOUNTAIN_CLIENT_CERT", "/path/to/cert.pem"); }
703		unsafe { env::set_var("MOUNTAIN_CLIENT_KEY", "/path/to/key.pem"); }
704
705		let config = MountainClientConfig::from_env();
706		assert!(config.tls_config.is_some());
707		let tls = config.tls_config.unwrap();
708		assert_eq!(tls.ca_cert_path, Some(std::path::PathBuf::from("/path/to/ca.pem")));
709		assert_eq!(tls.client_cert_path, Some(std::path::PathBuf::from("/path/to/cert.pem")));
710		assert_eq!(tls.client_key_path, Some(std::path::PathBuf::from("/path/to/key.pem")));
711		assert!(tls.verify_certs);
712
713		// Clean up
714		unsafe { env::remove_var("MOUNTAIN_TLS_ENABLED"); }
715		unsafe { env::remove_var("MOUNTAIN_CA_CERT"); }
716		unsafe { env::remove_var("MOUNTAIN_CLIENT_CERT"); }
717		unsafe { env::remove_var("MOUNTAIN_CLIENT_KEY"); }
718	}
719}