1use std::collections::HashMap;
33use std::net::IpAddr;
34use std::path::PathBuf;
35use std::sync::Arc;
36
37use tokio::net::TcpStream;
38use tokio::sync::{broadcast, RwLock};
39
40use crate::envelope::codec::{EnvelopeCodec, JsonCodec};
41use crate::envelope::{Envelope, EnvelopeError};
42use crate::network::tailscale::{TailscaleConfig, TailscaleProvider};
43use crate::network::{
44 HealthInfo, NetworkProvider, NetworkUdpSocket, NodeIdentity, PingResult,
45};
46use crate::session::{PeerEvent, PeerRegistry, PeerState};
47use crate::transport::websocket::WebSocketTransport;
48use crate::transport::{RawListener, WsConfig};
49
50#[derive(Debug, Clone)]
59pub struct NamespacedMessage {
60 pub from: String,
62 pub namespace: String,
64 pub msg_type: String,
66 pub payload: serde_json::Value,
68 pub timestamp: Option<u64>,
70}
71
72#[derive(Debug, Clone)]
82pub struct Peer {
83 pub id: String,
85 pub name: String,
87 pub ip: IpAddr,
89 pub online: bool,
91 pub connected: bool,
93 pub connection_type: String,
95 pub os: Option<String>,
97 pub last_seen: Option<String>,
99}
100
101impl From<PeerState> for Peer {
102 fn from(s: PeerState) -> Self {
103 Self {
104 id: s.id,
105 name: s.name,
106 ip: s.ip,
107 online: s.online,
108 connected: s.connected,
109 connection_type: s.connection_type,
110 os: s.os,
111 last_seen: s.last_seen,
112 }
113 }
114}
115
116#[derive(Debug, thiserror::Error)]
122pub enum NodeError {
123 #[error("peer not found: {0}")]
125 PeerNotFound(String),
126
127 #[error("connection failed: {0}")]
129 ConnectionFailed(String),
130
131 #[error("send failed: {0}")]
133 SendFailed(String),
134
135 #[error("envelope error: {0}")]
137 Envelope(#[from] EnvelopeError),
138
139 #[error("session error: {0}")]
141 Session(#[from] crate::session::SessionError),
142
143 #[error("network error: {0}")]
145 Network(#[from] crate::network::NetworkError),
146
147 #[error("transport error: {0}")]
149 Transport(#[from] crate::transport::TransportError),
150
151 #[error("not implemented: {0}")]
153 NotImplemented(String),
154
155 #[error("node stopped")]
157 Stopped,
158
159 #[error("build error: {0}")]
161 BuildError(String),
162}
163
164pub struct Node<N: NetworkProvider + 'static> {
180 network: Arc<N>,
182 session: Arc<PeerRegistry<N>>,
184 codec: Arc<dyn EnvelopeCodec>,
186 #[allow(dead_code)]
189 incoming_tx: broadcast::Sender<NamespacedMessage>,
190 namespace_filters: Arc<RwLock<HashMap<String, broadcast::Sender<NamespacedMessage>>>>,
192}
193
194impl<N: NetworkProvider + 'static> Node<N> {
195 pub(crate) fn from_parts(
201 network: Arc<N>,
202 session: Arc<PeerRegistry<N>>,
203 codec: Arc<dyn EnvelopeCodec>,
204 ) -> Self {
205 let (incoming_tx, _) = broadcast::channel(1024);
206 let namespace_filters: Arc<RwLock<HashMap<String, broadcast::Sender<NamespacedMessage>>>> =
207 Arc::new(RwLock::new(HashMap::new()));
208
209 let node = Self {
210 network,
211 session: session.clone(),
212 codec: codec.clone(),
213 incoming_tx: incoming_tx.clone(),
214 namespace_filters: namespace_filters.clone(),
215 };
216
217 node.spawn_envelope_router(session, codec, incoming_tx, namespace_filters);
219
220 node
221 }
222
223 fn spawn_envelope_router(
227 &self,
228 session: Arc<PeerRegistry<N>>,
229 codec: Arc<dyn EnvelopeCodec>,
230 incoming_tx: broadcast::Sender<NamespacedMessage>,
231 namespace_filters: Arc<RwLock<HashMap<String, broadcast::Sender<NamespacedMessage>>>>,
232 ) {
233 let mut rx = session.subscribe();
234
235 tokio::spawn(async move {
236 loop {
237 match rx.recv().await {
238 Ok(msg) => {
239 if let Ok(envelope) = codec.decode(&msg.data) {
240 let namespaced = NamespacedMessage {
241 from: msg.from,
242 namespace: envelope.namespace.clone(),
243 msg_type: envelope.msg_type,
244 payload: envelope.payload,
245 timestamp: envelope.timestamp,
246 };
247
248 let _ = incoming_tx.send(namespaced.clone());
250
251 let filters = namespace_filters.read().await;
253 if let Some(tx) = filters.get(&namespaced.namespace) {
254 let _ = tx.send(namespaced);
255 }
256 } else {
257 tracing::warn!(
258 from = %msg.from,
259 data_len = msg.data.len(),
260 "node: failed to decode envelope from incoming message"
261 );
262 }
263 }
264 Err(broadcast::error::RecvError::Lagged(n)) => {
265 tracing::warn!(
266 missed = n,
267 "node: envelope router lagged, missed {n} messages"
268 );
269 continue;
270 }
271 Err(broadcast::error::RecvError::Closed) => {
272 tracing::debug!("node: session incoming channel closed, router exiting");
273 break;
274 }
275 }
276 }
277 });
278 }
279
280 pub fn builder() -> NodeBuilder {
284 NodeBuilder::default()
285 }
286
287 pub async fn stop(&self) {
295 tracing::info!("node: stopping");
296 }
300
301 pub fn local_info(&self) -> NodeIdentity {
305 self.network.local_identity()
306 }
307
308 pub async fn peers(&self) -> Vec<Peer> {
315 self.session
316 .peers()
317 .await
318 .into_iter()
319 .map(Peer::from)
320 .collect()
321 }
322
323 pub fn on_peer_change(&self) -> broadcast::Receiver<PeerEvent> {
325 self.session.on_peer_change()
326 }
327
328 pub async fn resolve_peer_id(&self, peer_id: &str) -> Result<String, NodeError> {
334 let peers = self.session.peers().await;
335 peers
336 .iter()
337 .find(|p| p.id == peer_id || p.name == peer_id)
338 .map(|p| p.id.clone())
339 .ok_or_else(|| NodeError::PeerNotFound(peer_id.to_string()))
340 }
341
342 pub async fn ping(&self, peer_id: &str) -> Result<PingResult, NodeError> {
348 let peers = self.session.peers().await;
349 let peer = peers
350 .iter()
351 .find(|p| p.id == peer_id || p.name == peer_id)
352 .ok_or_else(|| NodeError::PeerNotFound(peer_id.to_string()))?;
353
354 let addr = peer.ip.to_string();
355 self.network
356 .ping(&addr)
357 .await
358 .map_err(NodeError::Network)
359 }
360
361 pub async fn health(&self) -> HealthInfo {
363 self.network.health().await
364 }
365
366 pub async fn send(
374 &self,
375 peer_id: &str,
376 namespace: &str,
377 data: &[u8],
378 ) -> Result<(), NodeError> {
379 let payload = std::str::from_utf8(data)
385 .ok()
386 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
387 .unwrap_or_else(|| serde_json::Value::from(data.to_vec()));
388
389 let envelope = Envelope::new(
390 namespace,
391 "message",
392 payload,
393 )
394 .with_timestamp();
395
396 let encoded = self.codec.encode(&envelope)?;
397 self.session.send(peer_id, &encoded).await?;
398 Ok(())
399 }
400
401 pub async fn broadcast(&self, namespace: &str, data: &[u8]) {
406 let payload = std::str::from_utf8(data)
407 .ok()
408 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
409 .unwrap_or_else(|| serde_json::Value::from(data.to_vec()));
410
411 let envelope = Envelope::new(
412 namespace,
413 "message",
414 payload,
415 )
416 .with_timestamp();
417
418 match self.codec.encode(&envelope) {
419 Ok(encoded) => {
420 self.session.broadcast(&encoded).await;
421 }
422 Err(e) => {
423 tracing::error!("node: failed to encode broadcast envelope: {e}");
424 }
425 }
426 }
427
428 pub fn subscribe(&self, namespace: &str) -> broadcast::Receiver<NamespacedMessage> {
434 {
436 let filters = self.namespace_filters.blocking_lock_read();
437 if let Some(tx) = filters.get(namespace) {
438 return tx.subscribe();
439 }
440 }
441
442 let mut filters = self.namespace_filters.blocking_lock_write();
444 if let Some(tx) = filters.get(namespace) {
446 return tx.subscribe();
447 }
448 let (tx, rx) = broadcast::channel(256);
449 filters.insert(namespace.to_string(), tx);
450 rx
451 }
452
453 pub async fn open_tcp(
461 &self,
462 peer_id: &str,
463 port: u16,
464 ) -> Result<TcpStream, NodeError> {
465 let peers = self.session.peers().await;
466 let peer = peers
467 .iter()
468 .find(|p| p.id == peer_id || p.name == peer_id)
469 .ok_or_else(|| NodeError::PeerNotFound(peer_id.to_string()))?;
470
471 let addr = peer.ip.to_string();
472 self.network
473 .dial_tcp(&addr, port)
474 .await
475 .map_err(|e| NodeError::ConnectionFailed(e.to_string()))
476 }
477
478 pub async fn listen_tcp(&self, port: u16) -> Result<RawListener, NodeError> {
483 use crate::transport::tcp::TcpTransport;
484 use crate::transport::RawTransport;
485
486 let tcp = TcpTransport::new(self.network.clone());
487 tcp.listen(port).await.map_err(NodeError::Transport)
488 }
489
490 pub async fn open_quic(&self, _peer_id: &str) -> Result<(), NodeError> {
494 Err(NodeError::NotImplemented(
495 "QUIC connections are not yet implemented".to_string(),
496 ))
497 }
498
499 pub async fn open_udp(&self, _peer_id: &str) -> Result<NetworkUdpSocket, NodeError> {
503 Err(NodeError::NotImplemented(
504 "UDP sockets are not yet implemented".to_string(),
505 ))
506 }
507}
508
509trait RwLockBlockingExt<T> {
517 fn blocking_lock_read(&self) -> tokio::sync::RwLockReadGuard<'_, T>;
518 fn blocking_lock_write(&self) -> tokio::sync::RwLockWriteGuard<'_, T>;
519}
520
521impl<T> RwLockBlockingExt<T> for RwLock<T> {
522 fn blocking_lock_read(&self) -> tokio::sync::RwLockReadGuard<'_, T> {
523 self.try_read().unwrap_or_else(|_| {
525 panic!("node: namespace_filters read lock contended in sync context")
528 })
529 }
530
531 fn blocking_lock_write(&self) -> tokio::sync::RwLockWriteGuard<'_, T> {
532 self.try_write().unwrap_or_else(|_| {
533 panic!("node: namespace_filters write lock contended in sync context")
534 })
535 }
536}
537
538#[derive(Debug, Clone)]
558pub struct NodeBuilder {
559 name: Option<String>,
560 sidecar_path: Option<PathBuf>,
561 state_dir: Option<String>,
562 auth_key: Option<String>,
563 ephemeral: bool,
564 ws_port: u16,
565}
566
567impl Default for NodeBuilder {
568 fn default() -> Self {
569 Self {
570 name: None,
571 sidecar_path: None,
572 state_dir: None,
573 auth_key: None,
574 ephemeral: false,
575 ws_port: 9417,
576 }
577 }
578}
579
580impl NodeBuilder {
581 pub fn name(mut self, name: &str) -> Self {
583 self.name = Some(name.to_string());
584 self
585 }
586
587 pub fn sidecar_path(mut self, path: impl Into<PathBuf>) -> Self {
589 self.sidecar_path = Some(path.into());
590 self
591 }
592
593 pub fn state_dir(mut self, dir: &str) -> Self {
595 self.state_dir = Some(dir.to_string());
596 self
597 }
598
599 pub fn auth_key(mut self, key: &str) -> Self {
601 self.auth_key = Some(key.to_string());
602 self
603 }
604
605 pub fn ephemeral(mut self, val: bool) -> Self {
607 self.ephemeral = val;
608 self
609 }
610
611 pub fn ws_port(mut self, port: u16) -> Self {
613 self.ws_port = port;
614 self
615 }
616
617 pub async fn build(self) -> Result<Node<TailscaleProvider>, NodeError> {
628 let binary_path = self
629 .sidecar_path
630 .ok_or_else(|| NodeError::BuildError("sidecar_path is required".into()))?;
631
632 let hostname = self
633 .name
634 .ok_or_else(|| NodeError::BuildError("name is required".into()))?;
635
636 let state_dir = self
637 .state_dir
638 .unwrap_or_else(|| format!("/tmp/truffle-{hostname}"));
639
640 let config = TailscaleConfig {
642 binary_path,
643 hostname,
644 state_dir,
645 auth_key: self.auth_key,
646 ephemeral: if self.ephemeral { Some(true) } else { None },
647 tags: None,
648 };
649
650 let mut provider = TailscaleProvider::new(config);
651 provider.start().await.map_err(NodeError::Network)?;
652
653 let network = Arc::new(provider);
654
655 let ws_config = WsConfig {
657 port: self.ws_port,
658 ..Default::default()
659 };
660 let ws_transport = Arc::new(WebSocketTransport::new(network.clone(), ws_config));
661
662 let session = Arc::new(PeerRegistry::new(network.clone(), ws_transport));
664 session.start().await;
665
666 let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
668 let node = Node::from_parts(network, session, codec);
669
670 tracing::info!("node: started successfully");
671 Ok(node)
672 }
673}
674
675#[cfg(test)]
680mod tests {
681 use super::*;
682 use crate::network::{
683 HealthInfo, IncomingConnection, NetworkError, NetworkPeer, NetworkPeerEvent,
684 NetworkTcpListener, NetworkUdpSocket, PeerAddr,
685 };
686 use crate::transport::WsConfig;
687 use serde_json::json;
688 use std::time::Duration;
689 use tokio::sync::{broadcast, mpsc};
690
691 struct MockNetworkProvider {
694 identity: NodeIdentity,
695 local_addr: PeerAddr,
696 peer_event_tx: broadcast::Sender<NetworkPeerEvent>,
697 mock_peers: Arc<RwLock<Vec<NetworkPeer>>>,
699 }
700
701 impl MockNetworkProvider {
702 fn new(id: &str) -> Self {
703 let (peer_event_tx, _) = broadcast::channel(64);
704 Self {
705 identity: NodeIdentity {
706 id: id.to_string(),
707 hostname: format!("truffle-test-{id}"),
708 name: format!("Test Node {id}"),
709 dns_name: None,
710 ip: Some("127.0.0.1".parse().unwrap()),
711 },
712 local_addr: PeerAddr {
713 ip: Some("127.0.0.1".parse().unwrap()),
714 hostname: format!("truffle-test-{id}"),
715 dns_name: None,
716 },
717 peer_event_tx,
718 mock_peers: Arc::new(RwLock::new(Vec::new())),
719 }
720 }
721
722 fn event_sender(&self) -> broadcast::Sender<NetworkPeerEvent> {
723 self.peer_event_tx.clone()
724 }
725 }
726
727 impl NetworkProvider for MockNetworkProvider {
728 async fn start(&mut self) -> Result<(), NetworkError> {
729 Ok(())
730 }
731
732 async fn stop(&mut self) -> Result<(), NetworkError> {
733 Ok(())
734 }
735
736 fn local_identity(&self) -> NodeIdentity {
737 self.identity.clone()
738 }
739
740 fn local_addr(&self) -> PeerAddr {
741 self.local_addr.clone()
742 }
743
744 fn peer_events(&self) -> broadcast::Receiver<NetworkPeerEvent> {
745 self.peer_event_tx.subscribe()
746 }
747
748 async fn peers(&self) -> Vec<NetworkPeer> {
749 self.mock_peers.read().await.clone()
750 }
751
752 async fn dial_tcp(&self, addr: &str, port: u16) -> Result<TcpStream, NetworkError> {
753 let target = format!("{addr}:{port}");
754 TcpStream::connect(&target)
755 .await
756 .map_err(|e| NetworkError::DialFailed(format!("mock dial {target}: {e}")))
757 }
758
759 async fn listen_tcp(&self, port: u16) -> Result<NetworkTcpListener, NetworkError> {
760 let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{port}"))
761 .await
762 .map_err(|e| NetworkError::ListenFailed(format!("mock listen :{port}: {e}")))?;
763
764 let actual_port = listener.local_addr().unwrap().port();
765 let (tx, rx) = mpsc::channel::<IncomingConnection>(64);
766
767 tokio::spawn(async move {
768 loop {
769 match listener.accept().await {
770 Ok((stream, addr)) => {
771 let conn = IncomingConnection {
772 stream,
773 remote_addr: addr.to_string(),
774 remote_identity: String::new(),
775 port: actual_port,
776 };
777 if tx.send(conn).await.is_err() {
778 break;
779 }
780 }
781 Err(e) => {
782 tracing::debug!("mock listener error: {e}");
783 break;
784 }
785 }
786 }
787 });
788
789 Ok(NetworkTcpListener {
790 port: actual_port,
791 incoming: rx,
792 })
793 }
794
795 async fn unlisten_tcp(&self, _port: u16) -> Result<(), NetworkError> {
796 Ok(())
797 }
798
799 async fn bind_udp(&self, _port: u16) -> Result<NetworkUdpSocket, NetworkError> {
800 Err(NetworkError::Internal("mock: UDP not supported".into()))
801 }
802
803 async fn ping(&self, _addr: &str) -> Result<PingResult, NetworkError> {
804 Ok(PingResult {
805 latency: Duration::from_millis(1),
806 connection: "direct".to_string(),
807 peer_addr: None,
808 })
809 }
810
811 async fn health(&self) -> HealthInfo {
812 HealthInfo {
813 state: "running".to_string(),
814 healthy: true,
815 ..Default::default()
816 }
817 }
818 }
819
820 fn make_loopback_peer(id: &str) -> NetworkPeer {
823 NetworkPeer {
824 id: id.to_string(),
825 hostname: format!("truffle-test-{id}"),
826 ip: "127.0.0.1".parse().unwrap(),
827 online: true,
828 cur_addr: Some("127.0.0.1:41641".to_string()),
829 relay: None,
830 os: Some("linux".to_string()),
831 last_seen: Some("2026-03-25T12:00:00Z".to_string()),
832 key_expiry: None,
833 dns_name: None,
834 }
835 }
836
837 fn ws_config(port: u16) -> WsConfig {
838 WsConfig {
839 port,
840 ping_interval: Duration::from_secs(300),
841 pong_timeout: Duration::from_secs(300),
842 ..Default::default()
843 }
844 }
845
846 async fn random_port() -> u16 {
847 let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
848 l.local_addr().unwrap().port()
849 }
850
851 async fn make_test_node(
853 id: &str,
854 ws_port: u16,
855 ) -> (
856 Node<MockNetworkProvider>,
857 broadcast::Sender<NetworkPeerEvent>,
858 Arc<MockNetworkProvider>,
859 ) {
860 let provider = MockNetworkProvider::new(id);
861 let event_tx = provider.event_sender();
862 let network = Arc::new(provider);
863 let ws_transport = Arc::new(WebSocketTransport::new(
864 network.clone(),
865 ws_config(ws_port),
866 ));
867 let session = Arc::new(PeerRegistry::new(network.clone(), ws_transport));
868 session.start().await;
869
870 let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
871 let node = Node::from_parts(network.clone(), session, codec);
872
873 (node, event_tx, network)
874 }
875
876 #[tokio::test]
879 async fn test_node_builder_creates_node() {
880 let ws_port = random_port().await;
881 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
882
883 let identity = node.local_info();
884 assert_eq!(identity.id, "node-1");
885 assert!(identity.hostname.contains("node-1"));
886 }
887
888 #[tokio::test]
889 async fn test_node_peers_from_network() {
890 let ws_port = random_port().await;
891 let (node, event_tx, _network) = make_test_node("node-1", ws_port).await;
892
893 let peers = node.peers().await;
895 assert!(peers.is_empty());
896
897 let peer = make_loopback_peer("peer-a");
899 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
900 tokio::time::sleep(Duration::from_millis(50)).await;
901
902 let peers = node.peers().await;
903 assert_eq!(peers.len(), 1);
904 assert_eq!(peers[0].id, "peer-a");
905 assert!(peers[0].online);
906 assert!(!peers[0].connected);
907 }
908
909 #[tokio::test]
910 async fn test_node_send_to_unknown_peer_errors() {
911 let ws_port = random_port().await;
912 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
913
914 let result = node.send("nonexistent", "test", b"hello").await;
915 assert!(result.is_err());
916 let err_str = result.unwrap_err().to_string();
917 assert!(
918 err_str.contains("unknown peer") || err_str.contains("not found"),
919 "expected unknown peer error, got: {err_str}"
920 );
921 }
922
923 #[tokio::test]
924 async fn test_node_send_wraps_in_envelope() {
925 let codec = JsonCodec;
928 let data = b"hello world";
929 let envelope = Envelope::new(
930 "test-ns",
931 "message",
932 serde_json::Value::from(data.to_vec()),
933 )
934 .with_timestamp();
935
936 let encoded = codec.encode(&envelope).unwrap();
937 let decoded = codec.decode(&encoded).unwrap();
938
939 assert_eq!(decoded.namespace, "test-ns");
940 assert_eq!(decoded.msg_type, "message");
941 assert!(decoded.timestamp.is_some());
942 }
943
944 #[tokio::test]
945 async fn test_node_subscribe_filters_by_namespace() {
946 let ws_port = random_port().await;
947 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
948
949 let _rx_chat = node.subscribe("chat");
951 let _rx_ft = node.subscribe("ft");
952
953 let _rx_chat2 = node.subscribe("chat");
955 }
956
957 #[tokio::test]
958 async fn test_node_broadcast() {
959 let ws_port = random_port().await;
960 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
961
962 node.broadcast("test", b"hello everyone").await;
964 }
965
966 #[tokio::test]
967 async fn test_node_open_tcp_resolves_peer() {
968 let ws_port = random_port().await;
969 let (node, event_tx, _network) = make_test_node("node-1", ws_port).await;
970
971 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
973 let tcp_port = listener.local_addr().unwrap().port();
974
975 let peer = make_loopback_peer("peer-tcp");
977 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
978 tokio::time::sleep(Duration::from_millis(50)).await;
979
980 let accept_handle = tokio::spawn(async move {
982 let (stream, _) = listener.accept().await.unwrap();
983 stream
984 });
985
986 let stream = node.open_tcp("peer-tcp", tcp_port).await;
988 assert!(stream.is_ok(), "open_tcp failed: {:?}", stream.err());
989
990 let _ = accept_handle.await;
991 }
992
993 #[tokio::test]
994 async fn test_node_open_tcp_unknown_peer_errors() {
995 let ws_port = random_port().await;
996 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
997
998 let result = node.open_tcp("nonexistent", 8080).await;
999 assert!(result.is_err());
1000 let err_str = result.unwrap_err().to_string();
1001 assert!(
1002 err_str.contains("not found"),
1003 "expected peer not found error, got: {err_str}"
1004 );
1005 }
1006
1007 #[tokio::test]
1008 async fn test_node_ping_resolves_peer() {
1009 let ws_port = random_port().await;
1010 let (node, event_tx, _network) = make_test_node("node-1", ws_port).await;
1011
1012 let result = node.ping("peer-ping").await;
1014 assert!(result.is_err());
1015
1016 let peer = make_loopback_peer("peer-ping");
1018 let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
1019 tokio::time::sleep(Duration::from_millis(50)).await;
1020
1021 let result = node.ping("peer-ping").await;
1023 assert!(result.is_ok());
1024 assert_eq!(result.unwrap().latency, Duration::from_millis(1));
1025 }
1026
1027 #[tokio::test]
1028 async fn test_node_health() {
1029 let ws_port = random_port().await;
1030 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1031
1032 let health = node.health().await;
1033 assert!(health.healthy);
1034 assert_eq!(health.state, "running");
1035 }
1036
1037 #[tokio::test]
1038 async fn test_node_open_quic_not_implemented() {
1039 let ws_port = random_port().await;
1040 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1041
1042 let result = node.open_quic("peer").await;
1043 assert!(matches!(result, Err(NodeError::NotImplemented(_))));
1044 }
1045
1046 #[tokio::test]
1047 async fn test_node_open_udp_not_implemented() {
1048 let ws_port = random_port().await;
1049 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1050
1051 let result = node.open_udp("peer").await;
1052 assert!(matches!(result, Err(NodeError::NotImplemented(_))));
1053 }
1054
1055 #[tokio::test]
1056 async fn test_node_listen_tcp() {
1057 let ws_port = random_port().await;
1058 let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1059
1060 let listener = node.listen_tcp(0).await;
1062 assert!(listener.is_ok(), "listen_tcp failed: {:?}", listener.err());
1063 }
1064
1065 #[tokio::test]
1066 async fn test_envelope_serialize_deserialize() {
1067 let envelope =
1068 Envelope::new("chat", "message", json!({"text": "hello"})).with_timestamp();
1069
1070 let bytes = envelope.serialize().unwrap();
1071 let decoded = Envelope::deserialize(&bytes).unwrap();
1072
1073 assert_eq!(decoded.namespace, "chat");
1074 assert_eq!(decoded.msg_type, "message");
1075 assert_eq!(decoded.payload["text"], "hello");
1076 assert!(decoded.timestamp.is_some());
1077 }
1078
1079 #[tokio::test]
1080 async fn test_envelope_codec_json() {
1081 let codec = JsonCodec;
1082 let envelope = Envelope::new("ft", "offer", json!({"file": "test.bin"}));
1083
1084 let encoded = codec.encode(&envelope).unwrap();
1085 let decoded = codec.decode(&encoded).unwrap();
1086
1087 assert_eq!(decoded.namespace, "ft");
1088 assert_eq!(decoded.payload["file"], "test.bin");
1089 }
1090
1091 #[tokio::test]
1092 async fn test_envelope_unknown_fields_ignored() {
1093 let json_bytes = br#"{
1094 "namespace": "v2",
1095 "msg_type": "new",
1096 "payload": {},
1097 "future_field": "ignored"
1098 }"#;
1099
1100 let codec = JsonCodec;
1101 let decoded = codec.decode(json_bytes).unwrap();
1102 assert_eq!(decoded.namespace, "v2");
1103 assert_eq!(decoded.msg_type, "new");
1104 }
1105
1106 #[tokio::test]
1107 async fn test_node_send_and_receive_roundtrip() {
1108 let port_a = random_port().await;
1110 let port_b = random_port().await;
1111
1112 let (node_a, event_tx_a, _net_a) = make_test_node("node-a", port_a).await;
1113 let (node_b, event_tx_b, _net_b) = make_test_node("node-b", port_b).await;
1114
1115 let peer_b = NetworkPeer {
1117 id: "node-b".to_string(),
1118 hostname: "truffle-test-node-b".to_string(),
1119 ip: "127.0.0.1".parse().unwrap(),
1120 online: true,
1121 cur_addr: Some("127.0.0.1:41641".to_string()),
1122 relay: None,
1123 os: None,
1124 last_seen: None,
1125 key_expiry: None,
1126 dns_name: None,
1127 };
1128 let peer_a = NetworkPeer {
1129 id: "node-a".to_string(),
1130 hostname: "truffle-test-node-a".to_string(),
1131 ip: "127.0.0.1".parse().unwrap(),
1132 online: true,
1133 cur_addr: Some("127.0.0.1:41641".to_string()),
1134 relay: None,
1135 os: None,
1136 last_seen: None,
1137 key_expiry: None,
1138 dns_name: None,
1139 };
1140
1141 let _ = event_tx_a.send(NetworkPeerEvent::Joined(peer_b));
1142 let _ = event_tx_b.send(NetworkPeerEvent::Joined(peer_a));
1143 tokio::time::sleep(Duration::from_millis(100)).await;
1144
1145 let mut rx = node_b.subscribe("test");
1147
1148 let send_result = node_a.send("node-b", "test", b"hello from a").await;
1151
1152 if send_result.is_ok() {
1160 let msg = tokio::time::timeout(Duration::from_secs(2), rx.recv()).await;
1162 if let Ok(Ok(msg)) = msg {
1163 assert_eq!(msg.namespace, "test");
1164 }
1165 }
1166 }
1169}