Skip to main content

offline_protocol_services/
services.rs

1use crate::error::ServiceError;
2use crate::events::ServiceEvent;
3use crate::payloads::{
4    ServiceDiscoveryQueryPayload, ServiceDiscoveryResponsePayload, ServiceRequestPayload,
5    ServiceResponsePayload, DISCOVERY_GOSSIP_MAX_FANOUT, DISCOVERY_INITIAL_BROADCAST_MAX,
6    DISCOVERY_QUERY_DEDUP_TTL_SECS, DISCOVERY_QUERY_DEFAULT_MAX_HOPS,
7    DISCOVERY_QUERY_MAX_DEDUP_ENTRIES, MAX_SERVICE_BODY_SIZE, MAX_SERVICE_METHOD_LEN,
8    MAX_SERVICE_PAYLOAD_SIZE, SVC_DISCOVER_QUERY, SVC_DISCOVER_RESPONSE, SVC_REQUEST, SVC_RESPONSE,
9    VALID_SERVICE_STATUSES,
10};
11use offline_protocol_core::{MessagePriority, ServiceDescriptor};
12use std::collections::{HashMap, VecDeque};
13use std::time::{Duration, Instant};
14use tracing::{debug, info, warn};
15
16/// A message to send to a peer.
17#[derive(Debug, Clone)]
18pub struct OutboundMessage {
19    /// The recipient peer ID.
20    pub recipient: String,
21    /// The serialized message content.
22    pub content: String,
23    /// The message priority.
24    pub priority: MessagePriority,
25}
26
27/// Result of initiating a service discovery broadcast.
28#[must_use]
29pub struct DiscoverResult {
30    /// Unique query identifier for correlating responses.
31    pub query_id: String,
32    /// Messages to send to peers.
33    pub messages: Vec<OutboundMessage>,
34}
35
36/// Result of sending a service request.
37#[must_use]
38pub struct SendRequestResult {
39    /// Unique request identifier for correlating the response.
40    pub request_id: String,
41    /// Message to send to the provider.
42    pub message: OutboundMessage,
43}
44
45/// Result of responding to a service request.
46#[must_use]
47pub struct SendResponseResult {
48    /// Message to send to the requester.
49    pub message: OutboundMessage,
50}
51
52/// Action returned from handling an incoming service message.
53pub enum ServiceAction {
54    /// The message was not a service message; caller should continue processing.
55    NotHandled,
56    /// The message was consumed as a service message.
57    Consumed {
58        /// Messages to send to peers.
59        messages_to_send: Vec<OutboundMessage>,
60        /// Events to emit to the application.
61        events_to_emit: Vec<ServiceEvent>,
62    },
63}
64
65/// Mesh service registry and message handler.
66///
67/// Manages local service registration, discovery query generation and handling,
68/// and service request/response routing. All methods return **actions** (messages
69/// to send, events to emit) rather than performing I/O directly.
70///
71/// ## Multi-hop discovery limitation
72///
73/// Discovery **queries** are forwarded across multiple hops via gossip. However,
74/// discovery **responses** are currently sent only to the direct sender of the
75/// query (one hop back) rather than being relayed all the way to the original
76/// querier. In practice this means that services more than one hop away will
77/// generate responses that reach intermediate forwarders but not the node that
78/// initiated the query. Multi-hop response relay is planned for a future release.
79pub struct MeshServices {
80    local_services: HashMap<String, ServiceDescriptor>,
81    seen_discovery_queries: HashMap<String, Instant>,
82    /// Insertion-order index for O(1) oldest-entry eviction.
83    seen_discovery_order: VecDeque<String>,
84}
85
86impl MeshServices {
87    /// Creates a new empty service registry.
88    pub fn new() -> Self {
89        Self {
90            local_services: HashMap::new(),
91            seen_discovery_queries: HashMap::new(),
92            seen_discovery_order: VecDeque::new(),
93        }
94    }
95
96    /// Registers a local service.
97    pub fn register_service(&mut self, descriptor: ServiceDescriptor) -> Result<(), ServiceError> {
98        let key = descriptor.service_id.as_str().to_string();
99        self.local_services.insert(key, descriptor);
100        Ok(())
101    }
102
103    /// Unregisters a local service. Returns `true` if it was found and removed.
104    pub fn unregister_service(&mut self, service_id: &str) -> Result<bool, ServiceError> {
105        Ok(self.local_services.remove(service_id).is_some())
106    }
107
108    /// Returns `true` if a service with the given ID is registered locally.
109    pub fn has_service(&self, service_id: &str) -> bool {
110        self.local_services.contains_key(service_id)
111    }
112
113    /// Generates a discovery broadcast to known peers, capped at
114    /// [`DISCOVERY_INITIAL_BROADCAST_MAX`] recipients.
115    ///
116    /// See the [multi-hop limitation](MeshServices#multi-hop-discovery-limitation)
117    /// note on `MeshServices`.
118    pub fn discover_services(
119        &mut self,
120        user_id: &str,
121        known_peers: &[String],
122        service_id: Option<&str>,
123    ) -> Result<DiscoverResult, ServiceError> {
124        let query_id = uuid::Uuid::new_v4().to_string();
125
126        let payload = ServiceDiscoveryQueryPayload {
127            query_id: query_id.clone(),
128            originator: user_id.to_string(),
129            service_id: service_id.map(|s| s.to_string()),
130            remaining_hops: DISCOVERY_QUERY_DEFAULT_MAX_HOPS,
131        };
132        let serialized = serde_json::to_string(&payload)
133            .map_err(|e| ServiceError::Serialization(e.to_string()))?;
134        let content = format!("{}{}", SVC_DISCOVER_QUERY, serialized);
135
136        self.record_seen_query(query_id.clone());
137
138        // Apply fanout limit to the initial broadcast to bound message generation.
139        let selected_peers = select_fanout_peers(
140            known_peers,
141            DISCOVERY_INITIAL_BROADCAST_MAX,
142            &query_id,
143            user_id,
144        );
145
146        let messages: Vec<OutboundMessage> = selected_peers
147            .into_iter()
148            .map(|peer| OutboundMessage {
149                recipient: peer,
150                content: content.clone(),
151                priority: MessagePriority::Medium,
152            })
153            .collect();
154
155        info!(query_id = %query_id, service_id = ?service_id, peer_count = messages.len(), "Broadcast service discovery query");
156        Ok(DiscoverResult { query_id, messages })
157    }
158
159    /// Generates a service request message to a specific provider peer.
160    pub fn send_service_request(
161        &self,
162        provider: &str,
163        service_id: &str,
164        method: &str,
165        body: &str,
166    ) -> Result<SendRequestResult, ServiceError> {
167        if body.len() > MAX_SERVICE_BODY_SIZE {
168            return Err(ServiceError::PayloadTooLarge(format!(
169                "request body size {} exceeds maximum {}",
170                body.len(),
171                MAX_SERVICE_BODY_SIZE
172            )));
173        }
174        if method.len() > MAX_SERVICE_METHOD_LEN {
175            return Err(ServiceError::PayloadTooLarge(format!(
176                "method length {} exceeds maximum {}",
177                method.len(),
178                MAX_SERVICE_METHOD_LEN
179            )));
180        }
181
182        let request_id = uuid::Uuid::new_v4().to_string();
183
184        let payload = ServiceRequestPayload {
185            request_id: request_id.clone(),
186            service_id: service_id.to_string(),
187            method: method.to_string(),
188            body: body.to_string(),
189        };
190        let serialized = serde_json::to_string(&payload)
191            .map_err(|e| ServiceError::Serialization(e.to_string()))?;
192        let content = format!("{}{}", SVC_REQUEST, serialized);
193
194        info!(request_id = %request_id, provider = %provider, service_id = %service_id, method = %method, "Sent service request");
195        Ok(SendRequestResult {
196            request_id,
197            message: OutboundMessage {
198                recipient: provider.to_string(),
199                content,
200                priority: MessagePriority::High,
201            },
202        })
203    }
204
205    /// Generates a service response message.
206    pub fn respond_to_service_request(
207        &self,
208        request_id: &str,
209        requester: &str,
210        service_id: &str,
211        status: &str,
212        body: &str,
213    ) -> Result<SendResponseResult, ServiceError> {
214        if !VALID_SERVICE_STATUSES.contains(&status) {
215            return Err(ServiceError::InvalidStatus(status.to_string()));
216        }
217        if body.len() > MAX_SERVICE_BODY_SIZE {
218            return Err(ServiceError::PayloadTooLarge(format!(
219                "response body size {} exceeds maximum {}",
220                body.len(),
221                MAX_SERVICE_BODY_SIZE
222            )));
223        }
224
225        let payload = ServiceResponsePayload {
226            request_id: request_id.to_string(),
227            service_id: service_id.to_string(),
228            status: status.to_string(),
229            body: body.to_string(),
230        };
231        let serialized = serde_json::to_string(&payload)
232            .map_err(|e| ServiceError::Serialization(e.to_string()))?;
233        let content = format!("{}{}", SVC_RESPONSE, serialized);
234
235        info!(request_id = %request_id, requester = %requester, status = %status, "Sent service response");
236        Ok(SendResponseResult {
237            message: OutboundMessage {
238                recipient: requester.to_string(),
239                content,
240                priority: MessagePriority::High,
241            },
242        })
243    }
244
245    /// Handles an incoming message, returning a `ServiceAction`.
246    ///
247    /// `hop_count` is the hop count from the incoming message (used in discovery responses).
248    /// `known_peers` should exclude the sender.
249    pub fn handle_incoming_message(
250        &mut self,
251        content: &str,
252        sender: &str,
253        hop_count: u8,
254        user_id: &str,
255        known_peers: &[String],
256    ) -> ServiceAction {
257        if let Some(action) =
258            self.try_handle_discover_query(content, sender, hop_count, user_id, known_peers)
259        {
260            return action;
261        }
262        if let Some(action) = self.try_handle_discover_response(content, sender, user_id) {
263            return action;
264        }
265        if let Some(action) = self.try_handle_request(content, sender) {
266            return action;
267        }
268        if let Some(action) = self.try_handle_response(content, sender) {
269            return action;
270        }
271        ServiceAction::NotHandled
272    }
273
274    /// Removes expired discovery query dedup entries.
275    pub fn cleanup_expired(&mut self) {
276        // Ages are compared with `saturating_duration_since` rather than a
277        // precomputed `now - TTL` cutoff: on platforms where the monotonic
278        // clock starts at boot, that subtraction underflows (and panics)
279        // when the process is younger than the TTL.
280        let now = Instant::now();
281        let ttl = Duration::from_secs(DISCOVERY_QUERY_DEDUP_TTL_SECS);
282        self.seen_discovery_queries
283            .retain(|_, ts| now.saturating_duration_since(*ts) < ttl);
284        self.seen_discovery_order
285            .retain(|id| self.seen_discovery_queries.contains_key(id));
286    }
287
288    /// Provides read access to `seen_discovery_queries` for testing / inspection.
289    pub fn seen_discovery_queries(&self) -> &HashMap<String, Instant> {
290        &self.seen_discovery_queries
291    }
292
293    /// Inserts a query ID into the dedup map, evicting the oldest entry if at capacity.
294    fn record_seen_query(&mut self, query_id: String) {
295        if self.seen_discovery_queries.len() >= DISCOVERY_QUERY_MAX_DEDUP_ENTRIES {
296            // Evict the oldest entry using insertion-order index (O(1)).
297            if let Some(oldest) = self.seen_discovery_order.pop_front() {
298                self.seen_discovery_queries.remove(&oldest);
299            }
300        }
301        self.seen_discovery_queries
302            .insert(query_id.clone(), Instant::now());
303        self.seen_discovery_order.push_back(query_id);
304    }
305
306    // --- private handlers ---
307
308    fn try_handle_discover_query(
309        &mut self,
310        content: &str,
311        sender: &str,
312        hop_count: u8,
313        user_id: &str,
314        known_peers: &[String],
315    ) -> Option<ServiceAction> {
316        let data = content.strip_prefix(SVC_DISCOVER_QUERY)?;
317
318        if data.len() > MAX_SERVICE_PAYLOAD_SIZE {
319            warn!(sender = %sender, size = data.len(), "Discovery query payload too large, dropping");
320            return Some(ServiceAction::Consumed {
321                messages_to_send: vec![],
322                events_to_emit: vec![],
323            });
324        }
325
326        let payload: ServiceDiscoveryQueryPayload = match serde_json::from_str(data) {
327            Ok(p) => p,
328            Err(_) => {
329                warn!(sender = %sender, "Failed to parse service discovery query payload");
330                return Some(ServiceAction::Consumed {
331                    messages_to_send: vec![],
332                    events_to_emit: vec![],
333                });
334            }
335        };
336
337        // Dedup
338        if self.seen_discovery_queries.contains_key(&payload.query_id) {
339            debug!(query_id = %payload.query_id, "Duplicate discovery query, skipping");
340            return Some(ServiceAction::Consumed {
341                messages_to_send: vec![],
342                events_to_emit: vec![],
343            });
344        }
345        self.record_seen_query(payload.query_id.clone());
346
347        let mut messages = Vec::new();
348
349        // Check local services for matches
350        let matches: Vec<_> = self
351            .local_services
352            .values()
353            .filter(|svc| {
354                payload
355                    .service_id
356                    .as_ref()
357                    .is_none_or(|q| q == svc.service_id.as_str())
358            })
359            .cloned()
360            .collect();
361
362        // Send discovery responses back to the *sender* (not the originator) to
363        // prevent a spoofed originator field from leaking our service list to an
364        // arbitrary peer. The sender relays the response back along the path.
365        for svc in &matches {
366            let response = ServiceDiscoveryResponsePayload {
367                query_id: payload.query_id.clone(),
368                service_id: svc.service_id.as_str().to_string(),
369                version: svc.version.clone(),
370                provider_peer_id: user_id.to_string(),
371                capabilities: svc.capabilities.clone(),
372                hop_count,
373                originator: payload.originator.clone(),
374            };
375            if let Ok(serialized) = serde_json::to_string(&response) {
376                let resp_content = format!("{}{}", SVC_DISCOVER_RESPONSE, serialized);
377                messages.push(OutboundMessage {
378                    recipient: sender.to_string(),
379                    content: resp_content,
380                    priority: MessagePriority::Medium,
381                });
382            }
383        }
384
385        // Forward query to other known peers (gossip) if hops remain
386        let forwarded_count = if payload.remaining_hops > 0 {
387            let mut fwd_payload = payload.clone();
388            fwd_payload.remaining_hops -= 1;
389            let fwd_content = match serde_json::to_string(&fwd_payload) {
390                Ok(serialized) => format!("{}{}", SVC_DISCOVER_QUERY, serialized),
391                Err(e) => {
392                    warn!("Failed to re-serialize discovery query for forwarding: {e}");
393                    return Some(ServiceAction::Consumed {
394                        messages_to_send: messages,
395                        events_to_emit: vec![],
396                    });
397                }
398            };
399            let eligible_peers: Vec<String> = known_peers
400                .iter()
401                .filter(|p| p.as_str() != sender && p.as_str() != payload.originator)
402                .cloned()
403                .collect();
404            let forward_peers = select_fanout_peers(
405                &eligible_peers,
406                DISCOVERY_GOSSIP_MAX_FANOUT,
407                &payload.query_id,
408                user_id,
409            );
410            for peer in &forward_peers {
411                messages.push(OutboundMessage {
412                    recipient: peer.clone(),
413                    content: fwd_content.clone(),
414                    priority: MessagePriority::Medium,
415                });
416            }
417            forward_peers.len()
418        } else {
419            debug!(query_id = %payload.query_id, "Discovery query reached max hops, not forwarding");
420            0
421        };
422
423        debug!(
424            query_id = %payload.query_id,
425            matches = matches.len(),
426            forwarded_to = forwarded_count,
427            "Processed service discovery query"
428        );
429
430        Some(ServiceAction::Consumed {
431            messages_to_send: messages,
432            events_to_emit: vec![],
433        })
434    }
435
436    fn try_handle_discover_response(
437        &self,
438        content: &str,
439        sender: &str,
440        user_id: &str,
441    ) -> Option<ServiceAction> {
442        let data = content.strip_prefix(SVC_DISCOVER_RESPONSE)?;
443
444        if data.len() > MAX_SERVICE_PAYLOAD_SIZE {
445            warn!(sender = %sender, size = data.len(), "Discovery response payload too large, dropping");
446            return Some(ServiceAction::Consumed {
447                messages_to_send: vec![],
448                events_to_emit: vec![],
449            });
450        }
451
452        let payload: ServiceDiscoveryResponsePayload = match serde_json::from_str(data) {
453            Ok(p) => p,
454            Err(_) => {
455                warn!(sender = %sender, "Failed to parse service discovery response payload");
456                return Some(ServiceAction::Consumed {
457                    messages_to_send: vec![],
458                    events_to_emit: vec![],
459                });
460            }
461        };
462
463        info!(
464            query_id = %payload.query_id,
465            service_id = %payload.service_id,
466            provider = %payload.provider_peer_id,
467            "Service discovered"
468        );
469
470        // If we are not the originator, forward the response toward them
471        if !payload.originator.is_empty() && payload.originator != user_id {
472            debug!(
473                query_id = %payload.query_id,
474                originator = %payload.originator,
475                "Forwarding discovery response toward originator"
476            );
477            // Re-serialize and forward as unicast to the originator
478            let fwd_content = content.to_string();
479            return Some(ServiceAction::Consumed {
480                messages_to_send: vec![OutboundMessage {
481                    recipient: payload.originator,
482                    content: fwd_content,
483                    priority: MessagePriority::Medium,
484                }],
485                events_to_emit: vec![],
486            });
487        }
488
489        Some(ServiceAction::Consumed {
490            messages_to_send: vec![],
491            events_to_emit: vec![ServiceEvent::ServiceDiscovered {
492                query_id: payload.query_id,
493                service_id: payload.service_id,
494                version: payload.version,
495                provider_peer_id: payload.provider_peer_id,
496                capabilities: payload.capabilities,
497                hop_count: payload.hop_count,
498            }],
499        })
500    }
501
502    fn try_handle_request(&self, content: &str, sender: &str) -> Option<ServiceAction> {
503        let data = content.strip_prefix(SVC_REQUEST)?;
504
505        if data.len() > MAX_SERVICE_PAYLOAD_SIZE {
506            warn!(sender = %sender, size = data.len(), "Service request payload too large, dropping");
507            return Some(ServiceAction::Consumed {
508                messages_to_send: vec![],
509                events_to_emit: vec![],
510            });
511        }
512
513        let payload: ServiceRequestPayload = match serde_json::from_str(data) {
514            Ok(p) => p,
515            Err(_) => {
516                warn!(sender = %sender, "Failed to parse service request payload");
517                return Some(ServiceAction::Consumed {
518                    messages_to_send: vec![],
519                    events_to_emit: vec![],
520                });
521            }
522        };
523
524        // Validate field sizes from untrusted input
525        if payload.body.len() > MAX_SERVICE_BODY_SIZE {
526            warn!(sender = %sender, size = payload.body.len(), "Service request body too large, dropping");
527            return Some(ServiceAction::Consumed {
528                messages_to_send: vec![],
529                events_to_emit: vec![],
530            });
531        }
532        if payload.method.len() > MAX_SERVICE_METHOD_LEN {
533            warn!(sender = %sender, len = payload.method.len(), "Service request method too long, dropping");
534            return Some(ServiceAction::Consumed {
535                messages_to_send: vec![],
536                events_to_emit: vec![],
537            });
538        }
539
540        if !self.local_services.contains_key(&payload.service_id) {
541            // Auto-respond with not_found
542            let response = ServiceResponsePayload {
543                request_id: payload.request_id.clone(),
544                service_id: payload.service_id.clone(),
545                status: "not_found".to_string(),
546                body: String::new(),
547            };
548            let mut messages = Vec::new();
549            if let Ok(serialized) = serde_json::to_string(&response) {
550                let resp_content = format!("{}{}", SVC_RESPONSE, serialized);
551                messages.push(OutboundMessage {
552                    recipient: sender.to_string(),
553                    content: resp_content,
554                    priority: MessagePriority::High,
555                });
556            }
557            debug!(
558                request_id = %payload.request_id,
559                service_id = %payload.service_id,
560                "Service not found, auto-responded not_found"
561            );
562            return Some(ServiceAction::Consumed {
563                messages_to_send: messages,
564                events_to_emit: vec![],
565            });
566        }
567
568        info!(
569            request_id = %payload.request_id,
570            service_id = %payload.service_id,
571            method = %payload.method,
572            "Service request received"
573        );
574
575        Some(ServiceAction::Consumed {
576            messages_to_send: vec![],
577            events_to_emit: vec![ServiceEvent::ServiceRequestReceived {
578                request_id: payload.request_id,
579                service_id: payload.service_id,
580                method: payload.method,
581                body: payload.body,
582                sender: sender.to_string(),
583            }],
584        })
585    }
586
587    fn try_handle_response(&self, content: &str, sender: &str) -> Option<ServiceAction> {
588        let data = content.strip_prefix(SVC_RESPONSE)?;
589
590        if data.len() > MAX_SERVICE_PAYLOAD_SIZE {
591            warn!(sender = %sender, size = data.len(), "Service response payload too large, dropping");
592            return Some(ServiceAction::Consumed {
593                messages_to_send: vec![],
594                events_to_emit: vec![],
595            });
596        }
597
598        let payload: ServiceResponsePayload = match serde_json::from_str(data) {
599            Ok(p) => p,
600            Err(_) => {
601                warn!(sender = %sender, "Failed to parse service response payload");
602                return Some(ServiceAction::Consumed {
603                    messages_to_send: vec![],
604                    events_to_emit: vec![],
605                });
606            }
607        };
608
609        // Validate body size from untrusted input
610        if payload.body.len() > MAX_SERVICE_BODY_SIZE {
611            warn!(sender = %sender, size = payload.body.len(), "Service response body too large, dropping");
612            return Some(ServiceAction::Consumed {
613                messages_to_send: vec![],
614                events_to_emit: vec![],
615            });
616        }
617
618        // Validate status from untrusted input
619        if !VALID_SERVICE_STATUSES.contains(&payload.status.as_str()) {
620            warn!(sender = %sender, status = %payload.status, "Service response with invalid status, dropping");
621            return Some(ServiceAction::Consumed {
622                messages_to_send: vec![],
623                events_to_emit: vec![],
624            });
625        }
626
627        info!(
628            request_id = %payload.request_id,
629            service_id = %payload.service_id,
630            status = %payload.status,
631            "Service response received"
632        );
633
634        Some(ServiceAction::Consumed {
635            messages_to_send: vec![],
636            events_to_emit: vec![ServiceEvent::ServiceResponseReceived {
637                request_id: payload.request_id,
638                service_id: payload.service_id,
639                status: payload.status,
640                body: payload.body,
641                provider_peer_id: sender.to_string(),
642            }],
643        })
644    }
645}
646
647/// Selects up to `max` peers from `peers` using a deterministic pseudo-random
648/// subset selection. When `peers.len() <= max`, all peers are returned.
649///
650/// The selection uses a simple hash-based seed derived from `query_id` and
651/// `user_id` so that different nodes select different subsets for the same query,
652/// improving mesh coverage. The distribution has some bias because we use a
653/// lightweight hash rather than a full PRNG — this is intentional as uniform
654/// randomness is not critical for gossip fanout and avoids adding a `rand`
655/// dependency.
656fn select_fanout_peers(peers: &[String], max: usize, query_id: &str, user_id: &str) -> Vec<String> {
657    if peers.len() <= max {
658        return peers.to_vec();
659    }
660
661    let seed = query_id
662        .bytes()
663        .chain(user_id.bytes())
664        .fold(0u64, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u64))
665        ^ peers.len() as u64;
666
667    let mut indices: Vec<usize> = (0..peers.len()).collect();
668    // Partial Fisher-Yates shuffle for the first `max` elements.
669    for i in 0..max {
670        let remaining = peers.len() - i;
671        let j = i + (seed.wrapping_mul((i as u64).wrapping_add(7)) as usize % remaining);
672        indices.swap(i, j);
673    }
674
675    indices[..max]
676        .iter()
677        .map(|&idx| peers[idx].clone())
678        .collect()
679}
680
681impl Default for MeshServices {
682    fn default() -> Self {
683        Self::new()
684    }
685}
686
687#[cfg(test)]
688mod tests {
689    use super::*;
690    use offline_protocol_core::ServiceId;
691
692    fn make_descriptor(id: &str) -> ServiceDescriptor {
693        ServiceDescriptor {
694            service_id: ServiceId::new(id).unwrap(),
695            version: "1.0".to_string(),
696            capabilities: HashMap::new(),
697        }
698    }
699
700    #[test]
701    fn test_register_and_unregister() {
702        let mut svc = MeshServices::new();
703        svc.register_service(make_descriptor("echo.v1")).unwrap();
704        assert!(svc.has_service("echo.v1"));
705
706        assert!(svc.unregister_service("echo.v1").unwrap());
707        assert!(!svc.has_service("echo.v1"));
708
709        assert!(!svc.unregister_service("echo.v1").unwrap());
710    }
711
712    #[test]
713    fn test_discover_services_generates_messages() {
714        let mut svc = MeshServices::new();
715        let peers = vec!["alice".to_string(), "bob".to_string()];
716        let result = svc.discover_services("user1", &peers, None).unwrap();
717        assert!(!result.query_id.is_empty());
718        assert_eq!(result.messages.len(), 2);
719        assert!(svc.seen_discovery_queries().contains_key(&result.query_id));
720    }
721
722    #[test]
723    fn test_discover_services_no_peers() {
724        let mut svc = MeshServices::new();
725        let result = svc
726            .discover_services("user1", &[], Some("weather"))
727            .unwrap();
728        assert!(result.messages.is_empty());
729    }
730
731    #[test]
732    fn test_discover_services_initial_broadcast_fanout_limit() {
733        let mut svc = MeshServices::new();
734        let peers: Vec<String> = (0..50).map(|i| format!("peer-{i}")).collect();
735        let result = svc.discover_services("user1", &peers, None).unwrap();
736        assert_eq!(result.messages.len(), DISCOVERY_INITIAL_BROADCAST_MAX);
737    }
738
739    #[test]
740    fn test_send_service_request() {
741        let svc = MeshServices::new();
742        let result = svc
743            .send_service_request("bob", "echo.v1", "ping", "{}")
744            .unwrap();
745        assert!(!result.request_id.is_empty());
746        assert_eq!(result.message.recipient, "bob");
747        assert!(result.message.content.starts_with(SVC_REQUEST));
748    }
749
750    #[test]
751    fn test_send_service_request_body_too_large() {
752        let svc = MeshServices::new();
753        let large_body = "x".repeat(MAX_SERVICE_BODY_SIZE + 1);
754        let result = svc.send_service_request("bob", "echo.v1", "ping", &large_body);
755        assert!(matches!(result, Err(ServiceError::PayloadTooLarge(_))));
756    }
757
758    #[test]
759    fn test_send_service_request_method_too_long() {
760        let svc = MeshServices::new();
761        let long_method = "m".repeat(MAX_SERVICE_METHOD_LEN + 1);
762        let result = svc.send_service_request("bob", "echo.v1", &long_method, "{}");
763        assert!(matches!(result, Err(ServiceError::PayloadTooLarge(_))));
764    }
765
766    #[test]
767    fn test_respond_to_service_request() {
768        let svc = MeshServices::new();
769        let result = svc
770            .respond_to_service_request("req-1", "alice", "echo.v1", "ok", "pong")
771            .unwrap();
772        assert_eq!(result.message.recipient, "alice");
773        assert!(result.message.content.starts_with(SVC_RESPONSE));
774    }
775
776    #[test]
777    fn test_respond_to_service_request_invalid_status() {
778        let svc = MeshServices::new();
779        let result = svc.respond_to_service_request("req-1", "alice", "echo.v1", "invalid", "");
780        assert!(matches!(result, Err(ServiceError::InvalidStatus(_))));
781    }
782
783    #[test]
784    fn test_respond_to_service_request_body_too_large() {
785        let svc = MeshServices::new();
786        let large_body = "x".repeat(MAX_SERVICE_BODY_SIZE + 1);
787        let result = svc.respond_to_service_request("req-1", "alice", "echo.v1", "ok", &large_body);
788        assert!(matches!(result, Err(ServiceError::PayloadTooLarge(_))));
789    }
790
791    #[test]
792    fn test_respond_valid_statuses() {
793        let svc = MeshServices::new();
794        for status in VALID_SERVICE_STATUSES {
795            let result =
796                svc.respond_to_service_request("req-1", "alice", "echo.v1", status, "body");
797            assert!(result.is_ok(), "status '{status}' should be valid");
798        }
799    }
800
801    #[test]
802    fn test_handle_discover_query_with_match() {
803        let mut svc = MeshServices::new();
804        svc.register_service(ServiceDescriptor {
805            service_id: ServiceId::new("weather").unwrap(),
806            version: "2.0".to_string(),
807            capabilities: {
808                let mut m = HashMap::new();
809                m.insert("format".to_string(), "json".to_string());
810                m
811            },
812        })
813        .unwrap();
814
815        let payload = ServiceDiscoveryQueryPayload {
816            query_id: "q-001".to_string(),
817            originator: "alice".to_string(),
818            service_id: Some("weather".to_string()),
819            remaining_hops: 5,
820        };
821        let content = format!(
822            "{}{}",
823            SVC_DISCOVER_QUERY,
824            serde_json::to_string(&payload).unwrap()
825        );
826
827        let peers = vec!["charlie".to_string()];
828        let action = svc.handle_incoming_message(&content, "alice", 1, "user1", &peers);
829        match action {
830            ServiceAction::Consumed {
831                messages_to_send,
832                events_to_emit,
833            } => {
834                assert!(!messages_to_send.is_empty());
835                assert!(events_to_emit.is_empty());
836                // Response should go to the sender ("alice"), not the originator field
837                assert!(messages_to_send.iter().any(
838                    |m| m.recipient == "alice" && m.content.starts_with(SVC_DISCOVER_RESPONSE)
839                ));
840            }
841            ServiceAction::NotHandled => panic!("Should have been handled"),
842        }
843        assert!(svc.seen_discovery_queries().contains_key("q-001"));
844    }
845
846    #[test]
847    fn test_handle_discover_query_dedup() {
848        let mut svc = MeshServices::new();
849
850        let payload = ServiceDiscoveryQueryPayload {
851            query_id: "q-dedup".to_string(),
852            originator: "alice".to_string(),
853            service_id: None,
854            remaining_hops: 5,
855        };
856        let content = format!(
857            "{}{}",
858            SVC_DISCOVER_QUERY,
859            serde_json::to_string(&payload).unwrap()
860        );
861
862        // First time: processes
863        let a1 = svc.handle_incoming_message(&content, "alice", 0, "user1", &[]);
864        assert!(matches!(a1, ServiceAction::Consumed { .. }));
865
866        // Second time: deduped
867        let a2 = svc.handle_incoming_message(&content, "alice", 0, "user1", &[]);
868        assert!(matches!(a2, ServiceAction::Consumed { .. }));
869    }
870
871    #[test]
872    fn test_handle_discover_response() {
873        let mut svc = MeshServices::new();
874
875        let payload = ServiceDiscoveryResponsePayload {
876            query_id: "q-123".to_string(),
877            service_id: "weather".to_string(),
878            version: "2.0".to_string(),
879            provider_peer_id: "bob".to_string(),
880            capabilities: HashMap::new(),
881            hop_count: 1,
882            originator: "user1".to_string(),
883        };
884        let content = format!(
885            "{}{}",
886            SVC_DISCOVER_RESPONSE,
887            serde_json::to_string(&payload).unwrap()
888        );
889
890        let action = svc.handle_incoming_message(&content, "bob", 0, "user1", &[]);
891        match action {
892            ServiceAction::Consumed { events_to_emit, .. } => {
893                assert_eq!(events_to_emit.len(), 1);
894                match &events_to_emit[0] {
895                    ServiceEvent::ServiceDiscovered {
896                        query_id,
897                        service_id,
898                        ..
899                    } => {
900                        assert_eq!(query_id, "q-123");
901                        assert_eq!(service_id, "weather");
902                    }
903                    other => panic!("Wrong event: {:?}", other),
904                }
905            }
906            ServiceAction::NotHandled => panic!("Should have been handled"),
907        }
908    }
909
910    #[test]
911    fn test_handle_request_unregistered() {
912        let mut svc = MeshServices::new();
913
914        let payload = ServiceRequestPayload {
915            request_id: "req-001".to_string(),
916            service_id: "nonexistent".to_string(),
917            method: "get".to_string(),
918            body: "{}".to_string(),
919        };
920        let content = format!(
921            "{}{}",
922            SVC_REQUEST,
923            serde_json::to_string(&payload).unwrap()
924        );
925
926        let action = svc.handle_incoming_message(&content, "alice", 0, "user1", &[]);
927        match action {
928            ServiceAction::Consumed {
929                messages_to_send,
930                events_to_emit,
931            } => {
932                assert_eq!(messages_to_send.len(), 1);
933                assert!(messages_to_send[0].content.contains("not_found"));
934                assert!(events_to_emit.is_empty());
935            }
936            ServiceAction::NotHandled => panic!("Should have been handled"),
937        }
938    }
939
940    #[test]
941    fn test_handle_request_registered() {
942        let mut svc = MeshServices::new();
943        svc.register_service(make_descriptor("echo")).unwrap();
944
945        let payload = ServiceRequestPayload {
946            request_id: "req-002".to_string(),
947            service_id: "echo".to_string(),
948            method: "ping".to_string(),
949            body: "hello".to_string(),
950        };
951        let content = format!(
952            "{}{}",
953            SVC_REQUEST,
954            serde_json::to_string(&payload).unwrap()
955        );
956
957        let action = svc.handle_incoming_message(&content, "alice", 0, "user1", &[]);
958        match action {
959            ServiceAction::Consumed {
960                messages_to_send,
961                events_to_emit,
962            } => {
963                assert!(messages_to_send.is_empty());
964                assert_eq!(events_to_emit.len(), 1);
965                match &events_to_emit[0] {
966                    ServiceEvent::ServiceRequestReceived {
967                        request_id,
968                        service_id,
969                        method,
970                        ..
971                    } => {
972                        assert_eq!(request_id, "req-002");
973                        assert_eq!(service_id, "echo");
974                        assert_eq!(method, "ping");
975                    }
976                    other => panic!("Wrong event: {:?}", other),
977                }
978            }
979            ServiceAction::NotHandled => panic!("Should have been handled"),
980        }
981    }
982
983    #[test]
984    fn test_handle_response() {
985        let mut svc = MeshServices::new();
986
987        let payload = ServiceResponsePayload {
988            request_id: "req-003".to_string(),
989            service_id: "echo".to_string(),
990            status: "ok".to_string(),
991            body: "pong".to_string(),
992        };
993        let content = format!(
994            "{}{}",
995            SVC_RESPONSE,
996            serde_json::to_string(&payload).unwrap()
997        );
998
999        let action = svc.handle_incoming_message(&content, "bob", 0, "user1", &[]);
1000        match action {
1001            ServiceAction::Consumed { events_to_emit, .. } => {
1002                assert_eq!(events_to_emit.len(), 1);
1003                match &events_to_emit[0] {
1004                    ServiceEvent::ServiceResponseReceived {
1005                        request_id, status, ..
1006                    } => {
1007                        assert_eq!(request_id, "req-003");
1008                        assert_eq!(status, "ok");
1009                    }
1010                    other => panic!("Wrong event: {:?}", other),
1011                }
1012            }
1013            ServiceAction::NotHandled => panic!("Should have been handled"),
1014        }
1015    }
1016
1017    #[test]
1018    fn test_not_handled() {
1019        let mut svc = MeshServices::new();
1020        let action = svc.handle_incoming_message("Hello, world!", "alice", 0, "user1", &[]);
1021        assert!(matches!(action, ServiceAction::NotHandled));
1022    }
1023
1024    #[test]
1025    fn test_cleanup_expired() {
1026        let mut svc = MeshServices::new();
1027        svc.seen_discovery_queries.insert(
1028            "old-query".to_string(),
1029            Instant::now() - Duration::from_secs(120),
1030        );
1031        svc.seen_discovery_order.push_back("old-query".to_string());
1032        svc.seen_discovery_queries
1033            .insert("fresh-query".to_string(), Instant::now());
1034        svc.seen_discovery_order
1035            .push_back("fresh-query".to_string());
1036
1037        svc.cleanup_expired();
1038
1039        assert!(!svc.seen_discovery_queries().contains_key("old-query"));
1040        assert!(svc.seen_discovery_queries().contains_key("fresh-query"));
1041    }
1042
1043    #[test]
1044    fn test_gossip_fanout_limit() {
1045        let mut svc = MeshServices::new();
1046
1047        let payload = ServiceDiscoveryQueryPayload {
1048            query_id: "q-fanout".to_string(),
1049            originator: "alice".to_string(),
1050            service_id: None,
1051            remaining_hops: 5,
1052        };
1053        let content = format!(
1054            "{}{}",
1055            SVC_DISCOVER_QUERY,
1056            serde_json::to_string(&payload).unwrap()
1057        );
1058
1059        // Create more peers than the fanout limit
1060        let peers: Vec<String> = (0..20).map(|i| format!("peer-{i}")).collect();
1061        let action = svc.handle_incoming_message(&content, "alice", 1, "user1", &peers);
1062        match action {
1063            ServiceAction::Consumed {
1064                messages_to_send, ..
1065            } => {
1066                // Should be capped at DISCOVERY_GOSSIP_MAX_FANOUT forwards
1067                assert_eq!(messages_to_send.len(), DISCOVERY_GOSSIP_MAX_FANOUT);
1068            }
1069            ServiceAction::NotHandled => panic!("Should have been handled"),
1070        }
1071    }
1072
1073    #[test]
1074    fn test_dedup_cap_evicts_oldest() {
1075        let mut svc = MeshServices::new();
1076
1077        // Fill to capacity using record_seen_query for consistent state
1078        let base = Instant::now() - Duration::from_secs(30);
1079        for i in 0..DISCOVERY_QUERY_MAX_DEDUP_ENTRIES {
1080            let key = format!("q-{i}");
1081            svc.seen_discovery_queries
1082                .insert(key.clone(), base + Duration::from_millis(i as u64));
1083            svc.seen_discovery_order.push_back(key);
1084        }
1085        assert_eq!(
1086            svc.seen_discovery_queries().len(),
1087            DISCOVERY_QUERY_MAX_DEDUP_ENTRIES
1088        );
1089
1090        // Insert one more — should evict the oldest (q-0)
1091        svc.record_seen_query("q-new".to_string());
1092        assert_eq!(
1093            svc.seen_discovery_queries().len(),
1094            DISCOVERY_QUERY_MAX_DEDUP_ENTRIES
1095        );
1096        assert!(!svc.seen_discovery_queries().contains_key("q-0"));
1097        assert!(svc.seen_discovery_queries().contains_key("q-new"));
1098    }
1099
1100    #[test]
1101    fn test_remaining_hops_zero_no_forwarding() {
1102        let mut svc = MeshServices::new();
1103
1104        let payload = ServiceDiscoveryQueryPayload {
1105            query_id: "q-nohop".to_string(),
1106            originator: "alice".to_string(),
1107            service_id: None,
1108            remaining_hops: 0,
1109        };
1110        let content = format!(
1111            "{}{}",
1112            SVC_DISCOVER_QUERY,
1113            serde_json::to_string(&payload).unwrap()
1114        );
1115
1116        let peers = vec!["bob".to_string(), "charlie".to_string()];
1117        let action = svc.handle_incoming_message(&content, "alice", 1, "user1", &peers);
1118        match action {
1119            ServiceAction::Consumed {
1120                messages_to_send, ..
1121            } => {
1122                // No local services, no forwarding — should be empty
1123                assert!(
1124                    messages_to_send.is_empty(),
1125                    "Should not forward when remaining_hops=0"
1126                );
1127            }
1128            ServiceAction::NotHandled => panic!("Should have been handled"),
1129        }
1130    }
1131
1132    #[test]
1133    fn test_oversized_payload_dropped() {
1134        let mut svc = MeshServices::new();
1135
1136        // Build a request with oversized body
1137        let huge_body = "x".repeat(MAX_SERVICE_PAYLOAD_SIZE + 1);
1138        let content = format!("{}{}", SVC_REQUEST, huge_body);
1139
1140        let action = svc.handle_incoming_message(&content, "alice", 0, "user1", &[]);
1141        assert!(matches!(
1142            action,
1143            ServiceAction::Consumed {
1144                messages_to_send,
1145                events_to_emit,
1146            } if messages_to_send.is_empty() && events_to_emit.is_empty()
1147        ));
1148    }
1149
1150    #[test]
1151    fn test_oversized_request_body_dropped() {
1152        let mut svc = MeshServices::new();
1153        svc.register_service(make_descriptor("echo")).unwrap();
1154
1155        let large_body = "x".repeat(MAX_SERVICE_BODY_SIZE + 1);
1156        let payload = ServiceRequestPayload {
1157            request_id: "req-big".to_string(),
1158            service_id: "echo".to_string(),
1159            method: "ping".to_string(),
1160            body: large_body,
1161        };
1162        let content = format!(
1163            "{}{}",
1164            SVC_REQUEST,
1165            serde_json::to_string(&payload).unwrap()
1166        );
1167
1168        let action = svc.handle_incoming_message(&content, "alice", 0, "user1", &[]);
1169        match action {
1170            ServiceAction::Consumed {
1171                messages_to_send,
1172                events_to_emit,
1173            } => {
1174                assert!(messages_to_send.is_empty());
1175                assert!(
1176                    events_to_emit.is_empty(),
1177                    "Should not emit event for oversized body"
1178                );
1179            }
1180            ServiceAction::NotHandled => panic!("Should have been handled"),
1181        }
1182    }
1183
1184    #[test]
1185    fn test_oversized_method_dropped() {
1186        let mut svc = MeshServices::new();
1187        svc.register_service(make_descriptor("echo")).unwrap();
1188
1189        let long_method = "m".repeat(MAX_SERVICE_METHOD_LEN + 1);
1190        let payload = ServiceRequestPayload {
1191            request_id: "req-longmethod".to_string(),
1192            service_id: "echo".to_string(),
1193            method: long_method,
1194            body: "{}".to_string(),
1195        };
1196        let content = format!(
1197            "{}{}",
1198            SVC_REQUEST,
1199            serde_json::to_string(&payload).unwrap()
1200        );
1201
1202        let action = svc.handle_incoming_message(&content, "alice", 0, "user1", &[]);
1203        match action {
1204            ServiceAction::Consumed {
1205                messages_to_send,
1206                events_to_emit,
1207            } => {
1208                assert!(messages_to_send.is_empty());
1209                assert!(
1210                    events_to_emit.is_empty(),
1211                    "Should not emit event for oversized method"
1212                );
1213            }
1214            ServiceAction::NotHandled => panic!("Should have been handled"),
1215        }
1216    }
1217
1218    #[test]
1219    fn test_handle_response_invalid_status_dropped() {
1220        let mut svc = MeshServices::new();
1221
1222        let payload = ServiceResponsePayload {
1223            request_id: "req-bad".to_string(),
1224            service_id: "echo".to_string(),
1225            status: "invalid_status".to_string(),
1226            body: "data".to_string(),
1227        };
1228        let content = format!(
1229            "{}{}",
1230            SVC_RESPONSE,
1231            serde_json::to_string(&payload).unwrap()
1232        );
1233
1234        let action = svc.handle_incoming_message(&content, "bob", 0, "user1", &[]);
1235        match action {
1236            ServiceAction::Consumed {
1237                messages_to_send,
1238                events_to_emit,
1239            } => {
1240                assert!(messages_to_send.is_empty());
1241                assert!(
1242                    events_to_emit.is_empty(),
1243                    "Should not emit event for invalid status"
1244                );
1245            }
1246            ServiceAction::NotHandled => panic!("Should have been handled"),
1247        }
1248    }
1249
1250    #[test]
1251    fn test_handle_discover_query_with_match_and_forwarding() {
1252        let mut svc = MeshServices::new();
1253        svc.register_service(ServiceDescriptor {
1254            service_id: ServiceId::new("weather").unwrap(),
1255            version: "2.0".to_string(),
1256            capabilities: HashMap::new(),
1257        })
1258        .unwrap();
1259
1260        let payload = ServiceDiscoveryQueryPayload {
1261            query_id: "q-combined".to_string(),
1262            originator: "alice".to_string(),
1263            service_id: Some("weather".to_string()),
1264            remaining_hops: 5,
1265        };
1266        let content = format!(
1267            "{}{}",
1268            SVC_DISCOVER_QUERY,
1269            serde_json::to_string(&payload).unwrap()
1270        );
1271
1272        // Provide peers to forward to (excluding sender "alice" and originator "alice")
1273        let peers = vec!["bob".to_string(), "charlie".to_string(), "dave".to_string()];
1274        let action = svc.handle_incoming_message(&content, "alice", 1, "user1", &peers);
1275        match action {
1276            ServiceAction::Consumed {
1277                messages_to_send,
1278                events_to_emit,
1279            } => {
1280                // Should have a discovery response back to sender
1281                let responses: Vec<_> = messages_to_send
1282                    .iter()
1283                    .filter(|m| m.content.starts_with(SVC_DISCOVER_RESPONSE))
1284                    .collect();
1285                assert_eq!(responses.len(), 1, "Should have one discovery response");
1286                assert_eq!(
1287                    responses[0].recipient, "alice",
1288                    "Response should go to sender"
1289                );
1290
1291                // Should also have forwarded query messages to other peers
1292                let forwards: Vec<_> = messages_to_send
1293                    .iter()
1294                    .filter(|m| m.content.starts_with(SVC_DISCOVER_QUERY))
1295                    .collect();
1296                assert_eq!(forwards.len(), 3, "Should forward to all 3 eligible peers");
1297                // Forwarded queries should NOT go to the sender
1298                assert!(
1299                    forwards.iter().all(|m| m.recipient != "alice"),
1300                    "Should not forward back to sender"
1301                );
1302
1303                // No events emitted for queries — events only on discovery responses
1304                assert!(events_to_emit.is_empty());
1305            }
1306            ServiceAction::NotHandled => panic!("Should have been handled"),
1307        }
1308    }
1309
1310    #[test]
1311    fn test_select_fanout_peers_all_when_under_limit() {
1312        let peers: Vec<String> = vec!["a".into(), "b".into(), "c".into()];
1313        let selected = select_fanout_peers(&peers, 5, "q-1", "user1");
1314        assert_eq!(selected.len(), 3);
1315    }
1316
1317    #[test]
1318    fn test_select_fanout_peers_caps_at_max() {
1319        let peers: Vec<String> = (0..50).map(|i| format!("p-{i}")).collect();
1320        let selected = select_fanout_peers(&peers, 5, "q-1", "user1");
1321        assert_eq!(selected.len(), 5);
1322    }
1323
1324    #[test]
1325    fn test_select_fanout_peers_deterministic() {
1326        let peers: Vec<String> = (0..20).map(|i| format!("p-{i}")).collect();
1327        let a = select_fanout_peers(&peers, 5, "q-1", "user1");
1328        let b = select_fanout_peers(&peers, 5, "q-1", "user1");
1329        assert_eq!(a, b, "Same inputs should produce same selection");
1330    }
1331
1332    #[test]
1333    fn test_select_fanout_peers_varies_by_user() {
1334        let peers: Vec<String> = (0..20).map(|i| format!("p-{i}")).collect();
1335        let a = select_fanout_peers(&peers, 5, "q-1", "user1");
1336        let b = select_fanout_peers(&peers, 5, "q-1", "user2");
1337        // Different users should (usually) select different subsets
1338        // This is probabilistic but with 20 peers and 5 selected, collision is unlikely
1339        assert_ne!(
1340            a, b,
1341            "Different users should typically select different peers"
1342        );
1343    }
1344
1345    #[test]
1346    fn test_discovery_response_forwarded_to_originator() {
1347        let mut svc = MeshServices::new();
1348
1349        // Response with originator != user_id → should forward
1350        let payload = ServiceDiscoveryResponsePayload {
1351            query_id: "q-200".to_string(),
1352            service_id: "svc1".to_string(),
1353            version: "1.0".to_string(),
1354            provider_peer_id: "provider1".to_string(),
1355            capabilities: HashMap::new(),
1356            hop_count: 2,
1357            originator: "original_requester".to_string(),
1358        };
1359        let content = format!(
1360            "{}{}",
1361            SVC_DISCOVER_RESPONSE,
1362            serde_json::to_string(&payload).unwrap()
1363        );
1364
1365        // We are "relay_node", not the originator
1366        let action = svc.handle_incoming_message(&content, "provider1", 0, "relay_node", &[]);
1367        match action {
1368            ServiceAction::Consumed {
1369                messages_to_send,
1370                events_to_emit,
1371            } => {
1372                // Should forward to originator, not emit event
1373                assert!(
1374                    events_to_emit.is_empty(),
1375                    "Relay node should not emit ServiceDiscovered"
1376                );
1377                assert_eq!(messages_to_send.len(), 1);
1378                assert_eq!(messages_to_send[0].recipient, "original_requester");
1379            }
1380            ServiceAction::NotHandled => panic!("Should have been handled"),
1381        }
1382    }
1383
1384    #[test]
1385    fn test_discovery_response_consumed_by_originator() {
1386        let mut svc = MeshServices::new();
1387
1388        // Response where originator == user_id → should emit event
1389        let payload = ServiceDiscoveryResponsePayload {
1390            query_id: "q-300".to_string(),
1391            service_id: "svc2".to_string(),
1392            version: "1.0".to_string(),
1393            provider_peer_id: "provider2".to_string(),
1394            capabilities: HashMap::new(),
1395            hop_count: 1,
1396            originator: "me".to_string(),
1397        };
1398        let content = format!(
1399            "{}{}",
1400            SVC_DISCOVER_RESPONSE,
1401            serde_json::to_string(&payload).unwrap()
1402        );
1403
1404        let action = svc.handle_incoming_message(&content, "relay_node", 0, "me", &[]);
1405        match action {
1406            ServiceAction::Consumed {
1407                messages_to_send,
1408                events_to_emit,
1409            } => {
1410                assert!(messages_to_send.is_empty(), "Originator should not forward");
1411                assert_eq!(events_to_emit.len(), 1);
1412                match &events_to_emit[0] {
1413                    ServiceEvent::ServiceDiscovered { service_id, .. } => {
1414                        assert_eq!(service_id, "svc2");
1415                    }
1416                    other => panic!("Wrong event: {:?}", other),
1417                }
1418            }
1419            ServiceAction::NotHandled => panic!("Should have been handled"),
1420        }
1421    }
1422}