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