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