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::{
25 collections::BTreeSet,
26 fmt,
27 sync::{Arc, RwLock as StdRwLock},
28};
29
30#[cfg(feature = "sqlite")]
31use futures_util::try_join;
32use homeserver_config::*;
33#[cfg(feature = "e2e-encryption")]
34use matrix_sdk_base::crypto::DecryptionSettings;
35#[cfg(feature = "e2e-encryption")]
36use matrix_sdk_base::crypto::{CollectStrategy, TrustRequirement};
37use matrix_sdk_base::{
38 BaseClient, DmRoomDefinition, ThreadingSupport, store::StoreConfig, ttl::TtlValue,
39};
40use matrix_sdk_common::cross_process_lock::CrossProcessLockConfig;
41#[cfg(feature = "sqlite")]
42use matrix_sdk_sqlite::SqliteStoreConfig;
43#[cfg(not(target_family = "wasm"))]
44use reqwest::Certificate;
45use ruma::{
46 OwnedServerName, ServerName,
47 api::{MatrixVersion, SupportedVersions, error::FromHttpResponseError},
48 presence::PresenceState,
49};
50use thiserror::Error;
51#[cfg(feature = "experimental-search")]
52use tokio::sync::Mutex;
53use tokio::sync::OnceCell;
54use tracing::{Span, debug, field::debug, instrument};
55
56use super::{Client, ClientInner};
57#[cfg(feature = "e2e-encryption")]
58use crate::encryption::EncryptionSettings;
59#[cfg(not(target_family = "wasm"))]
60use crate::http_client::HttpSettings;
61#[cfg(feature = "experimental-search")]
62use crate::search_index::SearchIndex;
63#[cfg(feature = "experimental-search")]
64use crate::search_index::SearchIndexStoreKind;
65use crate::{
66 HttpError, IdParseError,
67 authentication::AuthCtx,
68 client::caches::CachedValue::{Cached, NotSet},
69 config::RequestConfig,
70 error::RumaApiError,
71 http_client::HttpClient,
72 media::{DefaultMediaFetcher, MediaFetcher},
73 send_queue::SendQueueData,
74 sliding_sync::VersionBuilder as SlidingSyncVersionBuilder,
75};
76
77#[must_use]
116#[derive(Clone, Debug)]
117pub struct ClientBuilder {
118 homeserver_cfg: Option<HomeserverConfig>,
119 sliding_sync_version_builder: SlidingSyncVersionBuilder,
120 http_cfg: Option<HttpConfig>,
121 store_config: BuilderStoreConfig,
122 request_config: RequestConfig,
123 respect_login_well_known: bool,
124 server_versions: Option<BTreeSet<MatrixVersion>>,
125 handle_refresh_tokens: bool,
126 base_client: Option<BaseClient>,
127 #[cfg(feature = "e2e-encryption")]
128 encryption_settings: EncryptionSettings,
129 #[cfg(feature = "e2e-encryption")]
130 room_key_recipient_strategy: CollectStrategy,
131 #[cfg(feature = "e2e-encryption")]
132 decryption_settings: DecryptionSettings,
133 #[cfg(feature = "e2e-encryption")]
134 enable_share_history_on_invite: bool,
135 cross_process_lock_config: CrossProcessLockConfig,
136 threading_support: ThreadingSupport,
137 #[cfg(feature = "experimental-search")]
138 search_index_store_kind: SearchIndexStoreKind,
139 dm_room_definition: DmRoomDefinition,
140 media_fetcher: Arc<dyn MediaFetcher>,
141}
142
143impl ClientBuilder {
144 const DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME: &str = "main";
145
146 pub(crate) fn new() -> Self {
147 Self {
148 homeserver_cfg: None,
149 sliding_sync_version_builder: SlidingSyncVersionBuilder::Native,
150 http_cfg: None,
151 store_config: BuilderStoreConfig::Custom(StoreConfig::new(
152 CrossProcessLockConfig::multi_process(
153 Self::DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME,
154 ),
155 )),
156 request_config: Default::default(),
157 respect_login_well_known: true,
158 server_versions: None,
159 handle_refresh_tokens: false,
160 base_client: None,
161 #[cfg(feature = "e2e-encryption")]
162 encryption_settings: Default::default(),
163 #[cfg(feature = "e2e-encryption")]
164 room_key_recipient_strategy: Default::default(),
165 #[cfg(feature = "e2e-encryption")]
166 decryption_settings: DecryptionSettings {
167 sender_device_trust_requirement: TrustRequirement::Untrusted,
168 },
169 #[cfg(feature = "e2e-encryption")]
170 enable_share_history_on_invite: true,
171 cross_process_lock_config: CrossProcessLockConfig::MultiProcess {
172 holder_name: Self::DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME.to_owned(),
173 },
174 threading_support: ThreadingSupport::Disabled,
175 #[cfg(feature = "experimental-search")]
176 search_index_store_kind: SearchIndexStoreKind::InMemory,
177 dm_room_definition: DmRoomDefinition::MatrixSpec,
178 media_fetcher: Arc::new(DefaultMediaFetcher),
179 }
180 }
181
182 pub fn media_fetcher(mut self, media_fetcher: Arc<dyn MediaFetcher>) -> Self {
185 self.media_fetcher = media_fetcher.clone();
186 self
187 }
188
189 pub fn dm_room_definition(mut self, dm_room_definition: DmRoomDefinition) -> Self {
193 self.dm_room_definition = dm_room_definition;
194 self
195 }
196
197 pub fn homeserver_url(mut self, url: impl AsRef<str>) -> Self {
204 self.homeserver_cfg = Some(HomeserverConfig::HomeserverUrl(url.as_ref().to_owned()));
205 self
206 }
207
208 pub fn server_name(mut self, server_name: &ServerName) -> Self {
218 self.homeserver_cfg = Some(HomeserverConfig::ServerName {
219 server: server_name.to_owned(),
220 protocol: UrlScheme::Https,
222 });
223 self
224 }
225
226 pub fn insecure_server_name_no_tls(mut self, server_name: &ServerName) -> Self {
235 self.homeserver_cfg = Some(HomeserverConfig::ServerName {
236 server: server_name.to_owned(),
237 protocol: UrlScheme::Http,
238 });
239 self
240 }
241
242 pub fn server_name_or_homeserver_url(mut self, server_name_or_url: impl AsRef<str>) -> Self {
253 self.homeserver_cfg = Some(HomeserverConfig::ServerNameOrHomeserverUrl(
254 server_name_or_url.as_ref().to_owned(),
255 ));
256 self
257 }
258
259 pub fn sliding_sync_version_builder(
261 mut self,
262 version_builder: SlidingSyncVersionBuilder,
263 ) -> Self {
264 self.sliding_sync_version_builder = version_builder;
265 self
266 }
267
268 #[cfg(feature = "sqlite")]
270 pub fn sqlite_store(mut self, path: impl AsRef<Path>, passphrase: Option<&str>) -> Self {
271 let sqlite_store_config = SqliteStoreConfig::new(path).passphrase(passphrase);
272 self.store_config =
273 BuilderStoreConfig::Sqlite { config: sqlite_store_config, cache_path: None };
274
275 self
276 }
277
278 #[cfg(feature = "sqlite")]
281 pub fn sqlite_store_with_cache_path(
282 mut self,
283 path: impl AsRef<Path>,
284 cache_path: impl AsRef<Path>,
285 passphrase: Option<&str>,
286 ) -> Self {
287 let sqlite_store_config = SqliteStoreConfig::new(path).passphrase(passphrase);
288 self.store_config = BuilderStoreConfig::Sqlite {
289 config: sqlite_store_config,
290 cache_path: Some(cache_path.as_ref().to_owned()),
291 };
292
293 self
294 }
295
296 #[cfg(feature = "sqlite")]
299 pub fn sqlite_store_with_config_and_cache_path(
300 mut self,
301 config: SqliteStoreConfig,
302 cache_path: Option<impl AsRef<Path>>,
303 ) -> Self {
304 self.store_config = BuilderStoreConfig::Sqlite {
305 config,
306 cache_path: cache_path.map(|cache_path| cache_path.as_ref().to_owned()),
307 };
308
309 self
310 }
311
312 #[cfg(feature = "indexeddb")]
314 pub fn indexeddb_store(mut self, name: &str, passphrase: Option<&str>) -> Self {
315 self.store_config = BuilderStoreConfig::IndexedDb {
316 name: name.to_owned(),
317 passphrase: passphrase.map(ToOwned::to_owned),
318 };
319 self
320 }
321
322 pub fn store_config(mut self, store_config: StoreConfig) -> Self {
346 self.store_config = BuilderStoreConfig::Custom(store_config);
347 self
348 }
349
350 pub fn respect_login_well_known(mut self, value: bool) -> Self {
353 self.respect_login_well_known = value;
354 self
355 }
356
357 pub fn request_config(mut self, request_config: RequestConfig) -> Self {
359 self.request_config = request_config;
360 self
361 }
362
363 #[cfg(not(target_family = "wasm"))]
379 pub fn proxy(mut self, proxy: impl AsRef<str>) -> Self {
380 self.http_settings().proxy = Some(proxy.as_ref().to_owned());
381 self
382 }
383
384 #[cfg(not(target_family = "wasm"))]
386 pub fn disable_ssl_verification(mut self) -> Self {
387 self.http_settings().disable_ssl_verification = true;
388 self
389 }
390
391 #[cfg(not(target_family = "wasm"))]
393 pub fn user_agent(mut self, user_agent: impl AsRef<str>) -> Self {
394 self.http_settings().user_agent = Some(user_agent.as_ref().to_owned());
395 self
396 }
397
398 #[cfg(not(target_family = "wasm"))]
407 pub fn add_root_certificates(mut self, certificates: Vec<Certificate>) -> Self {
408 self.http_settings().additional_root_certificates = certificates;
409 self
410 }
411
412 #[cfg(not(target_family = "wasm"))]
416 pub fn disable_built_in_root_certificates(mut self) -> Self {
417 self.http_settings().disable_built_in_root_certificates = true;
418 self
419 }
420
421 pub fn http_client(mut self, client: reqwest::Client) -> Self {
431 self.http_cfg = Some(HttpConfig::Custom(client));
432 self
433 }
434
435 pub fn server_versions(mut self, value: impl IntoIterator<Item = MatrixVersion>) -> Self {
440 self.server_versions = Some(value.into_iter().collect());
441 self
442 }
443
444 #[cfg(not(target_family = "wasm"))]
445 fn http_settings(&mut self) -> &mut HttpSettings {
446 self.http_cfg.get_or_insert_with(Default::default).settings()
447 }
448
449 pub fn handle_refresh_tokens(mut self) -> Self {
471 self.handle_refresh_tokens = true;
472 self
473 }
474
475 #[doc(hidden)]
477 pub fn base_client(mut self, base_client: BaseClient) -> Self {
478 self.base_client = Some(base_client);
479 self
480 }
481
482 #[cfg(feature = "e2e-encryption")]
485 pub fn with_encryption_settings(mut self, settings: EncryptionSettings) -> Self {
486 self.encryption_settings = settings;
487 self
488 }
489
490 #[cfg(feature = "e2e-encryption")]
493 pub fn with_room_key_recipient_strategy(mut self, strategy: CollectStrategy) -> Self {
494 self.room_key_recipient_strategy = strategy;
495 self
496 }
497
498 #[cfg(feature = "e2e-encryption")]
500 pub fn with_decryption_settings(mut self, decryption_settings: DecryptionSettings) -> Self {
501 self.decryption_settings = decryption_settings;
502 self
503 }
504
505 #[cfg(feature = "e2e-encryption")]
513 pub fn with_enable_share_history_on_invite(
514 mut self,
515 enable_share_history_on_invite: bool,
516 ) -> Self {
517 self.enable_share_history_on_invite = enable_share_history_on_invite;
518 self
519 }
520
521 pub fn cross_process_store_config(
531 mut self,
532 cross_process_store_config: CrossProcessLockConfig,
533 ) -> Self {
534 self.cross_process_lock_config = cross_process_store_config;
535 self
536 }
537
538 pub fn with_threading_support(mut self, threading_support: ThreadingSupport) -> Self {
542 self.threading_support = threading_support;
543 self
544 }
545
546 #[cfg(feature = "experimental-search")]
548 pub fn search_index_store(mut self, kind: SearchIndexStoreKind) -> Self {
549 self.search_index_store_kind = kind;
550 self
551 }
552
553 #[instrument(skip_all, target = "matrix_sdk::client", fields(homeserver))]
566 pub async fn build(self) -> Result<Client, ClientBuildError> {
567 debug!("Starting to build the Client");
568
569 let homeserver_cfg = self.homeserver_cfg.ok_or(ClientBuildError::MissingHomeserver)?;
570 Span::current().record("homeserver", debug(&homeserver_cfg));
571
572 #[cfg_attr(target_family = "wasm", allow(clippy::infallible_destructuring_match))]
573 let inner_http_client = match self.http_cfg.unwrap_or_default() {
574 #[cfg(not(target_family = "wasm"))]
575 HttpConfig::Settings(mut settings) => {
576 settings.timeout = self.request_config.timeout;
577 settings.make_client()?
578 }
579 HttpConfig::Custom(c) => c,
580 };
581
582 let base_client = if let Some(base_client) = self.base_client {
583 base_client
584 } else {
585 #[allow(unused_mut)]
586 let mut client = BaseClient::new(
587 build_store_config(self.store_config, &self.cross_process_lock_config).await?,
588 self.threading_support,
589 self.dm_room_definition,
590 );
591
592 #[cfg(feature = "e2e-encryption")]
593 {
594 client.room_key_recipient_strategy = self.room_key_recipient_strategy;
595 client.decryption_settings = self.decryption_settings;
596 }
597
598 client
599 };
600
601 let http_client = HttpClient::new(inner_http_client.clone(), self.request_config);
602
603 #[allow(unused_variables)]
604 let HomeserverDiscoveryResult { server, homeserver, supported_versions, well_known } =
605 homeserver_cfg.discover(&http_client).await?;
606
607 let sliding_sync_version = {
608 let supported_versions = match supported_versions {
609 Some(versions) => Some(versions),
610 None if self.sliding_sync_version_builder.needs_get_supported_versions() => {
611 Some(get_supported_versions(&homeserver, &http_client).await?)
612 }
613 None => None,
614 };
615
616 let version = self.sliding_sync_version_builder.build(
617 supported_versions.map(|response| response.as_supported_versions()).as_ref(),
618 )?;
619
620 tracing::info!(?version, "selected sliding sync version");
621
622 version
623 };
624
625 let allow_insecure_oauth = homeserver.scheme() == "http";
626 let auth_ctx = Arc::new(AuthCtx::new(self.handle_refresh_tokens, allow_insecure_oauth));
627
628 let send_queue = Arc::new(SendQueueData::new(true));
630
631 let supported_versions = match self.server_versions {
632 Some(versions) => Cached(TtlValue::without_expiry(SupportedVersions {
633 versions,
634 features: Default::default(),
635 })),
636 None => NotSet,
637 };
638 let well_known = match well_known {
639 Some(well_known) => Cached(TtlValue::new(Some(well_known.into()))),
640 None => NotSet,
641 };
642
643 let event_cache = OnceCell::new();
644 let latest_events = OnceCell::new();
645 let thread_subscriptions_catchup = OnceCell::new();
646
647 #[cfg(feature = "experimental-search")]
648 let search_index =
649 SearchIndex::new(Arc::new(Mutex::new(HashMap::new())), self.search_index_store_kind);
650
651 let inner = ClientInner::new(
652 auth_ctx,
653 server,
654 homeserver,
655 sliding_sync_version,
656 Arc::new(StdRwLock::new(PresenceState::Online)),
657 http_client,
658 base_client,
659 supported_versions,
660 well_known,
661 self.respect_login_well_known,
662 event_cache,
663 send_queue,
664 latest_events,
665 #[cfg(feature = "e2e-encryption")]
666 self.encryption_settings,
667 #[cfg(feature = "e2e-encryption")]
668 self.enable_share_history_on_invite,
669 self.cross_process_lock_config,
670 #[cfg(feature = "experimental-search")]
671 search_index,
672 thread_subscriptions_catchup,
673 self.media_fetcher.clone(),
674 )
675 .await;
676
677 debug!("Done building the Client");
678
679 Ok(Client { inner })
680 }
681}
682
683pub fn sanitize_server_name(s: &str) -> crate::Result<OwnedServerName, IdParseError> {
687 ServerName::parse(
688 s.trim().trim_start_matches("http://").trim_start_matches("https://").trim_end_matches('/'),
689 )
690}
691
692#[allow(clippy::unused_async, unused)] async fn build_store_config(
694 builder_config: BuilderStoreConfig,
695 cross_process_store_config: &CrossProcessLockConfig,
696) -> Result<StoreConfig, ClientBuildError> {
697 #[allow(clippy::infallible_destructuring_match)]
698 let store_config = match builder_config {
699 #[cfg(feature = "sqlite")]
700 BuilderStoreConfig::Sqlite { config, cache_path } => {
701 let config_with_cache_path = if let Some(ref cache_path) = cache_path {
702 config.clone().path(cache_path)
703 } else {
704 config.clone()
705 };
706
707 #[cfg(feature = "e2e-encryption")]
708 let (state_store, event_cache_store, media_store, crypto_store) = try_join!(
709 matrix_sdk_sqlite::SqliteStateStore::open_with_config(&config),
710 matrix_sdk_sqlite::SqliteEventCacheStore::open_with_config(&config_with_cache_path),
711 matrix_sdk_sqlite::SqliteMediaStore::open_with_config(&config_with_cache_path),
712 matrix_sdk_sqlite::SqliteCryptoStore::open_with_config(&config),
713 )?;
714 #[cfg(not(feature = "e2e-encryption"))]
715 let (state_store, event_cache_store, media_store) = try_join!(
716 matrix_sdk_sqlite::SqliteStateStore::open_with_config(&config),
717 matrix_sdk_sqlite::SqliteEventCacheStore::open_with_config(&config_with_cache_path),
718 matrix_sdk_sqlite::SqliteMediaStore::open_with_config(&config),
719 )?;
720 let store_config = StoreConfig::new(cross_process_store_config.clone())
721 .state_store(state_store)
722 .event_cache_store(event_cache_store)
723 .media_store(media_store);
724
725 #[cfg(feature = "e2e-encryption")]
726 let store_config = store_config.crypto_store(crypto_store);
727
728 store_config
729 }
730
731 #[cfg(feature = "indexeddb")]
732 BuilderStoreConfig::IndexedDb { name, passphrase } => {
733 build_indexeddb_store_config(
734 &name,
735 passphrase.as_deref(),
736 cross_process_store_config.clone(),
737 )
738 .await?
739 }
740
741 BuilderStoreConfig::Custom(config) => config,
742 };
743 Ok(store_config)
744}
745
746#[cfg(all(target_family = "wasm", feature = "indexeddb"))]
749async fn build_indexeddb_store_config(
750 name: &str,
751 passphrase: Option<&str>,
752 cross_process_store_config: CrossProcessLockConfig,
753) -> Result<StoreConfig, ClientBuildError> {
754 let stores = matrix_sdk_indexeddb::IndexeddbStores::open(name, passphrase).await?;
755 let store_config = StoreConfig::new(cross_process_store_config)
756 .state_store(stores.state)
757 .event_cache_store(stores.event_cache)
758 .media_store(stores.media);
759
760 #[cfg(feature = "e2e-encryption")]
761 let store_config = store_config.crypto_store(stores.crypto);
762
763 Ok(store_config)
764}
765
766#[cfg(all(not(target_family = "wasm"), feature = "indexeddb"))]
767#[allow(clippy::unused_async)]
768async fn build_indexeddb_store_config(
769 _name: &str,
770 _passphrase: Option<&str>,
771 _cross_process_store_config: CrossProcessLockConfig,
772) -> Result<StoreConfig, ClientBuildError> {
773 panic!("the IndexedDB is only available on the 'wasm32' arch")
774}
775
776#[derive(Clone, Debug)]
777enum HttpConfig {
778 #[cfg(not(target_family = "wasm"))]
779 Settings(HttpSettings),
780 Custom(reqwest::Client),
781}
782
783#[cfg(not(target_family = "wasm"))]
784impl HttpConfig {
785 fn settings(&mut self) -> &mut HttpSettings {
786 match self {
787 Self::Settings(s) => s,
788 Self::Custom(_) => {
789 *self = Self::default();
790 match self {
791 Self::Settings(s) => s,
792 Self::Custom(_) => unreachable!(),
793 }
794 }
795 }
796 }
797}
798
799impl Default for HttpConfig {
800 fn default() -> Self {
801 #[cfg(not(target_family = "wasm"))]
802 return Self::Settings(HttpSettings::default());
803
804 #[cfg(target_family = "wasm")]
805 return Self::Custom(reqwest::Client::new());
806 }
807}
808
809#[derive(Clone)]
810enum BuilderStoreConfig {
811 #[cfg(feature = "sqlite")]
812 Sqlite {
813 config: SqliteStoreConfig,
814 cache_path: Option<PathBuf>,
815 },
816 #[cfg(feature = "indexeddb")]
817 IndexedDb {
818 name: String,
819 passphrase: Option<String>,
820 },
821 Custom(StoreConfig),
822}
823
824#[cfg(not(tarpaulin_include))]
825impl fmt::Debug for BuilderStoreConfig {
826 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
827 #[allow(clippy::infallible_destructuring_match)]
828 match self {
829 #[cfg(feature = "sqlite")]
830 Self::Sqlite { config, cache_path, .. } => f
831 .debug_struct("Sqlite")
832 .field("config", config)
833 .field("cache_path", cache_path)
834 .finish_non_exhaustive(),
835
836 #[cfg(feature = "indexeddb")]
837 Self::IndexedDb { name, .. } => {
838 f.debug_struct("IndexedDb").field("name", name).finish_non_exhaustive()
839 }
840
841 Self::Custom(store_config) => f.debug_tuple("Custom").field(store_config).finish(),
842 }
843 }
844}
845
846#[derive(Debug, Error)]
848pub enum ClientBuildError {
849 #[error("No homeserver or user ID was configured")]
851 MissingHomeserver,
852
853 #[error("The supplied server name is invalid")]
855 InvalidServerName,
856
857 #[error("Error looking up the .well-known endpoint on auto-discovery")]
859 AutoDiscovery(FromHttpResponseError<RumaApiError>),
860
861 #[error(transparent)]
863 SlidingSyncVersion(#[from] crate::sliding_sync::VersionBuilderError),
864
865 #[error(transparent)]
867 Url(#[from] url::ParseError),
868
869 #[error(transparent)]
871 Http(#[from] HttpError),
872
873 #[cfg(feature = "indexeddb")]
875 #[error(transparent)]
876 IndexeddbStore(#[from] matrix_sdk_indexeddb::OpenStoreError),
877
878 #[cfg(feature = "sqlite")]
880 #[error(transparent)]
881 SqliteStore(#[from] matrix_sdk_sqlite::OpenStoreError),
882}
883
884#[cfg(all(test, not(target_family = "wasm")))]
886pub(crate) mod tests {
887 use assert_matches::assert_matches;
888 use assert_matches2::assert_let;
889 use matrix_sdk_test::{async_test, test_json};
890 use serde_json::{Value as JsonValue, json_internal};
891 use wiremock::{
892 Mock, MockServer, ResponseTemplate,
893 matchers::{method, path},
894 };
895
896 use super::*;
897 use crate::sliding_sync::Version as SlidingSyncVersion;
898
899 #[test]
900 fn test_sanitize_server_name() {
901 assert_eq!(sanitize_server_name("matrix.org").unwrap().as_str(), "matrix.org");
902 assert_eq!(sanitize_server_name("https://matrix.org").unwrap().as_str(), "matrix.org");
903 assert_eq!(sanitize_server_name("http://matrix.org").unwrap().as_str(), "matrix.org");
904 assert_eq!(
905 sanitize_server_name("https://matrix.server.org").unwrap().as_str(),
906 "matrix.server.org"
907 );
908 assert_eq!(
909 sanitize_server_name("https://matrix.server.org/").unwrap().as_str(),
910 "matrix.server.org"
911 );
912 assert_eq!(
913 sanitize_server_name(" https://matrix.server.org// ").unwrap().as_str(),
914 "matrix.server.org"
915 );
916 assert_matches!(sanitize_server_name("https://matrix.server.org/something"), Err(_))
917 }
918
919 #[async_test]
926 async fn test_discovery_invalid_server() {
927 let mut builder = ClientBuilder::new();
929
930 builder = builder.server_name_or_homeserver_url("⚠️ This won't work 🚫");
932 let error = builder.build().await.unwrap_err();
933
934 assert_matches!(error, ClientBuildError::InvalidServerName);
936 }
937
938 #[async_test]
939 async fn test_discovery_no_server() {
940 let mut builder = ClientBuilder::new();
942
943 builder = builder.server_name_or_homeserver_url("localhost:3456");
945 let error = builder.build().await.unwrap_err();
946
947 println!("{error}");
949 assert_matches!(error, ClientBuildError::Http(_));
950 }
951
952 #[async_test]
953 async fn test_discovery_web_server() {
954 let server = MockServer::start().await;
957 let mut builder = ClientBuilder::new();
958
959 builder = builder.server_name_or_homeserver_url(server.uri());
961 let error = builder.build().await.unwrap_err();
962
963 assert_matches!(error, ClientBuildError::AutoDiscovery(FromHttpResponseError::Server(_)));
965 }
966
967 #[async_test]
968 async fn test_discovery_direct_legacy() {
969 let homeserver = make_mock_homeserver().await;
971 let mut builder = ClientBuilder::new();
972
973 builder = builder.server_name_or_homeserver_url(homeserver.uri());
975 let _client = builder.build().await.unwrap();
976
977 assert!(_client.sliding_sync_version().is_native());
979 }
980
981 #[async_test]
982 async fn test_discovery_well_known_parse_error() {
983 let server = MockServer::start().await;
985 let homeserver = make_mock_homeserver().await;
986 let mut builder = ClientBuilder::new();
987
988 let well_known = make_well_known_json(&homeserver.uri());
989 let bad_json = well_known.to_string().replace(',', "");
990 Mock::given(method("GET"))
991 .and(path("/.well-known/matrix/client"))
992 .respond_with(ResponseTemplate::new(200).set_body_json(bad_json))
993 .mount(&server)
994 .await;
995
996 builder = builder.server_name_or_homeserver_url(server.uri());
998 let error = builder.build().await.unwrap_err();
999
1000 assert_matches!(
1002 error,
1003 ClientBuildError::AutoDiscovery(FromHttpResponseError::Deserialization(_))
1004 );
1005 }
1006
1007 #[async_test]
1008 async fn test_discovery_well_known_legacy() {
1009 let server = MockServer::start().await;
1012 let homeserver = make_mock_homeserver().await;
1013 let mut builder = ClientBuilder::new();
1014
1015 Mock::given(method("GET"))
1016 .and(path("/.well-known/matrix/client"))
1017 .respond_with(
1018 ResponseTemplate::new(200).set_body_json(make_well_known_json(&homeserver.uri())),
1019 )
1020 .mount(&server)
1021 .await;
1022
1023 builder = builder.server_name_or_homeserver_url(server.uri());
1025 let client = builder.build().await.unwrap();
1026
1027 assert!(client.sliding_sync_version().is_native());
1030 }
1031
1032 #[async_test]
1033 async fn test_sliding_sync_discover_native() {
1034 let homeserver = make_mock_homeserver().await;
1036 let mut builder = ClientBuilder::new();
1037
1038 builder = builder
1041 .server_name_or_homeserver_url(homeserver.uri())
1042 .sliding_sync_version_builder(SlidingSyncVersionBuilder::DiscoverNative);
1043
1044 let client = builder.build().await.unwrap();
1045
1046 assert_matches!(client.sliding_sync_version(), SlidingSyncVersion::Native);
1048 }
1049
1050 #[async_test]
1051 #[cfg(feature = "e2e-encryption")]
1052 async fn test_set_up_decryption_trust_requirement_cross_signed() {
1053 let homeserver = make_mock_homeserver().await;
1054 let builder = ClientBuilder::new()
1055 .server_name_or_homeserver_url(homeserver.uri())
1056 .with_decryption_settings(DecryptionSettings {
1057 sender_device_trust_requirement: TrustRequirement::CrossSigned,
1058 });
1059
1060 let client = builder.build().await.unwrap();
1061 assert_matches!(
1062 client.base_client().decryption_settings.sender_device_trust_requirement,
1063 TrustRequirement::CrossSigned
1064 );
1065 }
1066
1067 #[async_test]
1068 #[cfg(feature = "e2e-encryption")]
1069 async fn test_set_up_decryption_trust_requirement_untrusted() {
1070 let homeserver = make_mock_homeserver().await;
1071
1072 let builder = ClientBuilder::new()
1073 .server_name_or_homeserver_url(homeserver.uri())
1074 .with_decryption_settings(DecryptionSettings {
1075 sender_device_trust_requirement: TrustRequirement::Untrusted,
1076 });
1077
1078 let client = builder.build().await.unwrap();
1079 assert_matches!(
1080 client.base_client().decryption_settings.sender_device_trust_requirement,
1081 TrustRequirement::Untrusted
1082 );
1083 }
1084
1085 async fn make_mock_homeserver() -> MockServer {
1088 let homeserver = MockServer::start().await;
1089 Mock::given(method("GET"))
1090 .and(path("/_matrix/client/versions"))
1091 .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::VERSIONS))
1092 .mount(&homeserver)
1093 .await;
1094 Mock::given(method("GET"))
1095 .and(path("/_matrix/client/r0/login"))
1096 .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::LOGIN_TYPES))
1097 .mount(&homeserver)
1098 .await;
1099 homeserver
1100 }
1101
1102 fn make_well_known_json(homeserver_url: &str) -> JsonValue {
1103 ::serde_json::Value::Object({
1104 let mut object = ::serde_json::Map::new();
1105 let _ = object.insert(
1106 "m.homeserver".into(),
1107 json_internal!({
1108 "base_url": homeserver_url
1109 }),
1110 );
1111
1112 object
1113 })
1114 }
1115
1116 #[async_test]
1117 async fn test_cross_process_store_locks_holder_name() {
1118 {
1119 let homeserver = make_mock_homeserver().await;
1120 let client =
1121 ClientBuilder::new().homeserver_url(homeserver.uri()).build().await.unwrap();
1122
1123 assert_let!(
1124 CrossProcessLockConfig::MultiProcess { holder_name } =
1125 client.cross_process_lock_config()
1126 );
1127 assert_eq!(holder_name, "main");
1128 }
1129
1130 {
1131 let homeserver = make_mock_homeserver().await;
1132 let client = ClientBuilder::new()
1133 .homeserver_url(homeserver.uri())
1134 .cross_process_store_config(CrossProcessLockConfig::multi_process("foo"))
1135 .build()
1136 .await
1137 .unwrap();
1138
1139 assert_let!(
1140 CrossProcessLockConfig::MultiProcess { holder_name } =
1141 client.cross_process_lock_config()
1142 );
1143 assert_eq!(holder_name, "foo");
1144 }
1145 }
1146}