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;
29use matrix_sdk_base::{BaseClient, ThreadingSupport, store::StoreConfig};
30#[cfg(feature = "sqlite")]
31use matrix_sdk_sqlite::SqliteStoreConfig;
32use ruma::{
33 OwnedServerName, ServerName,
34 api::{MatrixVersion, SupportedVersions, error::FromHttpResponseError},
35};
36use thiserror::Error;
37use tokio::sync::{Mutex, OnceCell, broadcast};
38use tracing::{Span, debug, field::debug, instrument};
39
40use super::{Client, ClientInner};
41#[cfg(feature = "e2e-encryption")]
42use crate::crypto::{CollectStrategy, TrustRequirement};
43#[cfg(feature = "e2e-encryption")]
44use crate::encryption::EncryptionSettings;
45#[cfg(not(target_family = "wasm"))]
46use crate::http_client::HttpSettings;
47#[cfg(feature = "experimental-search")]
48use crate::search_index::SearchIndex;
49#[cfg(feature = "experimental-search")]
50use crate::search_index::SearchIndexStoreKind;
51use crate::{
52 HttpError, IdParseError,
53 authentication::{AuthCtx, oauth::OAuthCtx},
54 client::{
55 CachedValue::{Cached, NotSet},
56 ClientServerInfo,
57 },
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_store_locks_holder_name: String,
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 Self::DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME.to_owned(),
139 )),
140 request_config: Default::default(),
141 respect_login_well_known: true,
142 server_versions: None,
143 handle_refresh_tokens: false,
144 base_client: None,
145 #[cfg(feature = "e2e-encryption")]
146 encryption_settings: Default::default(),
147 #[cfg(feature = "e2e-encryption")]
148 room_key_recipient_strategy: Default::default(),
149 #[cfg(feature = "e2e-encryption")]
150 decryption_settings: DecryptionSettings {
151 sender_device_trust_requirement: TrustRequirement::Untrusted,
152 },
153 #[cfg(feature = "e2e-encryption")]
154 enable_share_history_on_invite: false,
155 cross_process_store_locks_holder_name:
156 Self::DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME.to_owned(),
157 threading_support: ThreadingSupport::Disabled,
158 #[cfg(feature = "experimental-search")]
159 search_index_store_kind: SearchIndexStoreKind::InMemory,
160 }
161 }
162
163 pub fn homeserver_url(mut self, url: impl AsRef<str>) -> Self {
170 self.homeserver_cfg = Some(HomeserverConfig::HomeserverUrl(url.as_ref().to_owned()));
171 self
172 }
173
174 pub fn server_name(mut self, server_name: &ServerName) -> Self {
184 self.homeserver_cfg = Some(HomeserverConfig::ServerName {
185 server: server_name.to_owned(),
186 protocol: UrlScheme::Https,
188 });
189 self
190 }
191
192 pub fn insecure_server_name_no_tls(mut self, server_name: &ServerName) -> Self {
201 self.homeserver_cfg = Some(HomeserverConfig::ServerName {
202 server: server_name.to_owned(),
203 protocol: UrlScheme::Http,
204 });
205 self
206 }
207
208 pub fn server_name_or_homeserver_url(mut self, server_name_or_url: impl AsRef<str>) -> Self {
219 self.homeserver_cfg = Some(HomeserverConfig::ServerNameOrHomeserverUrl(
220 server_name_or_url.as_ref().to_owned(),
221 ));
222 self
223 }
224
225 pub fn sliding_sync_version_builder(
227 mut self,
228 version_builder: SlidingSyncVersionBuilder,
229 ) -> Self {
230 self.sliding_sync_version_builder = version_builder;
231 self
232 }
233
234 #[cfg(feature = "sqlite")]
236 pub fn sqlite_store(mut self, path: impl AsRef<Path>, passphrase: Option<&str>) -> Self {
237 let sqlite_store_config = SqliteStoreConfig::new(path).passphrase(passphrase);
238 self.store_config =
239 BuilderStoreConfig::Sqlite { config: sqlite_store_config, cache_path: None };
240
241 self
242 }
243
244 #[cfg(feature = "sqlite")]
247 pub fn sqlite_store_with_cache_path(
248 mut self,
249 path: impl AsRef<Path>,
250 cache_path: impl AsRef<Path>,
251 passphrase: Option<&str>,
252 ) -> Self {
253 let sqlite_store_config = SqliteStoreConfig::new(path).passphrase(passphrase);
254 self.store_config = BuilderStoreConfig::Sqlite {
255 config: sqlite_store_config,
256 cache_path: Some(cache_path.as_ref().to_owned()),
257 };
258
259 self
260 }
261
262 #[cfg(feature = "sqlite")]
265 pub fn sqlite_store_with_config_and_cache_path(
266 mut self,
267 config: SqliteStoreConfig,
268 cache_path: Option<impl AsRef<Path>>,
269 ) -> Self {
270 self.store_config = BuilderStoreConfig::Sqlite {
271 config,
272 cache_path: cache_path.map(|cache_path| cache_path.as_ref().to_owned()),
273 };
274
275 self
276 }
277
278 #[cfg(feature = "indexeddb")]
280 pub fn indexeddb_store(mut self, name: &str, passphrase: Option<&str>) -> Self {
281 self.store_config = BuilderStoreConfig::IndexedDb {
282 name: name.to_owned(),
283 passphrase: passphrase.map(ToOwned::to_owned),
284 };
285 self
286 }
287
288 pub fn store_config(mut self, store_config: StoreConfig) -> Self {
310 self.store_config = BuilderStoreConfig::Custom(store_config);
311 self
312 }
313
314 pub fn respect_login_well_known(mut self, value: bool) -> Self {
317 self.respect_login_well_known = value;
318 self
319 }
320
321 pub fn request_config(mut self, request_config: RequestConfig) -> Self {
323 self.request_config = request_config;
324 self
325 }
326
327 #[cfg(not(target_family = "wasm"))]
343 pub fn proxy(mut self, proxy: impl AsRef<str>) -> Self {
344 self.http_settings().proxy = Some(proxy.as_ref().to_owned());
345 self
346 }
347
348 #[cfg(not(target_family = "wasm"))]
350 pub fn disable_ssl_verification(mut self) -> Self {
351 self.http_settings().disable_ssl_verification = true;
352 self
353 }
354
355 #[cfg(not(target_family = "wasm"))]
357 pub fn user_agent(mut self, user_agent: impl AsRef<str>) -> Self {
358 self.http_settings().user_agent = Some(user_agent.as_ref().to_owned());
359 self
360 }
361
362 #[cfg(not(target_family = "wasm"))]
371 pub fn add_root_certificates(mut self, certificates: Vec<reqwest::Certificate>) -> Self {
372 self.http_settings().additional_root_certificates = certificates;
373 self
374 }
375
376 #[cfg(not(target_family = "wasm"))]
380 pub fn disable_built_in_root_certificates(mut self) -> Self {
381 self.http_settings().disable_built_in_root_certificates = true;
382 self
383 }
384
385 pub fn http_client(mut self, client: reqwest::Client) -> Self {
395 self.http_cfg = Some(HttpConfig::Custom(client));
396 self
397 }
398
399 pub fn server_versions(mut self, value: impl IntoIterator<Item = MatrixVersion>) -> Self {
404 self.server_versions = Some(value.into_iter().collect());
405 self
406 }
407
408 #[cfg(not(target_family = "wasm"))]
409 fn http_settings(&mut self) -> &mut HttpSettings {
410 self.http_cfg.get_or_insert_with(Default::default).settings()
411 }
412
413 pub fn handle_refresh_tokens(mut self) -> Self {
435 self.handle_refresh_tokens = true;
436 self
437 }
438
439 #[doc(hidden)]
441 pub fn base_client(mut self, base_client: BaseClient) -> Self {
442 self.base_client = Some(base_client);
443 self
444 }
445
446 #[cfg(feature = "e2e-encryption")]
449 pub fn with_encryption_settings(mut self, settings: EncryptionSettings) -> Self {
450 self.encryption_settings = settings;
451 self
452 }
453
454 #[cfg(feature = "e2e-encryption")]
457 pub fn with_room_key_recipient_strategy(mut self, strategy: CollectStrategy) -> Self {
458 self.room_key_recipient_strategy = strategy;
459 self
460 }
461
462 #[cfg(feature = "e2e-encryption")]
464 pub fn with_decryption_settings(mut self, decryption_settings: DecryptionSettings) -> Self {
465 self.decryption_settings = decryption_settings;
466 self
467 }
468
469 #[cfg(feature = "e2e-encryption")]
474 pub fn with_enable_share_history_on_invite(
475 mut self,
476 enable_share_history_on_invite: bool,
477 ) -> Self {
478 self.enable_share_history_on_invite = enable_share_history_on_invite;
479 self
480 }
481
482 pub fn cross_process_store_locks_holder_name(mut self, holder_name: String) -> Self {
492 self.cross_process_store_locks_holder_name = holder_name;
493 self
494 }
495
496 pub fn with_threading_support(mut self, threading_support: ThreadingSupport) -> Self {
500 self.threading_support = threading_support;
501 self
502 }
503
504 #[cfg(feature = "experimental-search")]
506 pub fn search_index_store(mut self, kind: SearchIndexStoreKind) -> Self {
507 self.search_index_store_kind = kind;
508 self
509 }
510
511 #[instrument(skip_all, target = "matrix_sdk::client", fields(homeserver))]
524 pub async fn build(self) -> Result<Client, ClientBuildError> {
525 debug!("Starting to build the Client");
526
527 let homeserver_cfg = self.homeserver_cfg.ok_or(ClientBuildError::MissingHomeserver)?;
528 Span::current().record("homeserver", debug(&homeserver_cfg));
529
530 #[cfg_attr(target_family = "wasm", allow(clippy::infallible_destructuring_match))]
531 let inner_http_client = match self.http_cfg.unwrap_or_default() {
532 #[cfg(not(target_family = "wasm"))]
533 HttpConfig::Settings(mut settings) => {
534 settings.timeout = self.request_config.timeout;
535 settings.make_client()?
536 }
537 HttpConfig::Custom(c) => c,
538 };
539
540 let base_client = if let Some(base_client) = self.base_client {
541 base_client
542 } else {
543 #[allow(unused_mut)]
544 let mut client = BaseClient::new(
545 build_store_config(self.store_config, &self.cross_process_store_locks_holder_name)
546 .await?,
547 self.threading_support,
548 );
549
550 #[cfg(feature = "e2e-encryption")]
551 {
552 client.room_key_recipient_strategy = self.room_key_recipient_strategy;
553 client.decryption_settings = self.decryption_settings;
554 }
555
556 client
557 };
558
559 let http_client = HttpClient::new(inner_http_client.clone(), self.request_config);
560
561 #[allow(unused_variables)]
562 let HomeserverDiscoveryResult { server, homeserver, supported_versions, well_known } =
563 homeserver_cfg.discover(&http_client).await?;
564
565 let sliding_sync_version = {
566 let supported_versions = match supported_versions {
567 Some(versions) => Some(versions),
568 None if self.sliding_sync_version_builder.needs_get_supported_versions() => {
569 Some(get_supported_versions(&homeserver, &http_client).await?)
570 }
571 None => None,
572 };
573
574 let version = self.sliding_sync_version_builder.build(
575 supported_versions.map(|response| response.as_supported_versions()).as_ref(),
576 )?;
577
578 tracing::info!(?version, "selected sliding sync version");
579
580 version
581 };
582
583 let allow_insecure_oauth = homeserver.scheme() == "http";
584
585 let auth_ctx = Arc::new(AuthCtx {
586 handle_refresh_tokens: self.handle_refresh_tokens,
587 refresh_token_lock: Arc::new(Mutex::new(Ok(()))),
588 session_change_sender: broadcast::Sender::new(1),
589 auth_data: OnceCell::default(),
590 tokens: OnceCell::default(),
591 reload_session_callback: OnceCell::default(),
592 save_session_callback: OnceCell::default(),
593 oauth: OAuthCtx::new(allow_insecure_oauth),
594 });
595
596 let send_queue = Arc::new(SendQueueData::new(true));
598
599 let server_info = ClientServerInfo {
600 supported_versions: match self.server_versions {
601 Some(versions) => {
602 Cached(SupportedVersions { versions, features: Default::default() })
603 }
604 None => NotSet,
605 },
606 well_known: Cached(well_known.map(Into::into)),
607 };
608
609 let event_cache = OnceCell::new();
610 let latest_events = OnceCell::new();
611 let thread_subscriptions_catchup = OnceCell::new();
612
613 #[cfg(feature = "experimental-search")]
614 let search_index =
615 SearchIndex::new(Arc::new(Mutex::new(HashMap::new())), self.search_index_store_kind);
616
617 let inner = ClientInner::new(
618 auth_ctx,
619 server,
620 homeserver,
621 sliding_sync_version,
622 http_client,
623 base_client,
624 server_info,
625 self.respect_login_well_known,
626 event_cache,
627 send_queue,
628 latest_events,
629 #[cfg(feature = "e2e-encryption")]
630 self.encryption_settings,
631 #[cfg(feature = "e2e-encryption")]
632 self.enable_share_history_on_invite,
633 self.cross_process_store_locks_holder_name,
634 #[cfg(feature = "experimental-search")]
635 search_index,
636 thread_subscriptions_catchup,
637 )
638 .await;
639
640 debug!("Done building the Client");
641
642 Ok(Client { inner })
643 }
644}
645
646pub fn sanitize_server_name(s: &str) -> crate::Result<OwnedServerName, IdParseError> {
650 ServerName::parse(
651 s.trim().trim_start_matches("http://").trim_start_matches("https://").trim_end_matches('/'),
652 )
653}
654
655#[allow(clippy::unused_async, unused)] async fn build_store_config(
657 builder_config: BuilderStoreConfig,
658 cross_process_store_locks_holder_name: &str,
659) -> Result<StoreConfig, ClientBuildError> {
660 #[allow(clippy::infallible_destructuring_match)]
661 let store_config = match builder_config {
662 #[cfg(feature = "sqlite")]
663 BuilderStoreConfig::Sqlite { config, cache_path } => {
664 let store_config = StoreConfig::new(cross_process_store_locks_holder_name.to_owned())
665 .state_store(
666 matrix_sdk_sqlite::SqliteStateStore::open_with_config(config.clone()).await?,
667 )
668 .event_cache_store({
669 let mut config = config.clone();
670
671 if let Some(ref cache_path) = cache_path {
672 config = config.path(cache_path);
673 }
674
675 matrix_sdk_sqlite::SqliteEventCacheStore::open_with_config(config).await?
676 })
677 .media_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::SqliteMediaStore::open_with_config(config).await?
685 });
686
687 #[cfg(feature = "e2e-encryption")]
688 let store_config = store_config.crypto_store(
689 matrix_sdk_sqlite::SqliteCryptoStore::open_with_config(config).await?,
690 );
691
692 store_config
693 }
694
695 #[cfg(feature = "indexeddb")]
696 BuilderStoreConfig::IndexedDb { name, passphrase } => {
697 build_indexeddb_store_config(
698 &name,
699 passphrase.as_deref(),
700 cross_process_store_locks_holder_name,
701 )
702 .await?
703 }
704
705 BuilderStoreConfig::Custom(config) => config,
706 };
707 Ok(store_config)
708}
709
710#[cfg(all(target_family = "wasm", feature = "indexeddb"))]
713async fn build_indexeddb_store_config(
714 name: &str,
715 passphrase: Option<&str>,
716 cross_process_store_locks_holder_name: &str,
717) -> Result<StoreConfig, ClientBuildError> {
718 let cross_process_store_locks_holder_name = cross_process_store_locks_holder_name.to_owned();
719
720 #[cfg(feature = "e2e-encryption")]
721 let store_config = {
722 let (state_store, crypto_store) =
723 matrix_sdk_indexeddb::open_stores_with_name(name, passphrase).await?;
724 StoreConfig::new(cross_process_store_locks_holder_name)
725 .state_store(state_store)
726 .crypto_store(crypto_store)
727 };
728
729 #[cfg(not(feature = "e2e-encryption"))]
730 let store_config = {
731 let state_store = matrix_sdk_indexeddb::open_state_store(name, passphrase).await?;
732 StoreConfig::new(cross_process_store_locks_holder_name).state_store(state_store)
733 };
734
735 let store_config = {
736 tracing::warn!(
737 "The IndexedDB backend does not implement an event cache store, \
738 falling back to the in-memory event cache store…"
739 );
740 store_config.event_cache_store(matrix_sdk_base::event_cache::store::MemoryStore::new())
741 };
742
743 Ok(store_config)
744}
745
746#[cfg(all(not(target_family = "wasm"), feature = "indexeddb"))]
747#[allow(clippy::unused_async)]
748async fn build_indexeddb_store_config(
749 _name: &str,
750 _passphrase: Option<&str>,
751 _event_cache_store_lock_holder_name: &str,
752) -> Result<StoreConfig, ClientBuildError> {
753 panic!("the IndexedDB is only available on the 'wasm32' arch")
754}
755
756#[derive(Clone, Debug)]
757enum HttpConfig {
758 #[cfg(not(target_family = "wasm"))]
759 Settings(HttpSettings),
760 Custom(reqwest::Client),
761}
762
763#[cfg(not(target_family = "wasm"))]
764impl HttpConfig {
765 fn settings(&mut self) -> &mut HttpSettings {
766 match self {
767 Self::Settings(s) => s,
768 Self::Custom(_) => {
769 *self = Self::default();
770 match self {
771 Self::Settings(s) => s,
772 Self::Custom(_) => unreachable!(),
773 }
774 }
775 }
776 }
777}
778
779impl Default for HttpConfig {
780 fn default() -> Self {
781 #[cfg(not(target_family = "wasm"))]
782 return Self::Settings(HttpSettings::default());
783
784 #[cfg(target_family = "wasm")]
785 return Self::Custom(reqwest::Client::new());
786 }
787}
788
789#[derive(Clone)]
790enum BuilderStoreConfig {
791 #[cfg(feature = "sqlite")]
792 Sqlite {
793 config: SqliteStoreConfig,
794 cache_path: Option<PathBuf>,
795 },
796 #[cfg(feature = "indexeddb")]
797 IndexedDb {
798 name: String,
799 passphrase: Option<String>,
800 },
801 Custom(StoreConfig),
802}
803
804#[cfg(not(tarpaulin_include))]
805impl fmt::Debug for BuilderStoreConfig {
806 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
807 #[allow(clippy::infallible_destructuring_match)]
808 match self {
809 #[cfg(feature = "sqlite")]
810 Self::Sqlite { config, cache_path, .. } => f
811 .debug_struct("Sqlite")
812 .field("config", config)
813 .field("cache_path", cache_path)
814 .finish_non_exhaustive(),
815
816 #[cfg(feature = "indexeddb")]
817 Self::IndexedDb { name, .. } => {
818 f.debug_struct("IndexedDb").field("name", name).finish_non_exhaustive()
819 }
820
821 Self::Custom(store_config) => f.debug_tuple("Custom").field(store_config).finish(),
822 }
823 }
824}
825
826#[derive(Debug, Error)]
828pub enum ClientBuildError {
829 #[error("no homeserver or user ID was configured")]
831 MissingHomeserver,
832
833 #[error("The supplied server name is invalid")]
835 InvalidServerName,
836
837 #[error("Error looking up the .well-known endpoint on auto-discovery")]
839 AutoDiscovery(FromHttpResponseError<RumaApiError>),
840
841 #[error(transparent)]
843 SlidingSyncVersion(#[from] crate::sliding_sync::VersionBuilderError),
844
845 #[error(transparent)]
847 Url(#[from] url::ParseError),
848
849 #[error(transparent)]
851 Http(#[from] HttpError),
852
853 #[cfg(feature = "indexeddb")]
855 #[error(transparent)]
856 IndexeddbStore(#[from] matrix_sdk_indexeddb::OpenStoreError),
857
858 #[cfg(feature = "sqlite")]
860 #[error(transparent)]
861 SqliteStore(#[from] matrix_sdk_sqlite::OpenStoreError),
862}
863
864#[cfg(all(test, not(target_family = "wasm")))]
866pub(crate) mod tests {
867 use assert_matches::assert_matches;
868 use matrix_sdk_test::{async_test, test_json};
869 use serde_json::{Value as JsonValue, json_internal};
870 use wiremock::{
871 Mock, MockServer, ResponseTemplate,
872 matchers::{method, path},
873 };
874
875 use super::*;
876 use crate::sliding_sync::Version as SlidingSyncVersion;
877
878 #[test]
879 fn test_sanitize_server_name() {
880 assert_eq!(sanitize_server_name("matrix.org").unwrap().as_str(), "matrix.org");
881 assert_eq!(sanitize_server_name("https://matrix.org").unwrap().as_str(), "matrix.org");
882 assert_eq!(sanitize_server_name("http://matrix.org").unwrap().as_str(), "matrix.org");
883 assert_eq!(
884 sanitize_server_name("https://matrix.server.org").unwrap().as_str(),
885 "matrix.server.org"
886 );
887 assert_eq!(
888 sanitize_server_name("https://matrix.server.org/").unwrap().as_str(),
889 "matrix.server.org"
890 );
891 assert_eq!(
892 sanitize_server_name(" https://matrix.server.org// ").unwrap().as_str(),
893 "matrix.server.org"
894 );
895 assert_matches!(sanitize_server_name("https://matrix.server.org/something"), Err(_))
896 }
897
898 #[async_test]
905 async fn test_discovery_invalid_server() {
906 let mut builder = ClientBuilder::new();
908
909 builder = builder.server_name_or_homeserver_url("⚠️ This won't work 🚫");
911 let error = builder.build().await.unwrap_err();
912
913 assert_matches!(error, ClientBuildError::InvalidServerName);
915 }
916
917 #[async_test]
918 async fn test_discovery_no_server() {
919 let mut builder = ClientBuilder::new();
921
922 builder = builder.server_name_or_homeserver_url("localhost:3456");
924 let error = builder.build().await.unwrap_err();
925
926 println!("{error}");
928 assert_matches!(error, ClientBuildError::Http(_));
929 }
930
931 #[async_test]
932 async fn test_discovery_web_server() {
933 let server = MockServer::start().await;
936 let mut builder = ClientBuilder::new();
937
938 builder = builder.server_name_or_homeserver_url(server.uri());
940 let error = builder.build().await.unwrap_err();
941
942 assert_matches!(error, ClientBuildError::AutoDiscovery(FromHttpResponseError::Server(_)));
944 }
945
946 #[async_test]
947 async fn test_discovery_direct_legacy() {
948 let homeserver = make_mock_homeserver().await;
950 let mut builder = ClientBuilder::new();
951
952 builder = builder.server_name_or_homeserver_url(homeserver.uri());
954 let _client = builder.build().await.unwrap();
955
956 assert!(_client.sliding_sync_version().is_native());
958 }
959
960 #[async_test]
961 async fn test_discovery_well_known_parse_error() {
962 let server = MockServer::start().await;
964 let homeserver = make_mock_homeserver().await;
965 let mut builder = ClientBuilder::new();
966
967 let well_known = make_well_known_json(&homeserver.uri());
968 let bad_json = well_known.to_string().replace(',', "");
969 Mock::given(method("GET"))
970 .and(path("/.well-known/matrix/client"))
971 .respond_with(ResponseTemplate::new(200).set_body_json(bad_json))
972 .mount(&server)
973 .await;
974
975 builder = builder.server_name_or_homeserver_url(server.uri());
977 let error = builder.build().await.unwrap_err();
978
979 assert_matches!(
981 error,
982 ClientBuildError::AutoDiscovery(FromHttpResponseError::Deserialization(_))
983 );
984 }
985
986 #[async_test]
987 async fn test_discovery_well_known_legacy() {
988 let server = MockServer::start().await;
991 let homeserver = make_mock_homeserver().await;
992 let mut builder = ClientBuilder::new();
993
994 Mock::given(method("GET"))
995 .and(path("/.well-known/matrix/client"))
996 .respond_with(
997 ResponseTemplate::new(200).set_body_json(make_well_known_json(&homeserver.uri())),
998 )
999 .mount(&server)
1000 .await;
1001
1002 builder = builder.server_name_or_homeserver_url(server.uri());
1004 let client = builder.build().await.unwrap();
1005
1006 assert!(client.sliding_sync_version().is_native());
1009 }
1010
1011 #[async_test]
1012 async fn test_sliding_sync_discover_native() {
1013 let homeserver = make_mock_homeserver().await;
1015 let mut builder = ClientBuilder::new();
1016
1017 builder = builder
1020 .server_name_or_homeserver_url(homeserver.uri())
1021 .sliding_sync_version_builder(SlidingSyncVersionBuilder::DiscoverNative);
1022
1023 let client = builder.build().await.unwrap();
1024
1025 assert_matches!(client.sliding_sync_version(), SlidingSyncVersion::Native);
1027 }
1028
1029 #[async_test]
1030 #[cfg(feature = "e2e-encryption")]
1031 async fn test_set_up_decryption_trust_requirement_cross_signed() {
1032 let homeserver = make_mock_homeserver().await;
1033 let builder = ClientBuilder::new()
1034 .server_name_or_homeserver_url(homeserver.uri())
1035 .with_decryption_settings(DecryptionSettings {
1036 sender_device_trust_requirement: TrustRequirement::CrossSigned,
1037 });
1038
1039 let client = builder.build().await.unwrap();
1040 assert_matches!(
1041 client.base_client().decryption_settings.sender_device_trust_requirement,
1042 TrustRequirement::CrossSigned
1043 );
1044 }
1045
1046 #[async_test]
1047 #[cfg(feature = "e2e-encryption")]
1048 async fn test_set_up_decryption_trust_requirement_untrusted() {
1049 let homeserver = make_mock_homeserver().await;
1050
1051 let builder = ClientBuilder::new()
1052 .server_name_or_homeserver_url(homeserver.uri())
1053 .with_decryption_settings(DecryptionSettings {
1054 sender_device_trust_requirement: TrustRequirement::Untrusted,
1055 });
1056
1057 let client = builder.build().await.unwrap();
1058 assert_matches!(
1059 client.base_client().decryption_settings.sender_device_trust_requirement,
1060 TrustRequirement::Untrusted
1061 );
1062 }
1063
1064 async fn make_mock_homeserver() -> MockServer {
1067 let homeserver = MockServer::start().await;
1068 Mock::given(method("GET"))
1069 .and(path("/_matrix/client/versions"))
1070 .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::VERSIONS))
1071 .mount(&homeserver)
1072 .await;
1073 Mock::given(method("GET"))
1074 .and(path("/_matrix/client/r0/login"))
1075 .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::LOGIN_TYPES))
1076 .mount(&homeserver)
1077 .await;
1078 homeserver
1079 }
1080
1081 fn make_well_known_json(homeserver_url: &str) -> JsonValue {
1082 ::serde_json::Value::Object({
1083 let mut object = ::serde_json::Map::new();
1084 let _ = object.insert(
1085 "m.homeserver".into(),
1086 json_internal!({
1087 "base_url": homeserver_url
1088 }),
1089 );
1090
1091 object
1092 })
1093 }
1094
1095 #[async_test]
1096 async fn test_cross_process_store_locks_holder_name() {
1097 {
1098 let homeserver = make_mock_homeserver().await;
1099 let client =
1100 ClientBuilder::new().homeserver_url(homeserver.uri()).build().await.unwrap();
1101
1102 assert_eq!(client.cross_process_store_locks_holder_name(), "main");
1103 }
1104
1105 {
1106 let homeserver = make_mock_homeserver().await;
1107 let client = ClientBuilder::new()
1108 .homeserver_url(homeserver.uri())
1109 .cross_process_store_locks_holder_name("foo".to_owned())
1110 .build()
1111 .await
1112 .unwrap();
1113
1114 assert_eq!(client.cross_process_store_locks_holder_name(), "foo");
1115 }
1116 }
1117}