ant_quic/node_config.rs
1// Copyright 2024 Saorsa Labs Ltd.
2//
3// This Saorsa Network Software is licensed under the General Public License (GPL), version 3.
4// Please see the file LICENSE-GPL, or visit <http://www.gnu.org/licenses/> for the full text.
5//
6// Full details available at https://saorsalabs.com/licenses
7
8//! Minimal configuration for zero-config P2P nodes
9//!
10//! This module provides [`NodeConfig`] - a simple configuration struct
11//! with only 3 optional fields. Most applications need zero configuration.
12//!
13//! # Zero Configuration
14//!
15//! ```rust,ignore
16//! use ant_quic::Node;
17//!
18//! // No configuration needed - just create a node
19//! let node = Node::new().await?;
20//! ```
21//!
22//! # Optional Configuration
23//!
24//! ```rust,ignore
25//! use ant_quic::{Node, NodeConfig};
26//!
27//! // Only configure what you need
28//! let config = NodeConfig::builder()
29//! .known_peer("quic.saorsalabs.com:9000".parse()?)
30//! .build();
31//!
32//! let node = Node::with_config(config).await?;
33//! ```
34
35use std::path::Path;
36use std::sync::Arc;
37
38use crate::bootstrap_cache::BootstrapCacheConfig;
39use crate::crypto::pqc::types::{MlDsaPublicKey, MlDsaSecretKey};
40use crate::host_identity::HostIdentity;
41use crate::transport::{TransportAddr, TransportProvider, TransportRegistry};
42use crate::unified_config::load_or_generate_endpoint_keypair;
43
44/// Minimal configuration for P2P nodes
45///
46/// All fields are optional - the node will auto-configure everything.
47/// - `bind_addr`: Defaults to `0.0.0.0:0` (random port)
48/// - `known_peers`: Defaults to empty (node can still accept connections)
49/// - `keypair`: Defaults to fresh generated keypair
50/// - `transport_providers`: Defaults to UDP plus best-effort constrained transports
51/// such as BLE when available
52///
53/// # Example
54///
55/// ```rust,ignore
56/// // Zero configuration
57/// let config = NodeConfig::default();
58///
59/// // Or with known peers
60/// let config = NodeConfig::builder()
61/// .known_peer("peer1.example.com:9000".parse()?)
62/// .build();
63///
64/// // Or with additional transport providers
65/// #[cfg(feature = "ble")]
66/// let config = NodeConfig::builder()
67/// .transport_provider(Arc::new(BleTransport::new().await?))
68/// .build();
69/// ```
70#[derive(Clone, Default)]
71pub struct NodeConfig {
72 /// Bind address. Default: 0.0.0.0:0 (random port)
73 pub bind_addr: Option<TransportAddr>,
74
75 /// Known peers for initial discovery. Default: empty
76 /// When empty, node can still accept incoming connections.
77 pub known_peers: Vec<TransportAddr>,
78
79 /// Identity keypair (ML-DSA-65). Default: fresh generated
80 /// Provide for persistent identity across restarts.
81 pub keypair: Option<(MlDsaPublicKey, MlDsaSecretKey)>,
82
83 /// Additional transport providers beyond the default transport set.
84 ///
85 /// UDP is always included, and constrained transports such as BLE are
86 /// registered automatically when compiled in and available at runtime.
87 /// Use this to add additional transports like LoRa, serial, etc.
88 ///
89 /// Transport capabilities are propagated to peer advertisements and
90 /// used for routing decisions.
91 pub transport_providers: Vec<Arc<dyn TransportProvider>>,
92
93 /// Data channel capacity (bounded mpsc between reader tasks and recv).
94 /// Default: 256.
95 pub data_channel_capacity: Option<usize>,
96
97 /// Maximum concurrent unidirectional QUIC streams per connection.
98 /// Default: 100.
99 pub max_concurrent_uni_streams: Option<u32>,
100
101 /// Maximum bytes accepted for a single message read from a stream.
102 /// Default: [`crate::unified_config::P2pConfig::DEFAULT_MAX_MESSAGE_SIZE`].
103 pub max_message_size: Option<usize>,
104
105 /// Reviewer P2 #2: surface ant-quic's best-effort UPnP IGD port-mapping
106 /// toggle at the simpler `NodeConfig` builder layer (the existing knob
107 /// is only on `P2pConfigBuilder`). When `Some(false)`, the UPnP
108 /// discovery + port-mapping task is skipped. When `None`, the ant-quic
109 /// default applies (currently enabled).
110 pub port_mapping_enabled: Option<bool>,
111
112 /// Enable or disable first-party mDNS discovery for this node.
113 ///
114 /// When `Some(false)`, mDNS browsing and advertising are disabled so
115 /// co-located daemons on the same LAN cannot discover and auto-connect
116 /// to this node via mDNS. When `None`, the ant-quic default applies
117 /// (currently enabled with auto-connect).
118 pub mdns_enabled: Option<bool>,
119
120 /// mDNS namespace isolating this node from other logical planes.
121 ///
122 /// When set, the namespace is advertised in the mDNS TXT record and
123 /// discovered services advertising a different (or no) namespace are
124 /// rejected as dial candidates. Callers running multiple logical
125 /// planes on one host (e.g. prod + testnet daemons) MUST set a
126 /// distinct namespace per plane, otherwise the daemons discover each
127 /// other via the shared `ant-quic` service and auto-connect, bridging
128 /// gossip across planes. When `None`, the ant-quic default applies
129 /// (no namespace; every `ant-quic` service on the LAN is eligible).
130 pub mdns_namespace: Option<String>,
131
132 /// Bootstrap peer cache configuration for the node's endpoint.
133 ///
134 /// The endpoint always owns exactly one [`BootstrapCacheConfig`]-backed
135 /// cache used for quality-scored reconnection, coordinator selection and
136 /// bootstrap tokens. When `None`, the ant-quic default applies — a
137 /// **host-shared** directory (`$TMPDIR/ant-quic-cache` or the platform
138 /// cache dir). Embedders running multiple nodes per host (or wanting
139 /// per-instance persistence) should set an explicit per-instance
140 /// `cache_dir`, or `persist(false)` for an in-memory-only cache.
141 /// Access the resulting shared cache via [`crate::Node::bootstrap_cache`].
142 pub bootstrap_cache: Option<BootstrapCacheConfig>,
143}
144
145impl std::fmt::Debug for NodeConfig {
146 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147 f.debug_struct("NodeConfig")
148 .field("bind_addr", &self.bind_addr)
149 .field("known_peers", &self.known_peers)
150 .field("keypair", &self.keypair.as_ref().map(|_| "[REDACTED]"))
151 .field("transport_providers", &self.transport_providers.len())
152 .finish()
153 }
154}
155
156impl NodeConfig {
157 /// Create a new config with defaults
158 pub fn new() -> Self {
159 Self::default()
160 }
161
162 /// Create a builder for fluent construction
163 pub fn builder() -> NodeConfigBuilder {
164 NodeConfigBuilder::default()
165 }
166
167 /// Create config with a specific bind address
168 pub fn with_bind_addr(addr: impl Into<TransportAddr>) -> Self {
169 Self {
170 bind_addr: Some(addr.into()),
171 ..Default::default()
172 }
173 }
174
175 /// Create config with known peers
176 pub fn with_known_peers(peers: impl IntoIterator<Item = impl Into<TransportAddr>>) -> Self {
177 Self {
178 known_peers: peers.into_iter().map(|p| p.into()).collect(),
179 ..Default::default()
180 }
181 }
182
183 /// Create config with a specific ML-DSA-65 keypair
184 pub fn with_keypair(public_key: MlDsaPublicKey, secret_key: MlDsaSecretKey) -> Self {
185 Self {
186 keypair: Some((public_key, secret_key)),
187 ..Default::default()
188 }
189 }
190}
191
192/// Builder for [`NodeConfig`]
193#[derive(Default)]
194pub struct NodeConfigBuilder {
195 bind_addr: Option<TransportAddr>,
196 known_peers: Vec<TransportAddr>,
197 keypair: Option<(MlDsaPublicKey, MlDsaSecretKey)>,
198 transport_providers: Vec<Arc<dyn TransportProvider>>,
199 data_channel_capacity: Option<usize>,
200 max_concurrent_uni_streams: Option<u32>,
201 max_message_size: Option<usize>,
202 port_mapping_enabled: Option<bool>,
203 mdns_enabled: Option<bool>,
204 mdns_namespace: Option<String>,
205 bootstrap_cache: Option<BootstrapCacheConfig>,
206}
207
208impl NodeConfigBuilder {
209 /// Set the local address to bind to
210 ///
211 /// Accepts any type implementing `Into<TransportAddr>`:
212 /// - `SocketAddr` - Auto-converts to `TransportAddr::Udp` (backward compatible)
213 /// - `TransportAddr` - Enables multi-transport support (BLE, LoRa, etc.)
214 ///
215 /// If not specified, defaults to `0.0.0.0:0` (random ephemeral port).
216 ///
217 /// # Examples
218 ///
219 /// ```rust,ignore
220 /// use ant_quic::NodeConfig;
221 /// use std::net::SocketAddr;
222 ///
223 /// // Backward compatible: SocketAddr
224 /// let config = NodeConfig::builder()
225 /// .bind_addr("0.0.0.0:9000".parse::<SocketAddr>().unwrap())
226 /// .build();
227 ///
228 /// // Multi-transport: Explicit TransportAddr
229 /// use ant_quic::transport::TransportAddr;
230 /// let config = NodeConfig::builder()
231 /// .bind_addr(TransportAddr::Udp("0.0.0.0:0".parse().unwrap()))
232 /// .build();
233 /// ```
234 pub fn bind_addr(mut self, addr: impl Into<TransportAddr>) -> Self {
235 self.bind_addr = Some(addr.into());
236 self
237 }
238
239 /// Add a known peer for initial network connectivity
240 ///
241 /// Known peers are used for initial discovery and connection establishment.
242 /// The node will learn about additional peers through the network.
243 ///
244 /// Accepts any type implementing `Into<TransportAddr>`:
245 /// - `SocketAddr` - Auto-converts to `TransportAddr::Udp`
246 /// - `TransportAddr` - Supports multiple transport types
247 ///
248 /// # Examples
249 ///
250 /// ```rust,ignore
251 /// use ant_quic::NodeConfig;
252 /// use std::net::SocketAddr;
253 ///
254 /// // Backward compatible: SocketAddr
255 /// let config = NodeConfig::builder()
256 /// .known_peer("peer.example.com:9000".parse::<SocketAddr>().unwrap())
257 /// .build();
258 ///
259 /// // Multi-transport: Mix different transport types
260 /// use ant_quic::transport::TransportAddr;
261 /// let config = NodeConfig::builder()
262 /// .known_peer(TransportAddr::Udp("192.168.1.1:9000".parse().unwrap()))
263 /// .known_peer(TransportAddr::ble([0x11, 0x22, 0x33, 0x44, 0x55, 0x66], None))
264 /// .build();
265 /// ```
266 pub fn known_peer(mut self, addr: impl Into<TransportAddr>) -> Self {
267 self.known_peers.push(addr.into());
268 self
269 }
270
271 /// Add multiple known peers at once
272 ///
273 /// Convenient method to add a collection of peers. Each item is automatically
274 /// converted via `Into<TransportAddr>`, supporting both `SocketAddr` and
275 /// `TransportAddr` for backward compatibility and multi-transport scenarios.
276 ///
277 /// # Examples
278 ///
279 /// ```rust,ignore
280 /// use ant_quic::NodeConfig;
281 /// use std::net::SocketAddr;
282 ///
283 /// // Backward compatible: Vec<SocketAddr>
284 /// let peers: Vec<SocketAddr> = vec![
285 /// "peer1.example.com:9000".parse().unwrap(),
286 /// "peer2.example.com:9000".parse().unwrap(),
287 /// ];
288 /// let config = NodeConfig::builder()
289 /// .known_peers(peers)
290 /// .build();
291 ///
292 /// // Multi-transport: Heterogeneous transport list
293 /// use ant_quic::transport::TransportAddr;
294 /// let mixed = vec![
295 /// TransportAddr::Udp("192.168.1.1:9000".parse().unwrap()),
296 /// TransportAddr::ble([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF], None),
297 /// TransportAddr::serial("/dev/ttyUSB0"),
298 /// ];
299 /// let config = NodeConfig::builder()
300 /// .known_peers(mixed)
301 /// .build();
302 /// ```
303 pub fn known_peers(
304 mut self,
305 addrs: impl IntoIterator<Item = impl Into<TransportAddr>>,
306 ) -> Self {
307 self.known_peers.extend(addrs.into_iter().map(|a| a.into()));
308 self
309 }
310
311 /// Set the identity keypair (ML-DSA-65)
312 pub fn keypair(mut self, public_key: MlDsaPublicKey, secret_key: MlDsaSecretKey) -> Self {
313 self.keypair = Some((public_key, secret_key));
314 self
315 }
316
317 /// Set the identity from a HostIdentity with encrypted storage
318 ///
319 /// This loads or generates a keypair using the HostIdentity for encryption.
320 /// The keypair is stored encrypted at rest in the specified directory.
321 ///
322 /// # Arguments
323 ///
324 /// * `host` - The HostIdentity for key derivation
325 /// * `network_id` - Network identifier for per-network keypair isolation
326 /// * `storage_dir` - Directory to store the encrypted keypair
327 ///
328 /// # Errors
329 ///
330 /// Returns an error if the keypair cannot be loaded or generated.
331 pub fn with_host_identity(
332 mut self,
333 host: &HostIdentity,
334 network_id: &[u8],
335 storage_dir: &Path,
336 ) -> Result<Self, String> {
337 let (public_key, secret_key) =
338 load_or_generate_endpoint_keypair(host, network_id, storage_dir)
339 .map_err(|e| format!("Failed to load/generate keypair: {e}"))?;
340 self.keypair = Some((public_key, secret_key));
341 Ok(self)
342 }
343
344 /// Add a transport provider
345 ///
346 /// Transport providers are used for multi-transport P2P networking.
347 /// UDP is always included, and constrained transports such as BLE are
348 /// registered automatically when compiled in and available at runtime.
349 ///
350 /// # Example
351 ///
352 /// ```rust,ignore
353 /// #[cfg(feature = "ble")]
354 /// let config = NodeConfig::builder()
355 /// .transport_provider(Arc::new(BleTransport::new().await?))
356 /// .build();
357 /// ```
358 pub fn transport_provider(mut self, provider: Arc<dyn TransportProvider>) -> Self {
359 self.transport_providers.push(provider);
360 self
361 }
362
363 /// Add multiple transport providers
364 pub fn transport_providers(
365 mut self,
366 providers: impl IntoIterator<Item = Arc<dyn TransportProvider>>,
367 ) -> Self {
368 self.transport_providers.extend(providers);
369 self
370 }
371
372 /// Set the data channel capacity (bounded mpsc between reader tasks and recv).
373 ///
374 /// Higher values reduce backpressure on reader tasks. Default: 256.
375 pub fn data_channel_capacity(mut self, capacity: usize) -> Self {
376 self.data_channel_capacity = Some(capacity);
377 self
378 }
379
380 /// Set the maximum concurrent unidirectional QUIC streams per connection.
381 ///
382 /// Each `send()` call opens a new unidirectional stream. Applications with
383 /// high message throughput should increase this. Default: 100.
384 pub fn max_concurrent_uni_streams(mut self, count: u32) -> Self {
385 self.max_concurrent_uni_streams = Some(count);
386 self
387 }
388
389 /// Set the maximum bytes accepted for a single message read from a stream.
390 ///
391 /// This mirrors [`crate::unified_config::P2pConfigBuilder::max_message_size`]
392 /// for applications using the simpler [`NodeConfig`] builder. Values must be
393 /// at least 1; [`crate::Node::with_config`] rejects zero with a configuration
394 /// error before constructing the endpoint.
395 pub fn max_message_size(mut self, bytes: usize) -> Self {
396 self.max_message_size = Some(bytes);
397 self
398 }
399
400 /// Reviewer P2 #2: enable or disable the best-effort UPnP IGD
401 /// port-mapping task. Default (when not called) follows the global
402 /// ant-quic default — currently enabled. Use `false` on networks
403 /// without IGD support, or where operator policy prohibits unsolicited
404 /// router port mappings.
405 pub fn port_mapping_enabled(mut self, enabled: bool) -> Self {
406 self.port_mapping_enabled = Some(enabled);
407 self
408 }
409
410 /// Enable or disable first-party mDNS discovery (see
411 /// [`NodeConfig::mdns_enabled`]). Use `false` to keep co-located
412 /// daemons on other planes from discovering this node via mDNS.
413 pub fn mdns_enabled(mut self, enabled: bool) -> Self {
414 self.mdns_enabled = Some(enabled);
415 self
416 }
417
418 /// Set the mDNS namespace isolating this node's logical plane (see
419 /// [`NodeConfig::mdns_namespace`]). Callers running multiple planes on
420 /// one host MUST set a distinct namespace per plane.
421 pub fn mdns_namespace(mut self, namespace: impl Into<String>) -> Self {
422 self.mdns_namespace = Some(namespace.into());
423 self
424 }
425
426 /// Configure the endpoint's bootstrap peer cache (see
427 /// [`NodeConfig::bootstrap_cache`])
428 pub fn bootstrap_cache(mut self, config: BootstrapCacheConfig) -> Self {
429 self.bootstrap_cache = Some(config);
430 self
431 }
432
433 /// Build the configuration
434 pub fn build(self) -> NodeConfig {
435 NodeConfig {
436 bind_addr: self.bind_addr,
437 known_peers: self.known_peers,
438 keypair: self.keypair,
439 transport_providers: self.transport_providers,
440 data_channel_capacity: self.data_channel_capacity,
441 max_concurrent_uni_streams: self.max_concurrent_uni_streams,
442 max_message_size: self.max_message_size,
443 port_mapping_enabled: self.port_mapping_enabled,
444 mdns_enabled: self.mdns_enabled,
445 mdns_namespace: self.mdns_namespace,
446 bootstrap_cache: self.bootstrap_cache,
447 }
448 }
449}
450
451impl NodeConfig {
452 /// Build a transport registry from this configuration
453 ///
454 /// Creates a registry containing all configured transport providers.
455 /// If no providers are configured, returns an empty registry (UDP
456 /// should be added by the caller based on bind_addr).
457 pub fn build_transport_registry(&self) -> TransportRegistry {
458 let mut registry = TransportRegistry::new();
459 for provider in &self.transport_providers {
460 registry.register(provider.clone());
461 }
462 registry
463 }
464
465 /// Check if this configuration has any non-UDP transport providers
466 pub fn has_constrained_transports(&self) -> bool {
467 use crate::transport::TransportType;
468 self.transport_providers
469 .iter()
470 .any(|p| p.transport_type() != TransportType::Udp)
471 }
472}
473
474#[cfg(test)]
475mod tests {
476 use super::*;
477 use std::net::SocketAddr;
478
479 #[test]
480 fn test_default_config() {
481 let config = NodeConfig::default();
482 assert!(config.bind_addr.is_none());
483 assert!(config.known_peers.is_empty());
484 assert!(config.keypair.is_none());
485 assert!(config.transport_providers.is_empty());
486 assert!(config.max_message_size.is_none());
487 }
488
489 /// The endpoint always opens exactly one bootstrap cache from
490 /// `NodeConfig::bootstrap_cache`; without this plumbing embedders were
491 /// forced to open a *second* cache instance (host-shared default dir)
492 /// that diverged from the endpoint's own.
493 #[test]
494 fn builder_plumbs_bootstrap_cache_config() {
495 let cache_config = BootstrapCacheConfig::builder()
496 .cache_dir("/tmp/per-instance-peers")
497 .persist(false)
498 .build();
499 let config = NodeConfig::builder().bootstrap_cache(cache_config).build();
500
501 let plumbed = config.bootstrap_cache.expect("bootstrap cache config set");
502 assert_eq!(
503 plumbed.cache_dir,
504 std::path::PathBuf::from("/tmp/per-instance-peers")
505 );
506 assert!(!plumbed.persist);
507 // Default stays None so existing embedders keep ant-quic's default.
508 assert!(NodeConfig::default().bootstrap_cache.is_none());
509 }
510
511 /// Issue #206: NodeConfig must expose mDNS plane-isolation knobs so
512 /// co-located daemons on different logical planes do not discover and
513 /// auto-connect to each other via the default `ant-quic` service.
514 #[test]
515 fn builder_plumbs_mdns_plane_isolation_config() {
516 let config = NodeConfig::builder()
517 .mdns_enabled(false)
518 .mdns_namespace("testnet")
519 .build();
520 assert_eq!(config.mdns_enabled, Some(false));
521 assert_eq!(config.mdns_namespace.as_deref(), Some("testnet"));
522
523 // Defaults stay None so existing embedders keep ant-quic's default.
524 let default = NodeConfig::default();
525 assert!(default.mdns_enabled.is_none());
526 assert!(default.mdns_namespace.is_none());
527 }
528
529 #[test]
530 fn test_builder_with_max_message_size() {
531 let config = NodeConfig::builder()
532 .max_message_size(10 * 1024 * 1024)
533 .build();
534 assert_eq!(config.max_message_size, Some(10 * 1024 * 1024));
535 }
536
537 #[test]
538 fn test_builder_records_zero_max_message_size_for_node_validation() {
539 let config = NodeConfig::builder().max_message_size(0).build();
540 assert_eq!(config.max_message_size, Some(0));
541 }
542
543 #[test]
544 fn test_builder_with_bind_addr() {
545 let addr: SocketAddr = "0.0.0.0:9000".parse().unwrap();
546 let config = NodeConfig::builder().bind_addr(addr).build();
547 assert_eq!(config.bind_addr, Some(TransportAddr::from(addr)));
548 }
549
550 #[test]
551 fn test_builder_with_known_peers() {
552 let peer1: SocketAddr = "127.0.0.1:9000".parse().unwrap();
553 let peer2: SocketAddr = "127.0.0.1:9001".parse().unwrap();
554
555 let config = NodeConfig::builder()
556 .known_peer(peer1)
557 .known_peer(peer2)
558 .build();
559
560 assert_eq!(config.known_peers.len(), 2);
561 assert!(config.known_peers.contains(&TransportAddr::from(peer1)));
562 assert!(config.known_peers.contains(&TransportAddr::from(peer2)));
563 }
564
565 #[test]
566 fn test_builder_with_multiple_peers() {
567 let peers: Vec<SocketAddr> = vec![
568 "127.0.0.1:9000".parse().unwrap(),
569 "127.0.0.1:9001".parse().unwrap(),
570 ];
571
572 let config = NodeConfig::builder().known_peers(peers.clone()).build();
573
574 assert_eq!(config.known_peers.len(), 2);
575 assert_eq!(
576 config.known_peers,
577 peers
578 .into_iter()
579 .map(TransportAddr::from)
580 .collect::<Vec<_>>()
581 );
582 }
583
584 #[test]
585 fn test_with_bind_addr() {
586 let addr: SocketAddr = "0.0.0.0:9000".parse().unwrap();
587 let config = NodeConfig::with_bind_addr(addr);
588 assert_eq!(config.bind_addr, Some(TransportAddr::from(addr)));
589 assert!(config.known_peers.is_empty());
590 assert!(config.keypair.is_none());
591 }
592
593 #[test]
594 fn test_with_known_peers() {
595 let peers: Vec<SocketAddr> = vec![
596 "127.0.0.1:9000".parse().unwrap(),
597 "127.0.0.1:9001".parse().unwrap(),
598 ];
599
600 let config = NodeConfig::with_known_peers(peers.clone());
601 assert!(config.bind_addr.is_none());
602 assert_eq!(
603 config.known_peers,
604 peers
605 .into_iter()
606 .map(TransportAddr::from)
607 .collect::<Vec<_>>()
608 );
609 assert!(config.keypair.is_none());
610 }
611
612 #[test]
613 fn test_debug_redacts_keypair() {
614 use crate::crypto::raw_public_keys::key_utils::generate_ml_dsa_keypair;
615 let (public_key, secret_key) = generate_ml_dsa_keypair().unwrap();
616 let config = NodeConfig::with_keypair(public_key, secret_key);
617 let debug_str = format!("{:?}", config);
618 assert!(debug_str.contains("[REDACTED]"));
619 assert!(!debug_str.contains(&format!("{:?}", config.keypair)));
620 }
621
622 #[test]
623 fn test_config_is_clone() {
624 let addr: SocketAddr = "0.0.0.0:9000".parse().unwrap();
625 let peer: SocketAddr = "127.0.0.1:9001".parse().unwrap();
626 let config = NodeConfig::builder()
627 .bind_addr(addr)
628 .known_peer(peer)
629 .build();
630
631 let cloned = config.clone();
632 assert_eq!(config.bind_addr, cloned.bind_addr);
633 assert_eq!(config.known_peers, cloned.known_peers);
634 }
635
636 #[test]
637 fn test_build_transport_registry() {
638 let config = NodeConfig::default();
639 let registry = config.build_transport_registry();
640 assert!(registry.is_empty());
641 }
642
643 #[test]
644 fn test_has_constrained_transports_default() {
645 let config = NodeConfig::default();
646 assert!(!config.has_constrained_transports());
647 }
648
649 #[test]
650 fn test_debug_shows_transport_count() {
651 let config = NodeConfig::default();
652 let debug_str = format!("{:?}", config);
653 assert!(debug_str.contains("transport_providers: 0"));
654 }
655
656 #[test]
657 fn test_node_config_with_transport_addr() {
658 // Create NodeConfig with TransportAddr bind and peers
659 let bind_addr = TransportAddr::from("0.0.0.0:9000".parse::<SocketAddr>().unwrap());
660 let peer1 = TransportAddr::from("127.0.0.1:9001".parse::<SocketAddr>().unwrap());
661 let peer2 = TransportAddr::from("127.0.0.1:9002".parse::<SocketAddr>().unwrap());
662
663 let config = NodeConfig::builder()
664 .bind_addr(bind_addr.clone())
665 .known_peer(peer1.clone())
666 .known_peer(peer2.clone())
667 .build();
668
669 // Verify fields set correctly
670 assert_eq!(config.bind_addr, Some(bind_addr));
671 assert_eq!(config.known_peers.len(), 2);
672 assert!(config.known_peers.contains(&peer1));
673 assert!(config.known_peers.contains(&peer2));
674 }
675
676 #[test]
677 fn test_node_config_builder_backward_compat() {
678 // Use builder with SocketAddr (should auto-convert via Into trait)
679 let bind_socket: SocketAddr = "0.0.0.0:9000".parse().unwrap();
680 let peer_socket: SocketAddr = "127.0.0.1:9001".parse().unwrap();
681
682 let config = NodeConfig::builder()
683 .bind_addr(bind_socket)
684 .known_peer(peer_socket)
685 .build();
686
687 // Verify Into trait conversion works
688 assert_eq!(config.bind_addr, Some(TransportAddr::from(bind_socket)));
689 assert_eq!(config.known_peers.len(), 1);
690 assert_eq!(config.known_peers[0], TransportAddr::from(peer_socket));
691
692 // Verify it's the same as explicit TransportAddr usage
693 let explicit_config = NodeConfig::builder()
694 .bind_addr(TransportAddr::from(bind_socket))
695 .known_peer(TransportAddr::from(peer_socket))
696 .build();
697
698 assert_eq!(config.bind_addr, explicit_config.bind_addr);
699 assert_eq!(config.known_peers, explicit_config.known_peers);
700 }
701
702 #[test]
703 fn test_node_config_transport_addr_preservation() {
704 // Create NodeConfig with various TransportAddr types
705 let udp_bind = TransportAddr::from("0.0.0.0:0".parse::<SocketAddr>().unwrap());
706 let udp_peer = TransportAddr::from("127.0.0.1:9000".parse::<SocketAddr>().unwrap());
707 let ipv6_peer = TransportAddr::from("[::1]:9001".parse::<SocketAddr>().unwrap());
708
709 let config = NodeConfig::builder()
710 .bind_addr(udp_bind.clone())
711 .known_peer(udp_peer.clone())
712 .known_peer(ipv6_peer.clone())
713 .build();
714
715 // Verify address types preserved
716 assert_eq!(config.bind_addr, Some(udp_bind));
717 assert_eq!(config.known_peers.len(), 2);
718
719 // Check that TransportAddr types are maintained
720 assert!(matches!(config.known_peers[0], TransportAddr::Udp(_)));
721 assert!(matches!(config.known_peers[1], TransportAddr::Udp(_)));
722
723 // Verify actual addresses match
724 assert_eq!(config.known_peers[0], udp_peer);
725 assert_eq!(config.known_peers[1], ipv6_peer);
726 }
727}