use std::collections::HashMap;
use std::net::IpAddr;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::net::TcpStream;
use tokio::sync::{broadcast, RwLock};
use crate::envelope::codec::{EnvelopeCodec, JsonCodec};
use crate::envelope::{Envelope, EnvelopeError};
use crate::file_transfer::{self, FileTransferState};
use crate::network::tailscale::{TailscaleConfig, TailscaleProvider};
use crate::network::{
HealthInfo, NetworkProvider, NetworkUdpSocket, NodeIdentity, PingResult,
};
use crate::session::{PeerEvent, PeerRegistry, PeerState};
use crate::transport::websocket::WebSocketTransport;
use crate::transport::{RawListener, WsConfig};
#[derive(Debug, Clone)]
pub struct NamespacedMessage {
pub from: String,
pub namespace: String,
pub msg_type: String,
pub payload: serde_json::Value,
pub timestamp: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct Peer {
pub id: String,
pub name: String,
pub ip: IpAddr,
pub online: bool,
pub connected: bool,
pub connection_type: String,
pub os: Option<String>,
pub last_seen: Option<String>,
}
impl From<PeerState> for Peer {
fn from(s: PeerState) -> Self {
Self {
id: s.id,
name: s.name,
ip: s.ip,
online: s.online,
connected: s.connected,
connection_type: s.connection_type,
os: s.os,
last_seen: s.last_seen,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum NodeError {
#[error("peer not found: {0}")]
PeerNotFound(String),
#[error("connection failed: {0}")]
ConnectionFailed(String),
#[error("send failed: {0}")]
SendFailed(String),
#[error("envelope error: {0}")]
Envelope(#[from] EnvelopeError),
#[error("session error: {0}")]
Session(#[from] crate::session::SessionError),
#[error("network error: {0}")]
Network(#[from] crate::network::NetworkError),
#[error("transport error: {0}")]
Transport(#[from] crate::transport::TransportError),
#[error("not implemented: {0}")]
NotImplemented(String),
#[error("node stopped")]
Stopped,
#[error("build error: {0}")]
BuildError(String),
}
pub struct Node<N: NetworkProvider + 'static> {
network: Arc<N>,
session: Arc<PeerRegistry<N>>,
codec: Arc<dyn EnvelopeCodec>,
#[allow(dead_code)]
incoming_tx: broadcast::Sender<NamespacedMessage>,
namespace_filters: Arc<RwLock<HashMap<String, broadcast::Sender<NamespacedMessage>>>>,
pub(crate) file_transfer_state: FileTransferState,
}
impl<N: NetworkProvider + 'static> Node<N> {
pub(crate) fn from_parts(
network: Arc<N>,
session: Arc<PeerRegistry<N>>,
codec: Arc<dyn EnvelopeCodec>,
) -> Self {
let (incoming_tx, _) = broadcast::channel(1024);
let namespace_filters: Arc<RwLock<HashMap<String, broadcast::Sender<NamespacedMessage>>>> =
Arc::new(RwLock::new(HashMap::new()));
let node = Self {
network,
session: session.clone(),
codec: codec.clone(),
incoming_tx: incoming_tx.clone(),
namespace_filters: namespace_filters.clone(),
file_transfer_state: FileTransferState::new(),
};
node.spawn_envelope_router(session, codec, incoming_tx, namespace_filters);
node
}
fn spawn_envelope_router(
&self,
session: Arc<PeerRegistry<N>>,
codec: Arc<dyn EnvelopeCodec>,
incoming_tx: broadcast::Sender<NamespacedMessage>,
namespace_filters: Arc<RwLock<HashMap<String, broadcast::Sender<NamespacedMessage>>>>,
) {
let mut rx = session.subscribe();
tokio::spawn(async move {
loop {
match rx.recv().await {
Ok(msg) => {
if let Ok(envelope) = codec.decode(&msg.data) {
let namespaced = NamespacedMessage {
from: msg.from,
namespace: envelope.namespace.clone(),
msg_type: envelope.msg_type,
payload: envelope.payload,
timestamp: envelope.timestamp,
};
tracing::debug!(
from = %namespaced.from,
namespace = %namespaced.namespace,
msg_type = %namespaced.msg_type,
"envelope router: dispatching message"
);
let _ = incoming_tx.send(namespaced.clone());
let filters = namespace_filters.read().await;
let _has_subscriber = filters.contains_key(&namespaced.namespace);
if let Some(tx) = filters.get(&namespaced.namespace) {
let send_result = tx.send(namespaced);
tracing::debug!(
namespace = %envelope.namespace,
subscriber_count = tx.receiver_count(),
sent = send_result.is_ok(),
"envelope router: sent to namespace subscriber"
);
} else {
tracing::debug!(
namespace = %envelope.namespace,
"envelope router: no subscriber for namespace"
);
}
} else {
tracing::warn!(
from = %msg.from,
data_len = msg.data.len(),
"node: failed to decode envelope from incoming message"
);
}
}
Err(broadcast::error::RecvError::Lagged(n)) => {
tracing::warn!(
missed = n,
"node: envelope router lagged, missed {n} messages"
);
continue;
}
Err(broadcast::error::RecvError::Closed) => {
tracing::debug!("node: session incoming channel closed, router exiting");
break;
}
}
}
});
}
pub fn builder() -> NodeBuilder {
NodeBuilder::default()
}
pub fn file_transfer(&self) -> file_transfer::FileTransfer<'_, N> {
file_transfer::FileTransfer::new(self)
}
pub async fn stop(&self) {
tracing::info!("node: stopping");
}
pub fn local_info(&self) -> NodeIdentity {
self.network.local_identity()
}
pub async fn peers(&self) -> Vec<Peer> {
self.session
.peers()
.await
.into_iter()
.map(Peer::from)
.collect()
}
pub fn on_peer_change(&self) -> broadcast::Receiver<PeerEvent> {
self.session.on_peer_change()
}
pub async fn resolve_peer_id(&self, peer_id: &str) -> Result<String, NodeError> {
let peers = self.session.peers().await;
peers
.iter()
.find(|p| p.id == peer_id || p.name == peer_id)
.map(|p| p.id.clone())
.ok_or_else(|| NodeError::PeerNotFound(peer_id.to_string()))
}
pub async fn ping(&self, peer_id: &str) -> Result<PingResult, NodeError> {
let peers = self.session.peers().await;
let peer = peers
.iter()
.find(|p| p.id == peer_id || p.name == peer_id)
.ok_or_else(|| NodeError::PeerNotFound(peer_id.to_string()))?;
let addr = peer.ip.to_string();
self.network
.ping(&addr)
.await
.map_err(NodeError::Network)
}
pub async fn health(&self) -> HealthInfo {
self.network.health().await
}
pub async fn send(
&self,
peer_id: &str,
namespace: &str,
data: &[u8],
) -> Result<(), NodeError> {
let payload = std::str::from_utf8(data)
.ok()
.and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
.unwrap_or_else(|| serde_json::Value::from(data.to_vec()));
let envelope = Envelope::new(
namespace,
"message",
payload,
)
.with_timestamp();
let encoded = self.codec.encode(&envelope)?;
self.session.send(peer_id, &encoded).await?;
Ok(())
}
pub async fn broadcast(&self, namespace: &str, data: &[u8]) {
let payload = std::str::from_utf8(data)
.ok()
.and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
.unwrap_or_else(|| serde_json::Value::from(data.to_vec()));
let envelope = Envelope::new(
namespace,
"message",
payload,
)
.with_timestamp();
match self.codec.encode(&envelope) {
Ok(encoded) => {
self.session.broadcast(&encoded).await;
}
Err(e) => {
tracing::error!("node: failed to encode broadcast envelope: {e}");
}
}
}
pub fn subscribe(&self, namespace: &str) -> broadcast::Receiver<NamespacedMessage> {
{
let filters = self.namespace_filters.blocking_lock_read();
if let Some(tx) = filters.get(namespace) {
return tx.subscribe();
}
}
let mut filters = self.namespace_filters.blocking_lock_write();
if let Some(tx) = filters.get(namespace) {
return tx.subscribe();
}
let (tx, rx) = broadcast::channel(256);
filters.insert(namespace.to_string(), tx);
rx
}
pub async fn open_tcp(
&self,
peer_id: &str,
port: u16,
) -> Result<TcpStream, NodeError> {
let peers = self.session.peers().await;
let peer = peers
.iter()
.find(|p| p.id == peer_id || p.name == peer_id)
.ok_or_else(|| NodeError::PeerNotFound(peer_id.to_string()))?;
let addr = peer.ip.to_string();
self.network
.dial_tcp(&addr, port)
.await
.map_err(|e| NodeError::ConnectionFailed(e.to_string()))
}
pub async fn listen_tcp(&self, port: u16) -> Result<RawListener, NodeError> {
use crate::transport::tcp::TcpTransport;
use crate::transport::RawTransport;
let tcp = TcpTransport::new(self.network.clone());
tcp.listen(port).await.map_err(NodeError::Transport)
}
pub async fn open_quic(&self, _peer_id: &str) -> Result<(), NodeError> {
Err(NodeError::NotImplemented(
"QUIC connections are not yet implemented".to_string(),
))
}
pub async fn open_udp(&self, _peer_id: &str) -> Result<NetworkUdpSocket, NodeError> {
Err(NodeError::NotImplemented(
"UDP sockets are not yet implemented".to_string(),
))
}
}
trait RwLockBlockingExt<T> {
fn blocking_lock_read(&self) -> tokio::sync::RwLockReadGuard<'_, T>;
fn blocking_lock_write(&self) -> tokio::sync::RwLockWriteGuard<'_, T>;
}
impl<T> RwLockBlockingExt<T> for RwLock<T> {
fn blocking_lock_read(&self) -> tokio::sync::RwLockReadGuard<'_, T> {
self.try_read().unwrap_or_else(|_| {
panic!("node: namespace_filters read lock contended in sync context")
})
}
fn blocking_lock_write(&self) -> tokio::sync::RwLockWriteGuard<'_, T> {
self.try_write().unwrap_or_else(|_| {
panic!("node: namespace_filters write lock contended in sync context")
})
}
}
#[derive(Debug, Clone)]
pub struct NodeBuilder {
name: Option<String>,
sidecar_path: Option<PathBuf>,
state_dir: Option<String>,
auth_key: Option<String>,
ephemeral: bool,
ws_port: u16,
}
impl Default for NodeBuilder {
fn default() -> Self {
Self {
name: None,
sidecar_path: None,
state_dir: None,
auth_key: None,
ephemeral: false,
ws_port: 9417,
}
}
}
impl NodeBuilder {
pub fn name(mut self, name: &str) -> Self {
self.name = Some(name.to_string());
self
}
pub fn sidecar_path(mut self, path: impl Into<PathBuf>) -> Self {
self.sidecar_path = Some(path.into());
self
}
pub fn state_dir(mut self, dir: &str) -> Self {
self.state_dir = Some(dir.to_string());
self
}
pub fn auth_key(mut self, key: &str) -> Self {
self.auth_key = Some(key.to_string());
self
}
pub fn ephemeral(mut self, val: bool) -> Self {
self.ephemeral = val;
self
}
pub fn ws_port(mut self, port: u16) -> Self {
self.ws_port = port;
self
}
pub async fn build(self) -> Result<Node<TailscaleProvider>, NodeError> {
let binary_path = self
.sidecar_path
.ok_or_else(|| NodeError::BuildError("sidecar_path is required".into()))?;
let hostname = self
.name
.ok_or_else(|| NodeError::BuildError("name is required".into()))?;
let state_dir = self
.state_dir
.unwrap_or_else(|| format!("/tmp/truffle-{hostname}"));
let config = TailscaleConfig {
binary_path,
hostname,
state_dir,
auth_key: self.auth_key,
ephemeral: if self.ephemeral { Some(true) } else { None },
tags: None,
};
let mut provider = TailscaleProvider::new(config);
provider.start().await.map_err(NodeError::Network)?;
let network = Arc::new(provider);
let ws_config = WsConfig {
port: self.ws_port,
..Default::default()
};
let ws_transport = Arc::new(WebSocketTransport::new(network.clone(), ws_config));
let session = Arc::new(PeerRegistry::new(network.clone(), ws_transport));
session.start().await;
let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
let node = Node::from_parts(network, session, codec);
tracing::info!("node: started successfully");
Ok(node)
}
pub async fn build_with_auth_handler(
self,
on_auth: impl Fn(String) + Send + 'static,
) -> Result<Node<TailscaleProvider>, NodeError> {
let binary_path = self
.sidecar_path
.ok_or_else(|| NodeError::BuildError("sidecar_path is required".into()))?;
let hostname = self
.name
.ok_or_else(|| NodeError::BuildError("name is required".into()))?;
let state_dir = self
.state_dir
.unwrap_or_else(|| format!("/tmp/truffle-{hostname}"));
let config = TailscaleConfig {
binary_path,
hostname,
state_dir,
auth_key: self.auth_key,
ephemeral: if self.ephemeral { Some(true) } else { None },
tags: None,
};
let mut provider = TailscaleProvider::new(config);
let mut auth_rx = provider.peer_events();
let auth_task = tokio::spawn(async move {
use crate::network::NetworkPeerEvent;
loop {
match auth_rx.recv().await {
Ok(NetworkPeerEvent::AuthRequired { url }) => {
on_auth(url);
}
Err(broadcast::error::RecvError::Closed) => break,
Err(broadcast::error::RecvError::Lagged(_)) => continue,
_ => {} }
}
});
let start_result = provider.start().await.map_err(NodeError::Network);
auth_task.abort();
start_result?;
let network = Arc::new(provider);
let ws_config = WsConfig {
port: self.ws_port,
..Default::default()
};
let ws_transport = Arc::new(WebSocketTransport::new(network.clone(), ws_config));
let session = Arc::new(PeerRegistry::new(network.clone(), ws_transport));
session.start().await;
let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
let node = Node::from_parts(network, session, codec);
tracing::info!("node: started successfully (with auth handler)");
Ok(node)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::network::{
HealthInfo, IncomingConnection, NetworkError, NetworkPeer, NetworkPeerEvent,
NetworkTcpListener, NetworkUdpSocket, PeerAddr,
};
use crate::transport::WsConfig;
use serde_json::json;
use std::time::Duration;
use tokio::sync::{broadcast, mpsc};
struct MockNetworkProvider {
identity: NodeIdentity,
local_addr: PeerAddr,
peer_event_tx: broadcast::Sender<NetworkPeerEvent>,
mock_peers: Arc<RwLock<Vec<NetworkPeer>>>,
}
impl MockNetworkProvider {
fn new(id: &str) -> Self {
let (peer_event_tx, _) = broadcast::channel(64);
Self {
identity: NodeIdentity {
id: id.to_string(),
hostname: format!("truffle-test-{id}"),
name: format!("Test Node {id}"),
dns_name: None,
ip: Some("127.0.0.1".parse().unwrap()),
},
local_addr: PeerAddr {
ip: Some("127.0.0.1".parse().unwrap()),
hostname: format!("truffle-test-{id}"),
dns_name: None,
},
peer_event_tx,
mock_peers: Arc::new(RwLock::new(Vec::new())),
}
}
fn event_sender(&self) -> broadcast::Sender<NetworkPeerEvent> {
self.peer_event_tx.clone()
}
}
impl NetworkProvider for MockNetworkProvider {
async fn start(&mut self) -> Result<(), NetworkError> {
Ok(())
}
async fn stop(&mut self) -> Result<(), NetworkError> {
Ok(())
}
fn local_identity(&self) -> NodeIdentity {
self.identity.clone()
}
fn local_addr(&self) -> PeerAddr {
self.local_addr.clone()
}
fn peer_events(&self) -> broadcast::Receiver<NetworkPeerEvent> {
self.peer_event_tx.subscribe()
}
async fn peers(&self) -> Vec<NetworkPeer> {
self.mock_peers.read().await.clone()
}
async fn dial_tcp(&self, addr: &str, port: u16) -> Result<TcpStream, NetworkError> {
let target = format!("{addr}:{port}");
TcpStream::connect(&target)
.await
.map_err(|e| NetworkError::DialFailed(format!("mock dial {target}: {e}")))
}
async fn listen_tcp(&self, port: u16) -> Result<NetworkTcpListener, NetworkError> {
let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{port}"))
.await
.map_err(|e| NetworkError::ListenFailed(format!("mock listen :{port}: {e}")))?;
let actual_port = listener.local_addr().unwrap().port();
let (tx, rx) = mpsc::channel::<IncomingConnection>(64);
tokio::spawn(async move {
loop {
match listener.accept().await {
Ok((stream, addr)) => {
let conn = IncomingConnection {
stream,
remote_addr: addr.to_string(),
remote_identity: String::new(),
port: actual_port,
};
if tx.send(conn).await.is_err() {
break;
}
}
Err(e) => {
tracing::debug!("mock listener error: {e}");
break;
}
}
}
});
Ok(NetworkTcpListener {
port: actual_port,
incoming: rx,
})
}
async fn unlisten_tcp(&self, _port: u16) -> Result<(), NetworkError> {
Ok(())
}
async fn bind_udp(&self, _port: u16) -> Result<NetworkUdpSocket, NetworkError> {
Err(NetworkError::Internal("mock: UDP not supported".into()))
}
async fn ping(&self, _addr: &str) -> Result<PingResult, NetworkError> {
Ok(PingResult {
latency: Duration::from_millis(1),
connection: "direct".to_string(),
peer_addr: None,
})
}
async fn health(&self) -> HealthInfo {
HealthInfo {
state: "running".to_string(),
healthy: true,
..Default::default()
}
}
}
fn make_loopback_peer(id: &str) -> NetworkPeer {
NetworkPeer {
id: id.to_string(),
hostname: format!("truffle-test-{id}"),
ip: "127.0.0.1".parse().unwrap(),
online: true,
cur_addr: Some("127.0.0.1:41641".to_string()),
relay: None,
os: Some("linux".to_string()),
last_seen: Some("2026-03-25T12:00:00Z".to_string()),
key_expiry: None,
dns_name: None,
}
}
fn ws_config(port: u16) -> WsConfig {
WsConfig {
port,
ping_interval: Duration::from_secs(300),
pong_timeout: Duration::from_secs(300),
..Default::default()
}
}
async fn random_port() -> u16 {
let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
l.local_addr().unwrap().port()
}
async fn make_test_node(
id: &str,
ws_port: u16,
) -> (
Node<MockNetworkProvider>,
broadcast::Sender<NetworkPeerEvent>,
Arc<MockNetworkProvider>,
) {
let provider = MockNetworkProvider::new(id);
let event_tx = provider.event_sender();
let network = Arc::new(provider);
let ws_transport = Arc::new(WebSocketTransport::new(
network.clone(),
ws_config(ws_port),
));
let session = Arc::new(PeerRegistry::new(network.clone(), ws_transport));
session.start().await;
let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
let node = Node::from_parts(network.clone(), session, codec);
(node, event_tx, network)
}
#[tokio::test]
async fn test_node_builder_creates_node() {
let ws_port = random_port().await;
let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
let identity = node.local_info();
assert_eq!(identity.id, "node-1");
assert!(identity.hostname.contains("node-1"));
}
#[tokio::test]
async fn test_node_peers_from_network() {
let ws_port = random_port().await;
let (node, event_tx, _network) = make_test_node("node-1", ws_port).await;
let peers = node.peers().await;
assert!(peers.is_empty());
let peer = make_loopback_peer("peer-a");
let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
tokio::time::sleep(Duration::from_millis(50)).await;
let peers = node.peers().await;
assert_eq!(peers.len(), 1);
assert_eq!(peers[0].id, "peer-a");
assert!(peers[0].online);
assert!(!peers[0].connected);
}
#[tokio::test]
async fn test_node_send_to_unknown_peer_errors() {
let ws_port = random_port().await;
let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
let result = node.send("nonexistent", "test", b"hello").await;
assert!(result.is_err());
let err_str = result.unwrap_err().to_string();
assert!(
err_str.contains("unknown peer") || err_str.contains("not found"),
"expected unknown peer error, got: {err_str}"
);
}
#[tokio::test]
async fn test_node_send_wraps_in_envelope() {
let codec = JsonCodec;
let data = b"hello world";
let envelope = Envelope::new(
"test-ns",
"message",
serde_json::Value::from(data.to_vec()),
)
.with_timestamp();
let encoded = codec.encode(&envelope).unwrap();
let decoded = codec.decode(&encoded).unwrap();
assert_eq!(decoded.namespace, "test-ns");
assert_eq!(decoded.msg_type, "message");
assert!(decoded.timestamp.is_some());
}
#[tokio::test]
async fn test_node_subscribe_filters_by_namespace() {
let ws_port = random_port().await;
let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
let _rx_chat = node.subscribe("chat");
let _rx_ft = node.subscribe("ft");
let _rx_chat2 = node.subscribe("chat");
}
#[tokio::test]
async fn test_node_broadcast() {
let ws_port = random_port().await;
let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
node.broadcast("test", b"hello everyone").await;
}
#[tokio::test]
async fn test_node_open_tcp_resolves_peer() {
let ws_port = random_port().await;
let (node, event_tx, _network) = make_test_node("node-1", ws_port).await;
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let tcp_port = listener.local_addr().unwrap().port();
let peer = make_loopback_peer("peer-tcp");
let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
tokio::time::sleep(Duration::from_millis(50)).await;
let accept_handle = tokio::spawn(async move {
let (stream, _) = listener.accept().await.unwrap();
stream
});
let stream = node.open_tcp("peer-tcp", tcp_port).await;
assert!(stream.is_ok(), "open_tcp failed: {:?}", stream.err());
let _ = accept_handle.await;
}
#[tokio::test]
async fn test_node_open_tcp_unknown_peer_errors() {
let ws_port = random_port().await;
let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
let result = node.open_tcp("nonexistent", 8080).await;
assert!(result.is_err());
let err_str = result.unwrap_err().to_string();
assert!(
err_str.contains("not found"),
"expected peer not found error, got: {err_str}"
);
}
#[tokio::test]
async fn test_node_ping_resolves_peer() {
let ws_port = random_port().await;
let (node, event_tx, _network) = make_test_node("node-1", ws_port).await;
let result = node.ping("peer-ping").await;
assert!(result.is_err());
let peer = make_loopback_peer("peer-ping");
let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
tokio::time::sleep(Duration::from_millis(50)).await;
let result = node.ping("peer-ping").await;
assert!(result.is_ok());
assert_eq!(result.unwrap().latency, Duration::from_millis(1));
}
#[tokio::test]
async fn test_node_health() {
let ws_port = random_port().await;
let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
let health = node.health().await;
assert!(health.healthy);
assert_eq!(health.state, "running");
}
#[tokio::test]
async fn test_node_open_quic_not_implemented() {
let ws_port = random_port().await;
let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
let result = node.open_quic("peer").await;
assert!(matches!(result, Err(NodeError::NotImplemented(_))));
}
#[tokio::test]
async fn test_node_open_udp_not_implemented() {
let ws_port = random_port().await;
let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
let result = node.open_udp("peer").await;
assert!(matches!(result, Err(NodeError::NotImplemented(_))));
}
#[tokio::test]
async fn test_node_listen_tcp() {
let ws_port = random_port().await;
let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
let listener = node.listen_tcp(0).await;
assert!(listener.is_ok(), "listen_tcp failed: {:?}", listener.err());
}
#[tokio::test]
async fn test_envelope_serialize_deserialize() {
let envelope =
Envelope::new("chat", "message", json!({"text": "hello"})).with_timestamp();
let bytes = envelope.serialize().unwrap();
let decoded = Envelope::deserialize(&bytes).unwrap();
assert_eq!(decoded.namespace, "chat");
assert_eq!(decoded.msg_type, "message");
assert_eq!(decoded.payload["text"], "hello");
assert!(decoded.timestamp.is_some());
}
#[tokio::test]
async fn test_envelope_codec_json() {
let codec = JsonCodec;
let envelope = Envelope::new("ft", "offer", json!({"file": "test.bin"}));
let encoded = codec.encode(&envelope).unwrap();
let decoded = codec.decode(&encoded).unwrap();
assert_eq!(decoded.namespace, "ft");
assert_eq!(decoded.payload["file"], "test.bin");
}
#[tokio::test]
async fn test_envelope_unknown_fields_ignored() {
let json_bytes = br#"{
"namespace": "v2",
"msg_type": "new",
"payload": {},
"future_field": "ignored"
}"#;
let codec = JsonCodec;
let decoded = codec.decode(json_bytes).unwrap();
assert_eq!(decoded.namespace, "v2");
assert_eq!(decoded.msg_type, "new");
}
#[tokio::test]
async fn test_node_send_and_receive_roundtrip() {
let port_a = random_port().await;
let port_b = random_port().await;
let (node_a, event_tx_a, _net_a) = make_test_node("node-a", port_a).await;
let (node_b, event_tx_b, _net_b) = make_test_node("node-b", port_b).await;
let peer_b = NetworkPeer {
id: "node-b".to_string(),
hostname: "truffle-test-node-b".to_string(),
ip: "127.0.0.1".parse().unwrap(),
online: true,
cur_addr: Some("127.0.0.1:41641".to_string()),
relay: None,
os: None,
last_seen: None,
key_expiry: None,
dns_name: None,
};
let peer_a = NetworkPeer {
id: "node-a".to_string(),
hostname: "truffle-test-node-a".to_string(),
ip: "127.0.0.1".parse().unwrap(),
online: true,
cur_addr: Some("127.0.0.1:41641".to_string()),
relay: None,
os: None,
last_seen: None,
key_expiry: None,
dns_name: None,
};
let _ = event_tx_a.send(NetworkPeerEvent::Joined(peer_b));
let _ = event_tx_b.send(NetworkPeerEvent::Joined(peer_a));
tokio::time::sleep(Duration::from_millis(100)).await;
let mut rx = node_b.subscribe("test");
let send_result = node_a.send("node-b", "test", b"hello from a").await;
if send_result.is_ok() {
let msg = tokio::time::timeout(Duration::from_secs(2), rx.recv()).await;
if let Ok(Ok(msg)) = msg {
assert_eq!(msg.namespace, "test");
}
}
}
}