use std::collections::{BTreeMap, BTreeSet};
use std::io;
#[cfg(not(target_arch = "wasm32"))]
use std::path::Path;
use meerkat_core::comms::{PeerAddress, PeerId, PeerName};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use thiserror::Error;
use crate::identity::{IdentityError, PubKey};
use crate::peer_meta::PeerMeta;
#[derive(Debug, Error)]
pub enum TrustError {
#[error("IO error: {0}")]
Io(#[from] io::Error),
#[error("JSON parse error: {0}")]
Json(#[from] serde_json::Error),
#[error("Invalid peer ID: {0}")]
InvalidPeerId(#[from] IdentityError),
#[error("duplicate peer id: {peer_id}")]
DuplicatePeerId { peer_id: PeerId },
#[error("trusted peer pubkey must be non-zero for {name}")]
ZeroPubkey { name: String },
#[error(
"trusted peer id {peer_id} does not match pubkey-derived id {derived_peer_id} for {name}"
)]
PeerIdPubkeyMismatch {
name: String,
peer_id: PeerId,
derived_peer_id: PeerId,
},
}
#[derive(Debug, Clone, Error, PartialEq, Eq)]
pub enum TrustResolveError {
#[error("no trusted peer with name {0:?}")]
NotFound(PeerName),
#[error("ambiguous peer name {name:?}: {} candidates", candidates.len())]
Ambiguous {
name: PeerName,
candidates: Vec<PeerId>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TrustEntry {
pub peer_id: PeerId,
pub name: PeerName,
pub pubkey: PubKey,
pub address: PeerAddress,
pub meta: PeerMeta,
}
impl TrustEntry {
fn validate(&self) -> Result<(), TrustError> {
if self.pubkey.is_zero() {
return Err(TrustError::ZeroPubkey {
name: self.name.as_str().to_string(),
});
}
let derived_peer_id = self.pubkey.to_peer_id();
if derived_peer_id != self.peer_id {
return Err(TrustError::PeerIdPubkeyMismatch {
name: self.name.as_str().to_string(),
peer_id: self.peer_id,
derived_peer_id,
});
}
Ok(())
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TrustStore {
entries: BTreeMap<PeerId, TrustEntry>,
}
impl TrustStore {
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, entry: TrustEntry) -> Result<(), TrustError> {
entry.validate()?;
if self.entries.contains_key(&entry.peer_id) {
return Err(TrustError::DuplicatePeerId {
peer_id: entry.peer_id,
});
}
self.entries.insert(entry.peer_id, entry);
Ok(())
}
pub fn upsert(&mut self, entry: TrustEntry) -> Result<Option<TrustEntry>, TrustError> {
entry.validate()?;
Ok(self.entries.insert(entry.peer_id, entry))
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn get(&self, peer_id: &PeerId) -> Option<&TrustEntry> {
self.entries.get(peer_id)
}
pub fn contains(&self, peer_id: &PeerId) -> bool {
self.entries.contains_key(peer_id)
}
pub fn remove(&mut self, peer_id: &PeerId) -> Option<TrustEntry> {
self.entries.remove(peer_id)
}
pub fn entries(&self) -> impl Iterator<Item = &TrustEntry> {
self.entries.values()
}
pub fn resolve_name(&self, name: &PeerName) -> Result<PeerId, TrustResolveError> {
let mut hits = self
.entries
.values()
.filter(|e| &e.name == name)
.map(|e| e.peer_id);
let first = match hits.next() {
Some(id) => id,
None => return Err(TrustResolveError::NotFound(name.clone())),
};
let mut candidates = vec![first];
for extra in hits {
candidates.push(extra);
}
if candidates.len() == 1 {
Ok(first)
} else {
Err(TrustResolveError::Ambiguous {
name: name.clone(),
candidates,
})
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TrustedPeer {
pub name: String,
pub pubkey: PubKey,
pub addr: String,
pub meta: PeerMeta,
}
impl TrustedPeer {
pub fn validate(&self) -> Result<(), TrustError> {
if self.pubkey.is_zero() {
return Err(TrustError::ZeroPubkey {
name: self.name.clone(),
});
}
Ok(())
}
pub(crate) fn has_raw_sendable_identity(&self) -> bool {
self.validate().is_ok()
}
}
impl Serialize for TrustedPeer {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
use serde::ser::SerializeStruct;
let mut s = serializer.serialize_struct("TrustedPeer", 4)?;
s.serialize_field("name", &self.name)?;
s.serialize_field("pubkey", &self.pubkey.to_pubkey_string())?;
s.serialize_field("addr", &self.addr)?;
s.serialize_field("meta", &self.meta)?;
s.end()
}
}
impl<'de> Deserialize<'de> for TrustedPeer {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct TrustedPeerHelper {
name: String,
pubkey: String,
addr: String,
#[serde(default)]
meta: PeerMeta,
}
let helper = TrustedPeerHelper::deserialize(deserializer)?;
let pubkey =
PubKey::from_pubkey_string(&helper.pubkey).map_err(serde::de::Error::custom)?;
let peer = TrustedPeer {
name: helper.name,
pubkey,
addr: helper.addr,
meta: helper.meta,
};
peer.validate().map_err(serde::de::Error::custom)?;
Ok(peer)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct TrustedPeers {
pub peers: Vec<TrustedPeer>,
}
impl TrustedPeers {
pub fn new() -> Self {
Self::default()
}
pub fn is_empty(&self) -> bool {
self.peers.is_empty()
}
pub fn has_peers(&self) -> bool {
!self.peers.is_empty()
}
pub fn len(&self) -> usize {
self.peers.len()
}
pub(crate) fn retain_raw_sendable_identities(&mut self) {
self.peers.retain(|peer| peer.validate().is_ok());
}
#[cfg(not(target_arch = "wasm32"))]
pub fn load_or_default(path: &Path) -> Result<Self, TrustError> {
if !path.exists() {
return Ok(Self::default());
}
let content = std::fs::read_to_string(path)?;
let peers: Self = serde_json::from_str(&content)?;
peers.validate()?;
Ok(peers)
}
#[cfg(not(target_arch = "wasm32"))]
pub async fn load(path: &Path) -> Result<Self, TrustError> {
let content = tokio::fs::read_to_string(path).await?;
let peers: Self = serde_json::from_str(&content)?;
peers.validate()?;
Ok(peers)
}
#[cfg(not(target_arch = "wasm32"))]
pub async fn save(&self, path: &Path) -> Result<(), TrustError> {
self.validate()?;
let content = serde_json::to_string_pretty(self)?;
tokio::fs::write(path, content).await?;
Ok(())
}
pub fn is_trusted(&self, pubkey: &PubKey) -> bool {
!pubkey.is_zero()
&& self
.peers
.iter()
.any(|p| !p.pubkey.is_zero() && &p.pubkey == pubkey)
}
pub fn get_peer(&self, pubkey: &PubKey) -> Option<&TrustedPeer> {
if pubkey.is_zero() {
return None;
}
self.peers
.iter()
.find(|p| !p.pubkey.is_zero() && &p.pubkey == pubkey)
}
pub fn remove(&mut self, pubkey: &PubKey) -> bool {
let len_before = self.peers.len();
self.peers.retain(|p| &p.pubkey != pubkey);
self.peers.len() != len_before
}
pub fn remove_by_peer_id(&mut self, peer_id: &PeerId) -> Option<TrustedPeer> {
let index = self
.peers
.iter()
.position(|p| crate::router::peer_id_from_pubkey(&p.pubkey) == *peer_id)?;
Some(self.peers.remove(index))
}
pub fn upsert(&mut self, peer: TrustedPeer) -> Result<(), TrustError> {
peer.validate()?;
if let Some(existing) = self.peers.iter_mut().find(|p| p.pubkey == peer.pubkey) {
*existing = peer;
} else {
self.peers.push(peer);
}
Ok(())
}
pub fn validate(&self) -> Result<(), TrustError> {
let mut peer_ids = BTreeSet::new();
for peer in &self.peers {
peer.validate()?;
let peer_id = peer.pubkey.to_peer_id();
if !peer_ids.insert(peer_id) {
return Err(TrustError::DuplicatePeerId { peer_id });
}
}
Ok(())
}
pub fn get_by_name(&self, name: &str) -> Option<&TrustedPeer> {
self.peers
.iter()
.find(|p| !p.pubkey.is_zero() && p.name == name)
}
pub fn find_by_peer_id(&self, peer_id: &PeerId) -> Option<&TrustedPeer> {
let mut matches = self.peers.iter().filter(|p| {
!p.pubkey.is_zero() && crate::router::peer_id_from_pubkey(&p.pubkey) == *peer_id
});
let peer = matches.next()?;
if matches.next().is_some() {
None
} else {
Some(peer)
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_trusted_peer_fields() {
let peer = TrustedPeer {
name: "test-peer".to_string(),
pubkey: PubKey::new([42u8; 32]),
addr: "tcp://127.0.0.1:4200".to_string(),
meta: PeerMeta::default(),
};
assert_eq!(peer.name, "test-peer");
assert_eq!(peer.pubkey.as_bytes()[0], 42);
assert_eq!(peer.addr, "tcp://127.0.0.1:4200");
}
#[test]
fn test_trusted_peers_fields() {
let peers = TrustedPeers {
peers: vec![TrustedPeer {
name: "peer1".to_string(),
pubkey: PubKey::new([1u8; 32]),
addr: "uds:///tmp/test.sock".to_string(),
meta: crate::PeerMeta::default(),
}],
};
assert_eq!(peers.peers.len(), 1);
assert_eq!(peers.peers[0].name, "peer1");
}
#[test]
fn test_trusted_peer_json_roundtrip() {
let peer = TrustedPeer {
name: "coding-meerkat".to_string(),
pubkey: PubKey::new([7u8; 32]),
addr: "uds:///tmp/meerkat-coding.sock".to_string(),
meta: crate::PeerMeta::default(),
};
let json = serde_json::to_string(&peer).unwrap();
let decoded: TrustedPeer = serde_json::from_str(&json).unwrap();
assert_eq!(peer, decoded);
}
#[test]
fn test_trusted_peers_json_roundtrip() {
let peers = TrustedPeers {
peers: vec![
TrustedPeer {
name: "peer1".to_string(),
pubkey: PubKey::new([1u8; 32]),
addr: "tcp://192.168.1.50:4200".to_string(),
meta: crate::PeerMeta::default(),
},
TrustedPeer {
name: "peer2".to_string(),
pubkey: PubKey::new([2u8; 32]),
addr: "uds:///tmp/peer2.sock".to_string(),
meta: crate::PeerMeta::default(),
},
],
};
let json = serde_json::to_string_pretty(&peers).unwrap();
let decoded: TrustedPeers = serde_json::from_str(&json).unwrap();
assert_eq!(peers, decoded);
}
#[tokio::test]
async fn test_trusted_peers_load() {
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("trusted_peers.json");
let json = r#"{
"peers": [
{ "name": "coding-meerkat", "pubkey": "ed25519:KioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKio=", "addr": "uds:///tmp/meerkat-coding.sock" }
]
}"#;
tokio::fs::write(&path, json).await.unwrap();
let peers = TrustedPeers::load(&path).await.unwrap();
assert_eq!(peers.peers.len(), 1);
assert_eq!(peers.peers[0].name, "coding-meerkat");
assert_eq!(peers.peers[0].pubkey, PubKey::new([42u8; 32]));
}
#[tokio::test]
async fn test_trusted_peers_save() {
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("trusted_peers.json");
let peers = TrustedPeers {
peers: vec![TrustedPeer {
name: "test-peer".to_string(),
pubkey: PubKey::new([1u8; 32]),
addr: "tcp://localhost:4200".to_string(),
meta: crate::PeerMeta::default(),
}],
};
peers.save(&path).await.unwrap();
assert!(path.exists());
let content = tokio::fs::read_to_string(&path).await.unwrap();
assert!(content.contains("test-peer"));
assert!(content.contains("ed25519:"));
}
#[test]
fn test_is_trusted_found() {
let pubkey = PubKey::new([42u8; 32]);
let peers = TrustedPeers {
peers: vec![TrustedPeer {
name: "trusted".to_string(),
pubkey,
addr: "tcp://localhost:4200".to_string(),
meta: crate::PeerMeta::default(),
}],
};
assert!(peers.is_trusted(&pubkey));
}
#[test]
fn test_is_trusted_not_found() {
let peers = TrustedPeers {
peers: vec![TrustedPeer {
name: "trusted".to_string(),
pubkey: PubKey::new([1u8; 32]),
addr: "tcp://localhost:4200".to_string(),
meta: crate::PeerMeta::default(),
}],
};
let unknown = PubKey::new([99u8; 32]);
assert!(!peers.is_trusted(&unknown));
}
#[test]
fn test_get_peer() {
let pubkey = PubKey::new([42u8; 32]);
let peers = TrustedPeers {
peers: vec![TrustedPeer {
name: "the-peer".to_string(),
pubkey,
addr: "tcp://localhost:4200".to_string(),
meta: crate::PeerMeta::default(),
}],
};
let found = peers.get_peer(&pubkey);
assert!(found.is_some());
assert_eq!(found.unwrap().name, "the-peer");
let unknown = PubKey::new([99u8; 32]);
assert!(peers.get_peer(&unknown).is_none());
}
#[test]
fn test_zero_pubkey_is_never_trusted_or_resolved() {
let zero = PubKey::new([0u8; 32]);
let peers = TrustedPeers {
peers: vec![TrustedPeer {
name: "zero".to_string(),
pubkey: zero,
addr: "inproc://zero".to_string(),
meta: crate::PeerMeta::default(),
}],
};
assert!(
!peers.is_trusted(&zero),
"zero pubkey must not be trusted even if a raw peer is present"
);
assert!(
peers.get_peer(&zero).is_none(),
"zero pubkey must not resolve through raw trust lookup"
);
}
#[test]
fn test_trusted_peers_validate_rejects_duplicate_peer_id() {
let pubkey = PubKey::new([42u8; 32]);
let peer_id = pubkey.to_peer_id();
let peers = TrustedPeers {
peers: vec![
TrustedPeer {
name: "primary".to_string(),
pubkey,
addr: "inproc://primary".to_string(),
meta: crate::PeerMeta::default(),
},
TrustedPeer {
name: "stale-shadow".to_string(),
pubkey,
addr: "inproc://stale-shadow".to_string(),
meta: crate::PeerMeta::default(),
},
],
};
let err = peers
.validate()
.expect_err("legacy TrustedPeers must reject duplicate canonical identities");
assert!(
matches!(err, TrustError::DuplicatePeerId { peer_id: id } if id == peer_id),
"expected duplicate PeerId error for raw TrustedPeers, got {err:?}"
);
}
#[test]
fn test_get_by_name() {
let peers = TrustedPeers {
peers: vec![
TrustedPeer {
name: "alpha".to_string(),
pubkey: PubKey::new([1u8; 32]),
addr: "tcp://localhost:4201".to_string(),
meta: crate::PeerMeta::default(),
},
TrustedPeer {
name: "beta".to_string(),
pubkey: PubKey::new([2u8; 32]),
addr: "tcp://localhost:4202".to_string(),
meta: crate::PeerMeta::default(),
},
],
};
let found = peers.get_by_name("beta");
assert!(found.is_some());
assert_eq!(found.unwrap().pubkey, PubKey::new([2u8; 32]));
assert!(peers.get_by_name("gamma").is_none());
}
#[test]
fn test_json_format_matches_spec() {
let peers = TrustedPeers {
peers: vec![TrustedPeer {
name: "coding-meerkat".to_string(),
pubkey: PubKey::new([7u8; 32]),
addr: "uds:///tmp/meerkat-coding.sock".to_string(),
meta: crate::PeerMeta::default(),
}],
};
let json = serde_json::to_string_pretty(&peers).unwrap();
assert!(json.contains("\"peers\""));
assert!(json.contains("\"name\""));
assert!(json.contains("\"pubkey\""));
assert!(json.contains("\"addr\""));
assert!(json.contains("ed25519:"));
assert!(json.contains("coding-meerkat"));
assert!(json.contains("uds:///tmp/meerkat-coding.sock"));
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
let pubkey_str = parsed["peers"][0]["pubkey"].as_str().unwrap();
assert!(pubkey_str.starts_with("ed25519:"));
}
#[tokio::test]
async fn test_trusted_peers_persistence_roundtrip() {
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("trusted_peers.json");
let original = TrustedPeers {
peers: vec![
TrustedPeer {
name: "peer1".to_string(),
pubkey: PubKey::new([1u8; 32]),
addr: "tcp://192.168.1.50:4200".to_string(),
meta: crate::PeerMeta::default(),
},
TrustedPeer {
name: "peer2".to_string(),
pubkey: PubKey::new([2u8; 32]),
addr: "uds:///tmp/peer2.sock".to_string(),
meta: crate::PeerMeta::default(),
},
],
};
original.save(&path).await.unwrap();
let loaded = TrustedPeers::load(&path).await.unwrap();
assert_eq!(original, loaded);
}
#[test]
fn test_upsert_adds_new_peer() {
let mut peers = TrustedPeers::new();
assert_eq!(peers.peers.len(), 0);
let peer = TrustedPeer {
name: "new-peer".to_string(),
pubkey: PubKey::new([42u8; 32]),
addr: "uds:///tmp/new.sock".to_string(),
meta: crate::PeerMeta::default(),
};
peers.upsert(peer).expect("valid peer should upsert");
assert_eq!(peers.peers.len(), 1);
assert_eq!(peers.peers[0].name, "new-peer");
}
#[test]
fn test_upsert_rejects_zero_pubkey_peer() {
let mut peers = TrustedPeers::new();
let result = peers.upsert(TrustedPeer {
name: "zero-peer".to_string(),
pubkey: PubKey::new([0u8; 32]),
addr: "inproc://zero-peer".to_string(),
meta: crate::PeerMeta::default(),
});
assert!(matches!(result, Err(TrustError::ZeroPubkey { .. })));
assert!(
peers.is_empty(),
"zero-pubkey peer must not enter the trust list through direct upsert"
);
}
#[test]
fn test_direct_zero_pubkey_entry_is_not_trusted() {
let zero_pubkey = PubKey::new([0u8; 32]);
let peers = TrustedPeers {
peers: vec![TrustedPeer {
name: "zero-peer".to_string(),
pubkey: zero_pubkey,
addr: "inproc://zero-peer".to_string(),
meta: crate::PeerMeta::default(),
}],
};
assert!(!peers.is_trusted(&zero_pubkey));
assert!(peers.get_peer(&zero_pubkey).is_none());
assert!(peers.get_by_name("zero-peer").is_none());
assert!(peers.find_by_peer_id(&zero_pubkey.to_peer_id()).is_none());
}
#[test]
fn test_trust_store_rejects_peer_id_pubkey_mismatch() {
let trusted_pubkey = PubKey::new([8u8; 32]);
let mismatched_peer_id = PubKey::new([7u8; 32]).to_peer_id();
let mut store = TrustStore::new();
let result = store.insert(TrustEntry {
peer_id: mismatched_peer_id,
name: PeerName::new("mismatched-peer").unwrap(),
pubkey: trusted_pubkey,
address: PeerAddress::parse("inproc://mismatched-peer").unwrap(),
meta: PeerMeta::default(),
});
assert!(
matches!(
result,
Err(TrustError::PeerIdPubkeyMismatch {
peer_id,
derived_peer_id,
..
}) if peer_id == mismatched_peer_id
&& derived_peer_id == trusted_pubkey.to_peer_id()
),
"raw TrustEntry authority must reject peer ids that do not derive from the pubkey"
);
assert!(
store.is_empty(),
"mismatched raw trust entry must not enter the canonical trust store"
);
let result = store.upsert(TrustEntry {
peer_id: mismatched_peer_id,
name: PeerName::new("mismatched-peer").unwrap(),
pubkey: trusted_pubkey,
address: PeerAddress::parse("inproc://mismatched-peer").unwrap(),
meta: PeerMeta::default(),
});
assert!(
matches!(
result,
Err(TrustError::PeerIdPubkeyMismatch {
peer_id,
derived_peer_id,
..
}) if peer_id == mismatched_peer_id
&& derived_peer_id == trusted_pubkey.to_peer_id()
),
"raw TrustEntry upsert must reject peer ids that do not derive from the pubkey"
);
assert!(
store.is_empty(),
"mismatched raw trust entry must not enter the canonical trust store through upsert"
);
}
#[test]
fn test_trust_store_duplicate_peer_id_uses_pubkey_derived_id() {
let pubkey = PubKey::new([9u8; 32]);
let peer_id = pubkey.to_peer_id();
let mut store = TrustStore::new();
store
.insert(TrustEntry {
peer_id,
name: PeerName::new("first-peer").unwrap(),
pubkey,
address: PeerAddress::parse("inproc://first-peer").unwrap(),
meta: PeerMeta::default(),
})
.expect("derived peer id should insert");
let result = store.insert(TrustEntry {
peer_id,
name: PeerName::new("duplicate-peer").unwrap(),
pubkey,
address: PeerAddress::parse("inproc://duplicate-peer").unwrap(),
meta: PeerMeta::default(),
});
assert!(
matches!(result, Err(TrustError::DuplicatePeerId { peer_id: id }) if id == peer_id)
);
assert_eq!(store.len(), 1);
}
#[test]
fn test_upsert_updates_existing_peer() {
let mut peers = TrustedPeers {
peers: vec![TrustedPeer {
name: "original".to_string(),
pubkey: PubKey::new([42u8; 32]),
addr: "uds:///tmp/original.sock".to_string(),
meta: crate::PeerMeta::default(),
}],
};
let updated = TrustedPeer {
name: "updated".to_string(),
pubkey: PubKey::new([42u8; 32]),
addr: "uds:///tmp/updated.sock".to_string(),
meta: crate::PeerMeta::default(),
};
peers.upsert(updated).expect("valid peer should upsert");
assert_eq!(peers.peers.len(), 1);
assert_eq!(peers.peers[0].name, "updated");
assert_eq!(peers.peers[0].addr, "uds:///tmp/updated.sock");
}
#[test]
fn test_remove_existing_peer() {
let mut peers = TrustedPeers {
peers: vec![
TrustedPeer {
name: "peer1".to_string(),
pubkey: PubKey::new([1u8; 32]),
addr: "tcp://localhost:4201".to_string(),
meta: crate::PeerMeta::default(),
},
TrustedPeer {
name: "peer2".to_string(),
pubkey: PubKey::new([2u8; 32]),
addr: "tcp://localhost:4202".to_string(),
meta: crate::PeerMeta::default(),
},
],
};
let removed = peers.remove(&PubKey::new([1u8; 32]));
assert!(removed);
assert_eq!(peers.peers.len(), 1);
assert_eq!(peers.peers[0].name, "peer2");
}
#[test]
fn test_remove_existing_peer_by_peer_id() {
let mut peers = TrustedPeers {
peers: vec![
TrustedPeer {
name: "peer1".to_string(),
pubkey: PubKey::new([1u8; 32]),
addr: "tcp://localhost:4201".to_string(),
meta: crate::PeerMeta::default(),
},
TrustedPeer {
name: "peer2".to_string(),
pubkey: PubKey::new([2u8; 32]),
addr: "tcp://localhost:4202".to_string(),
meta: crate::PeerMeta::default(),
},
],
};
let peer_id = PubKey::new([1u8; 32]).to_peer_id();
let removed = peers.remove_by_peer_id(&peer_id);
assert_eq!(
removed.as_ref().map(|peer| peer.name.as_str()),
Some("peer1")
);
assert_eq!(peers.peers.len(), 1);
assert_eq!(peers.peers[0].name, "peer2");
}
#[test]
fn test_is_empty() {
let peers = TrustedPeers::new();
assert!(peers.is_empty());
let peers_with_one = TrustedPeers {
peers: vec![TrustedPeer {
name: "peer1".to_string(),
pubkey: PubKey::new([1u8; 32]),
addr: "tcp://localhost:4201".to_string(),
meta: crate::PeerMeta::default(),
}],
};
assert!(!peers_with_one.is_empty());
}
#[test]
fn test_has_peers() {
let peers = TrustedPeers::new();
assert!(!peers.has_peers());
let peers_with_one = TrustedPeers {
peers: vec![TrustedPeer {
name: "peer1".to_string(),
pubkey: PubKey::new([1u8; 32]),
addr: "tcp://localhost:4201".to_string(),
meta: crate::PeerMeta::default(),
}],
};
assert!(peers_with_one.has_peers());
}
#[test]
fn test_len() {
let peers = TrustedPeers::new();
assert_eq!(peers.len(), 0);
let peers_with_two = TrustedPeers {
peers: vec![
TrustedPeer {
name: "peer1".to_string(),
pubkey: PubKey::new([1u8; 32]),
addr: "tcp://localhost:4201".to_string(),
meta: crate::PeerMeta::default(),
},
TrustedPeer {
name: "peer2".to_string(),
pubkey: PubKey::new([2u8; 32]),
addr: "tcp://localhost:4202".to_string(),
meta: crate::PeerMeta::default(),
},
],
};
assert_eq!(peers_with_two.len(), 2);
}
#[test]
fn test_remove_nonexistent_peer() {
let mut peers = TrustedPeers {
peers: vec![TrustedPeer {
name: "peer1".to_string(),
pubkey: PubKey::new([1u8; 32]),
addr: "tcp://localhost:4201".to_string(),
meta: crate::PeerMeta::default(),
}],
};
let removed = peers.remove(&PubKey::new([99u8; 32]));
assert!(!removed);
assert_eq!(peers.peers.len(), 1);
}
#[test]
fn test_trusted_peer_with_meta() {
let meta = PeerMeta::default()
.with_description("Reviews code")
.with_label("lang", "rust");
let peer = TrustedPeer {
name: "reviewer".to_string(),
pubkey: PubKey::new([42u8; 32]),
addr: "inproc://reviewer".to_string(),
meta: meta.clone(),
};
let json = serde_json::to_string(&peer).unwrap();
let decoded: TrustedPeer = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.meta, meta);
assert_eq!(decoded.meta.description.as_deref(), Some("Reviews code"));
assert_eq!(
decoded.meta.labels.get("lang").map(String::as_str),
Some("rust")
);
}
#[test]
fn test_trusted_peer_without_meta_backward_compat() {
let json = r#"{
"name": "legacy-peer",
"pubkey": "ed25519:KioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKio=",
"addr": "tcp://127.0.0.1:4200"
}"#;
let peer: TrustedPeer = serde_json::from_str(json).unwrap();
assert_eq!(peer.name, "legacy-peer");
assert_eq!(peer.meta, PeerMeta::default());
}
}