p2p/webrtc/signaling_method/
mod.rs

1//! WebRTC Signaling Transport Methods
2//!
3//! This module defines the different transport methods available for WebRTC signaling
4//! in OpenMina's peer-to-peer network. WebRTC requires an external signaling mechanism
5//! to exchange connection metadata before establishing direct peer-to-peer connections.
6//!
7//! ## Signaling Transport Methods
8//!
9//! OpenMina supports multiple signaling transport methods to accommodate different
10//! network environments and security requirements:
11//!
12//! ### HTTP/HTTPS Direct Connections
13//!
14//! - **HTTP**: Direct HTTP connections to signaling servers (typically for local/testing)
15//! - **HTTPS**: Secure HTTPS connections to signaling servers (recommended for production)
16//!
17//! These methods allow peers to directly contact signaling servers to exchange offers
18//! and answers for WebRTC connection establishment.
19//!
20//! ### HTTPS Proxy
21//!
22//! - **HTTPS Proxy**: Uses an SSL gateway/proxy server to reach the actual signaling server
23//!
24//! ### P2P Relay Signaling
25//!
26//! - **P2P Relay**: Uses existing peer connections to relay signaling messages
27//! - Enables signaling through already-established peer connections
28//! - Provides redundancy when direct signaling server access is unavailable
29//! - Supports bootstrapping new connections through existing network peers
30//!
31//! ## URL Format
32//!
33//! Signaling methods use a structured URL format:
34//!
35//! - HTTP: `/http/{host}/{port}`
36//! - HTTPS: `/https/{host}/{port}`
37//! - HTTPS Proxy: `/https_proxy/{cluster_id}/{host}/{port}`
38//! - P2P Relay: `/p2p/{peer_id}`
39//!
40//! ## Connection Strategy
41//!
42//! The signaling method determines how peers discover and connect to each other:
43//!
44//! 1. **Direct Methods** (HTTP/HTTPS) - Can connect immediately to signaling servers
45//! 2. **Proxy Methods** - Route through intermediate proxy infrastructure
46//! 3. **Relay Methods** - Require existing peer connections for message routing
47
48mod http;
49pub use http::HttpSignalingInfo;
50
51use std::{fmt, str::FromStr};
52
53use binprot_derive::{BinProtRead, BinProtWrite};
54use serde::{Deserialize, Serialize};
55use thiserror::Error;
56
57use crate::PeerId;
58
59/// WebRTC signaling transport method configuration.
60///
61/// `SignalingMethod` defines how WebRTC signaling messages (offers and answers)
62/// are transported between peers. Different methods provide flexibility for
63/// various network environments and infrastructure requirements.
64///
65/// # Method Types
66///
67/// - **HTTP/HTTPS**: Direct connections to signaling servers
68/// - **HTTPS Proxy**: Connections through SSL gateway/proxy servers
69/// - **P2P Relay**: Signaling through existing peer connections
70///
71/// Each method encapsulates the necessary connection information to establish
72/// the signaling channel, which is used before the actual WebRTC peer-to-peer
73/// connection is established.
74///
75/// # Usage
76///
77/// Signaling methods can be parsed from string representations or constructed
78/// programmatically. They support serialization for storage and network transmission.
79///
80/// # Example
81///
82/// ```
83/// // Direct HTTPS signaling
84/// let method = "/https/signal.example.com/443".parse::<SignalingMethod>()?;
85///
86/// // P2P relay through an existing peer
87/// let method = SignalingMethod::P2p { relay_peer_id: peer_id };
88/// ```
89#[derive(BinProtWrite, BinProtRead, Eq, PartialEq, Ord, PartialOrd, Debug, Clone)]
90pub enum SignalingMethod {
91    /// HTTP signaling server connection.
92    ///
93    /// Uses plain HTTP for signaling message exchange. Typically used for
94    /// local development or testing environments where encryption is not required.
95    Http(HttpSignalingInfo),
96
97    /// HTTPS signaling server connection.
98    ///
99    /// Uses secure HTTPS for signaling message exchange. Recommended for
100    /// production environments to protect signaling data in transit.
101    Https(HttpSignalingInfo),
102
103    /// HTTPS proxy signaling connection.
104    ///
105    /// Uses an SSL gateway/proxy server to reach the actual signaling server.
106    /// The first parameter is the cluster ID for routing, and the second
107    /// parameter contains the proxy server connection information.
108    HttpsProxy(u16, HttpSignalingInfo),
109
110    /// P2P relay signaling through an existing peer connection.
111    ///
112    /// Uses an already-established peer connection to relay signaling messages
113    /// to other peers. This enables signaling when direct access to signaling
114    /// servers is unavailable and provides redundancy in the signaling process.
115    P2p {
116        /// The peer ID of the relay peer that will forward signaling messages.
117        relay_peer_id: PeerId,
118    },
119}
120
121impl SignalingMethod {
122    /// Determines if this signaling method supports direct connections.
123    ///
124    /// Direct connection methods (HTTP, HTTPS, HTTPS Proxy) can establish
125    /// signaling channels immediately without requiring existing peer connections.
126    /// P2P relay methods require an already-established peer connection to function.
127    ///
128    /// # Returns
129    ///
130    /// * `true` for HTTP, HTTPS, and HTTPS Proxy methods
131    /// * `false` for P2P relay methods
132    ///
133    /// This is useful for connection strategy decisions and determining whether
134    /// bootstrap connections are needed before signaling can occur.
135    pub fn can_connect_directly(&self) -> bool {
136        match self {
137            Self::Http(_) | Self::Https(_) | Self::HttpsProxy(_, _) => true,
138            Self::P2p { .. } => false,
139        }
140    }
141
142    /// Constructs the HTTP(S) URL for sending WebRTC offers.
143    ///
144    /// This method generates the appropriate URL endpoint for sending WebRTC
145    /// signaling messages based on the signaling method configuration.
146    ///
147    /// # URL Formats
148    ///
149    /// - **HTTP**: `http://{host}:{port}/mina/webrtc/signal`
150    /// - **HTTPS**: `https://{host}:{port}/mina/webrtc/signal`
151    /// - **HTTPS Proxy**: `https://{host}:{port}/clusters/{cluster_id}/mina/webrtc/signal`
152    ///
153    /// # Returns
154    ///
155    /// * `Some(String)` containing the signaling URL for HTTP-based methods
156    /// * `None` for P2P relay methods that don't use HTTP endpoints
157    ///
158    /// # Example
159    ///
160    /// ```
161    /// let method = SignalingMethod::Https(info);
162    /// let url = method.http_url(); // Some("https://signal.example.com:443/mina/webrtc/signal")
163    /// ```
164    pub fn http_url(&self) -> Option<String> {
165        let (http, info) = match self {
166            Self::Http(info) => ("http", info),
167            Self::Https(info) => ("https", info),
168            Self::HttpsProxy(cluster_id, info) => {
169                return Some(format!(
170                    "https://{}:{}/clusters/{}/mina/webrtc/signal",
171                    info.host, info.port, cluster_id
172                ));
173            }
174            _ => return None,
175        };
176        Some(format!(
177            "{http}://{}:{}/mina/webrtc/signal",
178            info.host, info.port,
179        ))
180    }
181
182    /// Extracts the relay peer ID for P2P signaling methods.
183    ///
184    /// For P2P relay signaling methods, this returns the peer ID of the
185    /// intermediate peer that will forward signaling messages. This is used
186    /// to identify which existing peer connection should be used for relaying.
187    ///
188    /// # Returns
189    ///
190    /// * `Some(PeerId)` for P2P relay methods
191    /// * `None` for direct connection methods (HTTP/HTTPS)
192    ///
193    /// # Usage
194    ///
195    /// This method is typically used when setting up message routing for
196    /// P2P relay signaling to determine which peer connection should handle
197    /// the signaling traffic.
198    pub fn p2p_relay_peer_id(&self) -> Option<PeerId> {
199        match self {
200            Self::P2p { relay_peer_id } => Some(*relay_peer_id),
201            _ => None,
202        }
203    }
204}
205
206impl fmt::Display for SignalingMethod {
207    /// Formats the signaling method as a URL path string.
208    ///
209    /// This implementation converts the signaling method into its string
210    /// representation following the URL format patterns. The formatted
211    /// string can be parsed back using [`FromStr`].
212    ///
213    /// # Format Patterns
214    ///
215    /// - HTTP: `/http/{host}/{port}`
216    /// - HTTPS: `/https/{host}/{port}`
217    /// - HTTPS Proxy: `/https_proxy/{cluster_id}/{host}/{port}`
218    /// - P2P Relay: `/p2p/{peer_id}`
219    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220        match self {
221            Self::Http(signaling) => {
222                write!(f, "/http")?;
223                signaling.fmt(f)
224            }
225            Self::Https(signaling) => {
226                write!(f, "/https")?;
227                signaling.fmt(f)
228            }
229            Self::HttpsProxy(cluster_id, signaling) => {
230                write!(f, "/https_proxy/{cluster_id}")?;
231                signaling.fmt(f)
232            }
233            Self::P2p { relay_peer_id } => {
234                write!(f, "/p2p/{relay_peer_id}")
235            }
236        }
237    }
238}
239
240/// Errors that can occur when parsing signaling method strings.
241///
242/// `SignalingMethodParseError` provides detailed error information for
243/// parsing failures when converting string representations to [`SignalingMethod`]
244/// instances. This helps with debugging configuration and user input validation.
245///
246/// # Error Types
247///
248/// The parser can fail for various reasons including missing components,
249/// invalid formats, or unsupported method types. Each error variant provides
250/// specific context about what went wrong during parsing.
251#[derive(Error, Serialize, Deserialize, Debug, Clone)]
252pub enum SignalingMethodParseError {
253    /// Insufficient arguments provided for the signaling method.
254    ///
255    /// This occurs when the input string doesn't contain enough components
256    /// to construct a valid signaling method. For example, missing host
257    /// or port information for HTTP methods.
258    #[error("not enough args for the signaling method")]
259    NotEnoughArgs,
260
261    /// Unknown or unsupported signaling method type.
262    ///
263    /// This occurs when the method type (first component) is not recognized.
264    /// Supported methods are: `http`, `https`, `https_proxy`, `p2p`.
265    #[error("unknown signaling method: `{0}`")]
266    UnknownSignalingMethod(String),
267
268    /// Invalid cluster ID for HTTPS proxy methods.
269    ///
270    /// This occurs when the cluster ID component cannot be parsed as a
271    /// valid 16-bit unsigned integer for HTTPS proxy configurations.
272    #[error("invalid cluster id")]
273    InvalidClusterId,
274
275    /// Failed to parse the host component.
276    ///
277    /// This occurs when the host string cannot be parsed as a valid
278    /// hostname, IP address, or multiaddr format by the Host parser.
279    #[error("host parse error: {0}")]
280    HostParseError(String),
281
282    /// Failed to parse the port component.
283    ///
284    /// This occurs when the port string cannot be parsed as a valid
285    /// 16-bit unsigned integer port number.
286    #[error("port parse error: {0}")]
287    PortParseError(String),
288}
289
290impl FromStr for SignalingMethod {
291    type Err = SignalingMethodParseError;
292
293    /// Parses a string representation into a [`SignalingMethod`].
294    ///
295    /// This method parses URL-like strings that represent different signaling
296    /// transport methods. The parser supports the following formats:
297    ///
298    /// # Supported Formats
299    ///
300    /// - **HTTP**: `/http/{host}/{port}`
301    /// - **HTTPS**: `/https/{host}/{port}`
302    /// - **HTTPS Proxy**: `/https_proxy/{cluster_id}/{host}/{port}`
303    /// - **P2P Relay**: `/p2p/{peer_id}`
304    ///
305    /// # Examples
306    ///
307    /// ```
308    /// use openmina::signaling_method::SignalingMethod;
309    ///
310    /// // HTTP signaling
311    /// let method: SignalingMethod = "/http/localhost/8080".parse()?;
312    ///
313    /// // HTTPS signaling
314    /// let method: SignalingMethod = "/https/signal.example.com/443".parse()?;
315    ///
316    /// // HTTPS proxy with cluster ID
317    /// let method: SignalingMethod = "/https_proxy/123/proxy.example.com/443".parse()?;
318    /// ```
319    ///
320    /// # Errors
321    ///
322    /// Returns [`SignalingMethodParseError`] for various parsing failures:
323    /// - Missing components (host, port, etc.)
324    /// - Unknown method types
325    /// - Invalid numeric values (ports, cluster IDs)
326    /// - Invalid host formats
327    fn from_str(s: &str) -> Result<Self, Self::Err> {
328        if s.is_empty() {
329            return Err(SignalingMethodParseError::NotEnoughArgs);
330        }
331
332        let method_end_index = s[1..]
333            .find('/')
334            .map(|i| i + 1)
335            .filter(|i| s.len() > *i)
336            .ok_or(SignalingMethodParseError::NotEnoughArgs)?;
337
338        let rest = &s[method_end_index..];
339        match &s[1..method_end_index] {
340            "http" => Ok(Self::Http(rest.parse()?)),
341            "https" => Ok(Self::Https(rest.parse()?)),
342            "https_proxy" => {
343                let mut iter = rest.splitn(3, '/').filter(|v| !v.trim().is_empty());
344                let (cluster_id, rest) = (
345                    iter.next()
346                        .ok_or(SignalingMethodParseError::NotEnoughArgs)?,
347                    iter.next()
348                        .ok_or(SignalingMethodParseError::NotEnoughArgs)?,
349                );
350                let cluster_id: u16 = cluster_id
351                    .parse()
352                    .or(Err(SignalingMethodParseError::InvalidClusterId))?;
353                Ok(Self::HttpsProxy(cluster_id, rest.parse()?))
354            }
355            method => Err(SignalingMethodParseError::UnknownSignalingMethod(
356                method.to_owned(),
357            )),
358        }
359    }
360}
361
362impl Serialize for SignalingMethod {
363    /// Serializes the signaling method as a string.
364    ///
365    /// This uses the `Display` implementation to convert the signaling
366    /// method to its string representation for serialization.
367    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
368    where
369        S: serde::Serializer,
370    {
371        serializer.serialize_str(&self.to_string())
372    }
373}
374
375impl<'de> serde::Deserialize<'de> for SignalingMethod {
376    /// Deserializes a signaling method from a string.
377    ///
378    /// This uses the [`FromStr`] implementation to parse the string
379    /// representation back into a [`SignalingMethod`] instance.
380    ///
381    /// # Errors
382    ///
383    /// Returns a deserialization error if the string cannot be parsed
384    /// as a valid signaling method.
385    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
386    where
387        D: serde::Deserializer<'de>,
388    {
389        let s: String = Deserialize::deserialize(deserializer)?;
390        s.parse().map_err(serde::de::Error::custom)
391    }
392}
393
394#[cfg(test)]
395mod tests {
396    //! Unit tests for SignalingMethod parsing
397    //!
398    //! Run these tests with:
399    //! ```bash
400    //! cargo test -p p2p signaling_method::tests
401    //! ```
402
403    use super::*;
404    use crate::webrtc::Host;
405    use std::net::Ipv4Addr;
406
407    #[test]
408    fn test_from_str_valid_http() {
409        let method: SignalingMethod = "/http/example.com/8080".parse().unwrap();
410        match method {
411            SignalingMethod::Http(info) => {
412                assert_eq!(info.host, Host::Domain("example.com".to_string()));
413                assert_eq!(info.port, 8080);
414            }
415            _ => panic!("Expected Http variant"),
416        }
417    }
418
419    #[test]
420    fn test_from_str_valid_https() {
421        let method: SignalingMethod = "/https/signal.example.com/443".parse().unwrap();
422        match method {
423            SignalingMethod::Https(info) => {
424                assert_eq!(info.host, Host::Domain("signal.example.com".to_string()));
425                assert_eq!(info.port, 443);
426            }
427            _ => panic!("Expected Https variant"),
428        }
429    }
430
431    #[test]
432    fn test_from_str_valid_https_proxy() {
433        let method: SignalingMethod = "/https_proxy/123/proxy.example.com/443".parse().unwrap();
434        match method {
435            SignalingMethod::HttpsProxy(cluster_id, info) => {
436                assert_eq!(cluster_id, 123);
437                assert_eq!(info.host, Host::Domain("proxy.example.com".to_string()));
438                assert_eq!(info.port, 443);
439            }
440            _ => panic!("Expected HttpsProxy variant"),
441        }
442    }
443
444    #[test]
445    fn test_from_str_valid_https_proxy_max_cluster_id() {
446        let method: SignalingMethod = "/https_proxy/65535/proxy.example.com/443".parse().unwrap();
447        match method {
448            SignalingMethod::HttpsProxy(cluster_id, info) => {
449                assert_eq!(cluster_id, 65535);
450                assert_eq!(info.host, Host::Domain("proxy.example.com".to_string()));
451                assert_eq!(info.port, 443);
452            }
453            _ => panic!("Expected HttpsProxy variant"),
454        }
455    }
456
457    #[test]
458    fn test_from_str_valid_http_ipv4() {
459        let method: SignalingMethod = "/http/192.168.1.1/8080".parse().unwrap();
460        match method {
461            SignalingMethod::Http(info) => {
462                assert_eq!(info.host, Host::Ipv4(Ipv4Addr::new(192, 168, 1, 1)));
463                assert_eq!(info.port, 8080);
464            }
465            _ => panic!("Expected Http variant"),
466        }
467    }
468
469    #[test]
470    fn test_from_str_valid_https_ipv6() {
471        let method: SignalingMethod = "/https/[::1]/443".parse().unwrap();
472        match method {
473            SignalingMethod::Https(info) => {
474                assert!(matches!(info.host, Host::Ipv6(_)));
475                assert_eq!(info.port, 443);
476            }
477            _ => panic!("Expected Https variant"),
478        }
479    }
480
481    #[test]
482    fn test_from_str_empty_string() {
483        let result: Result<SignalingMethod, _> = "".parse();
484        assert!(result.is_err());
485        assert!(matches!(
486            result.unwrap_err(),
487            SignalingMethodParseError::NotEnoughArgs
488        ));
489    }
490
491    #[test]
492    fn test_from_str_no_leading_slash() {
493        let result: Result<SignalingMethod, _> = "http/example.com/8080".parse();
494        assert!(result.is_err());
495        // Without leading slash, it treats "http" as unknown method since
496        // there's no slash at start
497        assert!(matches!(
498            result.unwrap_err(),
499            SignalingMethodParseError::UnknownSignalingMethod(_)
500        ));
501    }
502
503    #[test]
504    fn test_from_str_only_slash() {
505        let result: Result<SignalingMethod, _> = "/".parse();
506        assert!(result.is_err());
507        assert!(matches!(
508            result.unwrap_err(),
509            SignalingMethodParseError::NotEnoughArgs
510        ));
511    }
512
513    #[test]
514    fn test_from_str_unknown_method() {
515        let result: Result<SignalingMethod, _> = "/websocket/example.com/8080".parse();
516        assert!(result.is_err());
517        assert!(matches!(
518            result.unwrap_err(),
519            SignalingMethodParseError::UnknownSignalingMethod(_)
520        ));
521    }
522
523    #[test]
524    fn test_from_str_unknown_method_with_valid_format() {
525        let result: Result<SignalingMethod, _> = "/ftp/example.com/21".parse();
526        assert!(result.is_err());
527        match result.unwrap_err() {
528            SignalingMethodParseError::UnknownSignalingMethod(method) => {
529                assert_eq!(method, "ftp");
530            }
531            _ => panic!("Expected UnknownSignalingMethod error"),
532        }
533    }
534
535    #[test]
536    fn test_from_str_http_missing_host() {
537        let result: Result<SignalingMethod, _> = "/http".parse();
538        assert!(result.is_err());
539        assert!(matches!(
540            result.unwrap_err(),
541            SignalingMethodParseError::NotEnoughArgs
542        ));
543    }
544
545    #[test]
546    fn test_from_str_http_missing_port() {
547        let result: Result<SignalingMethod, _> = "/http/example.com".parse();
548        assert!(result.is_err());
549        assert!(matches!(
550            result.unwrap_err(),
551            SignalingMethodParseError::NotEnoughArgs
552        ));
553    }
554
555    #[test]
556    fn test_from_str_http_invalid_port() {
557        let result: Result<SignalingMethod, _> = "/http/example.com/abc".parse();
558        assert!(result.is_err());
559        assert!(matches!(
560            result.unwrap_err(),
561            SignalingMethodParseError::PortParseError(_)
562        ));
563    }
564
565    #[test]
566    fn test_from_str_http_port_too_large() {
567        let result: Result<SignalingMethod, _> = "/http/example.com/99999".parse();
568        assert!(result.is_err());
569        assert!(matches!(
570            result.unwrap_err(),
571            SignalingMethodParseError::PortParseError(_)
572        ));
573    }
574
575    #[test]
576    fn test_from_str_https_proxy_missing_cluster_id() {
577        let result: Result<SignalingMethod, _> = "/https_proxy".parse();
578        assert!(result.is_err());
579        assert!(matches!(
580            result.unwrap_err(),
581            SignalingMethodParseError::NotEnoughArgs
582        ));
583    }
584
585    #[test]
586    fn test_from_str_https_proxy_missing_host() {
587        let result: Result<SignalingMethod, _> = "/https_proxy/123".parse();
588        assert!(result.is_err());
589        assert!(matches!(
590            result.unwrap_err(),
591            SignalingMethodParseError::NotEnoughArgs
592        ));
593    }
594
595    #[test]
596    fn test_from_str_https_proxy_invalid_cluster_id() {
597        let result: Result<SignalingMethod, _> = "/https_proxy/abc/proxy.example.com/443".parse();
598        assert!(result.is_err());
599        assert!(matches!(
600            result.unwrap_err(),
601            SignalingMethodParseError::InvalidClusterId
602        ));
603    }
604
605    #[test]
606    fn test_from_str_https_proxy_cluster_id_too_large() {
607        let result: Result<SignalingMethod, _> = "/https_proxy/99999/proxy.example.com/443".parse();
608        assert!(result.is_err());
609        assert!(matches!(
610            result.unwrap_err(),
611            SignalingMethodParseError::InvalidClusterId
612        ));
613    }
614
615    #[test]
616    fn test_from_str_https_proxy_negative_cluster_id() {
617        let result: Result<SignalingMethod, _> = "/https_proxy/-1/proxy.example.com/443".parse();
618        assert!(result.is_err());
619        assert!(matches!(
620            result.unwrap_err(),
621            SignalingMethodParseError::InvalidClusterId
622        ));
623    }
624
625    #[test]
626    fn test_from_str_invalid_host() {
627        // This will depend on Host's parsing behavior - assuming it rejects
628        // certain formats
629        let result: Result<SignalingMethod, _> = "/http//8080".parse();
630        assert!(result.is_err());
631        // Should be either NotEnoughArgs or HostParseError depending on
632        // implementation
633        assert!(matches!(
634            result.unwrap_err(),
635            SignalingMethodParseError::NotEnoughArgs | SignalingMethodParseError::HostParseError(_)
636        ));
637    }
638
639    #[test]
640    fn test_from_str_extra_slashes() {
641        let result: Result<SignalingMethod, _> = "//http//example.com//8080//".parse();
642        assert!(result.is_err());
643        // The extra slashes mean method parsing fails - "http" becomes unknown
644        // method
645        assert!(matches!(
646            result.unwrap_err(),
647            SignalingMethodParseError::UnknownSignalingMethod(_)
648        ));
649    }
650
651    #[test]
652    fn test_roundtrip_http() {
653        let original = SignalingMethod::Http(HttpSignalingInfo {
654            host: Host::Domain("example.com".to_string()),
655            port: 8080,
656        });
657
658        let serialized = original.to_string();
659        let deserialized: SignalingMethod = serialized.parse().unwrap();
660
661        assert_eq!(original, deserialized);
662    }
663
664    #[test]
665    fn test_roundtrip_https() {
666        let original = SignalingMethod::Https(HttpSignalingInfo {
667            host: Host::Ipv4(Ipv4Addr::new(127, 0, 0, 1)),
668            port: 443,
669        });
670
671        let serialized = original.to_string();
672        let deserialized: SignalingMethod = serialized.parse().unwrap();
673
674        assert_eq!(original, deserialized);
675    }
676
677    #[test]
678    fn test_roundtrip_https_proxy() {
679        let original = SignalingMethod::HttpsProxy(
680            123,
681            HttpSignalingInfo {
682                host: Host::Domain("proxy.example.com".to_string()),
683                port: 443,
684            },
685        );
686
687        let serialized = original.to_string();
688        let deserialized: SignalingMethod = serialized.parse().unwrap();
689
690        assert_eq!(original, deserialized);
691    }
692
693    #[test]
694    fn test_case_sensitivity() {
695        let result: Result<SignalingMethod, _> = "/HTTP/example.com/8080".parse();
696        assert!(result.is_err());
697        assert!(matches!(
698            result.unwrap_err(),
699            SignalingMethodParseError::UnknownSignalingMethod(_)
700        ));
701
702        let result: Result<SignalingMethod, _> = "/Http/example.com/8080".parse();
703        assert!(result.is_err());
704        assert!(matches!(
705            result.unwrap_err(),
706            SignalingMethodParseError::UnknownSignalingMethod(_)
707        ));
708    }
709
710    #[test]
711    fn test_whitespace_handling() {
712        // The parser should filter empty components from split
713        let result: Result<SignalingMethod, _> = "/http/ /8080".parse();
714        assert!(result.is_err());
715        assert!(matches!(
716            result.unwrap_err(),
717            SignalingMethodParseError::NotEnoughArgs
718        ));
719    }
720
721    #[test]
722    fn test_https_proxy_zero_cluster_id() {
723        let method: SignalingMethod = "/https_proxy/0/proxy.example.com/443".parse().unwrap();
724        match method {
725            SignalingMethod::HttpsProxy(cluster_id, info) => {
726                assert_eq!(cluster_id, 0);
727                assert_eq!(info.host, Host::Domain("proxy.example.com".to_string()));
728                assert_eq!(info.port, 443);
729            }
730            _ => panic!("Expected HttpsProxy variant"),
731        }
732    }
733
734    #[test]
735    fn test_standard_ports() {
736        let method: SignalingMethod = "/http/localhost/80".parse().unwrap();
737        match method {
738            SignalingMethod::Http(info) => {
739                assert_eq!(info.port, 80);
740            }
741            _ => panic!("Expected Http variant"),
742        }
743
744        let method: SignalingMethod = "/https/localhost/443".parse().unwrap();
745        match method {
746            SignalingMethod::Https(info) => {
747                assert_eq!(info.port, 443);
748            }
749            _ => panic!("Expected Https variant"),
750        }
751    }
752
753    #[test]
754    fn test_https_proxy_with_ipv4() {
755        let method: SignalingMethod = "/https_proxy/456/192.168.1.1/8443".parse().unwrap();
756        match method {
757            SignalingMethod::HttpsProxy(cluster_id, info) => {
758                assert_eq!(cluster_id, 456);
759                assert_eq!(info.host, Host::Ipv4(Ipv4Addr::new(192, 168, 1, 1)));
760                assert_eq!(info.port, 8443);
761            }
762            _ => panic!("Expected HttpsProxy variant"),
763        }
764    }
765}