1mod homeserver_config;
17
18#[cfg(feature = "experimental-search")]
19use std::collections::HashMap;
20#[cfg(feature = "sqlite")]
21use std::path::Path;
22#[cfg(any(feature = "experimental-search", feature = "sqlite"))]
23use std::path::PathBuf;
24use std::{collections::BTreeSet, fmt, sync::Arc};
25
26#[cfg(feature = "sqlite")]
27use futures_util::try_join;
28use homeserver_config::*;
29#[cfg(feature = "e2e-encryption")]
30use matrix_sdk_base::crypto::DecryptionSettings;
31#[cfg(feature = "e2e-encryption")]
32use matrix_sdk_base::crypto::{CollectStrategy, TrustRequirement};
33use matrix_sdk_base::{
34 BaseClient, DmRoomDefinition, ThreadingSupport, store::StoreConfig, ttl::TtlValue,
35};
36use matrix_sdk_common::cross_process_lock::CrossProcessLockConfig;
37#[cfg(feature = "sqlite")]
38use matrix_sdk_sqlite::SqliteStoreConfig;
39#[cfg(not(target_family = "wasm"))]
40use reqwest::Certificate;
41use ruma::{
42 OwnedServerName, ServerName,
43 api::{MatrixVersion, SupportedVersions, error::FromHttpResponseError},
44};
45use thiserror::Error;
46#[cfg(feature = "experimental-search")]
47use tokio::sync::Mutex;
48use tokio::sync::OnceCell;
49use tracing::{Span, debug, field::debug, instrument};
50
51use super::{Client, ClientInner};
52#[cfg(feature = "e2e-encryption")]
53use crate::encryption::EncryptionSettings;
54#[cfg(not(target_family = "wasm"))]
55use crate::http_client::HttpSettings;
56#[cfg(feature = "experimental-search")]
57use crate::search_index::SearchIndex;
58#[cfg(feature = "experimental-search")]
59use crate::search_index::SearchIndexStoreKind;
60use crate::{
61 HttpError, IdParseError,
62 authentication::AuthCtx,
63 client::caches::CachedValue::{Cached, NotSet},
64 config::RequestConfig,
65 error::RumaApiError,
66 http_client::HttpClient,
67 media::{DefaultMediaFetcher, MediaFetcher},
68 send_queue::SendQueueData,
69 sliding_sync::VersionBuilder as SlidingSyncVersionBuilder,
70};
71
72#[must_use]
111#[derive(Clone, Debug)]
112pub struct ClientBuilder {
113 homeserver_cfg: Option<HomeserverConfig>,
114 sliding_sync_version_builder: SlidingSyncVersionBuilder,
115 http_cfg: Option<HttpConfig>,
116 store_config: BuilderStoreConfig,
117 request_config: RequestConfig,
118 respect_login_well_known: bool,
119 server_versions: Option<BTreeSet<MatrixVersion>>,
120 handle_refresh_tokens: bool,
121 base_client: Option<BaseClient>,
122 #[cfg(feature = "e2e-encryption")]
123 encryption_settings: EncryptionSettings,
124 #[cfg(feature = "e2e-encryption")]
125 room_key_recipient_strategy: CollectStrategy,
126 #[cfg(feature = "e2e-encryption")]
127 decryption_settings: DecryptionSettings,
128 #[cfg(feature = "e2e-encryption")]
129 enable_share_history_on_invite: bool,
130 cross_process_lock_config: CrossProcessLockConfig,
131 threading_support: ThreadingSupport,
132 #[cfg(feature = "experimental-search")]
133 search_index_store_kind: SearchIndexStoreKind,
134 dm_room_definition: DmRoomDefinition,
135 media_fetcher: Arc<dyn MediaFetcher>,
136}
137
138impl ClientBuilder {
139 const DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME: &str = "main";
140
141 pub(crate) fn new() -> Self {
142 Self {
143 homeserver_cfg: None,
144 sliding_sync_version_builder: SlidingSyncVersionBuilder::Native,
145 http_cfg: None,
146 store_config: BuilderStoreConfig::Custom(StoreConfig::new(
147 CrossProcessLockConfig::multi_process(
148 Self::DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME,
149 ),
150 )),
151 request_config: Default::default(),
152 respect_login_well_known: true,
153 server_versions: None,
154 handle_refresh_tokens: false,
155 base_client: None,
156 #[cfg(feature = "e2e-encryption")]
157 encryption_settings: Default::default(),
158 #[cfg(feature = "e2e-encryption")]
159 room_key_recipient_strategy: Default::default(),
160 #[cfg(feature = "e2e-encryption")]
161 decryption_settings: DecryptionSettings {
162 sender_device_trust_requirement: TrustRequirement::Untrusted,
163 },
164 #[cfg(feature = "e2e-encryption")]
165 enable_share_history_on_invite: true,
166 cross_process_lock_config: CrossProcessLockConfig::MultiProcess {
167 holder_name: Self::DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME.to_owned(),
168 },
169 threading_support: ThreadingSupport::Disabled,
170 #[cfg(feature = "experimental-search")]
171 search_index_store_kind: SearchIndexStoreKind::InMemory,
172 dm_room_definition: DmRoomDefinition::MatrixSpec,
173 media_fetcher: Arc::new(DefaultMediaFetcher),
174 }
175 }
176
177 pub fn media_fetcher(mut self, media_fetcher: Arc<dyn MediaFetcher>) -> Self {
180 self.media_fetcher = media_fetcher.clone();
181 self
182 }
183
184 pub fn dm_room_definition(mut self, dm_room_definition: DmRoomDefinition) -> Self {
188 self.dm_room_definition = dm_room_definition;
189 self
190 }
191
192 pub fn homeserver_url(mut self, url: impl AsRef<str>) -> Self {
199 self.homeserver_cfg = Some(HomeserverConfig::HomeserverUrl(url.as_ref().to_owned()));
200 self
201 }
202
203 pub fn server_name(mut self, server_name: &ServerName) -> Self {
213 self.homeserver_cfg = Some(HomeserverConfig::ServerName {
214 server: server_name.to_owned(),
215 protocol: UrlScheme::Https,
217 });
218 self
219 }
220
221 pub fn insecure_server_name_no_tls(mut self, server_name: &ServerName) -> Self {
230 self.homeserver_cfg = Some(HomeserverConfig::ServerName {
231 server: server_name.to_owned(),
232 protocol: UrlScheme::Http,
233 });
234 self
235 }
236
237 pub fn server_name_or_homeserver_url(mut self, server_name_or_url: impl AsRef<str>) -> Self {
248 self.homeserver_cfg = Some(HomeserverConfig::ServerNameOrHomeserverUrl(
249 server_name_or_url.as_ref().to_owned(),
250 ));
251 self
252 }
253
254 pub fn sliding_sync_version_builder(
256 mut self,
257 version_builder: SlidingSyncVersionBuilder,
258 ) -> Self {
259 self.sliding_sync_version_builder = version_builder;
260 self
261 }
262
263 #[cfg(feature = "sqlite")]
265 pub fn sqlite_store(mut self, path: impl AsRef<Path>, passphrase: Option<&str>) -> Self {
266 let sqlite_store_config = SqliteStoreConfig::new(path).passphrase(passphrase);
267 self.store_config =
268 BuilderStoreConfig::Sqlite { config: sqlite_store_config, cache_path: None };
269
270 self
271 }
272
273 #[cfg(feature = "sqlite")]
276 pub fn sqlite_store_with_cache_path(
277 mut self,
278 path: impl AsRef<Path>,
279 cache_path: impl AsRef<Path>,
280 passphrase: Option<&str>,
281 ) -> Self {
282 let sqlite_store_config = SqliteStoreConfig::new(path).passphrase(passphrase);
283 self.store_config = BuilderStoreConfig::Sqlite {
284 config: sqlite_store_config,
285 cache_path: Some(cache_path.as_ref().to_owned()),
286 };
287
288 self
289 }
290
291 #[cfg(feature = "sqlite")]
294 pub fn sqlite_store_with_config_and_cache_path(
295 mut self,
296 config: SqliteStoreConfig,
297 cache_path: Option<impl AsRef<Path>>,
298 ) -> Self {
299 self.store_config = BuilderStoreConfig::Sqlite {
300 config,
301 cache_path: cache_path.map(|cache_path| cache_path.as_ref().to_owned()),
302 };
303
304 self
305 }
306
307 #[cfg(feature = "indexeddb")]
309 pub fn indexeddb_store(mut self, name: &str, passphrase: Option<&str>) -> Self {
310 self.store_config = BuilderStoreConfig::IndexedDb {
311 name: name.to_owned(),
312 passphrase: passphrase.map(ToOwned::to_owned),
313 };
314 self
315 }
316
317 pub fn store_config(mut self, store_config: StoreConfig) -> Self {
341 self.store_config = BuilderStoreConfig::Custom(store_config);
342 self
343 }
344
345 pub fn respect_login_well_known(mut self, value: bool) -> Self {
348 self.respect_login_well_known = value;
349 self
350 }
351
352 pub fn request_config(mut self, request_config: RequestConfig) -> Self {
354 self.request_config = request_config;
355 self
356 }
357
358 #[cfg(not(target_family = "wasm"))]
374 pub fn proxy(mut self, proxy: impl AsRef<str>) -> Self {
375 self.http_settings().proxy = Some(proxy.as_ref().to_owned());
376 self
377 }
378
379 #[cfg(not(target_family = "wasm"))]
381 pub fn disable_ssl_verification(mut self) -> Self {
382 self.http_settings().disable_ssl_verification = true;
383 self
384 }
385
386 #[cfg(not(target_family = "wasm"))]
388 pub fn user_agent(mut self, user_agent: impl AsRef<str>) -> Self {
389 self.http_settings().user_agent = Some(user_agent.as_ref().to_owned());
390 self
391 }
392
393 #[cfg(not(target_family = "wasm"))]
402 pub fn add_root_certificates(mut self, certificates: Vec<Certificate>) -> Self {
403 self.http_settings().additional_root_certificates = certificates;
404 self
405 }
406
407 #[cfg(not(target_family = "wasm"))]
411 pub fn disable_built_in_root_certificates(mut self) -> Self {
412 self.http_settings().disable_built_in_root_certificates = true;
413 self
414 }
415
416 pub fn http_client(mut self, client: reqwest::Client) -> Self {
426 self.http_cfg = Some(HttpConfig::Custom(client));
427 self
428 }
429
430 pub fn server_versions(mut self, value: impl IntoIterator<Item = MatrixVersion>) -> Self {
435 self.server_versions = Some(value.into_iter().collect());
436 self
437 }
438
439 #[cfg(not(target_family = "wasm"))]
440 fn http_settings(&mut self) -> &mut HttpSettings {
441 self.http_cfg.get_or_insert_with(Default::default).settings()
442 }
443
444 pub fn handle_refresh_tokens(mut self) -> Self {
466 self.handle_refresh_tokens = true;
467 self
468 }
469
470 #[doc(hidden)]
472 pub fn base_client(mut self, base_client: BaseClient) -> Self {
473 self.base_client = Some(base_client);
474 self
475 }
476
477 #[cfg(feature = "e2e-encryption")]
480 pub fn with_encryption_settings(mut self, settings: EncryptionSettings) -> Self {
481 self.encryption_settings = settings;
482 self
483 }
484
485 #[cfg(feature = "e2e-encryption")]
488 pub fn with_room_key_recipient_strategy(mut self, strategy: CollectStrategy) -> Self {
489 self.room_key_recipient_strategy = strategy;
490 self
491 }
492
493 #[cfg(feature = "e2e-encryption")]
495 pub fn with_decryption_settings(mut self, decryption_settings: DecryptionSettings) -> Self {
496 self.decryption_settings = decryption_settings;
497 self
498 }
499
500 #[cfg(feature = "e2e-encryption")]
508 pub fn with_enable_share_history_on_invite(
509 mut self,
510 enable_share_history_on_invite: bool,
511 ) -> Self {
512 self.enable_share_history_on_invite = enable_share_history_on_invite;
513 self
514 }
515
516 pub fn cross_process_store_config(
526 mut self,
527 cross_process_store_config: CrossProcessLockConfig,
528 ) -> Self {
529 self.cross_process_lock_config = cross_process_store_config;
530 self
531 }
532
533 pub fn with_threading_support(mut self, threading_support: ThreadingSupport) -> Self {
537 self.threading_support = threading_support;
538 self
539 }
540
541 #[cfg(feature = "experimental-search")]
543 pub fn search_index_store(mut self, kind: SearchIndexStoreKind) -> Self {
544 self.search_index_store_kind = kind;
545 self
546 }
547
548 #[instrument(skip_all, target = "matrix_sdk::client", fields(homeserver))]
561 pub async fn build(self) -> Result<Client, ClientBuildError> {
562 debug!("Starting to build the Client");
563
564 let homeserver_cfg = self.homeserver_cfg.ok_or(ClientBuildError::MissingHomeserver)?;
565 Span::current().record("homeserver", debug(&homeserver_cfg));
566
567 #[cfg_attr(target_family = "wasm", allow(clippy::infallible_destructuring_match))]
568 let inner_http_client = match self.http_cfg.unwrap_or_default() {
569 #[cfg(not(target_family = "wasm"))]
570 HttpConfig::Settings(mut settings) => {
571 settings.timeout = self.request_config.timeout;
572 settings.make_client()?
573 }
574 HttpConfig::Custom(c) => c,
575 };
576
577 let base_client = if let Some(base_client) = self.base_client {
578 base_client
579 } else {
580 #[allow(unused_mut)]
581 let mut client = BaseClient::new(
582 build_store_config(self.store_config, &self.cross_process_lock_config).await?,
583 self.threading_support,
584 self.dm_room_definition,
585 );
586
587 #[cfg(feature = "e2e-encryption")]
588 {
589 client.room_key_recipient_strategy = self.room_key_recipient_strategy;
590 client.decryption_settings = self.decryption_settings;
591 }
592
593 client
594 };
595
596 let http_client = HttpClient::new(inner_http_client.clone(), self.request_config);
597
598 #[allow(unused_variables)]
599 let HomeserverDiscoveryResult { server, homeserver, supported_versions, well_known } =
600 homeserver_cfg.discover(&http_client).await?;
601
602 let sliding_sync_version = {
603 let supported_versions = match supported_versions {
604 Some(versions) => Some(versions),
605 None if self.sliding_sync_version_builder.needs_get_supported_versions() => {
606 Some(get_supported_versions(&homeserver, &http_client).await?)
607 }
608 None => None,
609 };
610
611 let version = self.sliding_sync_version_builder.build(
612 supported_versions.map(|response| response.as_supported_versions()).as_ref(),
613 )?;
614
615 tracing::info!(?version, "selected sliding sync version");
616
617 version
618 };
619
620 let allow_insecure_oauth = homeserver.scheme() == "http";
621 let auth_ctx = Arc::new(AuthCtx::new(self.handle_refresh_tokens, allow_insecure_oauth));
622
623 let send_queue = Arc::new(SendQueueData::new(true));
625
626 let supported_versions = match self.server_versions {
627 Some(versions) => Cached(TtlValue::without_expiry(SupportedVersions {
628 versions,
629 features: Default::default(),
630 })),
631 None => NotSet,
632 };
633 let well_known = match well_known {
634 Some(well_known) => Cached(TtlValue::new(Some(well_known.into()))),
635 None => NotSet,
636 };
637
638 let event_cache = OnceCell::new();
639 let latest_events = OnceCell::new();
640 let thread_subscriptions_catchup = OnceCell::new();
641
642 #[cfg(feature = "experimental-search")]
643 let search_index =
644 SearchIndex::new(Arc::new(Mutex::new(HashMap::new())), self.search_index_store_kind);
645
646 let inner = ClientInner::new(
647 auth_ctx,
648 server,
649 homeserver,
650 sliding_sync_version,
651 http_client,
652 base_client,
653 supported_versions,
654 well_known,
655 self.respect_login_well_known,
656 event_cache,
657 send_queue,
658 latest_events,
659 #[cfg(feature = "e2e-encryption")]
660 self.encryption_settings,
661 #[cfg(feature = "e2e-encryption")]
662 self.enable_share_history_on_invite,
663 self.cross_process_lock_config,
664 #[cfg(feature = "experimental-search")]
665 search_index,
666 thread_subscriptions_catchup,
667 self.media_fetcher.clone(),
668 )
669 .await;
670
671 debug!("Done building the Client");
672
673 Ok(Client { inner })
674 }
675}
676
677pub fn sanitize_server_name(s: &str) -> crate::Result<OwnedServerName, IdParseError> {
681 ServerName::parse(
682 s.trim().trim_start_matches("http://").trim_start_matches("https://").trim_end_matches('/'),
683 )
684}
685
686#[allow(clippy::unused_async, unused)] async fn build_store_config(
688 builder_config: BuilderStoreConfig,
689 cross_process_store_config: &CrossProcessLockConfig,
690) -> Result<StoreConfig, ClientBuildError> {
691 #[allow(clippy::infallible_destructuring_match)]
692 let store_config = match builder_config {
693 #[cfg(feature = "sqlite")]
694 BuilderStoreConfig::Sqlite { config, cache_path } => {
695 let config_with_cache_path = if let Some(ref cache_path) = cache_path {
696 config.clone().path(cache_path)
697 } else {
698 config.clone()
699 };
700
701 #[cfg(feature = "e2e-encryption")]
702 let (state_store, event_cache_store, media_store, crypto_store) = try_join!(
703 matrix_sdk_sqlite::SqliteStateStore::open_with_config(&config),
704 matrix_sdk_sqlite::SqliteEventCacheStore::open_with_config(&config_with_cache_path),
705 matrix_sdk_sqlite::SqliteMediaStore::open_with_config(&config_with_cache_path),
706 matrix_sdk_sqlite::SqliteCryptoStore::open_with_config(&config),
707 )?;
708 #[cfg(not(feature = "e2e-encryption"))]
709 let (state_store, event_cache_store, media_store) = try_join!(
710 matrix_sdk_sqlite::SqliteStateStore::open_with_config(&config),
711 matrix_sdk_sqlite::SqliteEventCacheStore::open_with_config(&config_with_cache_path),
712 matrix_sdk_sqlite::SqliteMediaStore::open_with_config(&config),
713 )?;
714 let store_config = StoreConfig::new(cross_process_store_config.clone())
715 .state_store(state_store)
716 .event_cache_store(event_cache_store)
717 .media_store(media_store);
718
719 #[cfg(feature = "e2e-encryption")]
720 let store_config = store_config.crypto_store(crypto_store);
721
722 store_config
723 }
724
725 #[cfg(feature = "indexeddb")]
726 BuilderStoreConfig::IndexedDb { name, passphrase } => {
727 build_indexeddb_store_config(
728 &name,
729 passphrase.as_deref(),
730 cross_process_store_config.clone(),
731 )
732 .await?
733 }
734
735 BuilderStoreConfig::Custom(config) => config,
736 };
737 Ok(store_config)
738}
739
740#[cfg(all(target_family = "wasm", feature = "indexeddb"))]
743async fn build_indexeddb_store_config(
744 name: &str,
745 passphrase: Option<&str>,
746 cross_process_store_config: CrossProcessLockConfig,
747) -> Result<StoreConfig, ClientBuildError> {
748 let stores = matrix_sdk_indexeddb::IndexeddbStores::open(name, passphrase).await?;
749 let store_config = StoreConfig::new(cross_process_store_config)
750 .state_store(stores.state)
751 .event_cache_store(stores.event_cache)
752 .media_store(stores.media);
753
754 #[cfg(feature = "e2e-encryption")]
755 let store_config = store_config.crypto_store(stores.crypto);
756
757 Ok(store_config)
758}
759
760#[cfg(all(not(target_family = "wasm"), feature = "indexeddb"))]
761#[allow(clippy::unused_async)]
762async fn build_indexeddb_store_config(
763 _name: &str,
764 _passphrase: Option<&str>,
765 _cross_process_store_config: CrossProcessLockConfig,
766) -> Result<StoreConfig, ClientBuildError> {
767 panic!("the IndexedDB is only available on the 'wasm32' arch")
768}
769
770#[derive(Clone, Debug)]
771enum HttpConfig {
772 #[cfg(not(target_family = "wasm"))]
773 Settings(HttpSettings),
774 Custom(reqwest::Client),
775}
776
777#[cfg(not(target_family = "wasm"))]
778impl HttpConfig {
779 fn settings(&mut self) -> &mut HttpSettings {
780 match self {
781 Self::Settings(s) => s,
782 Self::Custom(_) => {
783 *self = Self::default();
784 match self {
785 Self::Settings(s) => s,
786 Self::Custom(_) => unreachable!(),
787 }
788 }
789 }
790 }
791}
792
793impl Default for HttpConfig {
794 fn default() -> Self {
795 #[cfg(not(target_family = "wasm"))]
796 return Self::Settings(HttpSettings::default());
797
798 #[cfg(target_family = "wasm")]
799 return Self::Custom(reqwest::Client::new());
800 }
801}
802
803#[derive(Clone)]
804enum BuilderStoreConfig {
805 #[cfg(feature = "sqlite")]
806 Sqlite {
807 config: SqliteStoreConfig,
808 cache_path: Option<PathBuf>,
809 },
810 #[cfg(feature = "indexeddb")]
811 IndexedDb {
812 name: String,
813 passphrase: Option<String>,
814 },
815 Custom(StoreConfig),
816}
817
818#[cfg(not(tarpaulin_include))]
819impl fmt::Debug for BuilderStoreConfig {
820 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
821 #[allow(clippy::infallible_destructuring_match)]
822 match self {
823 #[cfg(feature = "sqlite")]
824 Self::Sqlite { config, cache_path, .. } => f
825 .debug_struct("Sqlite")
826 .field("config", config)
827 .field("cache_path", cache_path)
828 .finish_non_exhaustive(),
829
830 #[cfg(feature = "indexeddb")]
831 Self::IndexedDb { name, .. } => {
832 f.debug_struct("IndexedDb").field("name", name).finish_non_exhaustive()
833 }
834
835 Self::Custom(store_config) => f.debug_tuple("Custom").field(store_config).finish(),
836 }
837 }
838}
839
840#[derive(Debug, Error)]
842pub enum ClientBuildError {
843 #[error("No homeserver or user ID was configured")]
845 MissingHomeserver,
846
847 #[error("The supplied server name is invalid")]
849 InvalidServerName,
850
851 #[error("Error looking up the .well-known endpoint on auto-discovery")]
853 AutoDiscovery(FromHttpResponseError<RumaApiError>),
854
855 #[error(transparent)]
857 SlidingSyncVersion(#[from] crate::sliding_sync::VersionBuilderError),
858
859 #[error(transparent)]
861 Url(#[from] url::ParseError),
862
863 #[error(transparent)]
865 Http(#[from] HttpError),
866
867 #[cfg(feature = "indexeddb")]
869 #[error(transparent)]
870 IndexeddbStore(#[from] matrix_sdk_indexeddb::OpenStoreError),
871
872 #[cfg(feature = "sqlite")]
874 #[error(transparent)]
875 SqliteStore(#[from] matrix_sdk_sqlite::OpenStoreError),
876}
877
878#[cfg(all(test, not(target_family = "wasm")))]
880pub(crate) mod tests {
881 use assert_matches::assert_matches;
882 use assert_matches2::assert_let;
883 use matrix_sdk_test::{async_test, test_json};
884 use serde_json::{Value as JsonValue, json_internal};
885 use wiremock::{
886 Mock, MockServer, ResponseTemplate,
887 matchers::{method, path},
888 };
889
890 use super::*;
891 use crate::sliding_sync::Version as SlidingSyncVersion;
892
893 #[test]
894 fn test_sanitize_server_name() {
895 assert_eq!(sanitize_server_name("matrix.org").unwrap().as_str(), "matrix.org");
896 assert_eq!(sanitize_server_name("https://matrix.org").unwrap().as_str(), "matrix.org");
897 assert_eq!(sanitize_server_name("http://matrix.org").unwrap().as_str(), "matrix.org");
898 assert_eq!(
899 sanitize_server_name("https://matrix.server.org").unwrap().as_str(),
900 "matrix.server.org"
901 );
902 assert_eq!(
903 sanitize_server_name("https://matrix.server.org/").unwrap().as_str(),
904 "matrix.server.org"
905 );
906 assert_eq!(
907 sanitize_server_name(" https://matrix.server.org// ").unwrap().as_str(),
908 "matrix.server.org"
909 );
910 assert_matches!(sanitize_server_name("https://matrix.server.org/something"), Err(_))
911 }
912
913 #[async_test]
920 async fn test_discovery_invalid_server() {
921 let mut builder = ClientBuilder::new();
923
924 builder = builder.server_name_or_homeserver_url("⚠️ This won't work 🚫");
926 let error = builder.build().await.unwrap_err();
927
928 assert_matches!(error, ClientBuildError::InvalidServerName);
930 }
931
932 #[async_test]
933 async fn test_discovery_no_server() {
934 let mut builder = ClientBuilder::new();
936
937 builder = builder.server_name_or_homeserver_url("localhost:3456");
939 let error = builder.build().await.unwrap_err();
940
941 println!("{error}");
943 assert_matches!(error, ClientBuildError::Http(_));
944 }
945
946 #[async_test]
947 async fn test_discovery_web_server() {
948 let server = MockServer::start().await;
951 let mut builder = ClientBuilder::new();
952
953 builder = builder.server_name_or_homeserver_url(server.uri());
955 let error = builder.build().await.unwrap_err();
956
957 assert_matches!(error, ClientBuildError::AutoDiscovery(FromHttpResponseError::Server(_)));
959 }
960
961 #[async_test]
962 async fn test_discovery_direct_legacy() {
963 let homeserver = make_mock_homeserver().await;
965 let mut builder = ClientBuilder::new();
966
967 builder = builder.server_name_or_homeserver_url(homeserver.uri());
969 let _client = builder.build().await.unwrap();
970
971 assert!(_client.sliding_sync_version().is_native());
973 }
974
975 #[async_test]
976 async fn test_discovery_well_known_parse_error() {
977 let server = MockServer::start().await;
979 let homeserver = make_mock_homeserver().await;
980 let mut builder = ClientBuilder::new();
981
982 let well_known = make_well_known_json(&homeserver.uri());
983 let bad_json = well_known.to_string().replace(',', "");
984 Mock::given(method("GET"))
985 .and(path("/.well-known/matrix/client"))
986 .respond_with(ResponseTemplate::new(200).set_body_json(bad_json))
987 .mount(&server)
988 .await;
989
990 builder = builder.server_name_or_homeserver_url(server.uri());
992 let error = builder.build().await.unwrap_err();
993
994 assert_matches!(
996 error,
997 ClientBuildError::AutoDiscovery(FromHttpResponseError::Deserialization(_))
998 );
999 }
1000
1001 #[async_test]
1002 async fn test_discovery_well_known_legacy() {
1003 let server = MockServer::start().await;
1006 let homeserver = make_mock_homeserver().await;
1007 let mut builder = ClientBuilder::new();
1008
1009 Mock::given(method("GET"))
1010 .and(path("/.well-known/matrix/client"))
1011 .respond_with(
1012 ResponseTemplate::new(200).set_body_json(make_well_known_json(&homeserver.uri())),
1013 )
1014 .mount(&server)
1015 .await;
1016
1017 builder = builder.server_name_or_homeserver_url(server.uri());
1019 let client = builder.build().await.unwrap();
1020
1021 assert!(client.sliding_sync_version().is_native());
1024 }
1025
1026 #[async_test]
1027 async fn test_sliding_sync_discover_native() {
1028 let homeserver = make_mock_homeserver().await;
1030 let mut builder = ClientBuilder::new();
1031
1032 builder = builder
1035 .server_name_or_homeserver_url(homeserver.uri())
1036 .sliding_sync_version_builder(SlidingSyncVersionBuilder::DiscoverNative);
1037
1038 let client = builder.build().await.unwrap();
1039
1040 assert_matches!(client.sliding_sync_version(), SlidingSyncVersion::Native);
1042 }
1043
1044 #[async_test]
1045 #[cfg(feature = "e2e-encryption")]
1046 async fn test_set_up_decryption_trust_requirement_cross_signed() {
1047 let homeserver = make_mock_homeserver().await;
1048 let builder = ClientBuilder::new()
1049 .server_name_or_homeserver_url(homeserver.uri())
1050 .with_decryption_settings(DecryptionSettings {
1051 sender_device_trust_requirement: TrustRequirement::CrossSigned,
1052 });
1053
1054 let client = builder.build().await.unwrap();
1055 assert_matches!(
1056 client.base_client().decryption_settings.sender_device_trust_requirement,
1057 TrustRequirement::CrossSigned
1058 );
1059 }
1060
1061 #[async_test]
1062 #[cfg(feature = "e2e-encryption")]
1063 async fn test_set_up_decryption_trust_requirement_untrusted() {
1064 let homeserver = make_mock_homeserver().await;
1065
1066 let builder = ClientBuilder::new()
1067 .server_name_or_homeserver_url(homeserver.uri())
1068 .with_decryption_settings(DecryptionSettings {
1069 sender_device_trust_requirement: TrustRequirement::Untrusted,
1070 });
1071
1072 let client = builder.build().await.unwrap();
1073 assert_matches!(
1074 client.base_client().decryption_settings.sender_device_trust_requirement,
1075 TrustRequirement::Untrusted
1076 );
1077 }
1078
1079 async fn make_mock_homeserver() -> MockServer {
1082 let homeserver = MockServer::start().await;
1083 Mock::given(method("GET"))
1084 .and(path("/_matrix/client/versions"))
1085 .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::VERSIONS))
1086 .mount(&homeserver)
1087 .await;
1088 Mock::given(method("GET"))
1089 .and(path("/_matrix/client/r0/login"))
1090 .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::LOGIN_TYPES))
1091 .mount(&homeserver)
1092 .await;
1093 homeserver
1094 }
1095
1096 fn make_well_known_json(homeserver_url: &str) -> JsonValue {
1097 ::serde_json::Value::Object({
1098 let mut object = ::serde_json::Map::new();
1099 let _ = object.insert(
1100 "m.homeserver".into(),
1101 json_internal!({
1102 "base_url": homeserver_url
1103 }),
1104 );
1105
1106 object
1107 })
1108 }
1109
1110 #[async_test]
1111 async fn test_cross_process_store_locks_holder_name() {
1112 {
1113 let homeserver = make_mock_homeserver().await;
1114 let client =
1115 ClientBuilder::new().homeserver_url(homeserver.uri()).build().await.unwrap();
1116
1117 assert_let!(
1118 CrossProcessLockConfig::MultiProcess { holder_name } =
1119 client.cross_process_lock_config()
1120 );
1121 assert_eq!(holder_name, "main");
1122 }
1123
1124 {
1125 let homeserver = make_mock_homeserver().await;
1126 let client = ClientBuilder::new()
1127 .homeserver_url(homeserver.uri())
1128 .cross_process_store_config(CrossProcessLockConfig::multi_process("foo"))
1129 .build()
1130 .await
1131 .unwrap();
1132
1133 assert_let!(
1134 CrossProcessLockConfig::MultiProcess { holder_name } =
1135 client.cross_process_lock_config()
1136 );
1137 assert_eq!(holder_name, "foo");
1138 }
1139 }
1140}