From 8f1ecb2a5e17067e62ad5b2cfe57abdbf7511597 Mon Sep 17 00:00:00 2001 From: Mollusk Date: Tue, 2 Jun 2026 05:37:08 -0400 Subject: [PATCH] test(network): unit tests for ticket codec + gossip wire types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add #[cfg(test)] coverage for the join-link codec and gossip wire format: PeerSpeakTicket Display/FromStr round-trip and — the real gap — malformed-input rejection (empty / non-base64 / valid-base64-non-JSON all return NetError::InvalidTicket, never panic, since users paste untrusted text here); PeerState serde round-trip; and GossipMessage::Leave / Announce + GossipPayload round-trips. Tests-only; no production change. Implemented by Gemini (junior implementer), reviewed and verified by senior (cargo build + clippy --all-targets + cargo test all green). Co-Authored-By: Claude Opus 4.8 --- src/network/gossip.rs | 50 ++++++++++++++++++++++++++++++++++++ src/network/mod.rs | 60 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/src/network/gossip.rs b/src/network/gossip.rs index fb2498d..fd27e0a 100644 --- a/src/network/gossip.rs +++ b/src/network/gossip.rs @@ -285,3 +285,53 @@ impl RoomState for IrohGossipState { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::network::PeerState; + use iroh::SecretKey; + + fn sample_peer_state() -> PeerState { + let secret = SecretKey::generate(); + let public = secret.public(); + let addr = iroh::EndpointAddr::from(public); + PeerState { + name: "TestPeerGossip".to_string(), + is_muted: true, + addr, + } + } + + #[test] + fn test_gossip_message_leave_round_trip() { + let original = GossipMessage::Leave; + let serialized = serde_json::to_string(&original).unwrap(); + let deserialized: GossipMessage = serde_json::from_str(&serialized).unwrap(); + assert!(matches!(deserialized, GossipMessage::Leave)); + } + + #[test] + fn test_gossip_payload_announce_round_trip() { + let peer_state = sample_peer_state(); + let author = peer_state.addr.id; + let payload = GossipPayload { + author, + msg: GossipMessage::Announce(peer_state.clone()), + }; + + let serialized = serde_json::to_string(&payload).unwrap(); + let deserialized: GossipPayload = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(deserialized.author, author); + match deserialized.msg { + GossipMessage::Announce(state) => { + assert_eq!(state, peer_state); + } + GossipMessage::Leave => { + panic!("Expected GossipMessage::Announce, got Leave"); + } + } + } +} + diff --git a/src/network/mod.rs b/src/network/mod.rs index 80ee74a..d73f337 100644 --- a/src/network/mod.rs +++ b/src/network/mod.rs @@ -139,3 +139,63 @@ pub trait RoomState: Send + Sync { pub mod iroh_impl; pub mod gossip; + +#[cfg(test)] +mod tests { + use super::*; + use iroh::{SecretKey, EndpointAddr}; + + fn sample_peer_state() -> PeerState { + let secret = SecretKey::generate(); + let public = secret.public(); + let addr = EndpointAddr::from(public); + PeerState { + name: "TestPeer".to_string(), + is_muted: false, + addr, + } + } + + #[test] + fn test_ticket_round_trip() { + let state = sample_peer_state(); + let topic_id = [7u8; 32]; + let original_ticket = PeerSpeakTicket { + host_addr: state.addr.clone(), + topic_id, + }; + let ticket_str = original_ticket.to_string(); + let parsed_ticket = ticket_str.parse::().unwrap(); + assert_eq!(parsed_ticket.host_addr.id, original_ticket.host_addr.id); + assert_eq!(parsed_ticket.topic_id, original_ticket.topic_id); + } + + #[test] + fn test_malformed_rejection() { + // empty string + let res1 = "".parse::(); + assert!(matches!(res1, Err(NetError::InvalidTicket(_)))); + + // non-base64 garbage + let res2 = "!!!not base64!!!".parse::(); + assert!(matches!(res2, Err(NetError::InvalidTicket(_)))); + + // valid URL-safe-base64 that decodes to non-JSON bytes + let bad_json = b"hello world"; + let encoded = base64::Engine::encode( + &base64::engine::general_purpose::URL_SAFE_NO_PAD, + bad_json, + ); + let res3 = encoded.parse::(); + assert!(matches!(res3, Err(NetError::InvalidTicket(_)))); + } + + #[test] + fn test_peer_state_serde_round_trip() { + let original = sample_peer_state(); + let serialized = serde_json::to_string(&original).unwrap(); + let deserialized: PeerState = serde_json::from_str(&serialized).unwrap(); + assert_eq!(original, deserialized); + } +} +