1#![doc = include_str!("README.md")]
17
18mod builder;
19mod cache;
20mod client;
21mod error;
22mod list;
23
24use std::{
25 collections::{BTreeMap, btree_map::Entry},
26 fmt::Debug,
27 future::Future,
28 sync::{Arc, RwLock as StdRwLock, RwLockWriteGuard as StdRwLockWriteGuard},
29 time::Duration,
30};
31
32use async_stream::stream;
33pub use client::{Version, VersionBuilder};
34use futures_core::stream::Stream;
35use matrix_sdk_base::RequestedRequiredStates;
36#[cfg(feature = "e2e-encryption")]
37use matrix_sdk_common::executor::JoinHandleExt as _;
38use matrix_sdk_common::{executor::spawn, timer};
39use ruma::{
40 OwnedRoomId, RoomId,
41 api::{client::sync::sync_events::v5 as http, error::ErrorKind},
42 assign,
43};
44use tokio::{
45 select,
46 sync::{Mutex as AsyncMutex, OwnedMutexGuard, RwLock as AsyncRwLock, broadcast::Sender},
47};
48use tracing::{Instrument, Span, debug, error, info, instrument, trace, warn};
49
50pub use self::{builder::*, client::VersionBuilderError, error::*, list::*};
51use self::{cache::restore_sliding_sync_state, client::SlidingSyncResponseProcessor};
52use crate::{Client, Result, config::RequestConfig};
53
54#[derive(Clone, Debug)]
58pub struct SlidingSync {
59 inner: Arc<SlidingSyncInner>,
61}
62
63#[derive(Debug)]
64pub(super) struct SlidingSyncInner {
65 id: String,
69
70 client: Client,
72
73 poll_timeout: Duration,
75
76 network_timeout: Duration,
79
80 storage_key: String,
82
83 share_pos: bool,
90
91 position: Arc<AsyncMutex<SlidingSyncPositionMarkers>>,
104
105 lists: AsyncRwLock<BTreeMap<String, SlidingSyncList>>,
107
108 room_subscriptions: StdRwLock<BTreeMap<OwnedRoomId, http::request::RoomSubscription>>,
111
112 extensions: http::request::Extensions,
115
116 internal_channel: Sender<SlidingSyncInternalMessage>,
119}
120
121impl SlidingSync {
122 pub(super) fn new(inner: SlidingSyncInner) -> Self {
123 Self { inner: Arc::new(inner) }
124 }
125
126 async fn cache_to_storage(&self, position: &SlidingSyncPositionMarkers) -> Result<()> {
127 cache::store_sliding_sync_state(self, position).await
128 }
129
130 pub fn builder(id: String, client: Client) -> Result<SlidingSyncBuilder, Error> {
132 SlidingSyncBuilder::new(id, client)
133 }
134
135 pub fn subscribe_to_rooms(
142 &self,
143 room_ids: &[&RoomId],
144 settings: Option<http::request::RoomSubscription>,
145 cancel_in_flight_request: bool,
146 ) {
147 if subscribe_to_rooms(
148 self.inner.room_subscriptions.write().unwrap(),
149 &self.inner.client,
150 room_ids,
151 settings,
152 cancel_in_flight_request,
153 ) {
154 self.inner.internal_channel_send_if_possible(
155 SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration,
156 );
157 }
158 }
159
160 pub fn unsubscribe_to_rooms(&self, room_ids: &[&RoomId], cancel_in_flight_request: bool) {
162 let mut room_subscriptions = self.inner.room_subscriptions.write().unwrap();
163 let mut skip_over_current_sync_loop_iteration = false;
164
165 for room_id in room_ids {
166 if room_subscriptions.remove(*room_id).is_some() {
167 skip_over_current_sync_loop_iteration = true;
168 }
169 }
170
171 if cancel_in_flight_request && skip_over_current_sync_loop_iteration {
172 self.inner.internal_channel_send_if_possible(
173 SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration,
174 );
175 }
176 }
177
178 pub fn resubscribe_to_rooms(
196 &self,
197 room_ids: &[&RoomId],
198 settings: Option<http::request::RoomSubscription>,
199 cancel_in_flight_request: bool,
200 ) {
201 let mut room_subscriptions = self.inner.room_subscriptions.write().unwrap();
202 let mut skip_over_current_sync_loop_iteration = false;
203
204 room_subscriptions.retain(|room_id, _| {
206 if room_ids.contains(&room_id.as_ref()) {
207 true
208 } else {
209 skip_over_current_sync_loop_iteration = true;
210 false
211 }
212 });
213
214 if subscribe_to_rooms(
215 room_subscriptions,
216 &self.inner.client,
217 room_ids,
218 settings,
219 cancel_in_flight_request && skip_over_current_sync_loop_iteration,
220 ) {
221 self.inner.internal_channel_send_if_possible(
222 SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration,
223 );
224 }
225 }
226
227 pub fn clear_and_subscribe_to_rooms(
232 &self,
233 room_ids: &[&RoomId],
234 settings: Option<http::request::RoomSubscription>,
235 cancel_in_flight_request: bool,
236 ) {
237 let mut room_subscriptions = self.inner.room_subscriptions.write().unwrap();
238 room_subscriptions.clear();
239
240 if subscribe_to_rooms(
241 room_subscriptions,
242 &self.inner.client,
243 room_ids,
244 settings,
245 cancel_in_flight_request,
246 ) {
247 self.inner.internal_channel_send_if_possible(
248 SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration,
249 );
250 }
251 }
252
253 pub async fn on_list<Function, FunctionOutput, R>(
255 &self,
256 list_name: &str,
257 function: Function,
258 ) -> Option<R>
259 where
260 Function: FnOnce(&SlidingSyncList) -> FunctionOutput,
261 FunctionOutput: Future<Output = R>,
262 {
263 let lists = self.inner.lists.read().await;
264
265 match lists.get(list_name) {
266 Some(list) => Some(function(list).await),
267 None => None,
268 }
269 }
270
271 pub async fn add_list(
277 &self,
278 list_builder: SlidingSyncListBuilder,
279 ) -> Result<Option<SlidingSyncList>> {
280 let list = list_builder.build(self.inner.internal_channel.clone());
281
282 let old_list = self.inner.lists.write().await.insert(list.name().to_owned(), list);
283
284 self.inner.internal_channel_send_if_possible(
285 SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration,
286 );
287
288 Ok(old_list)
289 }
290
291 pub async fn add_cached_list(
298 &self,
299 mut list_builder: SlidingSyncListBuilder,
300 ) -> Result<Option<SlidingSyncList>> {
301 let _timer = timer!(format!("restoring (loading+processing) list {}", list_builder.name));
302
303 list_builder.set_cached_and_reload(&self.inner.client, &self.inner.storage_key).await?;
304
305 self.add_list(list_builder).await
306 }
307
308 #[instrument(skip_all)]
310 async fn handle_response(
311 &self,
312 mut sliding_sync_response: http::Response,
313 position: &mut SlidingSyncPositionMarkers,
314 requested_required_states: RequestedRequiredStates,
315 ) -> Result<UpdateSummary, crate::Error> {
316 let pos = Some(sliding_sync_response.pos.clone());
317
318 let must_process_rooms_response = self.must_process_rooms_response().await;
319
320 trace!(yes = must_process_rooms_response, "Must process rooms response?");
321
322 let sync_response = {
330 let _timer = timer!("response processor");
331
332 let response_processor = {
333 let state_store_guard = {
336 let _timer = timer!("acquiring the `state_store_lock`");
337
338 self.inner.client.base_client().state_store_lock().lock().await
339 };
340
341 let mut response_processor =
342 SlidingSyncResponseProcessor::new(self.inner.client.clone());
343
344 if self.is_thread_subscriptions_enabled() {
350 response_processor
351 .handle_thread_subscriptions(
352 position.pos.as_deref(),
353 std::mem::take(
354 &mut sliding_sync_response.extensions.thread_subscriptions,
355 ),
356 )
357 .await?;
358 }
359
360 #[cfg(feature = "e2e-encryption")]
361 if self.is_e2ee_enabled() {
362 response_processor
363 .handle_encryption(&sliding_sync_response.extensions, &state_store_guard)
364 .await?
365 }
366
367 if must_process_rooms_response {
370 response_processor
371 .handle_room_response(
372 &sliding_sync_response,
373 &requested_required_states,
374 &state_store_guard,
375 )
376 .await?;
377 }
378
379 response_processor
380 };
381
382 response_processor.process_and_take_response().await?
384 };
385
386 debug!("Sliding Sync response has been handled by the client");
387 trace!(?sync_response);
388
389 let update_summary = {
390 let updated_rooms = {
392 let mut updated_rooms = Vec::with_capacity(
393 sliding_sync_response.rooms.len() + sync_response.rooms.joined.len(),
394 );
395
396 updated_rooms.extend(sliding_sync_response.rooms.keys().cloned());
397
398 updated_rooms.extend(sync_response.rooms.joined.keys().cloned());
406
407 updated_rooms
408 };
409
410 let updated_lists = {
412 debug!(
413 lists = ?sliding_sync_response.lists,
414 "Update lists"
415 );
416
417 let mut updated_lists = Vec::with_capacity(sliding_sync_response.lists.len());
418 let mut lists = self.inner.lists.write().await;
419
420 for (name, list) in lists.iter_mut() {
423 if let Some(updates) = sliding_sync_response.lists.get(name) {
424 let maximum_number_of_rooms: u32 =
425 updates.count.try_into().expect("failed to convert `count` to `u32`");
426
427 if list.update(Some(maximum_number_of_rooms))? {
428 updated_lists.push(name.clone());
429 }
430 } else if list.update(None)? {
431 updated_lists.push(name.clone());
432 }
433 }
434
435 for name in sliding_sync_response.lists.keys() {
437 if !lists.contains_key(name) {
438 error!("Response for list `{name}` - unknown to us; skipping");
439 }
440 }
441
442 updated_lists
443 };
444
445 UpdateSummary { lists: updated_lists, rooms: updated_rooms }
446 };
447
448 debug!(previous_pos = position.pos, new_pos = pos, "Updating `pos`");
452
453 position.pos = pos;
454
455 Ok(update_summary)
456 }
457
458 async fn generate_sync_request(
459 &self,
460 ) -> Result<(http::Request, RequestConfig, OwnedMutexGuard<SlidingSyncPositionMarkers>)> {
461 let mut requests_lists = BTreeMap::new();
463
464 let timeout = {
465 let lists = self.inner.lists.read().await;
466
467 let mut timeout = PollTimeout::Default;
469
470 for (name, list) in lists.iter() {
471 requests_lists.insert(name.clone(), list.next_request()?);
472 timeout = timeout.min(list.requires_timeout());
473 }
474
475 timeout
476 };
477
478 let mut position_guard = {
486 debug!("Waiting to acquire the `position` lock");
487
488 let _timer = timer!("acquiring the `position` lock");
489
490 self.inner.position.clone().lock_owned().await
491 };
492
493 debug!(pos = ?position_guard.pos, "Got a position");
494
495 let to_device_enabled = self.inner.extensions.to_device.enabled == Some(true);
496
497 let restored_fields = if self.inner.share_pos || to_device_enabled {
498 restore_sliding_sync_state(&self.inner.client, &self.inner.storage_key).await?
499 } else {
500 None
501 };
502
503 let pos = if self.inner.share_pos {
506 if let Some(fields) = &restored_fields {
507 if fields.pos != position_guard.pos {
509 info!(
510 "Pos from previous request ('{:?}') was different from \
511 pos in database ('{:?}').",
512 position_guard.pos, fields.pos
513 );
514 position_guard.pos = fields.pos.clone();
515 }
516 fields.pos.clone()
517 } else {
518 position_guard.pos.clone()
519 }
520 } else {
521 position_guard.pos.clone()
522 };
523
524 #[cfg(feature = "e2e-encryption")]
533 if pos.is_none() && self.is_e2ee_enabled() {
534 info!("Marking all tracked users as dirty");
535
536 let olm_machine = self.inner.client.olm_machine().await;
537 let olm_machine = olm_machine.as_ref().ok_or(Error::NoOlmMachine)?;
538 olm_machine.mark_all_tracked_users_as_dirty().await?;
539 }
540
541 let timeout = match timeout {
546 PollTimeout::None => None,
547 PollTimeout::Some(timeout) => Some(Duration::from_secs(timeout.into())),
548 PollTimeout::Default => Some(self.inner.poll_timeout),
549 };
550
551 Span::current()
552 .record("pos", &pos)
553 .record("timeout", timeout.map(|duration| duration.as_millis()));
554
555 let mut request = assign!(http::Request::new(), {
556 conn_id: Some(self.inner.id.clone()),
557 pos,
558 set_presence: self.inner.client.sync_presence(),
559 timeout,
560 lists: requests_lists,
561 });
562
563 request.room_subscriptions = self.inner.room_subscriptions.read().unwrap().clone();
565
566 request.extensions = self.inner.extensions.clone();
568
569 if to_device_enabled {
571 request.extensions.to_device.since =
572 restored_fields.and_then(|fields| fields.to_device_token);
573 }
574
575 Ok((
576 request,
578 RequestConfig::default()
581 .timeout(self.inner.poll_timeout + self.inner.network_timeout)
582 .retry_limit(3),
583 position_guard,
584 ))
585 }
586
587 async fn send_sync_request(
591 &self,
592 request: http::Request,
593 request_config: RequestConfig,
594 mut position_guard: OwnedMutexGuard<SlidingSyncPositionMarkers>,
595 ) -> Result<UpdateSummary> {
596 debug!("Sending request");
597
598 let requested_required_states = RequestedRequiredStates::from(&request);
600 let request = self.inner.client.send(request).with_request_config(request_config);
601
602 #[cfg(feature = "e2e-encryption")]
609 let response = {
610 if self.is_e2ee_enabled() {
611 let client = self.inner.client.clone();
628 let e2ee_uploads = spawn(
629 async move {
630 if let Err(error) = client.send_outgoing_requests().await {
631 error!(?error, "Error while sending outgoing E2EE requests");
632 }
633 }
634 .instrument(Span::current()),
635 )
636 .abort_on_drop();
639
640 let response = request.await?;
642
643 e2ee_uploads.await.map_err(|error| Error::JoinError {
648 task_description: "e2ee_uploads".to_owned(),
649 error,
650 })?;
651
652 response
653 } else {
654 request.await?
655 }
656 };
657
658 #[cfg(not(feature = "e2e-encryption"))]
660 let response = request.await?;
661
662 debug!("Received response");
663
664 let this = self.clone();
674
675 let future = async move {
678 debug!("Start handling response");
679
680 let updates = this
686 .handle_response(response, &mut position_guard, requested_required_states)
687 .await?;
688
689 this.cache_to_storage(&position_guard).await?;
690
691 drop(position_guard);
694
695 debug!("Done handling response");
696
697 Ok(updates)
698 };
699
700 spawn(future.instrument(Span::current())).await.map_err(|error| Error::JoinError {
701 task_description: "handle_response".to_owned(),
702 error,
703 })?
704 }
705
706 #[cfg(feature = "e2e-encryption")]
708 fn is_e2ee_enabled(&self) -> bool {
709 self.inner.extensions.e2ee.enabled == Some(true)
710 }
711
712 fn is_thread_subscriptions_enabled(&self) -> bool {
715 self.inner.extensions.thread_subscriptions.enabled == Some(true)
716 }
717
718 #[cfg(not(feature = "e2e-encryption"))]
719 fn is_e2ee_enabled(&self) -> bool {
720 false
721 }
722
723 async fn must_process_rooms_response(&self) -> bool {
725 !self.inner.room_subscriptions.read().unwrap().is_empty()
728 || !self.inner.lists.read().await.is_empty()
729 }
730
731 #[doc(hidden)]
735 #[instrument(skip_all, fields(conn_id = self.inner.id, pos, timeout))]
736 pub async fn sync_once(&self) -> Result<UpdateSummary> {
737 let (request, request_config, position_guard) = self.generate_sync_request().await?;
738
739 let summaries = self.send_sync_request(request, request_config, position_guard).await?;
741
742 self.inner.client.inner.sync_beat.notify(usize::MAX);
744
745 Ok(summaries)
746 }
747
748 #[allow(unknown_lints, clippy::let_with_type_underscore)] #[instrument(name = "sync_stream", skip_all, fields(conn_id = self.inner.id, with_e2ee = self.is_e2ee_enabled()))]
758 pub fn sync(&self) -> impl Stream<Item = Result<UpdateSummary, crate::Error>> + '_ {
759 debug!("Starting sync stream");
760
761 let mut internal_channel_receiver = self.inner.internal_channel.subscribe();
762
763 stream! {
764 loop {
765 debug!("Sync stream is running");
766
767 select! {
768 biased;
769
770 internal_message = internal_channel_receiver.recv() => {
771 use SlidingSyncInternalMessage::*;
772
773 debug!(?internal_message, "Sync stream has received an internal message");
774
775 match internal_message {
776 Err(_) | Ok(SyncLoopStop) => {
777 break;
778 }
779
780 Ok(SyncLoopSkipOverCurrentIteration) => {
781 continue;
782 }
783 }
784 }
785
786 update_summary = self.sync_once() => {
787 match update_summary {
788 Ok(updates) => {
789 yield Ok(updates);
790 }
791
792 Err(error) => {
794 if error.client_api_error_kind() == Some(&ErrorKind::UnknownPos) {
795 self.expire_session().await;
797 }
798
799 yield Err(error);
800
801 break;
803 }
804 }
805 }
806 }
807 }
808
809 debug!("Sync stream has exited.");
810 }
811 }
812
813 pub fn stop_sync(&self) -> Result<()> {
822 Ok(self.inner.internal_channel_send(SlidingSyncInternalMessage::SyncLoopStop)?)
823 }
824
825 #[doc(hidden)]
836 pub async fn expire_session(&self) {
837 info!("Session expired; resetting `pos`");
838
839 {
840 let lists = self.inner.lists.read().await;
841
842 for list in lists.values() {
843 list.set_maximum_number_of_rooms(None);
845 }
846 }
847
848 {
850 let mut position = self.inner.position.lock().await;
851
852 position.pos = None;
854
855 if let Err(err) = self.cache_to_storage(&position).await {
859 warn!("Failed to invalidate cached sliding sync state: {err}");
860 }
861 }
862
863 {
864 self.inner.room_subscriptions.write().unwrap().clear();
867 }
868 }
869}
870
871fn subscribe_to_rooms(
874 mut room_subscriptions: StdRwLockWriteGuard<
875 '_,
876 BTreeMap<OwnedRoomId, http::request::RoomSubscription>,
877 >,
878 client: &Client,
879 room_ids: &[&RoomId],
880 settings: Option<http::request::RoomSubscription>,
881 cancel_in_flight_request: bool,
882) -> bool {
883 let settings = settings.unwrap_or_default();
884 let mut skip_over_current_sync_loop_iteration = false;
885
886 for room_id in room_ids {
887 if let Entry::Vacant(entry) = room_subscriptions.entry((*room_id).to_owned()) {
888 if let Some(room) = client.get_room(room_id) {
889 room.mark_members_missing();
890 }
891
892 entry.insert(settings.clone());
893
894 skip_over_current_sync_loop_iteration = true;
895 }
896 }
897
898 cancel_in_flight_request && skip_over_current_sync_loop_iteration
899}
900
901impl SlidingSyncInner {
902 #[instrument]
904 fn internal_channel_send(&self, message: SlidingSyncInternalMessage) -> Result<(), Error> {
905 self.internal_channel.send(message).map(|_| ()).map_err(|_| Error::InternalChannelIsBroken)
906 }
907
908 #[instrument]
911 fn internal_channel_send_if_possible(&self, message: SlidingSyncInternalMessage) {
912 let _ = self.internal_channel.send(message);
914 }
915}
916
917#[derive(Copy, Clone, Debug, PartialEq)]
918enum SlidingSyncInternalMessage {
919 SyncLoopStop,
921
922 SyncLoopSkipOverCurrentIteration,
925}
926
927#[cfg(any(test, feature = "testing"))]
928impl SlidingSync {
929 pub async fn set_pos(&self, new_pos: String) {
931 let mut position_lock = self.inner.position.lock().await;
932 position_lock.pos = Some(new_pos);
933 }
934}
935
936#[derive(Clone, Debug)]
937pub(super) struct SlidingSyncPositionMarkers {
938 pos: Option<String>,
941}
942
943#[derive(Debug, Clone)]
946pub struct UpdateSummary {
947 pub lists: Vec<String>,
949 pub rooms: Vec<OwnedRoomId>,
951}
952
953#[derive(Debug)]
962pub enum PollTimeout {
963 None,
965
966 Some(u32),
969
970 Default,
973}
974
975impl PollTimeout {
976 fn min(self, left: Self) -> Self {
987 match (self, left) {
988 (Self::None, _) => Self::None,
989
990 (Self::Some(_), Self::None) => Self::None,
991 (Self::Some(right), Self::Some(left)) => Self::Some(right.min(left)),
992 (Self::Some(right), Self::Default) => Self::Some(right),
993
994 (Self::Default, Self::None) => Self::None,
995 (Self::Default, Self::Some(left)) => Self::Some(left),
996 (Self::Default, Self::Default) => Self::Default,
997 }
998 }
999}
1000
1001#[cfg(all(test, not(target_family = "wasm")))]
1002#[allow(clippy::dbg_macro)]
1003mod tests {
1004 use std::{
1005 collections::BTreeMap,
1006 future::ready,
1007 ops::Not,
1008 sync::{Arc, Mutex},
1009 time::Duration,
1010 };
1011
1012 use assert_matches::assert_matches;
1013 use event_listener::Listener;
1014 use futures_util::{StreamExt, future::join_all, pin_mut};
1015 use matrix_sdk_base::{RequestedRequiredStates, RoomMemberships};
1016 use matrix_sdk_common::executor::spawn;
1017 use matrix_sdk_test::{ALICE, async_test, event_factory::EventFactory};
1018 use ruma::{
1019 OwnedRoomId, assign,
1020 events::{direct::DirectEvent, room::member::MembershipState},
1021 owned_room_id,
1022 presence::PresenceState,
1023 profile::{
1024 AvatarUrl, DisplayName, ProfileFieldName, UserProfileChanges, UserProfileUpdate,
1025 },
1026 room_id,
1027 serde::Raw,
1028 uint,
1029 };
1030 use serde::Deserialize;
1031 use serde_json::json;
1032 use stream_assert::assert_pending;
1033 use wiremock::{
1034 Match, Mock, MockServer, Request, ResponseTemplate, http::Method, matchers::method,
1035 };
1036
1037 use super::{
1038 SlidingSync, SlidingSyncBuilder, SlidingSyncList, SlidingSyncListBuilder, SlidingSyncMode,
1039 cache::restore_sliding_sync_state, http,
1040 };
1041 use crate::{
1042 Client, Result,
1043 test_utils::{logged_in_client, mocks::MatrixMockServer},
1044 };
1045
1046 #[derive(Copy, Clone)]
1047 struct SlidingSyncMatcher;
1048
1049 impl Match for SlidingSyncMatcher {
1050 fn matches(&self, request: &Request) -> bool {
1051 request.url.path() == "/_matrix/client/unstable/org.matrix.simplified_msc3575/sync"
1052 && request.method == Method::POST
1053 }
1054 }
1055
1056 async fn new_sliding_sync(
1057 lists: Vec<SlidingSyncListBuilder>,
1058 ) -> Result<(MockServer, SlidingSync)> {
1059 let server = MockServer::start().await;
1060 let client = logged_in_client(Some(server.uri())).await;
1061
1062 let mut sliding_sync_builder = client.sliding_sync("test-slidingsync")?;
1063
1064 for list in lists {
1065 sliding_sync_builder = sliding_sync_builder.add_list(list);
1066 }
1067
1068 let sliding_sync = sliding_sync_builder.build().await?;
1069
1070 Ok((server, sliding_sync))
1071 }
1072
1073 #[async_test]
1074 async fn test_subscribe_to_own_profile() {
1075 let client = logged_in_client(None).await;
1076 let own_user_id = client.user_id().expect("client should be logged in").to_owned();
1077
1078 let mut response = http::Response::new("0".to_owned());
1081
1082 let mut profile_changes = UserProfileChanges::new();
1083 profile_changes.updated.insert(ProfileFieldName::DisplayName, json!("Example"));
1084
1085 response
1086 .extensions
1087 .profiles
1088 .users
1089 .insert(own_user_id.clone(), UserProfileUpdate::Updated(profile_changes));
1090 client
1091 .process_sliding_sync_test_helper(&response, &RequestedRequiredStates::default())
1092 .await
1093 .expect("Failed to process sync");
1094
1095 let stream = client.subscribe_to_own_profile().expect("client should be logged in");
1097 pin_mut!(stream);
1098
1099 let profile = stream.next().await.expect("should emit the initial profile");
1100 assert_eq!(profile.get_static::<DisplayName>().unwrap().as_deref(), Some("Example"));
1101
1102 let mut response = http::Response::new("1".to_owned());
1104
1105 let mut profile_changes = UserProfileChanges::new();
1106 profile_changes.updated.insert(ProfileFieldName::DisplayName, json!("Alice"));
1107 response
1108 .extensions
1109 .profiles
1110 .users
1111 .insert(ALICE.to_owned(), UserProfileUpdate::Updated(profile_changes));
1112 client
1113 .process_sliding_sync_test_helper(&response, &RequestedRequiredStates::default())
1114 .await
1115 .expect("Failed to process sync");
1116
1117 assert_pending!(stream);
1118
1119 let mut response = http::Response::new("2".to_owned());
1121
1122 let mut profile_changes = UserProfileChanges::new();
1123 profile_changes
1124 .updated
1125 .insert(ProfileFieldName::AvatarUrl, json!("mxc://example.org/avatar"));
1126
1127 response
1128 .extensions
1129 .profiles
1130 .users
1131 .insert(own_user_id.clone(), UserProfileUpdate::Updated(profile_changes));
1132 client
1133 .process_sliding_sync_test_helper(&response, &RequestedRequiredStates::default())
1134 .await
1135 .expect("Failed to process sync");
1136
1137 let profile = stream.next().await.expect("should emit the updated profile");
1138 assert_eq!(profile.get_static::<DisplayName>().unwrap().as_deref(), Some("Example"));
1139 assert_eq!(
1140 profile.get_static::<AvatarUrl>().unwrap().map(|url| url.to_string()).as_deref(),
1141 Some("mxc://example.org/avatar")
1142 );
1143 }
1144
1145 #[async_test]
1146 async fn test_sliding_sync_request_uses_client_sync_presence() -> Result<()> {
1147 let (_server, sliding_sync) = new_sliding_sync(vec![]).await?;
1148 let client = sliding_sync.inner.client.clone();
1149
1150 {
1151 let (request, _, _position_guard) = sliding_sync.generate_sync_request().await?;
1152
1153 assert_eq!(request.set_presence, PresenceState::Online);
1154 }
1155
1156 client.set_presence(PresenceState::Unavailable, None, false).await?;
1157
1158 {
1159 let (request, _, _position_guard) = sliding_sync.generate_sync_request().await?;
1160
1161 assert_eq!(request.set_presence, PresenceState::Unavailable);
1162 }
1163
1164 client.set_presence(PresenceState::Offline, None, false).await?;
1165
1166 {
1167 let (request, _, _position_guard) = sliding_sync.generate_sync_request().await?;
1168
1169 assert_eq!(request.set_presence, PresenceState::Offline);
1170 }
1171
1172 Ok(())
1173 }
1174
1175 #[async_test]
1176 async fn test_subscribe_to_rooms() -> Result<()> {
1177 let (server, sliding_sync) = new_sliding_sync(vec![
1178 SlidingSyncList::builder("foo")
1179 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1180 ])
1181 .await?;
1182
1183 let stream = sliding_sync.sync();
1184 pin_mut!(stream);
1185
1186 let room_id_0 = room_id!("!r0:bar.org");
1187 let room_id_1 = room_id!("!r1:bar.org");
1188 let room_id_2 = room_id!("!r2:bar.org");
1189
1190 {
1191 let _mock_guard = Mock::given(SlidingSyncMatcher)
1192 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1193 "pos": "1",
1194 "lists": {},
1195 "rooms": {
1196 room_id_0: {
1197 "name": "Room #0",
1198 "initial": true,
1199 },
1200 room_id_1: {
1201 "name": "Room #1",
1202 "initial": true,
1203 },
1204 room_id_2: {
1205 "name": "Room #2",
1206 "initial": true,
1207 },
1208 }
1209 })))
1210 .mount_as_scoped(&server)
1211 .await;
1212
1213 let _ = stream.next().await.unwrap()?;
1214 }
1215
1216 let room0 = sliding_sync.inner.client.get_room(room_id_0).unwrap();
1217
1218 assert!(room0.are_members_synced().not());
1222
1223 {
1224 struct MemberMatcher(OwnedRoomId);
1225
1226 impl Match for MemberMatcher {
1227 fn matches(&self, request: &Request) -> bool {
1228 request.url.path()
1229 == format!("/_matrix/client/r0/rooms/{room_id}/members", room_id = self.0)
1230 && request.method == Method::GET
1231 }
1232 }
1233
1234 let _mock_guard = Mock::given(MemberMatcher(room_id_0.to_owned()))
1235 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1236 "chunk": [],
1237 })))
1238 .mount_as_scoped(&server)
1239 .await;
1240
1241 assert_matches!(room0.request_members().await, Ok(()));
1242 }
1243
1244 assert!(room0.are_members_synced());
1246
1247 sliding_sync.subscribe_to_rooms(&[room_id_0, room_id_1], None, true);
1248
1249 assert!(room0.are_members_synced().not());
1252
1253 {
1254 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1255
1256 assert!(room_subscriptions.contains_key(room_id_0));
1257 assert!(room_subscriptions.contains_key(room_id_1));
1258 assert!(!room_subscriptions.contains_key(room_id_2));
1259 }
1260
1261 {
1264 struct MemberMatcher(OwnedRoomId);
1265
1266 impl Match for MemberMatcher {
1267 fn matches(&self, request: &Request) -> bool {
1268 request.url.path()
1269 == format!("/_matrix/client/r0/rooms/{room_id}/members", room_id = self.0)
1270 && request.method == Method::GET
1271 }
1272 }
1273
1274 let _mock_guard = Mock::given(MemberMatcher(room_id_0.to_owned()))
1275 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1276 "chunk": [],
1277 })))
1278 .mount_as_scoped(&server)
1279 .await;
1280
1281 assert_matches!(room0.request_members().await, Ok(()));
1282 }
1283
1284 assert!(room0.are_members_synced());
1286
1287 sliding_sync.subscribe_to_rooms(&[room_id_0], None, false);
1288
1289 assert!(room0.are_members_synced());
1292
1293 Ok(())
1294 }
1295
1296 #[async_test]
1297 async fn test_subscribe_unsubscribe_and_clear_and_subscribe_to_rooms() -> Result<()> {
1298 let (_server, sliding_sync) = new_sliding_sync(vec![
1299 SlidingSyncList::builder("foo")
1300 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1301 ])
1302 .await?;
1303
1304 let room_id_0 = room_id!("!r0:bar.org");
1305 let room_id_1 = room_id!("!r1:bar.org");
1306 let room_id_2 = room_id!("!r2:bar.org");
1307 let room_id_3 = room_id!("!r3:bar.org");
1308
1309 {
1311 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1312
1313 assert!(room_subscriptions.is_empty());
1314 }
1315
1316 sliding_sync.subscribe_to_rooms(&[room_id_0, room_id_1], Default::default(), false);
1318
1319 {
1320 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1321
1322 assert_eq!(room_subscriptions.len(), 2);
1323 assert!(room_subscriptions.contains_key(room_id_0));
1324 assert!(room_subscriptions.contains_key(room_id_1));
1325 }
1326
1327 sliding_sync.unsubscribe_to_rooms(&[room_id_0], false);
1329
1330 {
1331 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1332
1333 assert_eq!(room_subscriptions.len(), 1);
1334 assert!(room_subscriptions.contains_key(room_id_1));
1335 }
1336
1337 sliding_sync.subscribe_to_rooms(&[room_id_0, room_id_1], Default::default(), false);
1339
1340 {
1341 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1342
1343 assert_eq!(room_subscriptions.len(), 2);
1344 assert!(room_subscriptions.contains_key(room_id_0));
1345 assert!(room_subscriptions.contains_key(room_id_1));
1346 }
1347
1348 sliding_sync.clear_and_subscribe_to_rooms(
1350 &[room_id_2, room_id_3],
1351 Default::default(),
1352 false,
1353 );
1354
1355 {
1356 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1357
1358 assert_eq!(room_subscriptions.len(), 2);
1359 assert!(room_subscriptions.contains_key(room_id_2));
1360 assert!(room_subscriptions.contains_key(room_id_3));
1361 }
1362
1363 Ok(())
1364 }
1365
1366 #[async_test]
1367 async fn test_resubscribe_to_rooms() -> Result<()> {
1368 let (server, sliding_sync) = new_sliding_sync(vec![
1369 SlidingSyncList::builder("foo")
1370 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1371 ])
1372 .await?;
1373
1374 let stream = sliding_sync.sync();
1375 pin_mut!(stream);
1376
1377 let room_id_0 = room_id!("!r0:bar.org");
1378 let room_id_1 = room_id!("!r1:bar.org");
1379 let room_id_2 = room_id!("!r2:bar.org");
1380
1381 {
1382 let _mock_guard = Mock::given(SlidingSyncMatcher)
1383 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1384 "pos": "1",
1385 "lists": {},
1386 "rooms": {
1387 room_id_0: {
1388 "name": "Room #0",
1389 "initial": true,
1390 },
1391 room_id_1: {
1392 "name": "Room #1",
1393 "initial": true,
1394 },
1395 room_id_2: {
1396 "name": "Room #2",
1397 "initial": true,
1398 },
1399 }
1400 })))
1401 .mount_as_scoped(&server)
1402 .await;
1403
1404 let _ = stream.next().await.unwrap()?;
1405 }
1406
1407 let room0 = sliding_sync.inner.client.get_room(room_id_0).unwrap();
1408
1409 assert!(room0.are_members_synced().not());
1413
1414 {
1415 struct MemberMatcher(OwnedRoomId);
1416
1417 impl Match for MemberMatcher {
1418 fn matches(&self, request: &Request) -> bool {
1419 request.url.path()
1420 == format!("/_matrix/client/r0/rooms/{room_id}/members", room_id = self.0)
1421 && request.method == Method::GET
1422 }
1423 }
1424
1425 let _mock_guard = Mock::given(MemberMatcher(room_id_0.to_owned()))
1426 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1427 "chunk": [],
1428 })))
1429 .mount_as_scoped(&server)
1430 .await;
1431
1432 assert_matches!(room0.request_members().await, Ok(()));
1433 }
1434
1435 assert!(room0.are_members_synced());
1437
1438 sliding_sync.resubscribe_to_rooms(&[room_id_0, room_id_1], None, true);
1439
1440 assert!(room0.are_members_synced().not());
1443
1444 {
1446 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1447
1448 assert!(room_subscriptions.contains_key(room_id_0));
1449 assert!(room_subscriptions.contains_key(room_id_1));
1450 assert!(!room_subscriptions.contains_key(room_id_2));
1451 }
1452
1453 {
1456 struct MemberMatcher(OwnedRoomId);
1457
1458 impl Match for MemberMatcher {
1459 fn matches(&self, request: &Request) -> bool {
1460 request.url.path()
1461 == format!("/_matrix/client/r0/rooms/{room_id}/members", room_id = self.0)
1462 && request.method == Method::GET
1463 }
1464 }
1465
1466 let _mock_guard = Mock::given(MemberMatcher(room_id_0.to_owned()))
1467 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1468 "chunk": [],
1469 })))
1470 .mount_as_scoped(&server)
1471 .await;
1472
1473 assert_matches!(room0.request_members().await, Ok(()));
1474 }
1475
1476 assert!(room0.are_members_synced());
1478
1479 sliding_sync.resubscribe_to_rooms(&[room_id_0], None, false);
1480
1481 assert!(room0.are_members_synced());
1484
1485 {
1487 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1488
1489 assert!(room_subscriptions.contains_key(room_id_0));
1490 assert!(!room_subscriptions.contains_key(room_id_1));
1491 assert!(!room_subscriptions.contains_key(room_id_2));
1492 }
1493 Ok(())
1494 }
1495
1496 #[async_test]
1497 async fn test_room_subscriptions_are_reset_when_session_expires() -> Result<()> {
1498 let (_server, sliding_sync) = new_sliding_sync(vec![
1499 SlidingSyncList::builder("foo")
1500 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1501 ])
1502 .await?;
1503
1504 let room_id_0 = room_id!("!r0:bar.org");
1505 let room_id_1 = room_id!("!r1:bar.org");
1506 let room_id_2 = room_id!("!r2:bar.org");
1507
1508 sliding_sync.subscribe_to_rooms(&[room_id_0, room_id_1], None, false);
1510
1511 {
1512 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1513
1514 assert!(room_subscriptions.contains_key(room_id_0));
1515 assert!(room_subscriptions.contains_key(room_id_1));
1516 assert!(room_subscriptions.contains_key(room_id_2).not());
1517 }
1518
1519 sliding_sync.subscribe_to_rooms(&[room_id_2], None, false);
1521
1522 {
1523 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1524
1525 assert!(room_subscriptions.contains_key(room_id_0));
1526 assert!(room_subscriptions.contains_key(room_id_1));
1527 assert!(room_subscriptions.contains_key(room_id_2));
1528 }
1529
1530 sliding_sync.expire_session().await;
1532
1533 {
1534 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1535
1536 assert!(room_subscriptions.is_empty());
1537 }
1538
1539 sliding_sync.subscribe_to_rooms(&[room_id_2], None, false);
1541
1542 {
1543 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1544
1545 assert!(room_subscriptions.contains_key(room_id_0).not());
1546 assert!(room_subscriptions.contains_key(room_id_1).not());
1547 assert!(room_subscriptions.contains_key(room_id_2));
1548 }
1549
1550 Ok(())
1551 }
1552
1553 #[async_test]
1554 async fn test_add_list() -> Result<()> {
1555 let (_server, sliding_sync) = new_sliding_sync(vec![
1556 SlidingSyncList::builder("foo")
1557 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1558 ])
1559 .await?;
1560
1561 let _stream = sliding_sync.sync();
1562 pin_mut!(_stream);
1563
1564 sliding_sync
1565 .add_list(
1566 SlidingSyncList::builder("bar")
1567 .sync_mode(SlidingSyncMode::new_selective().add_range(50..=60)),
1568 )
1569 .await?;
1570
1571 let lists = sliding_sync.inner.lists.read().await;
1572
1573 assert!(lists.contains_key("foo"));
1574 assert!(lists.contains_key("bar"));
1575
1576 Ok(())
1579 }
1580
1581 #[cfg(feature = "e2e-encryption")]
1582 #[async_test]
1583 async fn test_extensions_to_device_since_is_set() {
1584 use matrix_sdk_base::crypto::store::types::Changes;
1585
1586 let client = logged_in_client(None).await;
1587 let sliding_sync = SlidingSyncBuilder::new("foo".to_owned(), client.clone())
1588 .unwrap()
1589 .with_to_device_extension(assign!(
1590 http::request::ToDevice::default(),
1591 {
1592 enabled: Some(true),
1593 }
1594 ))
1595 .build()
1596 .await
1597 .unwrap();
1598
1599 {
1601 let to_device = &sliding_sync.inner.extensions.to_device;
1602
1603 assert_eq!(to_device.enabled, Some(true));
1604 assert!(to_device.since.is_none());
1605 }
1606
1607 {
1609 let (request, _, _) = sliding_sync.generate_sync_request().await.unwrap();
1610
1611 let to_device = &request.extensions.to_device;
1612
1613 assert_eq!(to_device.enabled, Some(true));
1614 assert!(to_device.since.is_none());
1615 }
1616
1617 let since_token = "depuis".to_owned();
1619
1620 {
1621 if let Some(olm_machine) = &*client.olm_machine().await {
1622 olm_machine
1623 .store()
1624 .save_changes(Changes {
1625 next_batch_token: Some(since_token.clone()),
1626 ..Default::default()
1627 })
1628 .await
1629 .unwrap();
1630 } else {
1631 panic!("Where is the Olm machine?");
1632 }
1633 }
1634
1635 {
1637 let (request, _, _) = sliding_sync.generate_sync_request().await.unwrap();
1638
1639 let to_device = &request.extensions.to_device;
1640
1641 assert_eq!(to_device.enabled, Some(true));
1642 assert_eq!(to_device.since, Some(since_token));
1643 }
1644 }
1645
1646 #[async_test]
1652 #[cfg(feature = "e2e-encryption")]
1653 async fn test_no_pos_with_e2ee_marks_all_tracked_users_as_dirty() -> anyhow::Result<()> {
1654 use matrix_sdk_base::crypto::types::requests::{AnyIncomingResponse, AnyOutgoingRequest};
1655 use matrix_sdk_test::ruma_response_from_json;
1656 use ruma::user_id;
1657
1658 let server = MockServer::start().await;
1659 let client = logged_in_client(Some(server.uri())).await;
1660
1661 let alice = user_id!("@alice:localhost");
1662 let bob = user_id!("@bob:localhost");
1663 let me = user_id!("@example:localhost");
1664
1665 {
1668 let olm_machine = client.olm_machine().await;
1669 let olm_machine = olm_machine.as_ref().unwrap();
1670
1671 olm_machine.update_tracked_users([alice, bob]).await?;
1672
1673 let outgoing_requests = olm_machine.outgoing_requests().await?;
1675
1676 assert_eq!(outgoing_requests.len(), 2);
1677 assert_matches!(outgoing_requests[0].request(), AnyOutgoingRequest::KeysUpload(_));
1678 assert_matches!(outgoing_requests[1].request(), AnyOutgoingRequest::KeysQuery(_));
1679
1680 olm_machine
1682 .mark_request_as_sent(
1683 outgoing_requests[0].request_id(),
1684 AnyIncomingResponse::KeysUpload(&ruma_response_from_json(&json!({
1685 "one_time_key_counts": {}
1686 }))),
1687 )
1688 .await?;
1689
1690 olm_machine
1691 .mark_request_as_sent(
1692 outgoing_requests[1].request_id(),
1693 AnyIncomingResponse::KeysQuery(&ruma_response_from_json(&json!({
1694 "device_keys": {
1695 alice: {},
1696 bob: {},
1697 }
1698 }))),
1699 )
1700 .await?;
1701
1702 let outgoing_requests = olm_machine.outgoing_requests().await?;
1704
1705 assert_eq!(outgoing_requests.len(), 1);
1706 assert_matches!(outgoing_requests[0].request(), AnyOutgoingRequest::KeysQuery(_));
1707
1708 olm_machine
1709 .mark_request_as_sent(
1710 outgoing_requests[0].request_id(),
1711 AnyIncomingResponse::KeysQuery(&ruma_response_from_json(&json!({
1712 "device_keys": {
1713 me: {},
1714 }
1715 }))),
1716 )
1717 .await?;
1718
1719 let outgoing_requests = olm_machine.outgoing_requests().await?;
1721
1722 assert!(outgoing_requests.is_empty());
1723 }
1724
1725 let sync = client
1726 .sliding_sync("test-slidingsync")?
1727 .add_list(SlidingSyncList::builder("new_list"))
1728 .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}))
1729 .build()
1730 .await?;
1731
1732 let (_request, _, _) = sync.generate_sync_request().await?;
1734
1735 {
1737 let olm_machine = client.olm_machine().await;
1738 let olm_machine = olm_machine.as_ref().unwrap();
1739
1740 let outgoing_requests = olm_machine.outgoing_requests().await?;
1742
1743 assert_eq!(outgoing_requests.len(), 1);
1744 assert_matches!(
1745 outgoing_requests[0].request(),
1746 AnyOutgoingRequest::KeysQuery(request) => {
1747 assert!(request.device_keys.contains_key(alice));
1748 assert!(request.device_keys.contains_key(bob));
1749 assert!(request.device_keys.contains_key(me));
1750 }
1751 );
1752
1753 olm_machine
1755 .mark_request_as_sent(
1756 outgoing_requests[0].request_id(),
1757 AnyIncomingResponse::KeysQuery(&ruma_response_from_json(&json!({
1758 "device_keys": {
1759 alice: {},
1760 bob: {},
1761 me: {},
1762 }
1763 }))),
1764 )
1765 .await?;
1766 }
1767
1768 sync.set_pos("chocolat".to_owned()).await;
1770
1771 let (_request, _, _) = sync.generate_sync_request().await?;
1772
1773 {
1775 let olm_machine = client.olm_machine().await;
1776 let olm_machine = olm_machine.as_ref().unwrap();
1777
1778 let outgoing_requests = olm_machine.outgoing_requests().await?;
1780
1781 assert!(outgoing_requests.is_empty());
1782 }
1783
1784 Ok(())
1785 }
1786
1787 #[cfg(feature = "e2e-encryption")]
1788 #[async_test]
1789 async fn test_sliding_sync_doesnt_remember_pos() -> Result<()> {
1790 let server = MockServer::start().await;
1791
1792 #[derive(Deserialize)]
1793 struct PartialRequest {
1794 txn_id: Option<String>,
1795 }
1796
1797 let server_pos = Arc::new(Mutex::new(0));
1798 let _mock_guard = Mock::given(SlidingSyncMatcher)
1799 .respond_with(move |request: &Request| {
1800 let request: PartialRequest = request.body_json().unwrap();
1802 let pos = {
1803 let mut pos = server_pos.lock().unwrap();
1804 let prev = *pos;
1805 *pos += 1;
1806 prev
1807 };
1808
1809 ResponseTemplate::new(200).set_body_json(json!({
1810 "txn_id": request.txn_id,
1811 "pos": pos.to_string(),
1812 }))
1813 })
1814 .mount_as_scoped(&server)
1815 .await;
1816
1817 let client = logged_in_client(Some(server.uri())).await;
1818
1819 let sliding_sync = client.sliding_sync("forgetful-sync")?.build().await?;
1820
1821 {
1823 assert!(sliding_sync.inner.position.lock().await.pos.is_none());
1824
1825 let (request, _, _) = sliding_sync.generate_sync_request().await?;
1826 assert!(request.pos.is_none());
1827 }
1828
1829 let sync = sliding_sync.sync();
1830 pin_mut!(sync);
1831
1832 let next = sync.next().await;
1835 assert_matches!(next, Some(Ok(_update_summary)));
1836
1837 assert_eq!(sliding_sync.inner.position.lock().await.pos.as_deref(), Some("0"));
1838
1839 let restored_fields = restore_sliding_sync_state(&client, &sliding_sync.inner.storage_key)
1840 .await?
1841 .expect("must have restored fields");
1842
1843 assert_eq!(restored_fields.pos.as_deref(), Some("0"));
1846
1847 {
1851 let other_sync = client.sliding_sync("forgetful-sync")?.build().await?;
1852
1853 let mut position_guard = other_sync.inner.position.lock().await;
1854 position_guard.pos = Some("yolo".to_owned());
1855
1856 other_sync.cache_to_storage(&position_guard).await?;
1857 }
1858
1859 {
1861 assert_eq!(sliding_sync.inner.position.lock().await.pos.as_deref(), Some("0"));
1862 let (request, _, _) = sliding_sync.generate_sync_request().await?;
1863 assert_eq!(request.pos.as_deref(), Some("0"));
1864 }
1865
1866 {
1869 let sliding_sync = client.sliding_sync("forgetful-sync")?.build().await?;
1870 assert!(sliding_sync.inner.position.lock().await.pos.is_none());
1871 }
1872
1873 Ok(())
1874 }
1875
1876 #[cfg(feature = "e2e-encryption")]
1877 #[async_test]
1878 async fn test_sliding_sync_does_remember_pos() -> Result<()> {
1879 let server = MockServer::start().await;
1880
1881 #[derive(Deserialize)]
1882 struct PartialRequest {
1883 txn_id: Option<String>,
1884 }
1885
1886 let server_pos = Arc::new(Mutex::new(0));
1887 let _mock_guard = Mock::given(SlidingSyncMatcher)
1888 .respond_with(move |request: &Request| {
1889 let request: PartialRequest = request.body_json().unwrap();
1891 let pos = {
1892 let mut pos = server_pos.lock().unwrap();
1893 let prev = *pos;
1894 *pos += 1;
1895 prev
1896 };
1897
1898 ResponseTemplate::new(200).set_body_json(json!({
1899 "txn_id": request.txn_id,
1900 "pos": pos.to_string(),
1901 }))
1902 })
1903 .mount_as_scoped(&server)
1904 .await;
1905
1906 let client = logged_in_client(Some(server.uri())).await;
1907
1908 let sliding_sync = client.sliding_sync("elephant-sync")?.share_pos().build().await?;
1909
1910 {
1912 let (request, _, _) = sliding_sync.generate_sync_request().await?;
1913
1914 assert!(request.pos.is_none());
1915 assert!(sliding_sync.inner.position.lock().await.pos.is_none());
1916 }
1917
1918 let sync = sliding_sync.sync();
1919 pin_mut!(sync);
1920
1921 let next = sync.next().await;
1924 assert_matches!(next, Some(Ok(_update_summary)));
1925
1926 assert_eq!(sliding_sync.inner.position.lock().await.pos, Some("0".to_owned()));
1927
1928 let restored_fields = restore_sliding_sync_state(&client, &sliding_sync.inner.storage_key)
1929 .await?
1930 .expect("must have restored fields");
1931
1932 assert_eq!(restored_fields.pos.as_deref(), Some("0"));
1935
1936 {
1938 let other_sync = client.sliding_sync("elephant-sync")?.build().await?;
1939
1940 let mut position_guard = other_sync.inner.position.lock().await;
1941 position_guard.pos = Some("42".to_owned());
1942
1943 other_sync.cache_to_storage(&position_guard).await?;
1944 }
1945
1946 {
1948 let (request, _, _) = sliding_sync.generate_sync_request().await?;
1949 assert_eq!(request.pos.as_deref(), Some("42"));
1950 assert_eq!(sliding_sync.inner.position.lock().await.pos.as_deref(), Some("42"));
1951 }
1952
1953 {
1955 let sliding_sync = client.sliding_sync("elephant-sync")?.share_pos().build().await?;
1956 assert_eq!(sliding_sync.inner.position.lock().await.pos.as_deref(), Some("42"));
1957
1958 let (request, _, _) = sliding_sync.generate_sync_request().await?;
1959 assert_eq!(request.pos.as_deref(), Some("42"));
1960 }
1961
1962 sliding_sync.expire_session().await;
1965
1966 {
1967 assert!(sliding_sync.inner.position.lock().await.pos.is_none());
1968
1969 let (request, _, _) = sliding_sync.generate_sync_request().await?;
1970 assert!(request.pos.is_none());
1971 }
1972
1973 {
1975 let sliding_sync = client.sliding_sync("elephant-sync")?.share_pos().build().await?;
1976 assert!(sliding_sync.inner.position.lock().await.pos.is_none());
1977
1978 let (request, _, _) = sliding_sync.generate_sync_request().await?;
1979 assert!(request.pos.is_none());
1980 }
1981
1982 Ok(())
1983 }
1984
1985 #[async_test]
1986 async fn test_stop_sync_loop() -> Result<()> {
1987 let (_server, sliding_sync) = new_sliding_sync(vec![
1988 SlidingSyncList::builder("foo")
1989 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1990 ])
1991 .await?;
1992
1993 let stream = sliding_sync.sync();
1995 pin_mut!(stream);
1996
1997 assert!(stream.next().await.is_some());
1999
2000 sliding_sync.stop_sync()?;
2002
2003 assert!(stream.next().await.is_none());
2005
2006 let stream = sliding_sync.sync();
2008 pin_mut!(stream);
2009
2010 assert!(stream.next().await.is_some());
2012
2013 Ok(())
2014 }
2015
2016 #[async_test]
2017 async fn test_process_read_receipts() -> Result<()> {
2018 let room = owned_room_id!("!pony:example.org");
2019
2020 let server = MockServer::start().await;
2021 let client = logged_in_client(Some(server.uri())).await;
2022 client.event_cache().subscribe().unwrap();
2023
2024 let sliding_sync = client
2025 .sliding_sync("test")?
2026 .with_receipt_extension(
2027 assign!(http::request::Receipts::default(), { enabled: Some(true) }),
2028 )
2029 .add_list(
2030 SlidingSyncList::builder("all")
2031 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=100)),
2032 )
2033 .build()
2034 .await?;
2035
2036 {
2038 let server_response = assign!(http::Response::new("0".to_owned()), {
2039 rooms: BTreeMap::from([(
2040 room.clone(),
2041 http::response::Room::default(),
2042 )])
2043 });
2044
2045 let _summary = {
2046 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2047 sliding_sync
2048 .handle_response(
2049 server_response.clone(),
2050 &mut pos_guard,
2051 RequestedRequiredStates::default(),
2052 )
2053 .await?
2054 };
2055 }
2056
2057 let server_response = assign!(http::Response::new("1".to_owned()), {
2058 extensions: assign!(http::response::Extensions::default(), {
2059 receipts: assign!(http::response::Receipts::default(), {
2060 rooms: BTreeMap::from([
2061 (
2062 room.clone(),
2063 Raw::from_json_string(
2064 json!({
2065 "room_id": room,
2066 "type": "m.receipt",
2067 "content": {
2068 "$event:bar.org": {
2069 "m.read": {
2070 client.user_id().unwrap(): {
2071 "ts": 1436451550,
2072 }
2073 }
2074 }
2075 }
2076 })
2077 .to_string(),
2078 ).unwrap()
2079 )
2080 ])
2081 })
2082 })
2083 });
2084
2085 let summary = {
2086 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2087 sliding_sync
2088 .handle_response(
2089 server_response.clone(),
2090 &mut pos_guard,
2091 RequestedRequiredStates::default(),
2092 )
2093 .await?
2094 };
2095
2096 assert!(summary.rooms.contains(&room));
2097
2098 Ok(())
2099 }
2100
2101 #[async_test]
2102 async fn test_process_marked_unread_room_account_data() -> Result<()> {
2103 let room_id = owned_room_id!("!unicorn:example.org");
2104
2105 let server = MockServer::start().await;
2106 let client = logged_in_client(Some(server.uri())).await;
2107
2108 let sliding_sync = client
2111 .sliding_sync("test")?
2112 .with_account_data_extension(
2113 assign!(http::request::AccountData::default(), { enabled: Some(true) }),
2114 )
2115 .add_list(
2116 SlidingSyncList::builder("all")
2117 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=100)),
2118 )
2119 .build()
2120 .await?;
2121
2122 {
2124 let server_response = assign!(http::Response::new("0".to_owned()), {
2125 rooms: BTreeMap::from([(
2126 room_id.clone(),
2127 http::response::Room::default(),
2128 )])
2129 });
2130
2131 let _summary = {
2132 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2133 sliding_sync
2134 .handle_response(
2135 server_response.clone(),
2136 &mut pos_guard,
2137 RequestedRequiredStates::default(),
2138 )
2139 .await?
2140 };
2141 }
2142
2143 let server_response = make_mark_unread_response("1", room_id.clone(), true, false);
2147
2148 let update_summary = {
2149 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2150 sliding_sync
2151 .handle_response(
2152 server_response.clone(),
2153 &mut pos_guard,
2154 RequestedRequiredStates::default(),
2155 )
2156 .await?
2157 };
2158
2159 assert!(update_summary.rooms.contains(&room_id));
2162
2163 let room = client.get_room(&room_id).unwrap();
2164
2165 assert!(room.is_marked_unread());
2168
2169 let server_response = make_mark_unread_response("2", room_id.clone(), false, true);
2172
2173 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2174 sliding_sync
2175 .handle_response(
2176 server_response.clone(),
2177 &mut pos_guard,
2178 RequestedRequiredStates::default(),
2179 )
2180 .await?;
2181
2182 let room = client.get_room(&room_id).unwrap();
2183
2184 assert!(!room.is_marked_unread());
2185
2186 Ok(())
2187 }
2188
2189 fn make_mark_unread_response(
2190 response_number: &str,
2191 room_id: OwnedRoomId,
2192 unread: bool,
2193 add_rooms_section: bool,
2194 ) -> http::Response {
2195 let rooms = if add_rooms_section {
2196 BTreeMap::from([(room_id.clone(), http::response::Room::default())])
2197 } else {
2198 BTreeMap::new()
2199 };
2200
2201 let extensions = assign!(http::response::Extensions::default(), {
2202 account_data: assign!(http::response::AccountData::default(), {
2203 rooms: BTreeMap::from([
2204 (
2205 room_id,
2206 vec![
2207 Raw::from_json_string(
2208 json!({
2209 "content": {
2210 "unread": unread
2211 },
2212 "type": "m.marked_unread"
2213 })
2214 .to_string(),
2215 ).unwrap()
2216 ]
2217 )
2218 ])
2219 })
2220 });
2221
2222 assign!(http::Response::new(response_number.to_owned()), { rooms: rooms, extensions: extensions })
2223 }
2224
2225 #[async_test]
2226 async fn test_process_rooms_account_data() -> Result<()> {
2227 let room = owned_room_id!("!pony:example.org");
2228
2229 let server = MockServer::start().await;
2230 let client = logged_in_client(Some(server.uri())).await;
2231
2232 let sliding_sync = client
2233 .sliding_sync("test")?
2234 .with_account_data_extension(
2235 assign!(http::request::AccountData::default(), { enabled: Some(true) }),
2236 )
2237 .add_list(
2238 SlidingSyncList::builder("all")
2239 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=100)),
2240 )
2241 .build()
2242 .await?;
2243
2244 {
2246 let server_response = assign!(http::Response::new("0".to_owned()), {
2247 rooms: BTreeMap::from([(
2248 room.clone(),
2249 http::response::Room::default(),
2250 )])
2251 });
2252
2253 let _summary = {
2254 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2255 sliding_sync
2256 .handle_response(
2257 server_response.clone(),
2258 &mut pos_guard,
2259 RequestedRequiredStates::default(),
2260 )
2261 .await?
2262 };
2263 }
2264
2265 let server_response = assign!(http::Response::new("1".to_owned()), {
2266 extensions: assign!(http::response::Extensions::default(), {
2267 account_data: assign!(http::response::AccountData::default(), {
2268 rooms: BTreeMap::from([
2269 (
2270 room.clone(),
2271 vec![
2272 Raw::from_json_string(
2273 json!({
2274 "content": {
2275 "tags": {
2276 "u.work": {
2277 "order": 0.9
2278 }
2279 }
2280 },
2281 "type": "m.tag"
2282 })
2283 .to_string(),
2284 ).unwrap()
2285 ]
2286 )
2287 ])
2288 })
2289 })
2290 });
2291 let summary = {
2292 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2293 sliding_sync
2294 .handle_response(
2295 server_response.clone(),
2296 &mut pos_guard,
2297 RequestedRequiredStates::default(),
2298 )
2299 .await?
2300 };
2301
2302 assert!(summary.rooms.contains(&room));
2303
2304 Ok(())
2305 }
2306
2307 #[async_test]
2308 #[cfg(feature = "e2e-encryption")]
2309 async fn test_process_only_encryption_events() -> Result<()> {
2310 use ruma::OneTimeKeyAlgorithm;
2311
2312 let room = owned_room_id!("!croissant:example.org");
2313
2314 let server = MockServer::start().await;
2315 let client = logged_in_client(Some(server.uri())).await;
2316
2317 let server_response = assign!(http::Response::new("0".to_owned()), {
2318 rooms: BTreeMap::from([(
2319 room.clone(),
2320 assign!(http::response::Room::default(), {
2321 name: Some("Croissants lovers".to_owned()),
2322 timeline: Vec::new(),
2323 }),
2324 )]),
2325
2326 extensions: assign!(http::response::Extensions::default(), {
2327 e2ee: assign!(http::response::E2EE::default(), {
2328 device_one_time_keys_count: BTreeMap::from([(OneTimeKeyAlgorithm::SignedCurve25519, uint!(42))])
2329 }),
2330 to_device: Some(assign!(http::response::ToDevice::default(), {
2331 next_batch: "to-device-token".to_owned(),
2332 })),
2333 })
2334 });
2335
2336 let sliding_sync = client
2340 .sliding_sync("test")?
2341 .with_to_device_extension(
2342 assign!(http::request::ToDevice::default(), { enabled: Some(true)}),
2343 )
2344 .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}))
2345 .build()
2346 .await?;
2347
2348 {
2349 let mut position_guard = sliding_sync.inner.position.clone().lock_owned().await;
2350
2351 sliding_sync
2352 .handle_response(
2353 server_response.clone(),
2354 &mut position_guard,
2355 RequestedRequiredStates::default(),
2356 )
2357 .await?;
2358 }
2359
2360 let uploaded_key_count = client.encryption().uploaded_key_count().await?;
2362 assert_eq!(uploaded_key_count, 42);
2363
2364 {
2365 let olm_machine = &*client.olm_machine_for_testing().await;
2366 assert_eq!(
2367 olm_machine.as_ref().unwrap().store().next_batch_token().await?.as_deref(),
2368 Some("to-device-token")
2369 );
2370 }
2371
2372 assert!(client.get_room(&room).is_none());
2374
2375 let client = logged_in_client(Some(server.uri())).await;
2378
2379 let sliding_sync = client
2380 .sliding_sync("test")?
2381 .add_list(SlidingSyncList::builder("thelist"))
2382 .build()
2383 .await?;
2384
2385 {
2386 let mut position_guard = sliding_sync.inner.position.clone().lock_owned().await;
2387
2388 sliding_sync
2389 .handle_response(
2390 server_response.clone(),
2391 &mut position_guard,
2392 RequestedRequiredStates::default(),
2393 )
2394 .await?;
2395 }
2396
2397 let uploaded_key_count = client.encryption().uploaded_key_count().await?;
2399 assert_eq!(uploaded_key_count, 0);
2400
2401 {
2402 let olm_machine = &*client.olm_machine_for_testing().await;
2403 assert_eq!(
2404 olm_machine.as_ref().unwrap().store().next_batch_token().await?.as_deref(),
2405 None
2406 );
2407 }
2408
2409 assert!(client.get_room(&room).is_some());
2411
2412 let client = logged_in_client(Some(server.uri())).await;
2414
2415 let sliding_sync = client
2416 .sliding_sync("test")?
2417 .add_list(SlidingSyncList::builder("thelist"))
2418 .with_to_device_extension(
2419 assign!(http::request::ToDevice::default(), { enabled: Some(true)}),
2420 )
2421 .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}))
2422 .build()
2423 .await?;
2424
2425 {
2426 let mut position_guard = sliding_sync.inner.position.clone().lock_owned().await;
2427
2428 sliding_sync
2429 .handle_response(
2430 server_response.clone(),
2431 &mut position_guard,
2432 RequestedRequiredStates::default(),
2433 )
2434 .await?;
2435 }
2436
2437 let uploaded_key_count = client.encryption().uploaded_key_count().await?;
2439 assert_eq!(uploaded_key_count, 42);
2440
2441 {
2442 let olm_machine = &*client.olm_machine_for_testing().await;
2443 assert_eq!(
2444 olm_machine.as_ref().unwrap().store().next_batch_token().await?.as_deref(),
2445 Some("to-device-token")
2446 );
2447 }
2448
2449 assert!(client.get_room(&room).is_some());
2451
2452 Ok(())
2453 }
2454
2455 #[async_test]
2456 async fn test_lock_multiple_requests() -> Result<()> {
2457 let server = MockServer::start().await;
2458 let client = logged_in_client(Some(server.uri())).await;
2459
2460 let pos = Arc::new(Mutex::new(0));
2461 let _mock_guard = Mock::given(SlidingSyncMatcher)
2462 .respond_with(move |_: &Request| {
2463 let mut pos = pos.lock().unwrap();
2464 *pos += 1;
2465 ResponseTemplate::new(200).set_body_json(json!({
2466 "pos": pos.to_string(),
2467 "lists": {},
2468 "rooms": {}
2469 }))
2470 })
2471 .mount_as_scoped(&server)
2472 .await;
2473
2474 let sliding_sync = client
2475 .sliding_sync("test")?
2476 .with_to_device_extension(
2477 assign!(http::request::ToDevice::default(), { enabled: Some(true)}),
2478 )
2479 .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}))
2480 .build()
2481 .await?;
2482
2483 let requests = join_all([sliding_sync.sync_once(), sliding_sync.sync_once()]);
2486
2487 for result in requests.await {
2488 result?;
2489 }
2490
2491 Ok(())
2492 }
2493
2494 #[async_test]
2495 async fn test_aborted_request_doesnt_update_future_requests() -> Result<()> {
2496 let server = MockServer::start().await;
2497 let client = logged_in_client(Some(server.uri())).await;
2498
2499 let pos = Arc::new(Mutex::new(0));
2500 let _mock_guard = Mock::given(SlidingSyncMatcher)
2501 .respond_with(move |_: &Request| {
2502 let mut pos = pos.lock().unwrap();
2503 *pos += 1;
2504 ResponseTemplate::new(200)
2506 .set_body_json(json!({
2507 "pos": pos.to_string(),
2508 "lists": {},
2509 "rooms": {}
2510 }))
2511 .set_delay(Duration::from_secs(2))
2512 })
2513 .mount_as_scoped(&server)
2514 .await;
2515
2516 let sliding_sync =
2517 client
2518 .sliding_sync("test")?
2519 .add_list(SlidingSyncList::builder("room-list").sync_mode(
2520 SlidingSyncMode::new_growing(10).maximum_number_of_rooms_to_fetch(100),
2521 ))
2522 .add_list(
2523 SlidingSyncList::builder("another-list")
2524 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
2525 )
2526 .build()
2527 .await?;
2528
2529 let stream = sliding_sync.sync();
2530 pin_mut!(stream);
2531
2532 let cloned_sync = sliding_sync.clone();
2533 spawn(async move {
2534 tokio::time::sleep(Duration::from_millis(100)).await;
2535
2536 cloned_sync
2537 .on_list("another-list", |list| {
2538 list.set_sync_mode(SlidingSyncMode::new_selective().add_range(10..=20));
2539 ready(())
2540 })
2541 .await;
2542 });
2543
2544 assert_matches!(stream.next().await, Some(Ok(_)));
2545
2546 sliding_sync.stop_sync().unwrap();
2547
2548 assert_matches!(stream.next().await, None);
2549
2550 let mut num_requests = 0;
2551
2552 for request in server.received_requests().await.unwrap() {
2553 if !SlidingSyncMatcher.matches(&request) {
2554 continue;
2555 }
2556
2557 let another_list_ranges = if num_requests == 0 {
2558 json!([[0, 10]])
2560 } else {
2561 json!([[10, 20]])
2563 };
2564
2565 num_requests += 1;
2566 assert!(num_requests <= 2, "more than one request hit the server");
2567
2568 let json_value = serde_json::from_slice::<serde_json::Value>(&request.body).unwrap();
2569
2570 if let Err(err) = assert_json_diff::assert_json_matches_no_panic(
2571 &json_value,
2572 &json!({
2573 "conn_id": "test",
2574 "lists": {
2575 "room-list": {
2576 "ranges": [[0, 9]],
2577 "required_state": [
2578 ["m.room.encryption", ""],
2579 ["m.room.tombstone", ""]
2580 ],
2581 },
2582 "another-list": {
2583 "ranges": another_list_ranges,
2584 "required_state": [
2585 ["m.room.encryption", ""],
2586 ["m.room.tombstone", ""]
2587 ],
2588 },
2589 }
2590 }),
2591 assert_json_diff::Config::new(assert_json_diff::CompareMode::Inclusive),
2592 ) {
2593 dbg!(json_value);
2594 panic!("json differ: {err}");
2595 }
2596 }
2597
2598 assert_eq!(num_requests, 2);
2599
2600 Ok(())
2601 }
2602
2603 #[async_test]
2604 async fn test_timeout_zero_list() -> Result<()> {
2605 let (_server, sliding_sync) = new_sliding_sync(vec![]).await?;
2606
2607 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2608
2609 assert!(request.timeout.is_some());
2612
2613 Ok(())
2614 }
2615
2616 #[async_test]
2617 async fn test_timeout_one_list() -> Result<()> {
2618 let (_server, sliding_sync) = new_sliding_sync(vec![
2619 SlidingSyncList::builder("foo").sync_mode(SlidingSyncMode::new_growing(10)),
2620 ])
2621 .await?;
2622
2623 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2624
2625 assert!(request.timeout.is_none());
2627
2628 {
2630 let server_response = assign!(http::Response::new("0".to_owned()), {
2631 lists: BTreeMap::from([(
2632 "foo".to_owned(),
2633 assign!(http::response::List::default(), {
2634 count: uint!(7),
2635 })
2636 )])
2637 });
2638
2639 let _summary = {
2640 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2641 sliding_sync
2642 .handle_response(
2643 server_response.clone(),
2644 &mut pos_guard,
2645 RequestedRequiredStates::default(),
2646 )
2647 .await?
2648 };
2649 }
2650
2651 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2652
2653 assert!(request.timeout.is_some());
2655
2656 Ok(())
2657 }
2658
2659 #[async_test]
2660 async fn test_timeout_three_lists() -> Result<()> {
2661 let (_server, sliding_sync) = new_sliding_sync(vec![
2662 SlidingSyncList::builder("foo").sync_mode(SlidingSyncMode::new_growing(10)),
2663 SlidingSyncList::builder("bar").sync_mode(SlidingSyncMode::new_paging(10)),
2664 SlidingSyncList::builder("baz")
2665 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
2666 ])
2667 .await?;
2668
2669 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2670
2671 assert!(request.timeout.is_none());
2673
2674 {
2676 let server_response = assign!(http::Response::new("0".to_owned()), {
2677 lists: BTreeMap::from([(
2678 "foo".to_owned(),
2679 assign!(http::response::List::default(), {
2680 count: uint!(7),
2681 })
2682 )])
2683 });
2684
2685 let _summary = {
2686 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2687 sliding_sync
2688 .handle_response(
2689 server_response.clone(),
2690 &mut pos_guard,
2691 RequestedRequiredStates::default(),
2692 )
2693 .await?
2694 };
2695 }
2696
2697 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2698
2699 assert!(request.timeout.is_none());
2701
2702 {
2704 let server_response = assign!(http::Response::new("1".to_owned()), {
2705 lists: BTreeMap::from([(
2706 "bar".to_owned(),
2707 assign!(http::response::List::default(), {
2708 count: uint!(7),
2709 })
2710 )])
2711 });
2712
2713 let _summary = {
2714 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2715 sliding_sync
2716 .handle_response(
2717 server_response.clone(),
2718 &mut pos_guard,
2719 RequestedRequiredStates::default(),
2720 )
2721 .await?
2722 };
2723 }
2724
2725 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2726
2727 assert!(request.timeout.is_some());
2729
2730 Ok(())
2731 }
2732
2733 #[async_test]
2734 async fn test_sync_beat_is_notified_on_sync_response() -> Result<()> {
2735 let server = MockServer::start().await;
2736 let client = logged_in_client(Some(server.uri())).await;
2737
2738 let _mock_guard = Mock::given(SlidingSyncMatcher)
2739 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
2740 "pos": "0",
2741 "lists": {},
2742 "rooms": {}
2743 })))
2744 .mount_as_scoped(&server)
2745 .await;
2746
2747 let sliding_sync = client
2748 .sliding_sync("test")?
2749 .with_to_device_extension(
2750 assign!(http::request::ToDevice::default(), { enabled: Some(true)}),
2751 )
2752 .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}))
2753 .build()
2754 .await?;
2755
2756 let sliding_sync = Arc::new(sliding_sync);
2757
2758 let sync_beat_listener = client.inner.sync_beat.listen();
2760 sliding_sync.sync_once().await?;
2761
2762 assert!(sync_beat_listener.wait_timeout(Duration::from_secs(1)).is_some());
2764 Ok(())
2765 }
2766
2767 #[async_test]
2768 async fn test_sync_beat_is_not_notified_on_sync_failure() -> Result<()> {
2769 let server = MockServer::start().await;
2770 let client = logged_in_client(Some(server.uri())).await;
2771
2772 let _mock_guard = Mock::given(SlidingSyncMatcher)
2773 .respond_with(ResponseTemplate::new(404))
2774 .mount_as_scoped(&server)
2775 .await;
2776
2777 let sliding_sync = client
2778 .sliding_sync("test")?
2779 .with_to_device_extension(
2780 assign!(http::request::ToDevice::default(), { enabled: Some(true)}),
2781 )
2782 .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}))
2783 .build()
2784 .await?;
2785
2786 let sliding_sync = Arc::new(sliding_sync);
2787
2788 let sync_beat_listener = client.inner.sync_beat.listen();
2790 let sync_result = sliding_sync.sync_once().await;
2791 assert!(sync_result.is_err());
2792
2793 assert!(sync_beat_listener.wait_timeout(Duration::from_secs(1)).is_none());
2795
2796 Ok(())
2797 }
2798
2799 #[async_test]
2800 async fn test_state_store_lock_is_released_before_calling_handlers() -> Result<()> {
2801 let server = MatrixMockServer::new().await;
2802 let client = server.client_builder().build().await;
2803 let room_id = room_id!("!mu5hr00m:example.org");
2804
2805 let _sync_mock_guard = Mock::given(SlidingSyncMatcher)
2806 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
2807 "pos": "0",
2808 "lists": {},
2809 "extensions": {
2810 "account_data": {
2811 "global": [
2812 {
2813 "type": "m.direct",
2814 "content": {
2815 "@de4dlockh0lmes:example.org": [
2816 "!mu5hr00m:example.org"
2817 ]
2818 }
2819 }
2820 ]
2821 }
2822 },
2823 "rooms": {
2824 room_id: {
2825 "name": "Mario Bros Fanbase Room",
2826 "initial": true,
2827 },
2828 }
2829 })))
2830 .mount_as_scoped(server.server())
2831 .await;
2832
2833 let f = EventFactory::new().room(room_id);
2834
2835 Mock::given(method("GET"))
2836 .and(wiremock::matchers::path_regex(r"/_matrix/client/v3/rooms/.*/members"))
2837 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
2838 "chunk": [
2839 f.member(&ALICE).membership(MembershipState::Join).into_raw_timeline(),
2840 ]
2841 })))
2842 .mount(server.server())
2843 .await;
2844
2845 let (tx, rx) = tokio::sync::oneshot::channel();
2846
2847 let tx = Arc::new(Mutex::new(Some(tx)));
2848 client.add_event_handler(move |_: DirectEvent, client: Client| async move {
2849 let members =
2851 client.get_room(room_id).unwrap().members(RoomMemberships::JOIN).await.unwrap();
2852 assert_eq!(members.len(), 1);
2853 tx.lock().unwrap().take().expect("sender consumed multiple times").send(()).unwrap();
2854 });
2855
2856 let sliding_sync = client
2857 .sliding_sync("test")?
2858 .add_list(SlidingSyncList::builder("thelist"))
2859 .with_account_data_extension(
2860 assign!(http::request::AccountData::default(), { enabled: Some(true) }),
2861 )
2862 .build()
2863 .await?;
2864
2865 tokio::time::timeout(Duration::from_secs(5), sliding_sync.sync_once())
2866 .await
2867 .expect("Sync did not complete in time")
2868 .expect("Sync failed");
2869
2870 tokio::time::timeout(Duration::from_secs(5), rx)
2872 .await
2873 .expect("Event handler did not complete in time")
2874 .expect("Event handler failed");
2875
2876 Ok(())
2877 }
2878
2879 #[cfg(feature = "e2e-encryption")]
2880 #[async_test]
2881 async fn test_syncing_one_time_key_counts_updates() -> Result<()> {
2882 macro_rules! assert_key_count {
2883 ($client: ident, $count:literal) => {{
2884 let machine = $client.olm_machine().await;
2885 let uploaded_key_counts =
2886 machine.as_ref().unwrap().uploaded_key_count().await.unwrap();
2887 assert_eq!(uploaded_key_counts, $count)
2888 }};
2889 }
2890
2891 macro_rules! sync_with_key_count {
2892 ($client: ident, $server:ident, $count:literal) => {
2893 let count = Some($count);
2894 sync_with_key_count!($client, $server, count);
2895 };
2896 ($client: ident, $server:ident, $count:ident) => {{
2897 let count: Option<u32> = $count;
2898
2899 let template = if let Some(count) = count {
2900 ResponseTemplate::new(200).set_body_json(json!({
2901 "pos": "0",
2902 "lists": {},
2903 "extensions": {
2904 "e2ee": {
2905 "device_one_time_keys_count": {
2906 "signed_curve25519": count,
2907 }
2908 }
2909 },
2910 }))
2911 } else {
2912 ResponseTemplate::new(200).set_body_json(json!({
2913 "pos": "0",
2914 "lists": {},
2915 "extensions": {
2916 "e2ee": {}
2917 },
2918 }))
2919 };
2920
2921 let _sync_mock_guard = Mock::given(SlidingSyncMatcher)
2922 .respond_with(template)
2923 .mount_as_scoped($server.server())
2924 .await;
2925
2926 let sliding_sync = $client
2927 .sliding_sync("test")?
2928 .with_e2ee_extension(
2929 assign!(http::request::E2EE::default(), { enabled: Some(true)}),
2930 )
2931 .build()
2932 .await?;
2933
2934 tokio::time::timeout(Duration::from_secs(5), sliding_sync.sync_once())
2935 .await
2936 .expect("Sync did not complete in time")
2937 .expect("Sync failed");
2938 }}
2939 }
2940
2941 let server = MatrixMockServer::new().await;
2942 let client = server.client_builder().build().await;
2943
2944 server.mock_upload_keys().ok_with_signed_curve_key_count(50).mock_once().mount().await;
2945
2946 assert_key_count!(client, 0);
2948
2949 sync_with_key_count!(client, server, None);
2951 assert_key_count!(client, 50);
2952
2953 sync_with_key_count!(client, server, None);
2955 assert_key_count!(client, 50);
2956
2957 sync_with_key_count!(client, server, 10);
2959 assert_key_count!(client, 10);
2960
2961 Ok(())
2962 }
2963}