1use crate::{Backoff, Error, QuicBackend, Reconnect};
2#[cfg(feature = "websocket")]
3use std::future::Future;
4use std::net;
5use url::Url;
6
7#[derive(Clone, Debug, clap::Parser, serde::Serialize, serde::Deserialize)]
9#[serde(deny_unknown_fields, default)]
10#[non_exhaustive]
11pub struct ClientConfig {
12 #[serde(skip_serializing_if = "Option::is_none")]
20 #[arg(id = "client-connect", long = "client-connect", env = "MOQ_CLIENT_CONNECT")]
21 pub connect: Option<Url>,
22
23 #[arg(
25 id = "client-bind",
26 long = "client-bind",
27 default_value = "[::]:0",
28 env = "MOQ_CLIENT_BIND"
29 )]
30 pub bind: net::SocketAddr,
31
32 #[arg(id = "client-backend", long = "client-backend", env = "MOQ_CLIENT_BACKEND")]
35 pub backend: Option<QuicBackend>,
36
37 #[command(flatten)]
39 #[serde(default)]
40 pub quic: crate::quic::Client,
41
42 #[serde(default, skip_serializing_if = "Vec::is_empty")]
50 #[arg(id = "client-version", long = "client-version", env = "MOQ_CLIENT_VERSION")]
51 pub version: Vec<moq_net::Version>,
52
53 #[command(flatten)]
54 #[serde(default)]
55 pub tls: crate::tls::Client,
56
57 #[command(flatten)]
58 #[serde(default)]
59 pub backoff: Backoff,
60
61 #[cfg(feature = "websocket")]
62 #[command(flatten)]
63 #[serde(default)]
64 pub websocket: crate::websocket::Client,
65}
66
67impl ClientConfig {
68 pub fn init(self) -> crate::Result<Client> {
69 Client::new(self)
70 }
71
72 pub fn versions(&self) -> moq_net::Versions {
74 if self.version.is_empty() {
75 moq_net::Versions::all()
76 } else {
77 moq_net::Versions::from(self.version.clone())
78 }
79 }
80}
81
82impl Default for ClientConfig {
83 fn default() -> Self {
84 Self {
85 connect: None,
86 bind: "[::]:0".parse().unwrap(),
87 backend: None,
88 quic: crate::quic::Client::default(),
89 version: Vec::new(),
90 tls: crate::tls::Client::default(),
91 backoff: Backoff::default(),
92 #[cfg(feature = "websocket")]
93 websocket: crate::websocket::Client::default(),
94 }
95 }
96}
97
98#[derive(Clone)]
102pub struct Client {
103 moq: moq_net::Client,
104 versions: moq_net::Versions,
109 connect: Option<Url>,
111 backoff: Backoff,
112 #[cfg(feature = "websocket")]
113 websocket: crate::websocket::Client,
114 tls: rustls::ClientConfig,
115 #[cfg(feature = "noq")]
116 noq: Option<crate::noq::NoqClient>,
117 #[cfg(feature = "quinn")]
118 quinn: Option<crate::quinn::QuinnClient>,
119 #[cfg(feature = "quiche")]
120 quiche: Option<crate::quiche::QuicheClient>,
121 #[cfg(feature = "iroh")]
122 iroh: Option<web_transport_iroh::iroh::Endpoint>,
123 #[cfg(feature = "iroh")]
124 iroh_addrs: Vec<std::net::SocketAddr>,
125}
126
127impl Client {
128 #[cfg(not(any(
129 feature = "noq",
130 feature = "quinn",
131 feature = "quiche",
132 feature = "websocket",
133 feature = "tcp",
134 feature = "uds"
135 )))]
136 pub fn new(_config: ClientConfig) -> crate::Result<Self> {
137 Err(Error::NoBackend(
138 "no QUIC or WebSocket backend compiled; enable noq, quinn, quiche, websocket, tcp, or uds feature",
139 ))
140 }
141
142 #[cfg(any(
144 feature = "noq",
145 feature = "quinn",
146 feature = "quiche",
147 feature = "websocket",
148 feature = "tcp",
149 feature = "uds"
150 ))]
151 pub fn new(config: ClientConfig) -> crate::Result<Self> {
152 #[cfg(any(feature = "noq", feature = "quinn", feature = "quiche"))]
153 let backend = config.backend.clone().unwrap_or({
154 #[cfg(feature = "quinn")]
155 {
156 QuicBackend::Quinn
157 }
158 #[cfg(all(feature = "noq", not(feature = "quinn")))]
159 {
160 QuicBackend::Noq
161 }
162 #[cfg(all(feature = "quiche", not(feature = "quinn"), not(feature = "noq")))]
163 {
164 QuicBackend::Quiche
165 }
166 #[cfg(all(not(feature = "quiche"), not(feature = "quinn"), not(feature = "noq")))]
167 panic!("no QUIC backend compiled; enable noq, quinn, or quiche feature");
168 });
169
170 let tls = config.tls.build()?;
171
172 #[cfg(feature = "noq")]
173 #[allow(unreachable_patterns)]
174 let noq = match backend {
175 QuicBackend::Noq => Some(crate::noq::NoqClient::new(&config)?),
176 _ => None,
177 };
178
179 #[cfg(feature = "quinn")]
180 #[allow(unreachable_patterns)]
181 let quinn = match backend {
182 QuicBackend::Quinn => Some(crate::quinn::QuinnClient::new(&config)?),
183 _ => None,
184 };
185
186 #[cfg(feature = "quiche")]
187 let quiche = match backend {
188 QuicBackend::Quiche => Some(crate::quiche::QuicheClient::new(&config)?),
189 _ => None,
190 };
191
192 let versions = config.versions();
193 Ok(Self {
194 moq: moq_net::Client::new().with_versions(versions.clone()),
195 versions,
196 connect: config.connect,
197 backoff: config.backoff,
198 #[cfg(feature = "websocket")]
199 websocket: config.websocket,
200 tls,
201 #[cfg(feature = "noq")]
202 noq,
203 #[cfg(feature = "quinn")]
204 quinn,
205 #[cfg(feature = "quiche")]
206 quiche,
207 #[cfg(feature = "iroh")]
208 iroh: None,
209 #[cfg(feature = "iroh")]
210 iroh_addrs: Vec::new(),
211 })
212 }
213
214 #[cfg(feature = "iroh")]
215 pub fn with_iroh(mut self, iroh: Option<web_transport_iroh::iroh::Endpoint>) -> Self {
216 self.iroh = iroh;
217 self
218 }
219
220 #[cfg(feature = "iroh")]
225 pub fn with_iroh_addrs(mut self, addrs: Vec<std::net::SocketAddr>) -> Self {
226 self.iroh_addrs = addrs;
227 self
228 }
229
230 pub fn with_publish(mut self, publish: impl Into<Option<moq_net::OriginConsumer>>) -> Self {
231 self.moq = self.moq.with_publish(publish);
232 self
233 }
234
235 pub fn with_consume(mut self, consume: impl Into<Option<moq_net::OriginProducer>>) -> Self {
236 self.moq = self.moq.with_consume(consume);
237 self
238 }
239
240 pub fn with_stats(mut self, stats: moq_net::StatsHandle) -> Self {
242 self.moq = self.moq.with_stats(stats);
243 self
244 }
245
246 pub fn reconnect(&self, url: Url) -> Reconnect {
251 Reconnect::new(self.clone(), url, self.backoff.clone())
252 }
253
254 pub fn publish(self, origin: moq_net::OriginConsumer) -> Option<Reconnect> {
260 let url = self.connect.clone()?;
261 Some(self.with_publish(origin).reconnect(url))
262 }
263
264 pub fn consume(self, origin: moq_net::OriginProducer) -> Option<Reconnect> {
270 let url = self.connect.clone()?;
271 Some(self.with_consume(origin).reconnect(url))
272 }
273
274 #[cfg(not(any(
275 feature = "noq",
276 feature = "quinn",
277 feature = "quiche",
278 feature = "iroh",
279 feature = "websocket",
280 feature = "tcp",
281 feature = "uds"
282 )))]
283 pub async fn connect(&self, _url: Url) -> crate::Result<moq_net::Session> {
284 Err(Error::NoBackend(
285 "no backend compiled; enable noq, quinn, quiche, iroh, websocket, tcp, or uds feature",
286 ))
287 }
288
289 #[cfg(any(
290 feature = "noq",
291 feature = "quinn",
292 feature = "quiche",
293 feature = "iroh",
294 feature = "websocket",
295 feature = "tcp",
296 feature = "uds"
297 ))]
298 pub async fn connect(&self, url: Url) -> crate::Result<moq_net::Session> {
299 let session = self.connect_inner(url).await?;
300 tracing::info!(version = %session.version(), "connected");
301 Ok(session)
302 }
303
304 #[cfg(any(
309 feature = "noq",
310 feature = "quinn",
311 feature = "quiche",
312 feature = "iroh",
313 feature = "websocket",
314 feature = "tcp",
315 feature = "uds"
316 ))]
317 fn connect_client(&self, url: &Url) -> moq_net::Client {
318 if transport_carries_path(url) {
319 return self.moq.clone();
320 }
321
322 match request_path(url) {
323 Some(path) => self.moq.clone().with_path(path),
324 None => self.moq.clone(),
325 }
326 }
327
328 #[cfg(any(
329 feature = "noq",
330 feature = "quinn",
331 feature = "quiche",
332 feature = "iroh",
333 feature = "websocket",
334 feature = "tcp",
335 feature = "uds"
336 ))]
337 async fn connect_inner(&self, url: Url) -> crate::Result<moq_net::Session> {
338 let moq = self.connect_client(&url);
341
342 #[cfg(feature = "tcp")]
345 if url.scheme() == "tcp" {
346 let session = crate::tcp::connect(url, &self.versions.alpns()).await?;
347 return Ok(moq.connect(session).await?);
348 }
349
350 #[cfg(all(feature = "uds", unix))]
353 if url.scheme() == "unix" {
354 let session = crate::unix::connect(url, &self.versions.alpns()).await?;
355 return Ok(moq.connect(session).await?);
356 }
357
358 #[cfg(feature = "iroh")]
359 if url.scheme() == "iroh" {
360 let endpoint = self.iroh.as_ref().ok_or(Error::IrohDisabled)?;
361 let session = crate::iroh::connect(endpoint, url, self.iroh_addrs.iter().copied()).await?;
362 let session = moq.connect(session).await?;
363 return Ok(session);
364 }
365
366 #[cfg(feature = "noq")]
367 if let Some(noq) = self.noq.as_ref() {
368 let tls = self.tls.clone();
369 let quic_url = url.clone();
370 let quic_handle = async { noq.connect(&tls, quic_url, &self.versions).await.map_err(Error::from) };
371
372 #[cfg(feature = "websocket")]
373 {
374 return self.race_moq_connect(&moq, url, quic_handle).await;
375 }
376
377 #[cfg(not(feature = "websocket"))]
378 {
379 let session = quic_handle.await?;
380 return Ok(moq.connect(session).await?);
381 }
382 }
383
384 #[cfg(feature = "quinn")]
385 if let Some(quinn) = self.quinn.as_ref() {
386 let tls = self.tls.clone();
387 let quic_url = url.clone();
388 let quic_handle = async { quinn.connect(&tls, quic_url, &self.versions).await.map_err(Error::from) };
389
390 #[cfg(feature = "websocket")]
391 {
392 return self.race_moq_connect(&moq, url, quic_handle).await;
393 }
394
395 #[cfg(not(feature = "websocket"))]
396 {
397 let session = quic_handle.await?;
398 return Ok(moq.connect(session).await?);
399 }
400 }
401
402 #[cfg(feature = "quiche")]
403 if let Some(quiche) = self.quiche.as_ref() {
404 let quic_url = url.clone();
405 let quic_handle = async { quiche.connect(quic_url, &self.versions).await.map_err(Error::from) };
406
407 #[cfg(feature = "websocket")]
408 {
409 return self.race_moq_connect(&moq, url, quic_handle).await;
410 }
411
412 #[cfg(not(feature = "websocket"))]
413 {
414 let session = quic_handle.await?;
415 return Ok(moq.connect(session).await?);
416 }
417 }
418
419 #[cfg(feature = "websocket")]
420 {
421 let alpns = self.versions.alpns();
422 let session = crate::websocket::connect(&self.websocket, &self.tls, url, &alpns).await?;
423 return Ok(moq.connect(session).await?);
424 }
425
426 #[cfg(not(feature = "websocket"))]
427 return Err(Error::NoBackend("no QUIC backend matched; this should not happen"));
428 }
429
430 #[cfg(feature = "websocket")]
431 async fn race_moq_connect<Q, S>(&self, moq: &moq_net::Client, url: Url, quic: Q) -> crate::Result<moq_net::Session>
432 where
433 Q: Future<Output = crate::Result<S>>,
434 S: web_transport_trait::Session,
435 {
436 let alpns = self.versions.alpns();
437 let ws_config = self.websocket.clone();
438 let ws_tls = self.tls.clone();
439 let websocket = async move {
440 crate::websocket::race_handle(&ws_config, &ws_tls, url, &alpns)
441 .await
442 .map(|res| res.map_err(Error::from))
443 };
444
445 match race_transport_connect(quic, websocket).await? {
446 TransportRace::Quic(quic) => Ok(moq.connect(quic).await?),
447 TransportRace::WebSocket(websocket) => Ok(moq.connect(websocket).await?),
448 }
449 }
450}
451
452#[cfg(any(
460 feature = "noq",
461 feature = "quinn",
462 feature = "quiche",
463 feature = "iroh",
464 feature = "websocket",
465 feature = "tcp",
466 feature = "uds"
467))]
468fn transport_carries_path(url: &Url) -> bool {
469 matches!(url.scheme(), "https" | "http" | "ws" | "wss")
470}
471
472#[cfg(any(
479 feature = "noq",
480 feature = "quinn",
481 feature = "quiche",
482 feature = "iroh",
483 feature = "websocket",
484 feature = "tcp",
485 feature = "uds"
486))]
487fn request_path(url: &Url) -> Option<String> {
488 if let Some((_, path)) = url.query_pairs().find(|(key, _)| key == "path") {
489 return Some(path.into_owned());
490 }
491 match url.scheme() {
492 "unix" => None,
493 _ => Some(url.path().to_string()),
494 }
495}
496
497#[cfg(feature = "websocket")]
498#[derive(Debug, PartialEq, Eq)]
499enum TransportRace<Q, W> {
500 Quic(Q),
501 WebSocket(W),
502}
503
504#[cfg(feature = "websocket")]
505async fn race_transport_connect<Q, W, QT, WT>(quic: Q, websocket: W) -> crate::Result<TransportRace<QT, WT>>
506where
507 Q: Future<Output = crate::Result<QT>>,
508 W: Future<Output = Option<crate::Result<WT>>>,
509{
510 tokio::pin!(quic);
511 tokio::pin!(websocket);
512
513 let mut quic_err = None;
514 let mut websocket_err = None;
515 let mut quic_done = false;
516 let mut websocket_done = false;
517
518 loop {
519 tokio::select! {
520 res = &mut quic, if !quic_done => {
521 match res {
522 Ok(session) => return Ok(TransportRace::Quic(session)),
523 Err(err) if err.is_auth() => return Err(err),
524 Err(err) => {
525 tracing::warn!(%err, "QUIC connection failed");
526 quic_err = Some(err);
527 quic_done = true;
528 }
529 }
530 }
531 res = &mut websocket, if !websocket_done => {
532 match res {
533 Some(Ok(session)) => return Ok(TransportRace::WebSocket(session)),
534 Some(Err(err)) if err.is_auth() => return Err(err),
535 Some(Err(err)) => {
536 tracing::warn!(%err, "WebSocket connection failed");
537 websocket_err = Some(err);
538 websocket_done = true;
539 }
540 None => {
541 websocket_done = true;
542 }
543 }
544 }
545 else => break,
546 }
547
548 if quic_done && websocket_done {
549 break;
550 }
551 }
552
553 match (quic_err, websocket_err) {
554 (Some(quic), Some(websocket)) => Err(Error::TransportRace {
555 quic: std::sync::Arc::new(quic),
556 websocket: std::sync::Arc::new(websocket),
557 }),
558 (Some(err), None) | (None, Some(err)) => Err(err),
559 (None, None) => Err(Error::ConnectFailed),
560 }
561}
562
563#[cfg(test)]
564mod tests {
565 use super::*;
566 use clap::Parser;
567
568 #[cfg(any(
569 feature = "noq",
570 feature = "quinn",
571 feature = "quiche",
572 feature = "iroh",
573 feature = "websocket",
574 feature = "tcp",
575 feature = "uds"
576 ))]
577 #[test]
578 fn classifies_transport_path() {
579 for url in ["https://h/p", "http://h/p", "wss://h/p", "ws://h/p"] {
580 assert!(
581 transport_carries_path(&Url::parse(url).unwrap()),
582 "{url} carries its own path"
583 );
584 }
585 for url in [
588 "tcp://h:1/p",
589 "unix:///run/s.sock",
590 "moqt://h/p",
591 "moql://h/p",
592 "iroh://node/p",
593 ] {
594 assert!(
595 !transport_carries_path(&Url::parse(url).unwrap()),
596 "{url} advertises in SETUP"
597 );
598 }
599 }
600
601 #[cfg(any(
602 feature = "noq",
603 feature = "quinn",
604 feature = "quiche",
605 feature = "iroh",
606 feature = "websocket",
607 feature = "tcp",
608 feature = "uds"
609 ))]
610 #[test]
611 fn request_path_from_url() {
612 for url in ["tcp://h:1/anycast", "moqt://h/anycast", "iroh://node/anycast"] {
614 assert_eq!(
615 request_path(&Url::parse(url).unwrap()).as_deref(),
616 Some("/anycast"),
617 "{url}"
618 );
619 }
620 assert_eq!(request_path(&Url::parse("unix:///run/s.sock").unwrap()), None);
622 let uds = Url::parse("unix:///run/s.sock?path=/anycast").unwrap();
624 assert_eq!(uds.path(), "/run/s.sock");
625 assert_eq!(request_path(&uds).as_deref(), Some("/anycast"));
626 assert_eq!(
628 request_path(&Url::parse("tcp://h:1/ignored?path=/win").unwrap()).as_deref(),
629 Some("/win")
630 );
631 }
632
633 #[test]
634 fn test_toml_disable_verify_survives_update_from() {
635 let toml = r#"
636 tls.disable_verify = true
637 "#;
638
639 let mut config: ClientConfig = toml::from_str(toml).unwrap();
640 assert_eq!(config.tls.disable_verify, Some(true));
641
642 config.update_from(["test"]);
644 assert_eq!(config.tls.disable_verify, Some(true));
645 }
646
647 #[test]
648 fn test_cli_disable_verify_flag() {
649 let config = ClientConfig::parse_from(["test", "--client-tls-disable-verify"]);
650 assert_eq!(config.tls.disable_verify, Some(true));
651 }
652
653 #[test]
654 fn test_cli_disable_verify_explicit_false() {
655 let config = ClientConfig::parse_from(["test", "--client-tls-disable-verify=false"]);
656 assert_eq!(config.tls.disable_verify, Some(false));
657 }
658
659 #[test]
660 fn test_cli_disable_verify_explicit_true() {
661 let config = ClientConfig::parse_from(["test", "--client-tls-disable-verify=true"]);
662 assert_eq!(config.tls.disable_verify, Some(true));
663 }
664
665 #[test]
666 fn test_cli_deprecated_tls_flags_fold_into_canonical() {
667 let config = ClientConfig::parse_from(["test", "--tls-disable-verify=true", "--tls-fingerprint", "abcd1234"]);
671 assert_eq!(
672 config.tls.disable_verify, None,
673 "deprecated flag must not set the canonical field"
674 );
675 assert_eq!(config.tls.effective_disable_verify(), Some(true));
676 assert_eq!(config.tls.effective_fingerprint(), vec!["abcd1234"]);
677 }
678
679 #[test]
680 fn test_canonical_tls_flag_wins_over_deprecated() {
681 let config = ClientConfig::parse_from([
683 "test",
684 "--client-tls-disable-verify=false",
685 "--tls-disable-verify=true",
686 "--client-tls-fingerprint",
687 "aaaa",
688 "--tls-fingerprint",
689 "bbbb",
690 ]);
691 assert_eq!(config.tls.effective_disable_verify(), Some(false));
692 assert_eq!(config.tls.effective_fingerprint(), vec!["aaaa", "bbbb"]);
693 }
694
695 #[test]
696 fn test_cli_no_disable_verify() {
697 let config = ClientConfig::parse_from(["test"]);
698 assert_eq!(config.tls.disable_verify, None);
699 }
700
701 #[test]
702 fn test_toml_fingerprint_survives_update_from() {
703 let toml = r#"
704 tls.fingerprint = ["abcd1234", "ef567890"]
705 "#;
706
707 let mut config: ClientConfig = toml::from_str(toml).unwrap();
708 assert_eq!(config.tls.fingerprint, vec!["abcd1234", "ef567890"]);
709
710 config.update_from(["test"]);
712 assert_eq!(config.tls.fingerprint, vec!["abcd1234", "ef567890"]);
713 }
714
715 #[test]
716 fn test_toml_fingerprint_accepts_single_string() {
717 let toml = r#"
718 tls.fingerprint = "abcd1234"
719 "#;
720
721 let config: ClientConfig = toml::from_str(toml).unwrap();
722 assert_eq!(config.tls.fingerprint, vec!["abcd1234"]);
723 }
724
725 #[test]
726 fn test_cli_fingerprint() {
727 let config = ClientConfig::parse_from(["test", "--client-tls-fingerprint", "abcd1234"]);
728 assert_eq!(config.tls.fingerprint, vec!["abcd1234"]);
729 }
730
731 #[test]
732 fn test_toml_version_survives_update_from() {
733 let toml = r#"
734 version = ["moq-lite-02"]
735 "#;
736
737 let mut config: ClientConfig = toml::from_str(toml).unwrap();
738 assert_eq!(config.version, vec!["moq-lite-02".parse::<moq_net::Version>().unwrap()]);
739
740 config.update_from(["test"]);
742 assert_eq!(config.version, vec!["moq-lite-02".parse::<moq_net::Version>().unwrap()]);
743 }
744
745 #[test]
746 fn test_cli_version() {
747 let config = ClientConfig::parse_from(["test", "--client-version", "moq-lite-03"]);
748 assert_eq!(config.version, vec!["moq-lite-03".parse::<moq_net::Version>().unwrap()]);
749 }
750
751 #[test]
752 fn test_toml_connect_survives_update_from() {
753 let toml = r#"
754 connect = "https://relay.example.com/anon"
755 "#;
756
757 let mut config: ClientConfig = toml::from_str(toml).unwrap();
758 assert_eq!(
759 config.connect.as_ref().unwrap().as_str(),
760 "https://relay.example.com/anon"
761 );
762
763 config.update_from(["test"]);
765 assert_eq!(
766 config.connect.as_ref().unwrap().as_str(),
767 "https://relay.example.com/anon"
768 );
769 }
770
771 #[test]
772 fn test_cli_connect() {
773 let config = ClientConfig::parse_from(["test", "--client-connect", "https://relay.example.com/anon"]);
774 assert_eq!(
775 config.connect.as_ref().unwrap().as_str(),
776 "https://relay.example.com/anon"
777 );
778 }
779
780 #[test]
781 fn test_toml_host_name_survives_update_from() {
782 let toml = r#"
783 tls.host_name = "example.host"
784 "#;
785
786 let mut config: ClientConfig = toml::from_str(toml).unwrap();
787 assert_eq!(config.tls.host_name.as_deref(), Some("example.host"));
788
789 config.update_from(["test"]);
791 assert_eq!(config.tls.host_name.as_deref(), Some("example.host"));
792 }
793
794 #[test]
795 fn test_cli_host_name() {
796 let config = ClientConfig::parse_from(["test", "--client-tls-host-name", "override.example"]);
797 assert_eq!(config.tls.host_name.as_deref(), Some("override.example"));
798 }
799
800 #[test]
801 fn test_cli_no_version_defaults_to_all() {
802 let config = ClientConfig::parse_from(["test"]);
803 assert!(config.version.is_empty());
804 assert_eq!(config.versions().alpns().len(), moq_net::ALPNS.len());
806 }
807
808 #[cfg(feature = "websocket")]
809 #[tokio::test]
810 async fn race_transport_connect_stops_on_quic_auth_error() {
811 let quic = async { Err::<usize, _>(crate::ConnectError::Unauthorized.into()) };
812 let websocket = async {
813 tokio::task::yield_now().await;
815 Some(Ok(1usize))
816 };
817
818 let err = super::race_transport_connect(quic, websocket).await.unwrap_err();
819 assert_eq!(err.connect_error(), Some(crate::ConnectError::Unauthorized));
820 }
821
822 #[cfg(feature = "websocket")]
823 #[tokio::test]
824 async fn race_transport_connect_keeps_websocket_after_quic_non_auth_error() {
825 let quic = async { Err::<usize, _>(Error::ConnectFailed) };
826 let websocket = async { Some(Ok(7usize)) };
827
828 let value = super::race_transport_connect(quic, websocket).await.unwrap();
829 assert_eq!(value, super::TransportRace::WebSocket(7));
830 }
831
832 #[cfg(feature = "websocket")]
833 #[tokio::test]
834 async fn race_transport_connect_returns_when_quic_transport_connects() {
835 let quic = async { Ok("quic") };
836 let websocket = std::future::pending::<Option<crate::Result<&str>>>();
837
838 let value = tokio::time::timeout(
839 std::time::Duration::from_secs(1),
840 super::race_transport_connect(quic, websocket),
841 )
842 .await
843 .expect("race waited for WebSocket after QUIC transport connected")
844 .unwrap();
845 assert_eq!(value, super::TransportRace::Quic("quic"));
846 }
847}