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