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)]
55 #[serde(default)]
56 pub tls: crate::tls::Client,
57
58 #[command(flatten)]
60 #[serde(default)]
61 pub backoff: Backoff,
62
63 #[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> {
74 Client::new(self)
75 }
76
77 pub fn versions(&self) -> moq_net::Versions {
79 if self.version.is_empty() {
80 moq_net::Versions::all()
81 } else {
82 moq_net::Versions::from(self.version.clone())
83 }
84 }
85}
86
87impl Default for ClientConfig {
88 fn default() -> Self {
89 Self {
90 connect: None,
91 bind: "[::]:0".parse().unwrap(),
92 backend: None,
93 quic: crate::quic::Client::default(),
94 version: Vec::new(),
95 tls: crate::tls::Client::default(),
96 backoff: Backoff::default(),
97 #[cfg(feature = "websocket")]
98 websocket: crate::websocket::Client::default(),
99 }
100 }
101}
102
103#[derive(Clone)]
107pub struct Client {
108 moq: moq_net::Client,
109 versions: moq_net::Versions,
114 connect: Option<Url>,
116 backoff: Backoff,
117 #[cfg(feature = "websocket")]
118 websocket: crate::websocket::Client,
119 tls: rustls::ClientConfig,
120 #[cfg(feature = "noq")]
121 noq: Option<crate::noq::NoqClient>,
122 #[cfg(feature = "quinn")]
123 quinn: Option<crate::quinn::QuinnClient>,
124 #[cfg(feature = "quiche")]
125 quiche: Option<crate::quiche::QuicheClient>,
126 #[cfg(feature = "iroh")]
127 iroh: Option<crate::iroh::Endpoint>,
128 #[cfg(feature = "iroh")]
129 iroh_addrs: Vec<std::net::SocketAddr>,
130}
131
132impl Client {
133 #[cfg(not(any(
137 feature = "noq",
138 feature = "quinn",
139 feature = "quiche",
140 feature = "websocket",
141 feature = "tcp",
142 feature = "uds"
143 )))]
144 pub fn new(_config: ClientConfig) -> crate::Result<Self> {
145 Err(Error::NoBackend(
146 "no QUIC or WebSocket backend compiled; enable noq, quinn, quiche, websocket, tcp, or uds feature",
147 ))
148 }
149
150 #[cfg(any(
152 feature = "noq",
153 feature = "quinn",
154 feature = "quiche",
155 feature = "websocket",
156 feature = "tcp",
157 feature = "uds"
158 ))]
159 pub fn new(config: ClientConfig) -> crate::Result<Self> {
160 #[cfg(any(feature = "noq", feature = "quinn", feature = "quiche"))]
161 let backend = config.backend.clone().unwrap_or_else(crate::default_quic_backend);
162
163 config.quic.validate()?;
164
165 let tls = config.tls.build()?;
166
167 #[cfg(feature = "noq")]
168 #[allow(unreachable_patterns)]
169 let noq = match backend {
170 QuicBackend::Noq => Some(crate::noq::NoqClient::new(&config)?),
171 _ => None,
172 };
173
174 #[cfg(feature = "quinn")]
175 #[allow(unreachable_patterns)]
176 let quinn = match backend {
177 QuicBackend::Quinn => Some(crate::quinn::QuinnClient::new(&config)?),
178 _ => None,
179 };
180
181 #[cfg(feature = "quiche")]
182 let quiche = match backend {
183 QuicBackend::Quiche => Some(crate::quiche::QuicheClient::new(&config)?),
184 _ => None,
185 };
186
187 let versions = config.versions();
188 Ok(Self {
189 moq: moq_net::Client::new().with_versions(versions.clone()),
190 versions,
191 connect: config.connect,
192 backoff: config.backoff,
193 #[cfg(feature = "websocket")]
194 websocket: config.websocket,
195 tls,
196 #[cfg(feature = "noq")]
197 noq,
198 #[cfg(feature = "quinn")]
199 quinn,
200 #[cfg(feature = "quiche")]
201 quiche,
202 #[cfg(feature = "iroh")]
203 iroh: None,
204 #[cfg(feature = "iroh")]
205 iroh_addrs: Vec::new(),
206 })
207 }
208
209 #[cfg(feature = "iroh")]
214 pub fn with_iroh(mut self, iroh: crate::iroh::Endpoint) -> Self {
215 self.iroh = Some(iroh);
216 self
217 }
218
219 #[cfg(feature = "iroh")]
224 pub fn with_iroh_addrs(mut self, addrs: Vec<std::net::SocketAddr>) -> Self {
225 self.iroh_addrs = addrs;
226 self
227 }
228
229 pub fn with_publisher(mut self, publish: impl moq_net::Consume<moq_net::origin::Consumer>) -> Self {
231 self.moq = self.moq.with_publisher(publish);
232 self
233 }
234
235 pub fn with_subscriber(mut self, subscribe: moq_net::origin::Producer) -> Self {
237 self.moq = self.moq.with_subscriber(subscribe);
238 self
239 }
240
241 pub fn with_stats(mut self, stats: moq_net::stats::Session) -> Self {
244 self.moq = self.moq.with_stats(stats);
245 self
246 }
247
248 pub fn with_cost(mut self, cost: u64) -> Self {
250 self.moq = self.moq.with_cost(cost);
251 self
252 }
253
254 pub fn with_peer_origin(mut self, origin: moq_net::Origin) -> Self {
257 self.moq = self.moq.with_peer_origin(origin);
258 self
259 }
260
261 pub fn reconnect(&self, url: Url) -> Reconnect {
266 Reconnect::new(self.clone(), url, self.backoff.clone())
267 }
268
269 pub fn publish(self, origin: moq_net::origin::Consumer) -> Option<Reconnect> {
275 let url = self.connect.clone()?;
276 Some(self.with_publisher(origin).reconnect(url))
277 }
278
279 pub fn consume(self, origin: moq_net::origin::Producer) -> Option<Reconnect> {
291 let url = self.connect.clone()?;
292 let origin = origin.with_linger(self.backoff.linger());
293 Some(self.with_subscriber(origin).reconnect(url))
294 }
295
296 #[cfg(not(any(
300 feature = "noq",
301 feature = "quinn",
302 feature = "quiche",
303 feature = "iroh",
304 feature = "websocket",
305 feature = "tcp",
306 feature = "uds"
307 )))]
308 pub async fn connect(&self, _url: Url) -> crate::Result<moq_net::Session> {
309 Err(Error::NoBackend(
310 "no backend compiled; enable noq, quinn, quiche, iroh, websocket, tcp, or uds feature",
311 ))
312 }
313
314 #[cfg(any(
321 feature = "noq",
322 feature = "quinn",
323 feature = "quiche",
324 feature = "iroh",
325 feature = "websocket",
326 feature = "tcp",
327 feature = "uds"
328 ))]
329 pub async fn connect(&self, url: Url) -> crate::Result<moq_net::Session> {
330 let pair = Box::pin(self.connect_inner(url)).await?;
333 tracing::info!(version = %pair.0.version(), "connected");
334 Ok(crate::spawn_session(pair))
335 }
336
337 #[cfg(any(
339 feature = "noq",
340 feature = "quinn",
341 feature = "quiche",
342 feature = "iroh",
343 feature = "websocket",
344 feature = "tcp",
345 feature = "uds"
346 ))]
347 fn moq_with_path(&self, path: Option<String>) -> moq_net::Client {
348 match path {
349 Some(path) => self.moq.clone().with_path(path),
350 None => self.moq.clone(),
351 }
352 }
353
354 #[cfg(any(
355 feature = "noq",
356 feature = "quinn",
357 feature = "quiche",
358 feature = "iroh",
359 feature = "websocket",
360 feature = "tcp",
361 feature = "uds"
362 ))]
363 async fn connect_inner(&self, url: Url) -> crate::Result<(moq_net::Session, moq_net::Driver)> {
364 let moq = self.moq_with_path(setup_path(&url));
368
369 #[cfg(feature = "tcp")]
372 if url.scheme() == "tcp" {
373 let session = crate::tcp::connect(url, &self.versions.alpns()).await?;
374 return Ok(moq.connect(session).await?);
375 }
376
377 #[cfg(all(feature = "uds", unix))]
380 if url.scheme() == "unix" {
381 let session = crate::unix::connect(url, &self.versions.alpns()).await?;
382 return Ok(moq.connect(session).await?);
383 }
384
385 #[cfg(feature = "iroh")]
390 if url.scheme() == "iroh" {
391 let endpoint = self.iroh.as_ref().ok_or(Error::IrohDisabled)?;
392 let target = request_target(&url);
393 let (session, binding) = crate::iroh::connect(endpoint, url, self.iroh_addrs.iter().copied()).await?;
394
395 let moq = match binding {
396 crate::iroh::Binding::Raw => self.moq_with_path(target),
397 crate::iroh::Binding::H3 => self.moq.clone(),
398 };
399
400 return Ok(moq.connect(session).await?);
401 }
402
403 #[cfg(feature = "noq")]
404 if let Some(noq) = self.noq.as_ref() {
405 let tls = self.tls.clone();
406 let quic_url = url.clone();
407 let quic_handle = async { noq.connect(&tls, quic_url, &self.versions).await.map_err(Error::from) };
408
409 #[cfg(feature = "websocket")]
410 {
411 return self.race_moq_connect(&moq, url, quic_handle).await;
412 }
413
414 #[cfg(not(feature = "websocket"))]
415 {
416 let session = quic_handle.await?;
417 return Ok(moq.connect(session).await?);
418 }
419 }
420
421 #[cfg(feature = "quinn")]
422 if let Some(quinn) = self.quinn.as_ref() {
423 let tls = self.tls.clone();
424 let quic_url = url.clone();
425 let quic_handle = async { quinn.connect(&tls, quic_url, &self.versions).await.map_err(Error::from) };
426
427 #[cfg(feature = "websocket")]
428 {
429 return self.race_moq_connect(&moq, url, quic_handle).await;
430 }
431
432 #[cfg(not(feature = "websocket"))]
433 {
434 let session = quic_handle.await?;
435 return Ok(moq.connect(session).await?);
436 }
437 }
438
439 #[cfg(feature = "quiche")]
440 if let Some(quiche) = self.quiche.as_ref() {
441 let quic_url = url.clone();
442 let quic_handle = async { quiche.connect(quic_url, &self.versions).await.map_err(Error::from) };
443
444 #[cfg(feature = "websocket")]
445 {
446 return self.race_moq_connect(&moq, url, quic_handle).await;
447 }
448
449 #[cfg(not(feature = "websocket"))]
450 {
451 let session = quic_handle.await?;
452 return Ok(moq.connect(session).await?);
453 }
454 }
455
456 #[cfg(feature = "websocket")]
457 {
458 let alpns = self.versions.alpns();
459 let session = crate::websocket::connect(&self.websocket, &self.tls, url, &alpns).await?;
460 return Ok(moq.connect(session).await?);
461 }
462
463 #[cfg(not(feature = "websocket"))]
464 return Err(Error::NoBackend("no QUIC backend matched; this should not happen"));
465 }
466
467 #[cfg(feature = "websocket")]
473 async fn race_moq_connect<Q, S>(
474 &self,
475 moq: &moq_net::Client,
476 url: Url,
477 quic: Q,
478 ) -> crate::Result<(moq_net::Session, moq_net::Driver)>
479 where
480 Q: Future<Output = crate::Result<S>>,
481 S: web_transport_trait::Session,
482 {
483 let alpns = self.versions.alpns();
484 let ws_config = self.websocket.clone();
485 let ws_tls = self.tls.clone();
486 let websocket = async move {
487 crate::websocket::race_handle(&ws_config, &ws_tls, url, &alpns)
488 .await
489 .map(|res| res.map_err(Error::from))
490 };
491
492 match race_transport_connect(quic, websocket).await? {
493 TransportRace::Quic(quic) => Ok(moq.connect(quic).await?),
494 TransportRace::WebSocket(websocket) => Ok(self.moq.connect(websocket).await?),
495 }
496 }
497}
498
499#[cfg(any(
506 feature = "noq",
507 feature = "quinn",
508 feature = "quiche",
509 feature = "iroh",
510 feature = "websocket",
511 feature = "tcp",
512 feature = "uds"
513))]
514fn request_target(url: &Url) -> Option<String> {
515 let target = match url.query().filter(|query| !query.is_empty()) {
519 Some(query) => format!("{}?{}", url.path(), query),
520 None => url.path().to_owned(),
521 };
522
523 (!target.is_empty()).then_some(target)
524}
525
526#[cfg(any(
534 feature = "noq",
535 feature = "quinn",
536 feature = "quiche",
537 feature = "iroh",
538 feature = "websocket",
539 feature = "tcp",
540 feature = "uds"
541))]
542fn setup_path(url: &Url) -> Option<String> {
543 match url.scheme() {
544 "unix" => url
548 .query_pairs()
549 .find(|(k, _)| k == "path")
550 .map(|(_, v)| v.into_owned())
551 .filter(|path| !path.is_empty()),
552 "moqt" | "moql" | "tcp" => request_target(url),
555 _ => None,
556 }
557}
558
559#[cfg(feature = "websocket")]
560#[derive(Debug, PartialEq, Eq)]
561enum TransportRace<Q, W> {
562 Quic(Q),
563 WebSocket(W),
564}
565
566#[cfg(feature = "websocket")]
567async fn race_transport_connect<Q, W, QT, WT>(quic: Q, websocket: W) -> crate::Result<TransportRace<QT, WT>>
568where
569 Q: Future<Output = crate::Result<QT>>,
570 W: Future<Output = Option<crate::Result<WT>>>,
571{
572 tokio::pin!(quic);
573 tokio::pin!(websocket);
574
575 let mut quic_err = None;
576 let mut websocket_err = None;
577 let mut quic_done = false;
578 let mut websocket_done = false;
579
580 loop {
581 tokio::select! {
582 res = &mut quic, if !quic_done => {
583 match res {
584 Ok(session) => return Ok(TransportRace::Quic(session)),
585 Err(err) if err.is_auth() => return Err(err),
586 Err(err) => {
587 tracing::warn!(%err, "QUIC connection failed");
588 quic_err = Some(err);
589 quic_done = true;
590 }
591 }
592 }
593 res = &mut websocket, if !websocket_done => {
594 match res {
595 Some(Ok(session)) => return Ok(TransportRace::WebSocket(session)),
596 Some(Err(err)) if err.is_auth() => return Err(err),
597 Some(Err(err)) => {
598 tracing::warn!(%err, "WebSocket connection failed");
599 websocket_err = Some(err);
600 websocket_done = true;
601 }
602 None => {
603 websocket_done = true;
604 }
605 }
606 }
607 else => break,
608 }
609
610 if quic_done && websocket_done {
611 break;
612 }
613 }
614
615 match (quic_err, websocket_err) {
616 (Some(quic), Some(websocket)) => Err(Error::TransportRace {
617 quic: std::sync::Arc::new(quic),
618 websocket: std::sync::Arc::new(websocket),
619 }),
620 (Some(err), None) | (None, Some(err)) => Err(err),
621 (None, None) => Err(Error::ConnectFailed),
622 }
623}
624
625#[cfg(test)]
626mod tests {
627 use super::*;
628 use clap::Parser;
629
630 #[cfg(any(
631 feature = "noq",
632 feature = "quinn",
633 feature = "quiche",
634 feature = "iroh",
635 feature = "websocket",
636 feature = "tcp",
637 feature = "uds"
638 ))]
639 #[test]
640 fn setup_path_covers_the_uri_less_transports() {
641 let cases = [
644 ("unix:///run/moq.sock?path=/room", Some("/room")),
645 ("unix:///run/moq.sock?path=/room%3Fjwt%3Dabc", Some("/room?jwt=abc")),
648 ("unix:///run/moq.sock?path=", None),
649 ("unix:///run/moq.sock", None),
650 ("tcp://localhost:4443/room", Some("/room")),
651 ("tcp://localhost:4443/room?jwt=abc", Some("/room?jwt=abc")),
652 ("tcp://localhost:4443", None),
653 ("moqt://relay.example.com/anon", Some("/anon")),
656 ("moqt://relay.example.com/anon?jwt=abc", Some("/anon?jwt=abc")),
657 ("moql://relay.example.com/anon?jwt=abc", Some("/anon?jwt=abc")),
658 ("moqt://relay.example.com", None),
659 ("moqt://relay.example.com/anon?jwt=abc#pos:12", Some("/anon?jwt=abc")),
661 ("moqt://relay.example.com/anon#pos:12", Some("/anon")),
662 ("moqt://relay.example.com/anon?", Some("/anon")),
664 ("moqt://relay.example.com?", None),
665 ("https://relay.example.com/anon?jwt=abc", None),
668 ("http://relay.example.com/anon", None),
669 ("wss://relay.example.com/anon?jwt=abc", None),
670 ("iroh://k5lnrlndqpqcgh4d5nhbnbnhcyrgvw6ttxwrsvsu4nlt6foorxaa/anon", None),
672 ];
673
674 for (url, want) in cases {
675 let url = Url::parse(url).unwrap();
676 let got = setup_path(&url);
677 assert_eq!(got.as_deref(), want, "{url}");
678 }
679 }
680
681 #[cfg(any(
684 feature = "noq",
685 feature = "quinn",
686 feature = "quiche",
687 feature = "iroh",
688 feature = "websocket",
689 feature = "tcp",
690 feature = "uds"
691 ))]
692 #[test]
693 fn request_target_joins_the_path_and_query() {
694 const PEER: &str = "k5lnrlndqpqcgh4d5nhbnbnhcyrgvw6ttxwrsvsu4nlt6foorxaa";
695
696 let cases = [
697 (format!("iroh://{PEER}/room?jwt=abc"), Some("/room?jwt=abc")),
698 (format!("iroh://{PEER}/room"), Some("/room")),
699 (format!("iroh://{PEER}"), None),
700 (format!("iroh://{PEER}/"), Some("/")),
701 ];
702
703 for (url, want) in cases {
704 let url = Url::parse(&url).unwrap();
705 let got = request_target(&url);
706 assert_eq!(got.as_deref(), want, "{url}");
707 }
708 }
709
710 #[test]
711 fn test_toml_disable_verify_survives_update_from() {
712 let toml = r#"
713 tls.disable_verify = true
714 "#;
715
716 let mut config: ClientConfig = toml::from_str(toml).unwrap();
717 assert_eq!(config.tls.disable_verify, Some(true));
718
719 config.update_from(["test"]);
721 assert_eq!(config.tls.disable_verify, Some(true));
722 }
723
724 #[test]
725 fn test_cli_disable_verify_flag() {
726 let config = ClientConfig::parse_from(["test", "--client-tls-disable-verify"]);
727 assert_eq!(config.tls.disable_verify, Some(true));
728 }
729
730 #[test]
731 fn test_cli_disable_verify_explicit_false() {
732 let config = ClientConfig::parse_from(["test", "--client-tls-disable-verify=false"]);
733 assert_eq!(config.tls.disable_verify, Some(false));
734 }
735
736 #[test]
737 fn test_cli_disable_verify_explicit_true() {
738 let config = ClientConfig::parse_from(["test", "--client-tls-disable-verify=true"]);
739 assert_eq!(config.tls.disable_verify, Some(true));
740 }
741
742 #[test]
743 fn test_cli_deprecated_tls_flags_fold_into_canonical() {
744 let config = ClientConfig::parse_from(["test", "--tls-disable-verify=true", "--tls-fingerprint", "abcd1234"]);
748 assert_eq!(
749 config.tls.disable_verify, None,
750 "deprecated flag must not set the canonical field"
751 );
752 assert_eq!(config.tls.effective_disable_verify(), Some(true));
753 assert_eq!(config.tls.effective_fingerprint(), vec!["abcd1234"]);
754 }
755
756 #[test]
757 fn test_canonical_tls_flag_wins_over_deprecated() {
758 let config = ClientConfig::parse_from([
760 "test",
761 "--client-tls-disable-verify=false",
762 "--tls-disable-verify=true",
763 "--client-tls-fingerprint",
764 "aaaa",
765 "--tls-fingerprint",
766 "bbbb",
767 ]);
768 assert_eq!(config.tls.effective_disable_verify(), Some(false));
769 assert_eq!(config.tls.effective_fingerprint(), vec!["aaaa", "bbbb"]);
770 }
771
772 #[test]
773 fn test_cli_no_disable_verify() {
774 let config = ClientConfig::parse_from(["test"]);
775 assert_eq!(config.tls.disable_verify, None);
776 }
777
778 #[test]
779 fn test_toml_fingerprint_survives_update_from() {
780 let toml = r#"
781 tls.fingerprint = ["abcd1234", "ef567890"]
782 "#;
783
784 let mut config: ClientConfig = toml::from_str(toml).unwrap();
785 assert_eq!(config.tls.fingerprint, vec!["abcd1234", "ef567890"]);
786
787 config.update_from(["test"]);
789 assert_eq!(config.tls.fingerprint, vec!["abcd1234", "ef567890"]);
790 }
791
792 #[test]
793 fn test_toml_fingerprint_accepts_single_string() {
794 let toml = r#"
795 tls.fingerprint = "abcd1234"
796 "#;
797
798 let config: ClientConfig = toml::from_str(toml).unwrap();
799 assert_eq!(config.tls.fingerprint, vec!["abcd1234"]);
800 }
801
802 #[test]
803 fn test_cli_fingerprint() {
804 let config = ClientConfig::parse_from(["test", "--client-tls-fingerprint", "abcd1234"]);
805 assert_eq!(config.tls.fingerprint, vec!["abcd1234"]);
806 }
807
808 #[test]
809 fn test_toml_version_survives_update_from() {
810 let toml = r#"
811 version = ["moq-lite-02"]
812 "#;
813
814 let mut config: ClientConfig = toml::from_str(toml).unwrap();
815 assert_eq!(config.version, vec!["moq-lite-02".parse::<moq_net::Version>().unwrap()]);
816
817 config.update_from(["test"]);
819 assert_eq!(config.version, vec!["moq-lite-02".parse::<moq_net::Version>().unwrap()]);
820 }
821
822 #[test]
823 fn test_cli_version() {
824 let config = ClientConfig::parse_from(["test", "--client-version", "moq-lite-03"]);
825 assert_eq!(config.version, vec!["moq-lite-03".parse::<moq_net::Version>().unwrap()]);
826 }
827
828 #[test]
829 fn test_toml_connect_survives_update_from() {
830 let toml = r#"
831 connect = "https://relay.example.com/anon"
832 "#;
833
834 let mut config: ClientConfig = toml::from_str(toml).unwrap();
835 assert_eq!(
836 config.connect.as_ref().unwrap().as_str(),
837 "https://relay.example.com/anon"
838 );
839
840 config.update_from(["test"]);
842 assert_eq!(
843 config.connect.as_ref().unwrap().as_str(),
844 "https://relay.example.com/anon"
845 );
846 }
847
848 #[test]
849 fn test_cli_connect() {
850 let config = ClientConfig::parse_from(["test", "--client-connect", "https://relay.example.com/anon"]);
851 assert_eq!(
852 config.connect.as_ref().unwrap().as_str(),
853 "https://relay.example.com/anon"
854 );
855 }
856
857 #[test]
858 fn test_toml_host_name_survives_update_from() {
859 let toml = r#"
860 tls.host_name = "example.host"
861 "#;
862
863 let mut config: ClientConfig = toml::from_str(toml).unwrap();
864 assert_eq!(config.tls.host_name.as_deref(), Some("example.host"));
865
866 config.update_from(["test"]);
868 assert_eq!(config.tls.host_name.as_deref(), Some("example.host"));
869 }
870
871 #[test]
872 fn test_cli_host_name() {
873 let config = ClientConfig::parse_from(["test", "--client-tls-host-name", "override.example"]);
874 assert_eq!(config.tls.host_name.as_deref(), Some("override.example"));
875 }
876
877 #[test]
878 fn test_cli_no_version_defaults_to_all() {
879 let config = ClientConfig::parse_from(["test"]);
880 assert!(config.version.is_empty());
881 assert_eq!(config.versions().alpns().len(), moq_net::ALPNS.len());
883 }
884
885 #[cfg(feature = "websocket")]
886 #[tokio::test]
887 async fn race_transport_connect_stops_on_quic_auth_error() {
888 let quic = async { Err::<usize, _>(crate::ConnectError::Unauthorized.into()) };
889 let websocket = async {
890 tokio::task::yield_now().await;
892 Some(Ok(1usize))
893 };
894
895 let err = super::race_transport_connect(quic, websocket).await.unwrap_err();
896 assert_eq!(err.connect_error(), Some(crate::ConnectError::Unauthorized));
897 }
898
899 #[cfg(feature = "websocket")]
900 #[tokio::test]
901 async fn race_transport_connect_keeps_websocket_after_quic_non_auth_error() {
902 let quic = async { Err::<usize, _>(Error::ConnectFailed) };
903 let websocket = async { Some(Ok(7usize)) };
904
905 let value = super::race_transport_connect(quic, websocket).await.unwrap();
906 assert_eq!(value, super::TransportRace::WebSocket(7));
907 }
908
909 #[cfg(feature = "websocket")]
910 #[tokio::test]
911 async fn race_transport_connect_returns_when_quic_transport_connects() {
912 let quic = async { Ok("quic") };
913 let websocket = std::future::pending::<Option<crate::Result<&str>>>();
914
915 let value = tokio::time::timeout(
916 std::time::Duration::from_secs(1),
917 super::race_transport_connect(quic, websocket),
918 )
919 .await
920 .expect("race waited for WebSocket after QUIC transport connected")
921 .unwrap();
922 assert_eq!(value, super::TransportRace::Quic("quic"));
923 }
924}