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 = "experimental-x509-identity-verification")]
36use matrix_sdk_base::crypto::x509::{RawX509Signer, RawX509Verifier};
37#[cfg(feature = "e2e-encryption")]
38use matrix_sdk_base::crypto::{CollectStrategy, TrustRequirement};
39use matrix_sdk_base::{
40 BaseClient, DmRoomDefinition, ThreadingSupport, store::StoreConfig, ttl::TtlValue,
41};
42use matrix_sdk_common::cross_process_lock::CrossProcessLockConfig;
43#[cfg(feature = "sqlite")]
44use matrix_sdk_sqlite::SqliteStoreConfig;
45#[cfg(not(target_family = "wasm"))]
46use reqwest::Certificate;
47use ruma::{
48 OwnedServerName, ServerName,
49 api::{MatrixVersion, SupportedVersions, error::FromHttpResponseError},
50 presence::PresenceState,
51};
52use thiserror::Error;
53#[cfg(feature = "experimental-search")]
54use tokio::sync::Mutex;
55use tokio::sync::OnceCell;
56use tracing::{Span, debug, field::debug, instrument};
57
58use super::{Client, ClientInner};
59#[cfg(feature = "e2e-encryption")]
60use crate::encryption::EncryptionSettings;
61#[cfg(not(target_family = "wasm"))]
62use crate::http_client::HttpSettings;
63#[cfg(feature = "experimental-search")]
64use crate::search_index::SearchIndex;
65#[cfg(feature = "experimental-search")]
66use crate::search_index::SearchIndexStoreKind;
67use crate::{
68 HttpError, IdParseError,
69 authentication::AuthCtx,
70 client::caches::CachedValue::{Cached, NotSet},
71 config::RequestConfig,
72 error::RumaApiError,
73 http_client::HttpClient,
74 media::{DefaultMediaFetcher, MediaFetcher},
75 send_queue::SendQueueData,
76 sliding_sync::VersionBuilder as SlidingSyncVersionBuilder,
77};
78
79#[must_use]
118#[derive(Clone, Debug)]
119pub struct ClientBuilder {
120 homeserver_cfg: Option<HomeserverConfig>,
121 sliding_sync_version_builder: SlidingSyncVersionBuilder,
122 http_cfg: Option<HttpConfig>,
123 store_config: BuilderStoreConfig,
124 request_config: RequestConfig,
125 respect_login_well_known: bool,
126 server_versions: Option<BTreeSet<MatrixVersion>>,
127 handle_refresh_tokens: bool,
128 base_client: Option<BaseClient>,
129 #[cfg(feature = "e2e-encryption")]
130 encryption_settings: EncryptionSettings,
131 #[cfg(feature = "e2e-encryption")]
132 room_key_recipient_strategy: CollectStrategy,
133 #[cfg(feature = "e2e-encryption")]
134 decryption_settings: DecryptionSettings,
135 #[cfg(feature = "e2e-encryption")]
136 enable_share_history_on_invite: bool,
137 cross_process_lock_config: CrossProcessLockConfig,
138 threading_support: ThreadingSupport,
139 #[cfg(feature = "experimental-search")]
140 search_index_store_kind: SearchIndexStoreKind,
141 #[cfg(feature = "experimental-x509-identity-verification")]
142 x509_signer: Option<Arc<dyn RawX509Signer>>,
143 #[cfg(feature = "experimental-x509-identity-verification")]
144 x509_verifier: Option<Arc<dyn RawX509Verifier>>,
145 dm_room_definition: DmRoomDefinition,
146 media_fetcher: Arc<dyn MediaFetcher>,
147}
148
149impl ClientBuilder {
150 const DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME: &str = "main";
151
152 pub(crate) fn new() -> Self {
153 Self {
154 homeserver_cfg: None,
155 sliding_sync_version_builder: SlidingSyncVersionBuilder::Native,
156 http_cfg: None,
157 store_config: BuilderStoreConfig::Custom(StoreConfig::new(
158 CrossProcessLockConfig::multi_process(
159 Self::DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME,
160 ),
161 )),
162 request_config: Default::default(),
163 respect_login_well_known: true,
164 server_versions: None,
165 handle_refresh_tokens: false,
166 base_client: None,
167 #[cfg(feature = "e2e-encryption")]
168 encryption_settings: Default::default(),
169 #[cfg(feature = "e2e-encryption")]
170 room_key_recipient_strategy: Default::default(),
171 #[cfg(feature = "e2e-encryption")]
172 decryption_settings: DecryptionSettings {
173 sender_device_trust_requirement: TrustRequirement::Untrusted,
174 },
175 #[cfg(feature = "e2e-encryption")]
176 enable_share_history_on_invite: true,
177 cross_process_lock_config: CrossProcessLockConfig::MultiProcess {
178 holder_name: Self::DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME.to_owned(),
179 },
180 threading_support: ThreadingSupport::Disabled,
181 #[cfg(feature = "experimental-search")]
182 search_index_store_kind: SearchIndexStoreKind::InMemory,
183 #[cfg(feature = "experimental-x509-identity-verification")]
184 x509_signer: None,
185 #[cfg(feature = "experimental-x509-identity-verification")]
186 x509_verifier: None,
187 dm_room_definition: DmRoomDefinition::MatrixSpec,
188 media_fetcher: Arc::new(DefaultMediaFetcher),
189 }
190 }
191
192 pub fn media_fetcher(mut self, media_fetcher: Arc<dyn MediaFetcher>) -> Self {
195 self.media_fetcher = media_fetcher.clone();
196 self
197 }
198
199 pub fn dm_room_definition(mut self, dm_room_definition: DmRoomDefinition) -> Self {
203 self.dm_room_definition = dm_room_definition;
204 self
205 }
206
207 pub fn homeserver_url(mut self, url: impl AsRef<str>) -> Self {
214 self.homeserver_cfg = Some(HomeserverConfig::HomeserverUrl(url.as_ref().to_owned()));
215 self
216 }
217
218 pub fn server_name(mut self, server_name: &ServerName) -> Self {
228 self.homeserver_cfg = Some(HomeserverConfig::ServerName {
229 server: server_name.to_owned(),
230 protocol: UrlScheme::Https,
232 });
233 self
234 }
235
236 pub fn insecure_server_name_no_tls(mut self, server_name: &ServerName) -> Self {
245 self.homeserver_cfg = Some(HomeserverConfig::ServerName {
246 server: server_name.to_owned(),
247 protocol: UrlScheme::Http,
248 });
249 self
250 }
251
252 pub fn server_name_or_homeserver_url(mut self, server_name_or_url: impl AsRef<str>) -> Self {
263 self.homeserver_cfg = Some(HomeserverConfig::ServerNameOrHomeserverUrl(
264 server_name_or_url.as_ref().to_owned(),
265 ));
266 self
267 }
268
269 pub fn sliding_sync_version_builder(
271 mut self,
272 version_builder: SlidingSyncVersionBuilder,
273 ) -> Self {
274 self.sliding_sync_version_builder = version_builder;
275 self
276 }
277
278 #[cfg(feature = "sqlite")]
280 pub fn sqlite_store(mut self, path: impl AsRef<Path>, passphrase: Option<&str>) -> Self {
281 let sqlite_store_config = SqliteStoreConfig::new(path).passphrase(passphrase);
282 self.store_config =
283 BuilderStoreConfig::Sqlite { config: sqlite_store_config, cache_path: None };
284
285 self
286 }
287
288 #[cfg(feature = "sqlite")]
291 pub fn sqlite_store_with_cache_path(
292 mut self,
293 path: impl AsRef<Path>,
294 cache_path: impl AsRef<Path>,
295 passphrase: Option<&str>,
296 ) -> Self {
297 let sqlite_store_config = SqliteStoreConfig::new(path).passphrase(passphrase);
298 self.store_config = BuilderStoreConfig::Sqlite {
299 config: sqlite_store_config,
300 cache_path: Some(cache_path.as_ref().to_owned()),
301 };
302
303 self
304 }
305
306 #[cfg(feature = "sqlite")]
309 pub fn sqlite_store_with_config_and_cache_path(
310 mut self,
311 config: SqliteStoreConfig,
312 cache_path: Option<impl AsRef<Path>>,
313 ) -> Self {
314 self.store_config = BuilderStoreConfig::Sqlite {
315 config,
316 cache_path: cache_path.map(|cache_path| cache_path.as_ref().to_owned()),
317 };
318
319 self
320 }
321
322 #[cfg(feature = "indexeddb")]
324 pub fn indexeddb_store(mut self, name: &str, passphrase: Option<&str>) -> Self {
325 self.store_config = BuilderStoreConfig::IndexedDb {
326 name: name.to_owned(),
327 passphrase: passphrase.map(ToOwned::to_owned),
328 };
329 self
330 }
331
332 pub fn store_config(mut self, store_config: StoreConfig) -> Self {
356 self.store_config = BuilderStoreConfig::Custom(store_config);
357 self
358 }
359
360 pub fn respect_login_well_known(mut self, value: bool) -> Self {
363 self.respect_login_well_known = value;
364 self
365 }
366
367 pub fn request_config(mut self, request_config: RequestConfig) -> Self {
369 self.request_config = request_config;
370 self
371 }
372
373 #[cfg(not(target_family = "wasm"))]
389 pub fn proxy(mut self, proxy: impl AsRef<str>) -> Self {
390 self.http_settings().proxy = Some(proxy.as_ref().to_owned());
391 self
392 }
393
394 #[cfg(not(target_family = "wasm"))]
396 pub fn disable_ssl_verification(mut self) -> Self {
397 self.http_settings().disable_ssl_verification = true;
398 self
399 }
400
401 #[cfg(not(target_family = "wasm"))]
403 pub fn user_agent(mut self, user_agent: impl AsRef<str>) -> Self {
404 self.http_settings().user_agent = Some(user_agent.as_ref().to_owned());
405 self
406 }
407
408 #[cfg(not(target_family = "wasm"))]
417 pub fn add_root_certificates(mut self, certificates: Vec<Certificate>) -> Self {
418 self.http_settings().additional_root_certificates = certificates;
419 self
420 }
421
422 #[cfg(not(target_family = "wasm"))]
426 pub fn disable_built_in_root_certificates(mut self) -> Self {
427 self.http_settings().disable_built_in_root_certificates = true;
428 self
429 }
430
431 pub fn http_client(mut self, client: reqwest::Client) -> Self {
441 self.http_cfg = Some(HttpConfig::Custom(client));
442 self
443 }
444
445 pub fn server_versions(mut self, value: impl IntoIterator<Item = MatrixVersion>) -> Self {
450 self.server_versions = Some(value.into_iter().collect());
451 self
452 }
453
454 #[cfg(not(target_family = "wasm"))]
455 fn http_settings(&mut self) -> &mut HttpSettings {
456 self.http_cfg.get_or_insert_with(Default::default).settings()
457 }
458
459 pub fn handle_refresh_tokens(mut self) -> Self {
481 self.handle_refresh_tokens = true;
482 self
483 }
484
485 #[doc(hidden)]
487 pub fn base_client(mut self, base_client: BaseClient) -> Self {
488 self.base_client = Some(base_client);
489 self
490 }
491
492 #[cfg(feature = "e2e-encryption")]
495 pub fn with_encryption_settings(mut self, settings: EncryptionSettings) -> Self {
496 self.encryption_settings = settings;
497 self
498 }
499
500 #[cfg(feature = "e2e-encryption")]
503 pub fn with_room_key_recipient_strategy(mut self, strategy: CollectStrategy) -> Self {
504 self.room_key_recipient_strategy = strategy;
505 self
506 }
507
508 #[cfg(feature = "e2e-encryption")]
510 pub fn with_decryption_settings(mut self, decryption_settings: DecryptionSettings) -> Self {
511 self.decryption_settings = decryption_settings;
512 self
513 }
514
515 #[cfg(feature = "e2e-encryption")]
523 pub fn with_enable_share_history_on_invite(
524 mut self,
525 enable_share_history_on_invite: bool,
526 ) -> Self {
527 self.enable_share_history_on_invite = enable_share_history_on_invite;
528 self
529 }
530
531 pub fn cross_process_store_config(
541 mut self,
542 cross_process_store_config: CrossProcessLockConfig,
543 ) -> Self {
544 self.cross_process_lock_config = cross_process_store_config;
545 self
546 }
547
548 pub fn with_threading_support(mut self, threading_support: ThreadingSupport) -> Self {
552 self.threading_support = threading_support;
553 self
554 }
555
556 #[cfg(feature = "experimental-search")]
558 pub fn search_index_store(mut self, kind: SearchIndexStoreKind) -> Self {
559 self.search_index_store_kind = kind;
560 self
561 }
562
563 #[cfg(feature = "experimental-x509-identity-verification")]
566 pub fn with_x509_signer(mut self, x509_signer: Option<Arc<dyn RawX509Signer>>) -> Self {
567 self.x509_signer = x509_signer;
568 self
569 }
570
571 #[cfg(feature = "experimental-x509-identity-verification")]
574 pub fn with_x509_verifier(mut self, x509_verifier: Option<Arc<dyn RawX509Verifier>>) -> Self {
575 self.x509_verifier = x509_verifier;
576 self
577 }
578
579 #[instrument(skip_all, target = "matrix_sdk::client", fields(homeserver))]
592 pub async fn build(self) -> Result<Client, ClientBuildError> {
593 debug!("Starting to build the Client");
594
595 let homeserver_cfg = self.homeserver_cfg.ok_or(ClientBuildError::MissingHomeserver)?;
596 Span::current().record("homeserver", debug(&homeserver_cfg));
597
598 #[cfg_attr(target_family = "wasm", allow(clippy::infallible_destructuring_match))]
599 let inner_http_client = match self.http_cfg.unwrap_or_default() {
600 #[cfg(not(target_family = "wasm"))]
601 HttpConfig::Settings(mut settings) => {
602 settings.timeout = self.request_config.timeout;
603 settings.make_client()?
604 }
605 HttpConfig::Custom(c) => c,
606 };
607
608 let base_client = if let Some(base_client) = self.base_client {
609 base_client
610 } else {
611 #[allow(unused_mut)]
612 let mut client = BaseClient::new(
613 build_store_config(self.store_config, &self.cross_process_lock_config).await?,
614 self.threading_support,
615 self.dm_room_definition,
616 );
617
618 #[cfg(feature = "e2e-encryption")]
619 {
620 client.room_key_recipient_strategy = self.room_key_recipient_strategy;
621 client.decryption_settings = self.decryption_settings;
622 }
623
624 #[cfg(feature = "experimental-x509-identity-verification")]
625 client.set_x509_signer(self.x509_signer);
626 #[cfg(feature = "experimental-x509-identity-verification")]
627 client.set_x509_verifier(self.x509_verifier);
628
629 client
630 };
631
632 let http_client = HttpClient::new(inner_http_client.clone(), self.request_config);
633
634 #[allow(unused_variables)]
635 let HomeserverDiscoveryResult { server, homeserver, supported_versions, well_known } =
636 homeserver_cfg.discover(&http_client).await?;
637
638 let sliding_sync_version = {
639 let supported_versions = match supported_versions {
640 Some(versions) => Some(versions),
641 None if self.sliding_sync_version_builder.needs_get_supported_versions() => {
642 Some(get_supported_versions(&homeserver, &http_client).await?)
643 }
644 None => None,
645 };
646
647 let version = self.sliding_sync_version_builder.build(
648 supported_versions.map(|response| response.as_supported_versions()).as_ref(),
649 )?;
650
651 tracing::info!(?version, "selected sliding sync version");
652
653 version
654 };
655
656 let allow_insecure_oauth = homeserver.scheme() == "http";
657 let auth_ctx = Arc::new(AuthCtx::new(self.handle_refresh_tokens, allow_insecure_oauth));
658
659 let send_queue = Arc::new(SendQueueData::new(true));
661
662 let supported_versions = match self.server_versions {
663 Some(versions) => Cached(TtlValue::without_expiry(SupportedVersions {
664 versions,
665 features: Default::default(),
666 })),
667 None => NotSet,
668 };
669 let well_known = match well_known {
670 Some(well_known) => Cached(TtlValue::new(Some(well_known.into()))),
671 None => NotSet,
672 };
673
674 let event_cache = OnceCell::new();
675 let latest_events = OnceCell::new();
676 let thread_subscriptions_catchup = OnceCell::new();
677
678 #[cfg(feature = "experimental-search")]
679 let search_index =
680 SearchIndex::new(Arc::new(Mutex::new(HashMap::new())), self.search_index_store_kind);
681
682 let inner = ClientInner::new(
683 auth_ctx,
684 server,
685 homeserver,
686 sliding_sync_version,
687 Arc::new(StdRwLock::new(PresenceState::Online)),
688 http_client,
689 base_client,
690 supported_versions,
691 well_known,
692 self.respect_login_well_known,
693 event_cache,
694 send_queue,
695 latest_events,
696 #[cfg(feature = "e2e-encryption")]
697 self.encryption_settings,
698 #[cfg(feature = "e2e-encryption")]
699 self.enable_share_history_on_invite,
700 self.cross_process_lock_config,
701 #[cfg(feature = "experimental-search")]
702 search_index,
703 thread_subscriptions_catchup,
704 self.media_fetcher.clone(),
705 )
706 .await;
707
708 debug!("Done building the Client");
709
710 Ok(Client { inner })
711 }
712}
713
714pub fn sanitize_server_name(s: &str) -> crate::Result<OwnedServerName, IdParseError> {
718 ServerName::parse(
719 s.trim().trim_start_matches("http://").trim_start_matches("https://").trim_end_matches('/'),
720 )
721}
722
723#[allow(clippy::unused_async, unused)] async fn build_store_config(
725 builder_config: BuilderStoreConfig,
726 cross_process_store_config: &CrossProcessLockConfig,
727) -> Result<StoreConfig, ClientBuildError> {
728 #[allow(clippy::infallible_destructuring_match)]
729 let store_config = match builder_config {
730 #[cfg(feature = "sqlite")]
731 BuilderStoreConfig::Sqlite { config, cache_path } => {
732 let config_with_cache_path = if let Some(ref cache_path) = cache_path {
733 config.clone().path(cache_path)
734 } else {
735 config.clone()
736 };
737
738 #[cfg(feature = "e2e-encryption")]
739 let (state_store, event_cache_store, media_store, crypto_store) = try_join!(
740 matrix_sdk_sqlite::SqliteStateStore::open_with_config(&config),
741 matrix_sdk_sqlite::SqliteEventCacheStore::open_with_config(&config_with_cache_path),
742 matrix_sdk_sqlite::SqliteMediaStore::open_with_config(&config_with_cache_path),
743 matrix_sdk_sqlite::SqliteCryptoStore::open_with_config(&config),
744 )?;
745 #[cfg(not(feature = "e2e-encryption"))]
746 let (state_store, event_cache_store, media_store) = try_join!(
747 matrix_sdk_sqlite::SqliteStateStore::open_with_config(&config),
748 matrix_sdk_sqlite::SqliteEventCacheStore::open_with_config(&config_with_cache_path),
749 matrix_sdk_sqlite::SqliteMediaStore::open_with_config(&config),
750 )?;
751 let store_config = StoreConfig::new(cross_process_store_config.clone())
752 .state_store(state_store)
753 .event_cache_store(event_cache_store)
754 .media_store(media_store);
755
756 #[cfg(feature = "e2e-encryption")]
757 let store_config = store_config.crypto_store(crypto_store);
758
759 store_config
760 }
761
762 #[cfg(feature = "indexeddb")]
763 BuilderStoreConfig::IndexedDb { name, passphrase } => {
764 build_indexeddb_store_config(
765 &name,
766 passphrase.as_deref(),
767 cross_process_store_config.clone(),
768 )
769 .await?
770 }
771
772 BuilderStoreConfig::Custom(config) => config,
773 };
774 Ok(store_config)
775}
776
777#[cfg(all(target_family = "wasm", feature = "indexeddb"))]
780async fn build_indexeddb_store_config(
781 name: &str,
782 passphrase: Option<&str>,
783 cross_process_store_config: CrossProcessLockConfig,
784) -> Result<StoreConfig, ClientBuildError> {
785 let stores = matrix_sdk_indexeddb::IndexeddbStores::open(name, passphrase).await?;
786 let store_config = StoreConfig::new(cross_process_store_config)
787 .state_store(stores.state)
788 .event_cache_store(stores.event_cache)
789 .media_store(stores.media);
790
791 #[cfg(feature = "e2e-encryption")]
792 let store_config = store_config.crypto_store(stores.crypto);
793
794 Ok(store_config)
795}
796
797#[cfg(all(not(target_family = "wasm"), feature = "indexeddb"))]
798#[allow(clippy::unused_async)]
799async fn build_indexeddb_store_config(
800 _name: &str,
801 _passphrase: Option<&str>,
802 _cross_process_store_config: CrossProcessLockConfig,
803) -> Result<StoreConfig, ClientBuildError> {
804 panic!("the IndexedDB is only available on the 'wasm32' arch")
805}
806
807#[derive(Clone, Debug)]
808enum HttpConfig {
809 #[cfg(not(target_family = "wasm"))]
810 Settings(HttpSettings),
811 Custom(reqwest::Client),
812}
813
814#[cfg(not(target_family = "wasm"))]
815impl HttpConfig {
816 fn settings(&mut self) -> &mut HttpSettings {
817 match self {
818 Self::Settings(s) => s,
819 Self::Custom(_) => {
820 *self = Self::default();
821 match self {
822 Self::Settings(s) => s,
823 Self::Custom(_) => unreachable!(),
824 }
825 }
826 }
827 }
828}
829
830impl Default for HttpConfig {
831 fn default() -> Self {
832 #[cfg(not(target_family = "wasm"))]
833 return Self::Settings(HttpSettings::default());
834
835 #[cfg(target_family = "wasm")]
836 return Self::Custom(reqwest::Client::new());
837 }
838}
839
840#[derive(Clone)]
841enum BuilderStoreConfig {
842 #[cfg(feature = "sqlite")]
843 Sqlite {
844 config: SqliteStoreConfig,
845 cache_path: Option<PathBuf>,
846 },
847 #[cfg(feature = "indexeddb")]
848 IndexedDb {
849 name: String,
850 passphrase: Option<String>,
851 },
852 Custom(StoreConfig),
853}
854
855#[cfg(not(tarpaulin_include))]
856impl fmt::Debug for BuilderStoreConfig {
857 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
858 #[allow(clippy::infallible_destructuring_match)]
859 match self {
860 #[cfg(feature = "sqlite")]
861 Self::Sqlite { config, cache_path, .. } => f
862 .debug_struct("Sqlite")
863 .field("config", config)
864 .field("cache_path", cache_path)
865 .finish_non_exhaustive(),
866
867 #[cfg(feature = "indexeddb")]
868 Self::IndexedDb { name, .. } => {
869 f.debug_struct("IndexedDb").field("name", name).finish_non_exhaustive()
870 }
871
872 Self::Custom(store_config) => f.debug_tuple("Custom").field(store_config).finish(),
873 }
874 }
875}
876
877#[derive(Debug, Error)]
879pub enum ClientBuildError {
880 #[error("No homeserver or user ID was configured")]
882 MissingHomeserver,
883
884 #[error("The supplied server name is invalid")]
886 InvalidServerName,
887
888 #[error("Error looking up the .well-known endpoint on auto-discovery")]
890 AutoDiscovery(Box<FromHttpResponseError<RumaApiError>>),
891
892 #[error(transparent)]
894 SlidingSyncVersion(#[from] crate::sliding_sync::VersionBuilderError),
895
896 #[error(transparent)]
898 Url(#[from] url::ParseError),
899
900 #[error(transparent)]
902 Http(#[from] HttpError),
903
904 #[cfg(feature = "indexeddb")]
906 #[error(transparent)]
907 IndexeddbStore(#[from] matrix_sdk_indexeddb::OpenStoreError),
908
909 #[cfg(feature = "sqlite")]
911 #[error(transparent)]
912 SqliteStore(#[from] matrix_sdk_sqlite::OpenStoreError),
913}
914
915#[cfg(all(test, not(target_family = "wasm")))]
917pub(crate) mod tests {
918 use assert_matches::assert_matches;
919 use assert_matches2::assert_let;
920 use matrix_sdk_test::{async_test, test_json};
921 use serde_json::{Value as JsonValue, json_internal};
922 use wiremock::{
923 Mock, MockServer, ResponseTemplate,
924 matchers::{method, path},
925 };
926
927 use super::*;
928 use crate::sliding_sync::Version as SlidingSyncVersion;
929
930 #[test]
931 fn test_sanitize_server_name() {
932 assert_eq!(sanitize_server_name("matrix.org").unwrap().as_str(), "matrix.org");
933 assert_eq!(sanitize_server_name("https://matrix.org").unwrap().as_str(), "matrix.org");
934 assert_eq!(sanitize_server_name("http://matrix.org").unwrap().as_str(), "matrix.org");
935 assert_eq!(
936 sanitize_server_name("https://matrix.server.org").unwrap().as_str(),
937 "matrix.server.org"
938 );
939 assert_eq!(
940 sanitize_server_name("https://matrix.server.org/").unwrap().as_str(),
941 "matrix.server.org"
942 );
943 assert_eq!(
944 sanitize_server_name(" https://matrix.server.org// ").unwrap().as_str(),
945 "matrix.server.org"
946 );
947 assert_matches!(sanitize_server_name("https://matrix.server.org/something"), Err(_))
948 }
949
950 #[async_test]
957 async fn test_discovery_invalid_server() {
958 let mut builder = ClientBuilder::new();
960
961 builder = builder.server_name_or_homeserver_url("⚠️ This won't work 🚫");
963 let error = builder.build().await.unwrap_err();
964
965 assert_matches!(error, ClientBuildError::InvalidServerName);
967 }
968
969 #[async_test]
970 async fn test_discovery_no_server() {
971 let mut builder = ClientBuilder::new();
973
974 builder = builder.server_name_or_homeserver_url("localhost:3456");
976 let error = builder.build().await.unwrap_err();
977
978 println!("{error}");
980 assert_matches!(error, ClientBuildError::Http(_));
981 }
982
983 #[async_test]
984 async fn test_discovery_web_server() {
985 let server = MockServer::start().await;
988 let mut builder = ClientBuilder::new();
989
990 builder = builder.server_name_or_homeserver_url(server.uri());
992 let error = builder.build().await.unwrap_err();
993
994 assert_let!(ClientBuildError::AutoDiscovery(e) = error);
996 assert_matches!(*e, FromHttpResponseError::Server(_));
997 }
998
999 #[async_test]
1000 async fn test_discovery_direct_legacy() {
1001 let homeserver = make_mock_homeserver().await;
1003 let mut builder = ClientBuilder::new();
1004
1005 builder = builder.server_name_or_homeserver_url(homeserver.uri());
1007 let _client = builder.build().await.unwrap();
1008
1009 assert!(_client.sliding_sync_version().is_native());
1011 }
1012
1013 #[async_test]
1014 async fn test_discovery_well_known_parse_error() {
1015 let server = MockServer::start().await;
1017 let homeserver = make_mock_homeserver().await;
1018 let mut builder = ClientBuilder::new();
1019
1020 let well_known = make_well_known_json(&homeserver.uri());
1021 let bad_json = well_known.to_string().replace(',', "");
1022 Mock::given(method("GET"))
1023 .and(path("/.well-known/matrix/client"))
1024 .respond_with(ResponseTemplate::new(200).set_body_json(bad_json))
1025 .mount(&server)
1026 .await;
1027
1028 builder = builder.server_name_or_homeserver_url(server.uri());
1030 let error = builder.build().await.unwrap_err();
1031
1032 assert_let!(ClientBuildError::AutoDiscovery(e) = error);
1034 assert_matches!(*e, FromHttpResponseError::Deserialization(_));
1035 }
1036
1037 #[async_test]
1038 async fn test_discovery_well_known_legacy() {
1039 let server = MockServer::start().await;
1042 let homeserver = make_mock_homeserver().await;
1043 let mut builder = ClientBuilder::new();
1044
1045 Mock::given(method("GET"))
1046 .and(path("/.well-known/matrix/client"))
1047 .respond_with(
1048 ResponseTemplate::new(200).set_body_json(make_well_known_json(&homeserver.uri())),
1049 )
1050 .mount(&server)
1051 .await;
1052
1053 builder = builder.server_name_or_homeserver_url(server.uri());
1055 let client = builder.build().await.unwrap();
1056
1057 assert!(client.sliding_sync_version().is_native());
1060 }
1061
1062 #[async_test]
1063 async fn test_sliding_sync_discover_native() {
1064 let homeserver = make_mock_homeserver().await;
1066 let mut builder = ClientBuilder::new();
1067
1068 builder = builder
1071 .server_name_or_homeserver_url(homeserver.uri())
1072 .sliding_sync_version_builder(SlidingSyncVersionBuilder::DiscoverNative);
1073
1074 let client = builder.build().await.unwrap();
1075
1076 assert_matches!(client.sliding_sync_version(), SlidingSyncVersion::Native);
1078 }
1079
1080 #[async_test]
1081 #[cfg(feature = "e2e-encryption")]
1082 async fn test_set_up_decryption_trust_requirement_cross_signed() {
1083 let homeserver = make_mock_homeserver().await;
1084 let builder = ClientBuilder::new()
1085 .server_name_or_homeserver_url(homeserver.uri())
1086 .with_decryption_settings(DecryptionSettings {
1087 sender_device_trust_requirement: TrustRequirement::CrossSigned,
1088 });
1089
1090 let client = builder.build().await.unwrap();
1091 assert_matches!(
1092 client.base_client().decryption_settings.sender_device_trust_requirement,
1093 TrustRequirement::CrossSigned
1094 );
1095 }
1096
1097 #[async_test]
1098 #[cfg(feature = "e2e-encryption")]
1099 async fn test_set_up_decryption_trust_requirement_untrusted() {
1100 let homeserver = make_mock_homeserver().await;
1101
1102 let builder = ClientBuilder::new()
1103 .server_name_or_homeserver_url(homeserver.uri())
1104 .with_decryption_settings(DecryptionSettings {
1105 sender_device_trust_requirement: TrustRequirement::Untrusted,
1106 });
1107
1108 let client = builder.build().await.unwrap();
1109 assert_matches!(
1110 client.base_client().decryption_settings.sender_device_trust_requirement,
1111 TrustRequirement::Untrusted
1112 );
1113 }
1114
1115 async fn make_mock_homeserver() -> MockServer {
1118 let homeserver = MockServer::start().await;
1119 Mock::given(method("GET"))
1120 .and(path("/_matrix/client/versions"))
1121 .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::VERSIONS))
1122 .mount(&homeserver)
1123 .await;
1124 Mock::given(method("GET"))
1125 .and(path("/_matrix/client/r0/login"))
1126 .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::LOGIN_TYPES))
1127 .mount(&homeserver)
1128 .await;
1129 homeserver
1130 }
1131
1132 fn make_well_known_json(homeserver_url: &str) -> JsonValue {
1133 ::serde_json::Value::Object({
1134 let mut object = ::serde_json::Map::new();
1135 let _ = object.insert(
1136 "m.homeserver".into(),
1137 json_internal!({
1138 "base_url": homeserver_url
1139 }),
1140 );
1141
1142 object
1143 })
1144 }
1145
1146 #[async_test]
1147 async fn test_cross_process_store_locks_holder_name() {
1148 {
1149 let homeserver = make_mock_homeserver().await;
1150 let client =
1151 ClientBuilder::new().homeserver_url(homeserver.uri()).build().await.unwrap();
1152
1153 assert_let!(
1154 CrossProcessLockConfig::MultiProcess { holder_name } =
1155 client.cross_process_lock_config()
1156 );
1157 assert_eq!(holder_name, "main");
1158 }
1159
1160 {
1161 let homeserver = make_mock_homeserver().await;
1162 let client = ClientBuilder::new()
1163 .homeserver_url(homeserver.uri())
1164 .cross_process_store_config(CrossProcessLockConfig::multi_process("foo"))
1165 .build()
1166 .await
1167 .unwrap();
1168
1169 assert_let!(
1170 CrossProcessLockConfig::MultiProcess { holder_name } =
1171 client.cross_process_lock_config()
1172 );
1173 assert_eq!(holder_name, "foo");
1174 }
1175 }
1176}