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}