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
26use homeserver_config::*;
27#[cfg(feature = "e2e-encryption")]
28use matrix_sdk_base::crypto::DecryptionSettings;
29#[cfg(feature = "e2e-encryption")]
30use matrix_sdk_base::crypto::{CollectStrategy, TrustRequirement};
31use matrix_sdk_base::{BaseClient, ThreadingSupport, store::StoreConfig};
32use matrix_sdk_common::cross_process_lock::CrossProcessLockConfig;
33#[cfg(feature = "sqlite")]
34use matrix_sdk_sqlite::SqliteStoreConfig;
35#[cfg(not(target_family = "wasm"))]
36use reqwest::Certificate;
37use ruma::{
38 OwnedServerName, ServerName,
39 api::{MatrixVersion, SupportedVersions, error::FromHttpResponseError},
40};
41use thiserror::Error;
42#[cfg(feature = "experimental-search")]
43use tokio::sync::Mutex;
44use tokio::sync::OnceCell;
45use tracing::{Span, debug, field::debug, instrument};
46
47use super::{Client, ClientInner};
48#[cfg(feature = "e2e-encryption")]
49use crate::encryption::EncryptionSettings;
50#[cfg(not(target_family = "wasm"))]
51use crate::http_client::HttpSettings;
52#[cfg(feature = "experimental-search")]
53use crate::search_index::SearchIndex;
54#[cfg(feature = "experimental-search")]
55use crate::search_index::SearchIndexStoreKind;
56use crate::{
57 HttpError, IdParseError,
58 authentication::AuthCtx,
59 client::caches::CachedValue::{Cached, NotSet},
60 config::RequestConfig,
61 error::RumaApiError,
62 http_client::HttpClient,
63 send_queue::SendQueueData,
64 sliding_sync::VersionBuilder as SlidingSyncVersionBuilder,
65};
66
67#[must_use]
106#[derive(Clone, Debug)]
107pub struct ClientBuilder {
108 homeserver_cfg: Option<HomeserverConfig>,
109 sliding_sync_version_builder: SlidingSyncVersionBuilder,
110 http_cfg: Option<HttpConfig>,
111 store_config: BuilderStoreConfig,
112 request_config: RequestConfig,
113 respect_login_well_known: bool,
114 server_versions: Option<BTreeSet<MatrixVersion>>,
115 handle_refresh_tokens: bool,
116 base_client: Option<BaseClient>,
117 #[cfg(feature = "e2e-encryption")]
118 encryption_settings: EncryptionSettings,
119 #[cfg(feature = "e2e-encryption")]
120 room_key_recipient_strategy: CollectStrategy,
121 #[cfg(feature = "e2e-encryption")]
122 decryption_settings: DecryptionSettings,
123 #[cfg(feature = "e2e-encryption")]
124 enable_share_history_on_invite: bool,
125 cross_process_lock_config: CrossProcessLockConfig,
126 threading_support: ThreadingSupport,
127 #[cfg(feature = "experimental-search")]
128 search_index_store_kind: SearchIndexStoreKind,
129}
130
131impl ClientBuilder {
132 const DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME: &str = "main";
133
134 pub(crate) fn new() -> Self {
135 Self {
136 homeserver_cfg: None,
137 sliding_sync_version_builder: SlidingSyncVersionBuilder::Native,
138 http_cfg: None,
139 store_config: BuilderStoreConfig::Custom(StoreConfig::new(
140 CrossProcessLockConfig::multi_process(
141 Self::DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME,
142 ),
143 )),
144 request_config: Default::default(),
145 respect_login_well_known: true,
146 server_versions: None,
147 handle_refresh_tokens: false,
148 base_client: None,
149 #[cfg(feature = "e2e-encryption")]
150 encryption_settings: Default::default(),
151 #[cfg(feature = "e2e-encryption")]
152 room_key_recipient_strategy: Default::default(),
153 #[cfg(feature = "e2e-encryption")]
154 decryption_settings: DecryptionSettings {
155 sender_device_trust_requirement: TrustRequirement::Untrusted,
156 },
157 #[cfg(feature = "e2e-encryption")]
158 enable_share_history_on_invite: false,
159 cross_process_lock_config: CrossProcessLockConfig::MultiProcess {
160 holder_name: Self::DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME.to_owned(),
161 },
162 threading_support: ThreadingSupport::Disabled,
163 #[cfg(feature = "experimental-search")]
164 search_index_store_kind: SearchIndexStoreKind::InMemory,
165 }
166 }
167
168 pub fn homeserver_url(mut self, url: impl AsRef<str>) -> Self {
175 self.homeserver_cfg = Some(HomeserverConfig::HomeserverUrl(url.as_ref().to_owned()));
176 self
177 }
178
179 pub fn server_name(mut self, server_name: &ServerName) -> Self {
189 self.homeserver_cfg = Some(HomeserverConfig::ServerName {
190 server: server_name.to_owned(),
191 protocol: UrlScheme::Https,
193 });
194 self
195 }
196
197 pub fn insecure_server_name_no_tls(mut self, server_name: &ServerName) -> Self {
206 self.homeserver_cfg = Some(HomeserverConfig::ServerName {
207 server: server_name.to_owned(),
208 protocol: UrlScheme::Http,
209 });
210 self
211 }
212
213 pub fn server_name_or_homeserver_url(mut self, server_name_or_url: impl AsRef<str>) -> Self {
224 self.homeserver_cfg = Some(HomeserverConfig::ServerNameOrHomeserverUrl(
225 server_name_or_url.as_ref().to_owned(),
226 ));
227 self
228 }
229
230 pub fn sliding_sync_version_builder(
232 mut self,
233 version_builder: SlidingSyncVersionBuilder,
234 ) -> Self {
235 self.sliding_sync_version_builder = version_builder;
236 self
237 }
238
239 #[cfg(feature = "sqlite")]
241 pub fn sqlite_store(mut self, path: impl AsRef<Path>, passphrase: Option<&str>) -> Self {
242 let sqlite_store_config = SqliteStoreConfig::new(path).passphrase(passphrase);
243 self.store_config =
244 BuilderStoreConfig::Sqlite { config: sqlite_store_config, cache_path: None };
245
246 self
247 }
248
249 #[cfg(feature = "sqlite")]
252 pub fn sqlite_store_with_cache_path(
253 mut self,
254 path: impl AsRef<Path>,
255 cache_path: impl AsRef<Path>,
256 passphrase: Option<&str>,
257 ) -> Self {
258 let sqlite_store_config = SqliteStoreConfig::new(path).passphrase(passphrase);
259 self.store_config = BuilderStoreConfig::Sqlite {
260 config: sqlite_store_config,
261 cache_path: Some(cache_path.as_ref().to_owned()),
262 };
263
264 self
265 }
266
267 #[cfg(feature = "sqlite")]
270 pub fn sqlite_store_with_config_and_cache_path(
271 mut self,
272 config: SqliteStoreConfig,
273 cache_path: Option<impl AsRef<Path>>,
274 ) -> Self {
275 self.store_config = BuilderStoreConfig::Sqlite {
276 config,
277 cache_path: cache_path.map(|cache_path| cache_path.as_ref().to_owned()),
278 };
279
280 self
281 }
282
283 #[cfg(feature = "indexeddb")]
285 pub fn indexeddb_store(mut self, name: &str, passphrase: Option<&str>) -> Self {
286 self.store_config = BuilderStoreConfig::IndexedDb {
287 name: name.to_owned(),
288 passphrase: passphrase.map(ToOwned::to_owned),
289 };
290 self
291 }
292
293 pub fn store_config(mut self, store_config: StoreConfig) -> Self {
317 self.store_config = BuilderStoreConfig::Custom(store_config);
318 self
319 }
320
321 pub fn respect_login_well_known(mut self, value: bool) -> Self {
324 self.respect_login_well_known = value;
325 self
326 }
327
328 pub fn request_config(mut self, request_config: RequestConfig) -> Self {
330 self.request_config = request_config;
331 self
332 }
333
334 #[cfg(not(target_family = "wasm"))]
350 pub fn proxy(mut self, proxy: impl AsRef<str>) -> Self {
351 self.http_settings().proxy = Some(proxy.as_ref().to_owned());
352 self
353 }
354
355 #[cfg(not(target_family = "wasm"))]
357 pub fn disable_ssl_verification(mut self) -> Self {
358 self.http_settings().disable_ssl_verification = true;
359 self
360 }
361
362 #[cfg(not(target_family = "wasm"))]
364 pub fn user_agent(mut self, user_agent: impl AsRef<str>) -> Self {
365 self.http_settings().user_agent = Some(user_agent.as_ref().to_owned());
366 self
367 }
368
369 #[cfg(not(target_family = "wasm"))]
378 pub fn add_root_certificates(mut self, certificates: Vec<Certificate>) -> Self {
379 self.http_settings().additional_root_certificates = certificates;
380 self
381 }
382
383 #[cfg(target_os = "android")]
388 pub fn add_raw_root_certificates(mut self, raw_certificates: Vec<Vec<u8>>) -> Self {
389 self.http_settings().additional_raw_root_certificates = raw_certificates;
390 self
391 }
392
393 #[cfg(not(target_family = "wasm"))]
397 pub fn disable_built_in_root_certificates(mut self) -> Self {
398 self.http_settings().disable_built_in_root_certificates = true;
399 self
400 }
401
402 pub fn http_client(mut self, client: reqwest::Client) -> Self {
412 self.http_cfg = Some(HttpConfig::Custom(client));
413 self
414 }
415
416 pub fn server_versions(mut self, value: impl IntoIterator<Item = MatrixVersion>) -> Self {
421 self.server_versions = Some(value.into_iter().collect());
422 self
423 }
424
425 #[cfg(not(target_family = "wasm"))]
426 fn http_settings(&mut self) -> &mut HttpSettings {
427 self.http_cfg.get_or_insert_with(Default::default).settings()
428 }
429
430 pub fn handle_refresh_tokens(mut self) -> Self {
452 self.handle_refresh_tokens = true;
453 self
454 }
455
456 #[doc(hidden)]
458 pub fn base_client(mut self, base_client: BaseClient) -> Self {
459 self.base_client = Some(base_client);
460 self
461 }
462
463 #[cfg(feature = "e2e-encryption")]
466 pub fn with_encryption_settings(mut self, settings: EncryptionSettings) -> Self {
467 self.encryption_settings = settings;
468 self
469 }
470
471 #[cfg(feature = "e2e-encryption")]
474 pub fn with_room_key_recipient_strategy(mut self, strategy: CollectStrategy) -> Self {
475 self.room_key_recipient_strategy = strategy;
476 self
477 }
478
479 #[cfg(feature = "e2e-encryption")]
481 pub fn with_decryption_settings(mut self, decryption_settings: DecryptionSettings) -> Self {
482 self.decryption_settings = decryption_settings;
483 self
484 }
485
486 #[cfg(feature = "e2e-encryption")]
491 pub fn with_enable_share_history_on_invite(
492 mut self,
493 enable_share_history_on_invite: bool,
494 ) -> Self {
495 self.enable_share_history_on_invite = enable_share_history_on_invite;
496 self
497 }
498
499 pub fn cross_process_store_config(
509 mut self,
510 cross_process_store_config: CrossProcessLockConfig,
511 ) -> Self {
512 self.cross_process_lock_config = cross_process_store_config;
513 self
514 }
515
516 pub fn with_threading_support(mut self, threading_support: ThreadingSupport) -> Self {
520 self.threading_support = threading_support;
521 self
522 }
523
524 #[cfg(feature = "experimental-search")]
526 pub fn search_index_store(mut self, kind: SearchIndexStoreKind) -> Self {
527 self.search_index_store_kind = kind;
528 self
529 }
530
531 #[instrument(skip_all, target = "matrix_sdk::client", fields(homeserver))]
544 pub async fn build(self) -> Result<Client, ClientBuildError> {
545 debug!("Starting to build the Client");
546
547 let homeserver_cfg = self.homeserver_cfg.ok_or(ClientBuildError::MissingHomeserver)?;
548 Span::current().record("homeserver", debug(&homeserver_cfg));
549
550 #[cfg_attr(target_family = "wasm", allow(clippy::infallible_destructuring_match))]
551 let inner_http_client = match self.http_cfg.unwrap_or_default() {
552 #[cfg(not(target_family = "wasm"))]
553 HttpConfig::Settings(mut settings) => {
554 settings.timeout = self.request_config.timeout;
555 settings.make_client()?
556 }
557 HttpConfig::Custom(c) => c,
558 };
559
560 let base_client = if let Some(base_client) = self.base_client {
561 base_client
562 } else {
563 #[allow(unused_mut)]
564 let mut client = BaseClient::new(
565 build_store_config(self.store_config, &self.cross_process_lock_config).await?,
566 self.threading_support,
567 );
568
569 #[cfg(feature = "e2e-encryption")]
570 {
571 client.room_key_recipient_strategy = self.room_key_recipient_strategy;
572 client.decryption_settings = self.decryption_settings;
573 }
574
575 client
576 };
577
578 let http_client = HttpClient::new(inner_http_client.clone(), self.request_config);
579
580 #[allow(unused_variables)]
581 let HomeserverDiscoveryResult { server, homeserver, supported_versions, well_known } =
582 homeserver_cfg.discover(&http_client).await?;
583
584 let sliding_sync_version = {
585 let supported_versions = match supported_versions {
586 Some(versions) => Some(versions),
587 None if self.sliding_sync_version_builder.needs_get_supported_versions() => {
588 Some(get_supported_versions(&homeserver, &http_client).await?)
589 }
590 None => None,
591 };
592
593 let version = self.sliding_sync_version_builder.build(
594 supported_versions.map(|response| response.as_supported_versions()).as_ref(),
595 )?;
596
597 tracing::info!(?version, "selected sliding sync version");
598
599 version
600 };
601
602 let allow_insecure_oauth = homeserver.scheme() == "http";
603 let auth_ctx = Arc::new(AuthCtx::new(self.handle_refresh_tokens, allow_insecure_oauth));
604
605 let send_queue = Arc::new(SendQueueData::new(true));
607
608 let supported_versions = match self.server_versions {
609 Some(versions) => Cached(SupportedVersions { versions, features: Default::default() }),
610 None => NotSet,
611 };
612 let well_known = match well_known {
613 Some(well_known) => Cached(Some(well_known.into())),
614 None => NotSet,
615 };
616
617 let event_cache = OnceCell::new();
618 let latest_events = OnceCell::new();
619 let thread_subscriptions_catchup = OnceCell::new();
620
621 #[cfg(feature = "experimental-search")]
622 let search_index =
623 SearchIndex::new(Arc::new(Mutex::new(HashMap::new())), self.search_index_store_kind);
624
625 let inner = ClientInner::new(
626 auth_ctx,
627 server,
628 homeserver,
629 sliding_sync_version,
630 http_client,
631 base_client,
632 supported_versions,
633 well_known,
634 self.respect_login_well_known,
635 event_cache,
636 send_queue,
637 latest_events,
638 #[cfg(feature = "e2e-encryption")]
639 self.encryption_settings,
640 #[cfg(feature = "e2e-encryption")]
641 self.enable_share_history_on_invite,
642 self.cross_process_lock_config,
643 #[cfg(feature = "experimental-search")]
644 search_index,
645 thread_subscriptions_catchup,
646 )
647 .await;
648
649 debug!("Done building the Client");
650
651 Ok(Client { inner })
652 }
653}
654
655pub fn sanitize_server_name(s: &str) -> crate::Result<OwnedServerName, IdParseError> {
659 ServerName::parse(
660 s.trim().trim_start_matches("http://").trim_start_matches("https://").trim_end_matches('/'),
661 )
662}
663
664#[allow(clippy::unused_async, unused)] async fn build_store_config(
666 builder_config: BuilderStoreConfig,
667 cross_process_store_config: &CrossProcessLockConfig,
668) -> Result<StoreConfig, ClientBuildError> {
669 #[allow(clippy::infallible_destructuring_match)]
670 let store_config = match builder_config {
671 #[cfg(feature = "sqlite")]
672 BuilderStoreConfig::Sqlite { config, cache_path } => {
673 let store_config = StoreConfig::new(cross_process_store_config.clone())
674 .state_store(
675 matrix_sdk_sqlite::SqliteStateStore::open_with_config(config.clone()).await?,
676 )
677 .event_cache_store({
678 let mut config = config.clone();
679
680 if let Some(ref cache_path) = cache_path {
681 config = config.path(cache_path);
682 }
683
684 matrix_sdk_sqlite::SqliteEventCacheStore::open_with_config(config).await?
685 })
686 .media_store({
687 let mut config = config.clone();
688
689 if let Some(ref cache_path) = cache_path {
690 config = config.path(cache_path);
691 }
692
693 matrix_sdk_sqlite::SqliteMediaStore::open_with_config(config).await?
694 });
695
696 #[cfg(feature = "e2e-encryption")]
697 let store_config = store_config.crypto_store(
698 matrix_sdk_sqlite::SqliteCryptoStore::open_with_config(config).await?,
699 );
700
701 store_config
702 }
703
704 #[cfg(feature = "indexeddb")]
705 BuilderStoreConfig::IndexedDb { name, passphrase } => {
706 build_indexeddb_store_config(
707 &name,
708 passphrase.as_deref(),
709 cross_process_store_config.clone(),
710 )
711 .await?
712 }
713
714 BuilderStoreConfig::Custom(config) => config,
715 };
716 Ok(store_config)
717}
718
719#[cfg(all(target_family = "wasm", feature = "indexeddb"))]
722async fn build_indexeddb_store_config(
723 name: &str,
724 passphrase: Option<&str>,
725 cross_process_store_config: CrossProcessLockConfig,
726) -> Result<StoreConfig, ClientBuildError> {
727 let stores = matrix_sdk_indexeddb::IndexeddbStores::open(name, passphrase).await?;
728 let store_config = StoreConfig::new(cross_process_store_config)
729 .state_store(stores.state)
730 .event_cache_store(stores.event_cache)
731 .media_store(stores.media);
732
733 #[cfg(feature = "e2e-encryption")]
734 let store_config = store_config.crypto_store(stores.crypto);
735
736 Ok(store_config)
737}
738
739#[cfg(all(not(target_family = "wasm"), feature = "indexeddb"))]
740#[allow(clippy::unused_async)]
741async fn build_indexeddb_store_config(
742 _name: &str,
743 _passphrase: Option<&str>,
744 _cross_process_store_config: CrossProcessLockConfig,
745) -> Result<StoreConfig, ClientBuildError> {
746 panic!("the IndexedDB is only available on the 'wasm32' arch")
747}
748
749#[derive(Clone, Debug)]
750enum HttpConfig {
751 #[cfg(not(target_family = "wasm"))]
752 Settings(HttpSettings),
753 Custom(reqwest::Client),
754}
755
756#[cfg(not(target_family = "wasm"))]
757impl HttpConfig {
758 fn settings(&mut self) -> &mut HttpSettings {
759 match self {
760 Self::Settings(s) => s,
761 Self::Custom(_) => {
762 *self = Self::default();
763 match self {
764 Self::Settings(s) => s,
765 Self::Custom(_) => unreachable!(),
766 }
767 }
768 }
769 }
770}
771
772impl Default for HttpConfig {
773 fn default() -> Self {
774 #[cfg(not(target_family = "wasm"))]
775 return Self::Settings(HttpSettings::default());
776
777 #[cfg(target_family = "wasm")]
778 return Self::Custom(reqwest::Client::new());
779 }
780}
781
782#[derive(Clone)]
783enum BuilderStoreConfig {
784 #[cfg(feature = "sqlite")]
785 Sqlite {
786 config: SqliteStoreConfig,
787 cache_path: Option<PathBuf>,
788 },
789 #[cfg(feature = "indexeddb")]
790 IndexedDb {
791 name: String,
792 passphrase: Option<String>,
793 },
794 Custom(StoreConfig),
795}
796
797#[cfg(not(tarpaulin_include))]
798impl fmt::Debug for BuilderStoreConfig {
799 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
800 #[allow(clippy::infallible_destructuring_match)]
801 match self {
802 #[cfg(feature = "sqlite")]
803 Self::Sqlite { config, cache_path, .. } => f
804 .debug_struct("Sqlite")
805 .field("config", config)
806 .field("cache_path", cache_path)
807 .finish_non_exhaustive(),
808
809 #[cfg(feature = "indexeddb")]
810 Self::IndexedDb { name, .. } => {
811 f.debug_struct("IndexedDb").field("name", name).finish_non_exhaustive()
812 }
813
814 Self::Custom(store_config) => f.debug_tuple("Custom").field(store_config).finish(),
815 }
816 }
817}
818
819#[derive(Debug, Error)]
821pub enum ClientBuildError {
822 #[error("No homeserver or user ID was configured")]
824 MissingHomeserver,
825
826 #[error("The supplied server name is invalid")]
828 InvalidServerName,
829
830 #[error("Error looking up the .well-known endpoint on auto-discovery")]
832 AutoDiscovery(FromHttpResponseError<RumaApiError>),
833
834 #[error(transparent)]
836 SlidingSyncVersion(#[from] crate::sliding_sync::VersionBuilderError),
837
838 #[error(transparent)]
840 Url(#[from] url::ParseError),
841
842 #[error(transparent)]
844 Http(#[from] HttpError),
845
846 #[cfg(feature = "indexeddb")]
848 #[error(transparent)]
849 IndexeddbStore(#[from] matrix_sdk_indexeddb::OpenStoreError),
850
851 #[cfg(feature = "sqlite")]
853 #[error(transparent)]
854 SqliteStore(#[from] matrix_sdk_sqlite::OpenStoreError),
855}
856
857#[cfg(all(test, not(target_family = "wasm")))]
859pub(crate) mod tests {
860 use assert_matches::assert_matches;
861 use assert_matches2::assert_let;
862 use matrix_sdk_test::{async_test, test_json};
863 use serde_json::{Value as JsonValue, json_internal};
864 use wiremock::{
865 Mock, MockServer, ResponseTemplate,
866 matchers::{method, path},
867 };
868
869 use super::*;
870 use crate::sliding_sync::Version as SlidingSyncVersion;
871
872 #[test]
873 fn test_sanitize_server_name() {
874 assert_eq!(sanitize_server_name("matrix.org").unwrap().as_str(), "matrix.org");
875 assert_eq!(sanitize_server_name("https://matrix.org").unwrap().as_str(), "matrix.org");
876 assert_eq!(sanitize_server_name("http://matrix.org").unwrap().as_str(), "matrix.org");
877 assert_eq!(
878 sanitize_server_name("https://matrix.server.org").unwrap().as_str(),
879 "matrix.server.org"
880 );
881 assert_eq!(
882 sanitize_server_name("https://matrix.server.org/").unwrap().as_str(),
883 "matrix.server.org"
884 );
885 assert_eq!(
886 sanitize_server_name(" https://matrix.server.org// ").unwrap().as_str(),
887 "matrix.server.org"
888 );
889 assert_matches!(sanitize_server_name("https://matrix.server.org/something"), Err(_))
890 }
891
892 #[async_test]
899 async fn test_discovery_invalid_server() {
900 let mut builder = ClientBuilder::new();
902
903 builder = builder.server_name_or_homeserver_url("⚠️ This won't work 🚫");
905 let error = builder.build().await.unwrap_err();
906
907 assert_matches!(error, ClientBuildError::InvalidServerName);
909 }
910
911 #[async_test]
912 async fn test_discovery_no_server() {
913 let mut builder = ClientBuilder::new();
915
916 builder = builder.server_name_or_homeserver_url("localhost:3456");
918 let error = builder.build().await.unwrap_err();
919
920 println!("{error}");
922 assert_matches!(error, ClientBuildError::Http(_));
923 }
924
925 #[async_test]
926 async fn test_discovery_web_server() {
927 let server = MockServer::start().await;
930 let mut builder = ClientBuilder::new();
931
932 builder = builder.server_name_or_homeserver_url(server.uri());
934 let error = builder.build().await.unwrap_err();
935
936 assert_matches!(error, ClientBuildError::AutoDiscovery(FromHttpResponseError::Server(_)));
938 }
939
940 #[async_test]
941 async fn test_discovery_direct_legacy() {
942 let homeserver = make_mock_homeserver().await;
944 let mut builder = ClientBuilder::new();
945
946 builder = builder.server_name_or_homeserver_url(homeserver.uri());
948 let _client = builder.build().await.unwrap();
949
950 assert!(_client.sliding_sync_version().is_native());
952 }
953
954 #[async_test]
955 async fn test_discovery_well_known_parse_error() {
956 let server = MockServer::start().await;
958 let homeserver = make_mock_homeserver().await;
959 let mut builder = ClientBuilder::new();
960
961 let well_known = make_well_known_json(&homeserver.uri());
962 let bad_json = well_known.to_string().replace(',', "");
963 Mock::given(method("GET"))
964 .and(path("/.well-known/matrix/client"))
965 .respond_with(ResponseTemplate::new(200).set_body_json(bad_json))
966 .mount(&server)
967 .await;
968
969 builder = builder.server_name_or_homeserver_url(server.uri());
971 let error = builder.build().await.unwrap_err();
972
973 assert_matches!(
975 error,
976 ClientBuildError::AutoDiscovery(FromHttpResponseError::Deserialization(_))
977 );
978 }
979
980 #[async_test]
981 async fn test_discovery_well_known_legacy() {
982 let server = MockServer::start().await;
985 let homeserver = make_mock_homeserver().await;
986 let mut builder = ClientBuilder::new();
987
988 Mock::given(method("GET"))
989 .and(path("/.well-known/matrix/client"))
990 .respond_with(
991 ResponseTemplate::new(200).set_body_json(make_well_known_json(&homeserver.uri())),
992 )
993 .mount(&server)
994 .await;
995
996 builder = builder.server_name_or_homeserver_url(server.uri());
998 let client = builder.build().await.unwrap();
999
1000 assert!(client.sliding_sync_version().is_native());
1003 }
1004
1005 #[async_test]
1006 async fn test_sliding_sync_discover_native() {
1007 let homeserver = make_mock_homeserver().await;
1009 let mut builder = ClientBuilder::new();
1010
1011 builder = builder
1014 .server_name_or_homeserver_url(homeserver.uri())
1015 .sliding_sync_version_builder(SlidingSyncVersionBuilder::DiscoverNative);
1016
1017 let client = builder.build().await.unwrap();
1018
1019 assert_matches!(client.sliding_sync_version(), SlidingSyncVersion::Native);
1021 }
1022
1023 #[async_test]
1024 #[cfg(feature = "e2e-encryption")]
1025 async fn test_set_up_decryption_trust_requirement_cross_signed() {
1026 let homeserver = make_mock_homeserver().await;
1027 let builder = ClientBuilder::new()
1028 .server_name_or_homeserver_url(homeserver.uri())
1029 .with_decryption_settings(DecryptionSettings {
1030 sender_device_trust_requirement: TrustRequirement::CrossSigned,
1031 });
1032
1033 let client = builder.build().await.unwrap();
1034 assert_matches!(
1035 client.base_client().decryption_settings.sender_device_trust_requirement,
1036 TrustRequirement::CrossSigned
1037 );
1038 }
1039
1040 #[async_test]
1041 #[cfg(feature = "e2e-encryption")]
1042 async fn test_set_up_decryption_trust_requirement_untrusted() {
1043 let homeserver = make_mock_homeserver().await;
1044
1045 let builder = ClientBuilder::new()
1046 .server_name_or_homeserver_url(homeserver.uri())
1047 .with_decryption_settings(DecryptionSettings {
1048 sender_device_trust_requirement: TrustRequirement::Untrusted,
1049 });
1050
1051 let client = builder.build().await.unwrap();
1052 assert_matches!(
1053 client.base_client().decryption_settings.sender_device_trust_requirement,
1054 TrustRequirement::Untrusted
1055 );
1056 }
1057
1058 async fn make_mock_homeserver() -> MockServer {
1061 let homeserver = MockServer::start().await;
1062 Mock::given(method("GET"))
1063 .and(path("/_matrix/client/versions"))
1064 .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::VERSIONS))
1065 .mount(&homeserver)
1066 .await;
1067 Mock::given(method("GET"))
1068 .and(path("/_matrix/client/r0/login"))
1069 .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::LOGIN_TYPES))
1070 .mount(&homeserver)
1071 .await;
1072 homeserver
1073 }
1074
1075 fn make_well_known_json(homeserver_url: &str) -> JsonValue {
1076 ::serde_json::Value::Object({
1077 let mut object = ::serde_json::Map::new();
1078 let _ = object.insert(
1079 "m.homeserver".into(),
1080 json_internal!({
1081 "base_url": homeserver_url
1082 }),
1083 );
1084
1085 object
1086 })
1087 }
1088
1089 #[async_test]
1090 async fn test_cross_process_store_locks_holder_name() {
1091 {
1092 let homeserver = make_mock_homeserver().await;
1093 let client =
1094 ClientBuilder::new().homeserver_url(homeserver.uri()).build().await.unwrap();
1095
1096 assert_let!(
1097 CrossProcessLockConfig::MultiProcess { holder_name } =
1098 client.cross_process_lock_config()
1099 );
1100 assert_eq!(holder_name, "main");
1101 }
1102
1103 {
1104 let homeserver = make_mock_homeserver().await;
1105 let client = ClientBuilder::new()
1106 .homeserver_url(homeserver.uri())
1107 .cross_process_store_config(CrossProcessLockConfig::multi_process("foo"))
1108 .build()
1109 .await
1110 .unwrap();
1111
1112 assert_let!(
1113 CrossProcessLockConfig::MultiProcess { holder_name } =
1114 client.cross_process_lock_config()
1115 );
1116 assert_eq!(holder_name, "foo");
1117 }
1118 }
1119}