Skip to main content

matrix_sdk_base/
sliding_sync.rs

1// Copyright 2023 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Extend `BaseClient` with capabilities to handle MSC4186.
16
17#[cfg(feature = "e2e-encryption")]
18use matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent;
19use matrix_sdk_common::timer;
20use ruma::{
21    OwnedRoomId, api::client::sync::sync_events::v5 as http, events::receipt::SyncReceiptEvent,
22    serde::Raw,
23};
24use tokio::sync::MutexGuard;
25use tracing::{instrument, trace};
26
27use super::BaseClient;
28use crate::{
29    RequestedRequiredStates,
30    error::Result,
31    response_processors as processors,
32    store::{AvatarCache, ambiguity_map::AmbiguityCache},
33    sync::{RoomUpdates, SyncResponse},
34};
35
36impl BaseClient {
37    /// Processes the E2EE-related events from the Sliding Sync response.
38    ///
39    /// In addition to writes to the crypto store, this may also write into the
40    /// state store, in particular it may write latest-events to the state
41    /// store.
42    ///
43    /// Returns whether any change happened.
44    #[cfg(feature = "e2e-encryption")]
45    pub async fn process_sliding_sync_e2ee(
46        &self,
47        to_device: Option<&http::response::ToDevice>,
48        e2ee: &http::response::E2EE,
49        state_store_guard: &MutexGuard<'_, ()>,
50    ) -> Result<Option<Vec<ProcessedToDeviceEvent>>> {
51        if to_device.is_none() && e2ee.is_empty() {
52            return Ok(None);
53        }
54
55        trace!(
56            to_device_events =
57                to_device.map(|to_device| to_device.events.len()).unwrap_or_default(),
58            device_one_time_keys_count = e2ee.device_one_time_keys_count.len(),
59            device_unused_fallback_key_types =
60                e2ee.device_unused_fallback_key_types.as_ref().map(|v| v.len()),
61            "Processing sliding sync e2ee events",
62        );
63
64        let olm_machine = self.olm_machine().await;
65
66        let context = processors::Context::default();
67
68        let processors::e2ee::to_device::Output { processed_to_device_events } =
69            processors::e2ee::to_device::from_msc4186(
70                to_device,
71                e2ee,
72                olm_machine.as_ref(),
73                &self.decryption_settings,
74            )
75            .await?;
76
77        processors::changes::save_and_apply(
78            context,
79            &self.state_store,
80            state_store_guard,
81            &self.ignore_user_list_changes,
82            None,
83        )
84        .await?;
85
86        Ok(Some(processed_to_device_events))
87    }
88
89    /// Process a response from a sliding sync call.
90    ///
91    /// # Arguments
92    ///
93    /// - `response` - The response that we received after a successful sliding
94    ///   sync.
95    #[instrument(skip_all, level = "trace")]
96    pub async fn process_sliding_sync(
97        &self,
98        response: &http::Response,
99        requested_required_states: &RequestedRequiredStates,
100        state_store_guard: &MutexGuard<'_, ()>,
101    ) -> Result<SyncResponse> {
102        let http::Response { rooms, lists, extensions, .. } = response;
103
104        trace!(
105            rooms = rooms.len(),
106            lists = lists.len(),
107            has_extensions = !extensions.is_empty(),
108            "Processing sliding sync room events"
109        );
110
111        if rooms.is_empty() && extensions.is_empty() {
112            // we received a room reshuffling event only, there won't be
113            // anything for us to process. stop early
114            return Ok(SyncResponse::default());
115        }
116
117        let _timer = timer!(tracing::Level::TRACE, "_method");
118
119        let mut context = processors::Context::default();
120
121        let state_store = self.state_store.clone();
122        let mut ambiguity_cache = AmbiguityCache::new(state_store.inner.clone());
123        let mut avatar_cache = AvatarCache::new(state_store.inner.clone());
124
125        let global_account_data_processor =
126            processors::account_data::global(&extensions.account_data.global);
127        let push_rules = self.get_push_rules(&global_account_data_processor).await?;
128
129        let mut room_updates = RoomUpdates::default();
130        let mut notifications = Default::default();
131
132        let user_id = self
133            .session_meta()
134            .expect("Sliding sync shouldn't run without an authenticated user")
135            .user_id
136            .to_owned();
137
138        for (room_id, room_response) in rooms {
139            let Some((room_info, room_update)) = processors::room::msc4186::update_any_room(
140                &mut context,
141                &user_id,
142                processors::room::RoomCreationData::new(
143                    room_id,
144                    requested_required_states,
145                    &mut ambiguity_cache,
146                    &mut avatar_cache,
147                ),
148                room_response,
149                &extensions.account_data.rooms,
150                #[cfg(feature = "e2e-encryption")]
151                processors::e2ee::E2EE::new(
152                    self.olm_machine().await.as_ref(),
153                    &self.decryption_settings,
154                    self.handle_verification_events,
155                ),
156                processors::notification::Notification::new(
157                    &push_rules,
158                    &mut notifications,
159                    &self.state_store,
160                ),
161            )
162            .await?
163            else {
164                continue;
165            };
166
167            context.state_changes.add_room(room_info);
168
169            let room_id = room_id.to_owned();
170
171            use processors::room::msc4186::RoomUpdateKind;
172
173            match room_update {
174                RoomUpdateKind::Joined(joined_room_update) => {
175                    room_updates.joined.insert(room_id, joined_room_update);
176                }
177                RoomUpdateKind::Left(left_room_update) => {
178                    room_updates.left.insert(room_id, left_room_update);
179                }
180                RoomUpdateKind::Invited(invited_room_update) => {
181                    room_updates.invited.insert(room_id, invited_room_update);
182                }
183                RoomUpdateKind::Knocked(knocked_room_update) => {
184                    room_updates.knocked.insert(room_id, knocked_room_update);
185                }
186            }
187        }
188
189        // Handle read receipts and typing notifications independently of the
190        // rooms: these both live in a different subsection of the server's
191        // response, so they may exist without any update for the associated
192        // room.
193        processors::room::msc4186::extensions::dispatch_typing_ephemeral_events(
194            &extensions.typing,
195            &mut room_updates.joined,
196        );
197
198        // Handle room account data.
199        processors::room::msc4186::extensions::room_account_data(
200            &mut context,
201            &extensions.account_data,
202            &mut room_updates,
203            &self.state_store,
204        );
205
206        global_account_data_processor.apply(&mut context, &state_store).await;
207
208        context.state_changes.ambiguity_maps = ambiguity_cache.cache;
209
210        // Persist any global profile updates received through the profiles
211        // extension.
212        context.state_changes.global_profiles = extensions.profiles.users.clone();
213
214        // Save the changes and apply them.
215        processors::changes::save_and_apply(
216            context,
217            &self.state_store,
218            state_store_guard,
219            &self.ignore_user_list_changes,
220            None,
221        )
222        .await?;
223
224        // Profile-only updates don't modify any rooms, so nothing else
225        // broadcasts them. Surface the change so subscribers can react
226        // accordingly.
227        self.notify_global_profile_updates(
228            extensions.profiles.users.keys().cloned().collect(),
229            state_store_guard,
230        )?;
231
232        // Handle sticky events, now that the rooms are saved and their state is
233        // up to date.
234        #[cfg(feature = "unstable-msc4354")]
235        {
236            #[cfg(feature = "e2e-encryption")]
237            let olm_machine = self.olm_machine().await;
238
239            processors::room::msc4186::extensions::sticky_events(
240                &extensions.sticky_events,
241                rooms,
242                &room_updates.joined,
243                &self.state_store,
244                #[cfg(feature = "e2e-encryption")]
245                &processors::e2ee::E2EE::new(
246                    olm_machine.as_ref(),
247                    &self.decryption_settings,
248                    false,
249                ),
250            )
251            .await;
252        }
253
254        let mut context = processors::Context::default();
255
256        // Now that all the rooms information have been saved, update the
257        // display name of the updated rooms (which relies on information stored
258        // in the database).
259        processors::room::display_name::update_for_rooms(
260            &mut context,
261            &room_updates,
262            &self.state_store,
263        )
264        .await;
265
266        // Save the new display name updates if any.
267        processors::changes::save_only(context, &self.state_store, state_store_guard).await?;
268
269        Ok(SyncResponse {
270            rooms: room_updates,
271            notifications,
272            presence: Default::default(),
273            account_data: extensions.account_data.global.clone(),
274            to_device: Default::default(),
275        })
276    }
277
278    /// Process the `receipts` extension, and compute (and save) the unread
279    /// counts based on read receipts, for a particular room.
280    #[doc(hidden)]
281    pub async fn process_sliding_sync_receipts_extension_for_room(
282        &self,
283        room_id: &OwnedRoomId,
284        response: &http::Response,
285        state_store_guard: &MutexGuard<'_, ()>,
286    ) -> Result<Option<Raw<SyncReceiptEvent>>> {
287        let mut context = processors::Context::default();
288
289        let mut save_context = false;
290
291        // Handle the receipt ephemeral event.
292        let receipt_ephemeral_event = if let Some(receipt_ephemeral_event) =
293            response.extensions.receipts.rooms.get(room_id)
294        {
295            processors::room::msc4186::extensions::dispatch_receipt_ephemeral_event_for_room(
296                &mut context,
297                room_id,
298                receipt_ephemeral_event,
299            );
300            save_context = true;
301            Some(receipt_ephemeral_event.clone())
302        } else {
303            None
304        };
305
306        // Save the new `RoomInfo` if updated.
307        if save_context {
308            processors::changes::save_only(context, &self.state_store, state_store_guard).await?;
309        }
310
311        Ok(receipt_ephemeral_event)
312    }
313}
314
315#[cfg(all(test, not(target_family = "wasm")))]
316mod tests {
317    use std::collections::{BTreeMap, HashSet};
318
319    use assert_matches::assert_matches;
320    use matrix_sdk_test::async_test;
321    #[cfg(feature = "unstable-msc4354")]
322    use ruma::events::AnySyncTimelineEvent;
323    use ruma::{
324        JsOption, MxcUri, OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, UserId,
325        api::client::sync::sync_events::UnreadNotificationsCount,
326        assign, event_id,
327        events::{
328            GlobalAccountDataEventContent, StateEventContent, StateEventType,
329            direct::{DirectEventContent, DirectUserIdentifier, OwnedDirectUserIdentifier},
330            room::{
331                avatar::RoomAvatarEventContent,
332                canonical_alias::RoomCanonicalAliasEventContent,
333                encryption::RoomEncryptionEventContent,
334                member::{MembershipState, RoomMemberEventContent},
335                name::RoomNameEventContent,
336                pinned_events::RoomPinnedEventsEventContent,
337            },
338        },
339        mxc_uri, owned_event_id, owned_mxc_uri, owned_user_id,
340        profile::{ProfileFieldName, UserProfileChanges, UserProfileUpdate},
341        room_alias_id, room_id,
342        serde::Raw,
343        uint, user_id,
344    };
345    use serde_json::json;
346    #[cfg(feature = "unstable-msc4426")]
347    use stream_assert::{assert_pending, assert_ready};
348
349    use super::http;
350    use crate::{
351        BaseClient, DmRoomDefinition, EncryptionState, RequestedRequiredStates,
352        RoomInfoNotableUpdate, RoomState, SessionMeta,
353        client::ThreadingSupport,
354        room::{RoomHero, RoomInfoNotableUpdateReasons},
355        store::{RoomLoadSettings, StoreConfig},
356        test_utils::logged_in_base_client,
357    };
358
359    #[async_test]
360    async fn test_invited_state_without_update_emits_invited_room() {
361        let client = logged_in_base_client(None).await;
362        let room_id = room_id!("!invite:e.uk");
363        let user_id = client.session_meta().unwrap().user_id.to_owned();
364
365        let mut room = http::response::Room::new();
366        room.invite_state = Some(invite_state_for(&user_id, MembershipState::Invite));
367
368        let response = response_with_room(room_id, room);
369
370        let sync_resp = client
371            .process_sliding_sync(
372                &response,
373                &RequestedRequiredStates::default(),
374                &client.state_store_lock().lock().await,
375            )
376            .await
377            .unwrap();
378
379        assert!(sync_resp.rooms.invited.contains_key(room_id));
380    }
381
382    use matrix_sdk_common::cross_process_lock::CrossProcessLockConfig;
383    use ruma::events::AnyStrippedStateEvent;
384
385    fn invite_state_for(
386        user_id: &UserId,
387        membership: MembershipState,
388    ) -> Vec<Raw<AnyStrippedStateEvent>> {
389        let content = RoomMemberEventContent::new(membership);
390
391        let raw: Raw<AnyStrippedStateEvent> = Raw::from_json_string(
392            serde_json::json!({
393                "type": "m.room.member",
394                "state_key": user_id,
395                "content": content,
396            })
397            .to_string(),
398        )
399        .unwrap();
400
401        vec![raw]
402    }
403
404    #[async_test]
405    async fn test_knocked_state_emits_invited_room() {
406        let client = logged_in_base_client(None).await;
407        let room_id = room_id!("!knock:e.uk");
408        let user_id = client.session_meta().unwrap().user_id.to_owned();
409
410        let mut room = http::response::Room::new();
411        room.invite_state = Some(invite_state_for(&user_id, MembershipState::Knock));
412
413        let response = response_with_room(room_id, room);
414
415        let sync_resp = client
416            .process_sliding_sync(
417                &response,
418                &RequestedRequiredStates::default(),
419                &client.state_store_lock().lock().await,
420            )
421            .await
422            .unwrap();
423
424        // ✅ Knocked rooms surface as invited rooms in Sliding Sync
425        assert!(sync_resp.rooms.invited.contains_key(room_id));
426    }
427
428    #[async_test]
429    async fn test_notification_count_set() {
430        let client = logged_in_base_client(None).await;
431
432        let mut response = http::Response::new("42".to_owned());
433        let room_id = room_id!("!room:example.org");
434        let count = assign!(UnreadNotificationsCount::default(), {
435            highlight_count: Some(uint!(13)),
436            notification_count: Some(uint!(37)),
437        });
438
439        response.rooms.insert(
440            room_id.to_owned(),
441            assign!(http::response::Room::new(), {
442                unread_notifications: count.clone()
443            }),
444        );
445
446        let sync_response = client
447            .process_sliding_sync(
448                &response,
449                &RequestedRequiredStates::default(),
450                &client.state_store_lock().lock().await,
451            )
452            .await
453            .expect("Failed to process sync");
454
455        // Check it's present in the response.
456        let room = sync_response.rooms.joined.get(room_id).unwrap();
457        assert_eq!(room.unread_notifications, count.clone().into());
458
459        // Check it's been updated in the store.
460        let room = client.get_room(room_id).expect("found room");
461        assert_eq!(room.unread_notification_counts(), count.into());
462    }
463
464    #[async_test]
465    async fn test_can_process_empty_sliding_sync_response() {
466        let client = logged_in_base_client(None).await;
467        let empty_response = http::Response::new("5".to_owned());
468        client
469            .process_sliding_sync(
470                &empty_response,
471                &RequestedRequiredStates::default(),
472                &client.state_store_lock().lock().await,
473            )
474            .await
475            .expect("Failed to process sync");
476    }
477
478    /// A sticky `m.rtc.member` event sent by Alice, sticky for 10 minutes from
479    /// now according to the server.
480    #[cfg(feature = "unstable-msc4354")]
481    fn sticky_event(event_id: &str, content: serde_json::Value) -> Raw<AnySyncTimelineEvent> {
482        serde_json::from_value(json!({
483            "type": "m.rtc.member",
484            "sender": "@alice:example.org",
485            "event_id": event_id,
486            "origin_server_ts": 1,
487            "content": content,
488            "msc4354_sticky": { "duration_ms": 600_000 },
489            "unsigned": { "msc4354_sticky_duration_ttl_ms": 600_000 },
490        }))
491        .unwrap()
492    }
493
494    #[cfg(feature = "unstable-msc4354")]
495    #[async_test]
496    async fn test_sticky_events_are_ingested_from_the_timeline() {
497        let client = logged_in_base_client(None).await;
498        let room_id = room_id!("!room:example.org");
499
500        let mut room = http::response::Room::new();
501        room.timeline.push(sticky_event(
502            "$a:example.org",
503            json!({ "msc4354_sticky_key": "slot", "application": "m.call" }),
504        ));
505
506        client
507            .process_sliding_sync(
508                &response_with_room(room_id, room),
509                &RequestedRequiredStates::default(),
510                &client.state_store_lock().lock().await,
511            )
512            .await
513            .expect("Failed to process sync");
514
515        let room = client.get_room(room_id).expect("found room");
516        let live = room.sticky_events().live();
517        assert_eq!(live.len(), 1);
518        assert_eq!(live[0].key.sender, "@alice:example.org");
519        assert_eq!(live[0].key.sticky_key, "slot");
520        assert_eq!(live[0].event_id, "$a:example.org");
521        assert!(live[0].encryption_info().is_none());
522    }
523
524    #[cfg(feature = "unstable-msc4354")]
525    #[async_test]
526    async fn test_sticky_events_are_ingested_from_the_extension() {
527        let client = logged_in_base_client(None).await;
528        let room_id = room_id!("!room:example.org");
529
530        // The room must be known for its sticky events to be tracked.
531        let mut response = response_with_room(room_id, http::response::Room::new());
532
533        let mut sticky_room = http::response::StickyEventsRoom::default();
534        sticky_room.events = vec![
535            sticky_event(
536                "$a:example.org",
537                json!({ "msc4354_sticky_key": "laptop", "application": "m.call" }),
538            ),
539            sticky_event(
540                "$b:example.org",
541                json!({ "msc4354_sticky_key": "phone", "application": "m.call" }),
542            ),
543        ];
544        response.extensions.sticky_events.rooms.insert(room_id.to_owned(), sticky_room);
545
546        // Sticky events for a room we don't know about are ignored.
547        let mut unknown_room = http::response::StickyEventsRoom::default();
548        unknown_room.events = vec![sticky_event(
549            "$c:example.org",
550            json!({ "msc4354_sticky_key": "slot", "application": "m.call" }),
551        )];
552        response
553            .extensions
554            .sticky_events
555            .rooms
556            .insert(room_id!("!unknown:example.org").to_owned(), unknown_room);
557
558        client
559            .process_sliding_sync(
560                &response,
561                &RequestedRequiredStates::default(),
562                &client.state_store_lock().lock().await,
563            )
564            .await
565            .expect("Failed to process sync");
566
567        let room = client.get_room(room_id).expect("found room");
568        let mut live = room.sticky_events().live();
569        live.sort_by(|a, b| a.key.sticky_key.cmp(&b.key.sticky_key));
570        assert_eq!(live.len(), 2);
571        assert_eq!(live[0].key.sticky_key, "laptop");
572        assert_eq!(live[1].key.sticky_key, "phone");
573
574        assert!(client.get_room(room_id!("!unknown:example.org")).is_none());
575    }
576
577    #[cfg(feature = "unstable-msc4354")]
578    #[async_test]
579    async fn test_sticky_events_are_forgotten_when_leaving_the_room() {
580        let client = logged_in_base_client(None).await;
581        let room_id = room_id!("!room:example.org");
582        let user_id = client.session_meta().unwrap().user_id.to_owned();
583
584        let mut room = http::response::Room::new();
585        set_room_joined(&mut room, &user_id);
586        room.timeline.push(sticky_event(
587            "$a:example.org",
588            json!({ "msc4354_sticky_key": "slot", "application": "m.call" }),
589        ));
590
591        client
592            .process_sliding_sync(
593                &response_with_room(room_id, room),
594                &RequestedRequiredStates::default(),
595                &client.state_store_lock().lock().await,
596            )
597            .await
598            .expect("Failed to process sync");
599
600        let room = client.get_room(room_id).expect("found room");
601        let mut subscriber = room.sticky_events().subscribe();
602        assert_eq!(room.sticky_events().live().len(), 1);
603
604        // We leave the room.
605        let mut room_response = http::response::Room::new();
606        set_room_left(&mut room_response, &user_id);
607
608        client
609            .process_sliding_sync(
610                &response_with_room(room_id, room_response),
611                &RequestedRequiredStates::default(),
612                &client.state_store_lock().lock().await,
613            )
614            .await
615            .expect("Failed to process sync");
616
617        assert_eq!(room.state(), RoomState::Left);
618        assert!(room.sticky_events().live().is_empty());
619
620        let update = subscriber.try_recv().unwrap();
621        assert_matches!(update.removed.as_slice(), [(key, crate::sticky::RemovalReason::RoomLeft)] => {
622            assert_eq!(key.sticky_key, "slot");
623        });
624    }
625
626    #[cfg(all(feature = "unstable-msc4354", feature = "e2e-encryption"))]
627    #[async_test]
628    async fn test_undecryptable_sticky_event_is_kept_aside_not_mapped() {
629        let client = logged_in_base_client(None).await;
630        let room_id = room_id!("!room:example.org");
631
632        // An encrypted sticky event for which we have no room key.
633        let encrypted: Raw<AnySyncTimelineEvent> = serde_json::from_value(json!({
634            "type": "m.room.encrypted",
635            "sender": "@alice:example.org",
636            "event_id": "$enc:example.org",
637            "origin_server_ts": 1,
638            "content": {
639                "algorithm": "m.megolm.v1.aes-sha2",
640                "ciphertext": "AAAA",
641                "sender_key": "senderkey",
642                "session_id": "session",
643                "device_id": "DEV",
644            },
645            "msc4354_sticky": { "duration_ms": 600_000 },
646            "unsigned": { "msc4354_sticky_duration_ttl_ms": 600_000 },
647        }))
648        .unwrap();
649
650        let mut room = http::response::Room::new();
651        room.timeline.push(encrypted.clone());
652        let mut response = response_with_room(room_id, room);
653
654        // Once in the timeline, once in the extension: neither may end up in the
655        // map as an `m.room.encrypted` entry, its real type and key are unknown.
656        let mut sticky_room = http::response::StickyEventsRoom::default();
657        sticky_room.events = vec![encrypted];
658        response.extensions.sticky_events.rooms.insert(room_id.to_owned(), sticky_room);
659
660        client
661            .process_sliding_sync(
662                &response,
663                &RequestedRequiredStates::default(),
664                &client.state_store_lock().lock().await,
665            )
666            .await
667            .expect("Failed to process sync");
668
669        let room = client.get_room(room_id).expect("found room");
670        assert!(room.sticky_events().live().is_empty());
671        assert!(room.sticky_events().has_pending());
672    }
673
674    #[async_test]
675    async fn test_profiles_extension_is_persisted_from_sliding_sync() {
676        let client = logged_in_base_client(None).await;
677
678        let alice = user_id!("@alice:e.uk");
679        let bob = user_id!("@bob:e.uk");
680
681        // Given a sliding sync response carrying the profiles extension
682        // (MSC4262) for two users, and no rooms.
683        let mut response = http::Response::new("0".to_owned());
684        response.extensions.profiles.users.insert(
685            alice.to_owned(),
686            make_profile_update(ProfileFieldName::DisplayName, json!("Alice")),
687        );
688        response.extensions.profiles.users.insert(
689            bob.to_owned(),
690            make_profile_update(ProfileFieldName::DisplayName, json!("Bob")),
691        );
692
693        // When the response is processed.
694        client
695            .process_sliding_sync(
696                &response,
697                &RequestedRequiredStates::default(),
698                &client.state_store_lock().lock().await,
699            )
700            .await
701            .expect("Failed to process sync");
702
703        // Then both users' global profiles are persisted in the store.
704        let store = client.state_store();
705
706        let alice_profile = store
707            .get_global_profile(alice)
708            .await
709            .expect("Failed to read profile")
710            .expect("Alice's profile should be saved");
711        let alice_map: BTreeMap<String, serde_json::Value> = alice_profile.into_iter().collect();
712        assert_eq!(alice_map.get("displayname"), Some(&json!("Alice")));
713
714        let bob_profile = store
715            .get_global_profile(bob)
716            .await
717            .expect("Failed to read profile")
718            .expect("Bob's profile should be saved");
719        let bob_map: BTreeMap<String, serde_json::Value> = bob_profile.into_iter().collect();
720        assert_eq!(bob_map.get("displayname"), Some(&json!("Bob")));
721
722        // When a subsequent response only carries an update for Alice.
723        let mut response = http::Response::new("1".to_owned());
724        response.extensions.profiles.users.insert(
725            alice.to_owned(),
726            make_profile_update(ProfileFieldName::DisplayName, json!("Alice Updated")),
727        );
728
729        client
730            .process_sliding_sync(
731                &response,
732                &RequestedRequiredStates::default(),
733                &client.state_store_lock().lock().await,
734            )
735            .await
736            .expect("Failed to process sync");
737
738        // Then Alice's profile is updated, and Bob's remains unchanged.
739        let alice_profile = store
740            .get_global_profile(alice)
741            .await
742            .expect("Failed to read profile")
743            .expect("Alice's profile should be saved");
744        let alice_map: BTreeMap<String, serde_json::Value> = alice_profile.into_iter().collect();
745        assert_eq!(alice_map.get("displayname"), Some(&json!("Alice Updated")));
746
747        let bob_profile = store
748            .get_global_profile(bob)
749            .await
750            .expect("Failed to read profile")
751            .expect("Bob's profile should still be saved");
752        let bob_map: BTreeMap<String, serde_json::Value> = bob_profile.into_iter().collect();
753        assert_eq!(bob_map.get("displayname"), Some(&json!("Bob")));
754    }
755
756    #[async_test]
757    async fn test_profiles_extension_broadcasts_global_profile_updates() {
758        let client = logged_in_base_client(None).await;
759
760        let alice = user_id!("@alice:e.uk");
761        let bob = user_id!("@bob:e.uk");
762
763        // Given a subscriber to global profile updates.
764        let mut global_profile_updates = client.subscribe_to_global_profile_updates();
765
766        // When a sliding sync response carries the profiles extension for two
767        // users.
768        let mut response = http::Response::new("0".to_owned());
769        response.extensions.profiles.users.insert(
770            alice.to_owned(),
771            make_profile_update(ProfileFieldName::DisplayName, json!("Alice")),
772        );
773        response.extensions.profiles.users.insert(
774            bob.to_owned(),
775            make_profile_update(ProfileFieldName::DisplayName, json!("Bob")),
776        );
777        client
778            .process_sliding_sync(
779                &response,
780                &RequestedRequiredStates::default(),
781                &client.state_store_lock().lock().await,
782            )
783            .await
784            .expect("Failed to process sync");
785
786        // Then the changed user IDs are broadcast.
787        let users =
788            global_profile_updates.recv().await.expect("should receive a global profile update");
789        assert_eq!(users.len(), 2);
790        assert!(users.contains(alice));
791        assert!(users.contains(bob));
792
793        // When a subsequent response only carries an update for Alice.
794        let mut response = http::Response::new("1".to_owned());
795        response.extensions.profiles.users.insert(
796            alice.to_owned(),
797            make_profile_update(ProfileFieldName::DisplayName, json!("Alice Updated")),
798        );
799        client
800            .process_sliding_sync(
801                &response,
802                &RequestedRequiredStates::default(),
803                &client.state_store_lock().lock().await,
804            )
805            .await
806            .expect("Failed to process sync");
807
808        // Then only Alice is broadcast.
809        let users =
810            global_profile_updates.recv().await.expect("should receive a global profile update");
811        assert_eq!(users.len(), 1);
812        assert!(users.contains(alice));
813    }
814
815    #[async_test]
816    async fn test_room_with_unspecified_state_is_added_to_client_and_joined_list() {
817        // Given a logged-in client
818        let client = logged_in_base_client(None).await;
819        let room_id = room_id!("!r:e.uk");
820
821        // When I send sliding sync response containing a room (with
822        // identifiable data in joined_count)
823        let mut room = http::response::Room::new();
824        room.joined_count = Some(uint!(41));
825        let response = response_with_room(room_id, room);
826        let sync_resp = client
827            .process_sliding_sync(
828                &response,
829                &RequestedRequiredStates::default(),
830                &client.state_store_lock().lock().await,
831            )
832            .await
833            .expect("Failed to process sync");
834
835        // Then the room appears in the client (with the same joined count)
836        let client_room = client.get_room(room_id).expect("No room found");
837        assert_eq!(client_room.room_id(), room_id);
838        assert_eq!(client_room.joined_members_count(), 41);
839        assert_eq!(client_room.state(), RoomState::Joined);
840
841        // And it is added to the list of joined rooms only.
842        assert!(sync_resp.rooms.joined.contains_key(room_id));
843        assert!(!sync_resp.rooms.left.contains_key(room_id));
844        assert!(!sync_resp.rooms.invited.contains_key(room_id));
845    }
846
847    #[async_test]
848    async fn test_missing_room_name_event() {
849        // Given a logged-in client
850        let client = logged_in_base_client(None).await;
851        let room_id = room_id!("!r:e.uk");
852
853        // When I send sliding sync response containing a room with a name set
854        // in the sliding sync response,
855        let mut room = http::response::Room::new();
856        room.name = Some("little room".to_owned());
857        let response = response_with_room(room_id, room);
858        let sync_resp = client
859            .process_sliding_sync(
860                &response,
861                &RequestedRequiredStates::default(),
862                &client.state_store_lock().lock().await,
863            )
864            .await
865            .expect("Failed to process sync");
866
867        // No m.room.name event, no heroes, no members => considered an empty
868        // room!
869        let client_room = client.get_room(room_id).expect("No room found");
870        assert!(client_room.name().is_none());
871        assert_eq!(
872            client_room.compute_display_name().await.unwrap().into_inner().to_string(),
873            "Empty Room"
874        );
875        assert_eq!(client_room.state(), RoomState::Joined);
876
877        // And it is added to the list of joined rooms only.
878        assert!(sync_resp.rooms.joined.contains_key(room_id));
879        assert!(!sync_resp.rooms.left.contains_key(room_id));
880        assert!(!sync_resp.rooms.invited.contains_key(room_id));
881        assert!(!sync_resp.rooms.knocked.contains_key(room_id));
882    }
883
884    #[async_test]
885    async fn test_room_name_event() {
886        // Given a logged-in client
887        let client = logged_in_base_client(None).await;
888        let room_id = room_id!("!r:e.uk");
889
890        // When I send sliding sync response containing a room with a name set
891        // in the sliding sync response, and a m.room.name event,
892        let mut room = http::response::Room::new();
893
894        room.name = Some("little room".to_owned());
895        set_room_name(&mut room, user_id!("@a:b.c"), "The Name".to_owned());
896
897        let response = response_with_room(room_id, room);
898        client
899            .process_sliding_sync(
900                &response,
901                &RequestedRequiredStates::default(),
902                &client.state_store_lock().lock().await,
903            )
904            .await
905            .expect("Failed to process sync");
906
907        // The name is known.
908        let client_room = client.get_room(room_id).expect("No room found");
909        assert_eq!(client_room.name().as_deref(), Some("The Name"));
910        assert_eq!(
911            client_room.compute_display_name().await.unwrap().into_inner().to_string(),
912            "The Name"
913        );
914    }
915
916    #[async_test]
917    async fn test_missing_invited_room_name_event() {
918        // Given a logged-in client,
919        let client = logged_in_base_client(None).await;
920        let room_id = room_id!("!r:e.uk");
921        let user_id = user_id!("@w:e.uk");
922        let inviter = user_id!("@john:mastodon.org");
923
924        // When I send sliding sync response containing a room with a name set
925        // in the sliding sync response,
926        let mut room = http::response::Room::new();
927        set_room_invited(&mut room, inviter, user_id);
928        room.name = Some("name from sliding sync response".to_owned());
929        let response = response_with_room(room_id, room);
930        let sync_resp = client
931            .process_sliding_sync(
932                &response,
933                &RequestedRequiredStates::default(),
934                &client.state_store_lock().lock().await,
935            )
936            .await
937            .expect("Failed to process sync");
938
939        // Then the room doesn't have the name in the client.
940        let client_room = client.get_room(room_id).expect("No room found");
941        assert!(client_room.name().is_none());
942
943        // No m.room.name event, no heroes => using the invited member.
944        assert_eq!(client_room.compute_display_name().await.unwrap().into_inner().to_string(), "w");
945
946        assert_eq!(client_room.state(), RoomState::Invited);
947
948        // And it is added to the list of invited rooms only.
949        assert!(!sync_resp.rooms.joined.contains_key(room_id));
950        assert!(!sync_resp.rooms.left.contains_key(room_id));
951        assert!(sync_resp.rooms.invited.contains_key(room_id));
952        assert!(!sync_resp.rooms.knocked.contains_key(room_id));
953    }
954
955    #[async_test]
956    async fn test_invited_room_name_event() {
957        // Given a logged-in client,
958        let client = logged_in_base_client(None).await;
959        let room_id = room_id!("!r:e.uk");
960        let user_id = user_id!("@w:e.uk");
961        let inviter = user_id!("@john:mastodon.org");
962
963        // When I send sliding sync response containing a room with a name set
964        // in the sliding sync response, and a m.room.name event,
965        let mut room = http::response::Room::new();
966
967        set_room_invited(&mut room, inviter, user_id);
968
969        room.name = Some("name from sliding sync response".to_owned());
970        set_room_name(&mut room, user_id!("@a:b.c"), "The Name".to_owned());
971
972        let response = response_with_room(room_id, room);
973        client
974            .process_sliding_sync(
975                &response,
976                &RequestedRequiredStates::default(),
977                &client.state_store_lock().lock().await,
978            )
979            .await
980            .expect("Failed to process sync");
981
982        // The name is known.
983        let client_room = client.get_room(room_id).expect("No room found");
984        assert_eq!(client_room.name().as_deref(), Some("The Name"));
985        assert_eq!(
986            client_room.compute_display_name().await.unwrap().into_inner().to_string(),
987            "The Name"
988        );
989    }
990
991    #[async_test]
992    async fn test_receiving_a_knocked_room_membership_event_creates_a_knocked_room() {
993        // Given a logged-in client,
994        let client = logged_in_base_client(None).await;
995        let room_id = room_id!("!r:e.uk");
996        let user_id = client.session_meta().unwrap().user_id.to_owned();
997
998        // When the room is properly set as knocked with the current user id as
999        // state key,
1000        let mut room = http::response::Room::new();
1001        set_room_knocked(&mut room, &user_id);
1002
1003        let response = response_with_room(room_id, room);
1004        client
1005            .process_sliding_sync(
1006                &response,
1007                &RequestedRequiredStates::default(),
1008                &client.state_store_lock().lock().await,
1009            )
1010            .await
1011            .expect("Failed to process sync");
1012
1013        // The room is knocked.
1014        let client_room = client.get_room(room_id).expect("No room found");
1015        assert_eq!(client_room.state(), RoomState::Knocked);
1016    }
1017
1018    #[async_test]
1019    async fn test_receiving_a_knocked_room_membership_event_with_wrong_state_key_creates_an_invited_room()
1020     {
1021        // Given a logged-in client,
1022        let client = logged_in_base_client(None).await;
1023        let room_id = room_id!("!r:e.uk");
1024        let user_id = user_id!("@w:e.uk");
1025
1026        // When the room is set as knocked with a random user id as state key,
1027        let mut room = http::response::Room::new();
1028        set_room_knocked(&mut room, user_id);
1029
1030        let response = response_with_room(room_id, room);
1031        client
1032            .process_sliding_sync(
1033                &response,
1034                &RequestedRequiredStates::default(),
1035                &client.state_store_lock().lock().await,
1036            )
1037            .await
1038            .expect("Failed to process sync");
1039
1040        // The room is invited since the membership event doesn't belong to the
1041        // current user.
1042        let client_room = client.get_room(room_id).expect("No room found");
1043        assert_eq!(client_room.state(), RoomState::Invited);
1044    }
1045
1046    #[async_test]
1047    async fn test_receiving_an_unknown_room_membership_event_in_invite_state_creates_an_invited_room()
1048     {
1049        // Given a logged-in client,
1050        let client = logged_in_base_client(None).await;
1051        let room_id = room_id!("!r:e.uk");
1052        let user_id = client.session_meta().unwrap().user_id.to_owned();
1053
1054        // When the room has the wrong membership state in its invite_state
1055        let mut room = http::response::Room::new();
1056        let event = Raw::new(&json!({
1057            "type": "m.room.member",
1058            "sender": user_id,
1059            "content": {
1060                "is_direct": true,
1061                "membership": "join",
1062            },
1063            "state_key": user_id,
1064        }))
1065        .expect("Failed to make raw event")
1066        .cast_unchecked();
1067        room.invite_state = Some(vec![event]);
1068
1069        let response = response_with_room(room_id, room);
1070        client
1071            .process_sliding_sync(
1072                &response,
1073                &RequestedRequiredStates::default(),
1074                &client.state_store_lock().lock().await,
1075            )
1076            .await
1077            .expect("Failed to process sync");
1078
1079        // The room is marked as invited.
1080        let client_room = client.get_room(room_id).expect("No room found");
1081        assert_eq!(client_room.state(), RoomState::Invited);
1082    }
1083
1084    #[async_test]
1085    async fn test_left_a_room_from_required_state_event() {
1086        // Given a logged-in client
1087        let client = logged_in_base_client(None).await;
1088        let room_id = room_id!("!r:e.uk");
1089        let user_id = user_id!("@u:e.uk");
1090
1091        // When I join…
1092        let mut room = http::response::Room::new();
1093        set_room_joined(&mut room, user_id);
1094        let response = response_with_room(room_id, room);
1095        client
1096            .process_sliding_sync(
1097                &response,
1098                &RequestedRequiredStates::default(),
1099                &client.state_store_lock().lock().await,
1100            )
1101            .await
1102            .expect("Failed to process sync");
1103        assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Joined);
1104
1105        // And then leave with a `required_state` state event…
1106        let mut room = http::response::Room::new();
1107        set_room_left(&mut room, user_id);
1108        let response = response_with_room(room_id, room);
1109        let sync_resp = client
1110            .process_sliding_sync(
1111                &response,
1112                &RequestedRequiredStates::default(),
1113                &client.state_store_lock().lock().await,
1114            )
1115            .await
1116            .expect("Failed to process sync");
1117
1118        // The room is left.
1119        assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left);
1120
1121        // And it is added to the list of left rooms only.
1122        assert!(!sync_resp.rooms.joined.contains_key(room_id));
1123        assert!(sync_resp.rooms.left.contains_key(room_id));
1124        assert!(!sync_resp.rooms.invited.contains_key(room_id));
1125        assert!(!sync_resp.rooms.knocked.contains_key(room_id));
1126    }
1127
1128    #[async_test]
1129    async fn test_kick_or_ban_updates_room_to_left() {
1130        for membership in [MembershipState::Leave, MembershipState::Ban] {
1131            let room_id = room_id!("!r:e.uk");
1132            let user_a_id = user_id!("@a:e.uk");
1133            let user_b_id = user_id!("@b:e.uk");
1134            let client = logged_in_base_client(Some(user_a_id)).await;
1135
1136            // When I join…
1137            let mut room = http::response::Room::new();
1138            set_room_joined(&mut room, user_a_id);
1139            let response = response_with_room(room_id, room);
1140            client
1141                .process_sliding_sync(
1142                    &response,
1143                    &RequestedRequiredStates::default(),
1144                    &client.state_store_lock().lock().await,
1145                )
1146                .await
1147                .expect("Failed to process sync");
1148            assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Joined);
1149
1150            // And then get kicked/banned with a `required_state` state event…
1151            let mut room = http::response::Room::new();
1152            room.required_state.push(make_state_event(
1153                user_b_id,
1154                user_a_id.as_str(),
1155                RoomMemberEventContent::new(membership.clone()),
1156                None,
1157            ));
1158            let response = response_with_room(room_id, room);
1159            let sync_resp = client
1160                .process_sliding_sync(
1161                    &response,
1162                    &RequestedRequiredStates::default(),
1163                    &client.state_store_lock().lock().await,
1164                )
1165                .await
1166                .expect("Failed to process sync");
1167
1168            match membership {
1169                MembershipState::Leave => {
1170                    // The room is left.
1171                    assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left);
1172                }
1173                MembershipState::Ban => {
1174                    // The room is banned.
1175                    assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Banned);
1176                }
1177                _ => panic!("Unexpected membership state found: {membership}"),
1178            }
1179
1180            // And it is added to the list of left rooms only.
1181            assert!(!sync_resp.rooms.joined.contains_key(room_id));
1182            assert!(sync_resp.rooms.left.contains_key(room_id));
1183            assert!(!sync_resp.rooms.invited.contains_key(room_id));
1184            assert!(!sync_resp.rooms.knocked.contains_key(room_id));
1185        }
1186    }
1187
1188    #[async_test]
1189    async fn test_left_a_room_from_timeline_state_event() {
1190        // Given a logged-in client
1191        let client = logged_in_base_client(None).await;
1192        let room_id = room_id!("!r:e.uk");
1193        let user_id = user_id!("@u:e.uk");
1194
1195        // When I join…
1196        let mut room = http::response::Room::new();
1197        set_room_joined(&mut room, user_id);
1198        let response = response_with_room(room_id, room);
1199        client
1200            .process_sliding_sync(
1201                &response,
1202                &RequestedRequiredStates::default(),
1203                &client.state_store_lock().lock().await,
1204            )
1205            .await
1206            .expect("Failed to process sync");
1207        assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Joined);
1208
1209        // And then leave with a `timeline` state event…
1210        let mut room = http::response::Room::new();
1211        set_room_left_as_timeline_event(&mut room, user_id);
1212        let response = response_with_room(room_id, room);
1213        client
1214            .process_sliding_sync(
1215                &response,
1216                &RequestedRequiredStates::default(),
1217                &client.state_store_lock().lock().await,
1218            )
1219            .await
1220            .expect("Failed to process sync");
1221
1222        // The room is NOT left because state events from `timeline` must be
1223        // IGNORED!
1224        assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Joined);
1225    }
1226
1227    #[async_test]
1228    async fn test_can_be_reinvited_to_a_left_room() {
1229        // See https://github.com/matrix-org/matrix-rust-sdk/issues/1834
1230
1231        // Given a logged-in client
1232        let client = logged_in_base_client(None).await;
1233        let room_id = room_id!("!r:e.uk");
1234        let user_id = user_id!("@u:e.uk");
1235
1236        // When I join...
1237        let mut room = http::response::Room::new();
1238        set_room_joined(&mut room, user_id);
1239        let response = response_with_room(room_id, room);
1240        client
1241            .process_sliding_sync(
1242                &response,
1243                &RequestedRequiredStates::default(),
1244                &client.state_store_lock().lock().await,
1245            )
1246            .await
1247            .expect("Failed to process sync");
1248        // (sanity: state is join)
1249        assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Joined);
1250
1251        // And then leave...
1252        let mut room = http::response::Room::new();
1253        set_room_left(&mut room, user_id);
1254        let response = response_with_room(room_id, room);
1255        client
1256            .process_sliding_sync(
1257                &response,
1258                &RequestedRequiredStates::default(),
1259                &client.state_store_lock().lock().await,
1260            )
1261            .await
1262            .expect("Failed to process sync");
1263        // (sanity: state is left)
1264        assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left);
1265
1266        // And then get invited back
1267        let mut room = http::response::Room::new();
1268        set_room_invited(&mut room, user_id, user_id);
1269        let response = response_with_room(room_id, room);
1270        client
1271            .process_sliding_sync(
1272                &response,
1273                &RequestedRequiredStates::default(),
1274                &client.state_store_lock().lock().await,
1275            )
1276            .await
1277            .expect("Failed to process sync");
1278
1279        // Then the room is in the invite state
1280        assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Invited);
1281    }
1282
1283    #[async_test]
1284    async fn test_other_person_leaving_a_dm_is_reflected_in_their_membership_and_direct_targets() {
1285        let room_id = room_id!("!r:e.uk");
1286        let user_a_id = user_id!("@a:e.uk");
1287        let user_b_id = user_id!("@b:e.uk");
1288
1289        // Given we have a DM with B, who is joined
1290        let client = logged_in_base_client(None).await;
1291        create_dm(&client, room_id, user_a_id, user_b_id, MembershipState::Join).await;
1292
1293        // (Sanity: B is a direct target, and is in Join state)
1294        assert!(
1295            direct_targets(&client, room_id).contains(<&DirectUserIdentifier>::from(user_b_id))
1296        );
1297        assert_eq!(membership(&client, room_id, user_b_id).await, MembershipState::Join);
1298
1299        // When B leaves
1300        update_room_membership(&client, room_id, user_b_id, MembershipState::Leave).await;
1301
1302        // Then B is still a direct target, and is in Leave state (B is a direct
1303        // target because we want to return to our old DM in the UI even if the
1304        // other user left, so we can reinvite them. See
1305        // https://github.com/matrix-org/matrix-rust-sdk/issues/2017)
1306        assert!(
1307            direct_targets(&client, room_id).contains(<&DirectUserIdentifier>::from(user_b_id))
1308        );
1309        assert_eq!(membership(&client, room_id, user_b_id).await, MembershipState::Leave);
1310    }
1311
1312    #[async_test]
1313    async fn test_other_person_refusing_invite_to_a_dm_is_reflected_in_their_membership_and_direct_targets()
1314     {
1315        let room_id = room_id!("!r:e.uk");
1316        let user_a_id = user_id!("@a:e.uk");
1317        let user_b_id = user_id!("@b:e.uk");
1318
1319        // Given I have invited B to a DM
1320        let client = logged_in_base_client(None).await;
1321        create_dm(&client, room_id, user_a_id, user_b_id, MembershipState::Invite).await;
1322
1323        // (Sanity: B is a direct target, and is in Invite state)
1324        assert!(
1325            direct_targets(&client, room_id).contains(<&DirectUserIdentifier>::from(user_b_id))
1326        );
1327        assert_eq!(membership(&client, room_id, user_b_id).await, MembershipState::Invite);
1328
1329        // When B declines the invitation (i.e. leaves)
1330        update_room_membership(&client, room_id, user_b_id, MembershipState::Leave).await;
1331
1332        // Then B is still a direct target, and is in Leave state (B is a direct
1333        // target because we want to return to our old DM in the UI even if the
1334        // other user left, so we can reinvite them. See
1335        // https://github.com/matrix-org/matrix-rust-sdk/issues/2017)
1336        assert!(
1337            direct_targets(&client, room_id).contains(<&DirectUserIdentifier>::from(user_b_id))
1338        );
1339        assert_eq!(membership(&client, room_id, user_b_id).await, MembershipState::Leave);
1340    }
1341
1342    #[async_test]
1343    async fn test_members_count_in_a_dm_where_other_person_has_joined() {
1344        let room_id = room_id!("!r:bar.org");
1345        let user_a_id = user_id!("@a:bar.org");
1346        let user_b_id = user_id!("@b:bar.org");
1347
1348        // Given we have a DM with B, who is joined
1349        let client = logged_in_base_client(None).await;
1350        create_dm(&client, room_id, user_a_id, user_b_id, MembershipState::Join).await;
1351
1352        // (Sanity: A is in Join state)
1353        assert_eq!(membership(&client, room_id, user_a_id).await, MembershipState::Join);
1354
1355        // (Sanity: B is a direct target, and is in Join state)
1356        assert!(
1357            direct_targets(&client, room_id).contains(<&DirectUserIdentifier>::from(user_b_id))
1358        );
1359        assert_eq!(membership(&client, room_id, user_b_id).await, MembershipState::Join);
1360
1361        let room = client.get_room(room_id).unwrap();
1362
1363        assert_eq!(room.active_members_count(), 2);
1364        assert_eq!(room.joined_members_count(), 2);
1365        assert_eq!(room.invited_members_count(), 0);
1366    }
1367
1368    #[async_test]
1369    async fn test_members_count_in_a_dm_where_other_person_is_invited() {
1370        let room_id = room_id!("!r:bar.org");
1371        let user_a_id = user_id!("@a:bar.org");
1372        let user_b_id = user_id!("@b:bar.org");
1373
1374        // Given we have a DM with B, who is joined
1375        let client = logged_in_base_client(None).await;
1376        create_dm(&client, room_id, user_a_id, user_b_id, MembershipState::Invite).await;
1377
1378        // (Sanity: A is in Join state)
1379        assert_eq!(membership(&client, room_id, user_a_id).await, MembershipState::Join);
1380
1381        // (Sanity: B is a direct target, and is in Join state)
1382        assert!(
1383            direct_targets(&client, room_id).contains(<&DirectUserIdentifier>::from(user_b_id))
1384        );
1385        assert_eq!(membership(&client, room_id, user_b_id).await, MembershipState::Invite);
1386
1387        let room = client.get_room(room_id).unwrap();
1388
1389        assert_eq!(room.active_members_count(), 2);
1390        assert_eq!(room.joined_members_count(), 1);
1391        assert_eq!(room.invited_members_count(), 1);
1392    }
1393
1394    #[async_test]
1395    async fn test_avatar_is_found_when_processing_sliding_sync_response() {
1396        // Given a logged-in client
1397        let client = logged_in_base_client(None).await;
1398        let room_id = room_id!("!r:e.uk");
1399
1400        // When I send sliding sync response containing a room with an avatar
1401        let room = {
1402            let mut room = http::response::Room::new();
1403            room.avatar = JsOption::from_option(Some(owned_mxc_uri!("mxc://e.uk/med1")));
1404
1405            room
1406        };
1407        let response = response_with_room(room_id, room);
1408        client
1409            .process_sliding_sync(
1410                &response,
1411                &RequestedRequiredStates::default(),
1412                &client.state_store_lock().lock().await,
1413            )
1414            .await
1415            .expect("Failed to process sync");
1416
1417        // Then the room in the client has the avatar
1418        let client_room = client.get_room(room_id).expect("No room found");
1419        assert_eq!(
1420            client_room.avatar_url().expect("No avatar URL").media_id().expect("No media ID"),
1421            "med1"
1422        );
1423    }
1424
1425    #[async_test]
1426    async fn test_avatar_can_be_unset_when_processing_sliding_sync_response() {
1427        // Given a logged-in client
1428        let client = logged_in_base_client(None).await;
1429        let room_id = room_id!("!r:e.uk");
1430
1431        // Set the avatar.
1432
1433        // When I send sliding sync response containing a room with an avatar
1434        let room = {
1435            let mut room = http::response::Room::new();
1436            room.avatar = JsOption::from_option(Some(owned_mxc_uri!("mxc://e.uk/med1")));
1437
1438            room
1439        };
1440        let response = response_with_room(room_id, room);
1441        client
1442            .process_sliding_sync(
1443                &response,
1444                &RequestedRequiredStates::default(),
1445                &client.state_store_lock().lock().await,
1446            )
1447            .await
1448            .expect("Failed to process sync");
1449
1450        // Then the room in the client has the avatar
1451        let client_room = client.get_room(room_id).expect("No room found");
1452        assert_eq!(
1453            client_room.avatar_url().expect("No avatar URL").media_id().expect("No media ID"),
1454            "med1"
1455        );
1456
1457        // No avatar. Still here.
1458
1459        // When I send sliding sync response containing no avatar.
1460        let room = http::response::Room::new();
1461        let response = response_with_room(room_id, room);
1462        client
1463            .process_sliding_sync(
1464                &response,
1465                &RequestedRequiredStates::default(),
1466                &client.state_store_lock().lock().await,
1467            )
1468            .await
1469            .expect("Failed to process sync");
1470
1471        // Then the room in the client still has the avatar
1472        let client_room = client.get_room(room_id).expect("No room found");
1473        assert_eq!(
1474            client_room.avatar_url().expect("No avatar URL").media_id().expect("No media ID"),
1475            "med1"
1476        );
1477
1478        // Avatar is unset.
1479
1480        // When I send sliding sync response containing an avatar set to `null`
1481        // (!).
1482        let room = {
1483            let mut room = http::response::Room::new();
1484            room.avatar = JsOption::Null;
1485
1486            room
1487        };
1488        let response = response_with_room(room_id, room);
1489        client
1490            .process_sliding_sync(
1491                &response,
1492                &RequestedRequiredStates::default(),
1493                &client.state_store_lock().lock().await,
1494            )
1495            .await
1496            .expect("Failed to process sync");
1497
1498        // Then the room in the client has no more avatar
1499        let client_room = client.get_room(room_id).expect("No room found");
1500        assert!(client_room.avatar_url().is_none());
1501    }
1502
1503    #[async_test]
1504    async fn test_avatar_is_found_from_required_state_when_processing_sliding_sync_response() {
1505        // Given a logged-in client
1506        let client = logged_in_base_client(None).await;
1507        let room_id = room_id!("!r:e.uk");
1508        let user_id = user_id!("@u:e.uk");
1509
1510        // When I send sliding sync response containing a room with an avatar
1511        let room = room_with_avatar(mxc_uri!("mxc://e.uk/med1"), user_id);
1512        let response = response_with_room(room_id, room);
1513        client
1514            .process_sliding_sync(
1515                &response,
1516                &RequestedRequiredStates::default(),
1517                &client.state_store_lock().lock().await,
1518            )
1519            .await
1520            .expect("Failed to process sync");
1521
1522        // Then the room in the client has the avatar
1523        let client_room = client.get_room(room_id).expect("No room found");
1524        assert_eq!(
1525            client_room.avatar_url().expect("No avatar URL").media_id().expect("No media ID"),
1526            "med1"
1527        );
1528    }
1529
1530    #[async_test]
1531    async fn test_invitation_room_is_added_to_client_and_invite_list() {
1532        // Given a logged-in client
1533        let client = logged_in_base_client(None).await;
1534        let room_id = room_id!("!r:e.uk");
1535        let user_id = user_id!("@u:e.uk");
1536
1537        let mut room_info_notable_update = client.room_info_notable_update_receiver();
1538
1539        // When I send sliding sync response containing an invited room
1540        let mut room = http::response::Room::new();
1541        set_room_invited(&mut room, user_id, user_id);
1542        let response = response_with_room(room_id, room);
1543        let sync_resp = client
1544            .process_sliding_sync(
1545                &response,
1546                &RequestedRequiredStates::default(),
1547                &client.state_store_lock().lock().await,
1548            )
1549            .await
1550            .expect("Failed to process sync");
1551
1552        // Then the room is added to the client
1553        let client_room = client.get_room(room_id).expect("No room found");
1554        assert_eq!(client_room.room_id(), room_id);
1555        assert_eq!(client_room.state(), RoomState::Invited);
1556
1557        // And it is added to the list of invited rooms, not the joined ones
1558        assert!(!sync_resp.rooms.invited[room_id].invite_state.is_empty());
1559        assert!(!sync_resp.rooms.joined.contains_key(room_id));
1560
1561        assert_matches!(
1562            room_info_notable_update.recv().await,
1563            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons }) => {
1564                assert_eq!(received_room_id, room_id);
1565                // The reason we are looking for :-].
1566                assert!(reasons.contains(RoomInfoNotableUpdateReasons::MEMBERSHIP));
1567            }
1568        );
1569        assert_matches!(
1570            room_info_notable_update.recv().await,
1571            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons }) => {
1572                assert_eq!(received_room_id, room_id);
1573                // The reason we are looking for :-].
1574                assert!(reasons.contains(RoomInfoNotableUpdateReasons::DISPLAY_NAME));
1575            }
1576        );
1577        assert!(room_info_notable_update.is_empty());
1578    }
1579
1580    #[async_test]
1581    async fn test_knock_room_is_added_to_client_and_knock_list() {
1582        // Given a logged-in client
1583        let client = logged_in_base_client(None).await;
1584        let room_id = room_id!("!r:e.uk");
1585        let user_id = user_id!("@u:e.uk");
1586
1587        let mut room_info_notable_update = client.room_info_notable_update_receiver();
1588
1589        // When I send sliding sync response containing an invited room
1590        let mut room = http::response::Room::new();
1591        set_room_knocked(&mut room, user_id);
1592        let response = response_with_room(room_id, room);
1593        let sync_resp = client
1594            .process_sliding_sync(
1595                &response,
1596                &RequestedRequiredStates::default(),
1597                &client.state_store_lock().lock().await,
1598            )
1599            .await
1600            .expect("Failed to process sync");
1601
1602        // Then the room is added to the client
1603        let client_room = client.get_room(room_id).expect("No room found");
1604        assert_eq!(client_room.room_id(), room_id);
1605        assert_eq!(client_room.state(), RoomState::Knocked);
1606
1607        // And it is added to the list of invited rooms, not the joined ones
1608        assert!(!sync_resp.rooms.knocked[room_id].knock_state.is_empty());
1609        assert!(!sync_resp.rooms.joined.contains_key(room_id));
1610
1611        assert_matches!(
1612            room_info_notable_update.recv().await,
1613            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons }) => {
1614                assert_eq!(received_room_id, room_id);
1615                // The reason we are looking for :-].
1616                assert!(reasons.contains(RoomInfoNotableUpdateReasons::MEMBERSHIP));
1617            }
1618        );
1619        assert_matches!(
1620            room_info_notable_update.recv().await,
1621            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons }) => {
1622                assert_eq!(received_room_id, room_id);
1623                // The reason we are looking for :-].
1624                assert!(reasons.contains(RoomInfoNotableUpdateReasons::DISPLAY_NAME));
1625            }
1626        );
1627        assert!(room_info_notable_update.is_empty());
1628    }
1629
1630    #[async_test]
1631    async fn test_invited_room_stays_invited_when_a_later_response_has_no_invite_state() {
1632        // Given a logged-in client that knows about an invited room…
1633        let client = logged_in_base_client(None).await;
1634        let room_id = room_id!("!r:e.uk");
1635        let user_id = user_id!("@u:e.uk");
1636
1637        let mut room = http::response::Room::new();
1638        set_room_invited(&mut room, user_id, user_id);
1639        let response = response_with_room(room_id, room);
1640        client
1641            .process_sliding_sync(
1642                &response,
1643                &RequestedRequiredStates::default(),
1644                &client.state_store_lock().lock().await,
1645            )
1646            .await
1647            .expect("Failed to process sync");
1648
1649        // (sanity: state is invite)
1650        assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Invited);
1651
1652        // When a later response mentions the room again, but has no
1653        // `invite_state` and no membership event…
1654        let response = response_with_room(room_id, http::response::Room::new());
1655        let sync_resp = client
1656            .process_sliding_sync(
1657                &response,
1658                &RequestedRequiredStates::default(),
1659                &client.state_store_lock().lock().await,
1660            )
1661            .await
1662            .expect("Failed to process sync");
1663
1664        // … the room is still invited, and still reported as such.
1665        assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Invited);
1666        assert!(sync_resp.rooms.invited.contains_key(room_id));
1667        assert!(!sync_resp.rooms.joined.contains_key(room_id));
1668    }
1669
1670    #[async_test]
1671    async fn test_invited_room_becomes_joined_from_required_state_event() {
1672        // Given a logged-in client that knows about an invited room…
1673        let client = logged_in_base_client(None).await;
1674        let room_id = room_id!("!r:e.uk");
1675        let user_id = user_id!("@u:e.uk");
1676
1677        let mut room = http::response::Room::new();
1678        set_room_invited(&mut room, user_id, user_id);
1679        let response = response_with_room(room_id, room);
1680        client
1681            .process_sliding_sync(
1682                &response,
1683                &RequestedRequiredStates::default(),
1684                &client.state_store_lock().lock().await,
1685            )
1686            .await
1687            .expect("Failed to process sync");
1688
1689        // (sanity: state is invite)
1690        assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Invited);
1691
1692        let mut room_info_notable_update = client.room_info_notable_update_receiver();
1693
1694        // When the invite is accepted, the server sends the new membership
1695        // event in `required_state`…
1696        let mut room = http::response::Room::new();
1697        set_room_joined(&mut room, user_id);
1698        let response = response_with_room(room_id, room);
1699        let sync_resp = client
1700            .process_sliding_sync(
1701                &response,
1702                &RequestedRequiredStates::default(),
1703                &client.state_store_lock().lock().await,
1704            )
1705            .await
1706            .expect("Failed to process sync");
1707
1708        // … and the room becomes joined.
1709        assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Joined);
1710
1711        assert!(sync_resp.rooms.joined.contains_key(room_id));
1712        assert!(!sync_resp.rooms.invited.contains_key(room_id));
1713
1714        // The membership change is notable.
1715        assert_matches!(
1716            room_info_notable_update.recv().await,
1717            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons }) => {
1718                assert_eq!(received_room_id, room_id);
1719                assert!(reasons.contains(RoomInfoNotableUpdateReasons::MEMBERSHIP));
1720            }
1721        );
1722    }
1723
1724    #[async_test]
1725    async fn test_avatar_is_found_in_invitation_room_when_processing_sliding_sync_response() {
1726        // Given a logged-in client
1727        let client = logged_in_base_client(None).await;
1728        let room_id = room_id!("!r:e.uk");
1729        let user_id = user_id!("@u:e.uk");
1730
1731        // When I send sliding sync response containing an invited room with an
1732        // avatar
1733        let mut room = room_with_avatar(mxc_uri!("mxc://e.uk/med1"), user_id);
1734        set_room_invited(&mut room, user_id, user_id);
1735        let response = response_with_room(room_id, room);
1736        client
1737            .process_sliding_sync(
1738                &response,
1739                &RequestedRequiredStates::default(),
1740                &client.state_store_lock().lock().await,
1741            )
1742            .await
1743            .expect("Failed to process sync");
1744
1745        // Then the room in the client has the avatar
1746        let client_room = client.get_room(room_id).expect("No room found");
1747        assert_eq!(
1748            client_room.avatar_url().expect("No avatar URL").media_id().expect("No media ID"),
1749            "med1"
1750        );
1751    }
1752
1753    #[async_test]
1754    async fn test_canonical_alias_is_found_in_invitation_room_when_processing_sliding_sync_response()
1755     {
1756        // Given a logged-in client
1757        let client = logged_in_base_client(None).await;
1758        let room_id = room_id!("!r:e.uk");
1759        let user_id = user_id!("@u:e.uk");
1760        let room_alias_id = room_alias_id!("#myroom:e.uk");
1761
1762        // When I send sliding sync response containing an invited room with an
1763        // avatar
1764        let mut room = room_with_canonical_alias(room_alias_id, user_id);
1765        set_room_invited(&mut room, user_id, user_id);
1766        let response = response_with_room(room_id, room);
1767        client
1768            .process_sliding_sync(
1769                &response,
1770                &RequestedRequiredStates::default(),
1771                &client.state_store_lock().lock().await,
1772            )
1773            .await
1774            .expect("Failed to process sync");
1775
1776        // Then the room in the client has the avatar
1777        let client_room = client.get_room(room_id).expect("No room found");
1778        assert_eq!(client_room.canonical_alias(), Some(room_alias_id.to_owned()));
1779    }
1780
1781    #[async_test]
1782    async fn test_display_name_from_sliding_sync_doesnt_override_alias() {
1783        // Given a logged-in client
1784        let client = logged_in_base_client(None).await;
1785        let room_id = room_id!("!r:e.uk");
1786        let user_id = user_id!("@u:e.uk");
1787        let room_alias_id = room_alias_id!("#myroom:e.uk");
1788
1789        // When the sliding sync response contains an explicit room name as well
1790        // as an alias
1791        let mut room = room_with_canonical_alias(room_alias_id, user_id);
1792        room.name = Some("This came from the server".to_owned());
1793        let response = response_with_room(room_id, room);
1794        client
1795            .process_sliding_sync(
1796                &response,
1797                &RequestedRequiredStates::default(),
1798                &client.state_store_lock().lock().await,
1799            )
1800            .await
1801            .expect("Failed to process sync");
1802
1803        // Then the room's name is NOT overridden by the server-computed display
1804        // name.
1805        let client_room = client.get_room(room_id).expect("No room found");
1806        assert_eq!(
1807            client_room.compute_display_name().await.unwrap().into_inner().to_string(),
1808            "myroom"
1809        );
1810        assert!(client_room.name().is_none());
1811    }
1812
1813    #[async_test]
1814    async fn test_display_name_is_cached_and_emits_a_notable_update_reason() {
1815        let client = logged_in_base_client(None).await;
1816        let user_id = user_id!("@u:e.uk");
1817        let room_id = room_id!("!r:e.uk");
1818
1819        let mut room_info_notable_update = client.room_info_notable_update_receiver();
1820
1821        let room = room_with_name("Hello World", user_id);
1822        let response = response_with_room(room_id, room);
1823        client
1824            .process_sliding_sync(
1825                &response,
1826                &RequestedRequiredStates::default(),
1827                &client.state_store_lock().lock().await,
1828            )
1829            .await
1830            .expect("Failed to process sync");
1831
1832        let room = client.get_room(room_id).expect("No room found");
1833        assert_eq!(room.cached_display_name().unwrap().to_string(), "Hello World");
1834
1835        assert_matches!(
1836            room_info_notable_update.recv().await,
1837            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons }) => {
1838                assert_eq!(received_room_id, room_id);
1839                assert!(reasons.contains(RoomInfoNotableUpdateReasons::NONE));
1840            }
1841        );
1842        assert_matches!(
1843            room_info_notable_update.recv().await,
1844            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons }) => {
1845                assert_eq!(received_room_id, room_id);
1846                // The reason we are looking for :-].
1847                assert!(reasons.contains(RoomInfoNotableUpdateReasons::DISPLAY_NAME));
1848            }
1849        );
1850        assert!(room_info_notable_update.is_empty());
1851    }
1852
1853    #[async_test]
1854    async fn test_display_name_is_persisted_from_sliding_sync() {
1855        let user_id = user_id!("@u:e.uk");
1856        let room_id = room_id!("!r:e.uk");
1857        let session_meta = SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() };
1858        let state_store;
1859
1860        {
1861            let client = {
1862                let store = StoreConfig::new(CrossProcessLockConfig::SingleProcess);
1863                state_store = store.state_store.clone();
1864
1865                let client =
1866                    BaseClient::new(store, ThreadingSupport::Disabled, DmRoomDefinition::default());
1867                client
1868                    .activate(
1869                        session_meta.clone(),
1870                        RoomLoadSettings::default(),
1871                        #[cfg(feature = "e2e-encryption")]
1872                        None,
1873                    )
1874                    .await
1875                    .expect("`activate` failed!");
1876
1877                client
1878            };
1879
1880            // When the sliding sync response contains an explicit room name as
1881            // well as an alias
1882            let room = room_with_name("Hello World", user_id);
1883            let response = response_with_room(room_id, room);
1884            client
1885                .process_sliding_sync(
1886                    &response,
1887                    &RequestedRequiredStates::default(),
1888                    &client.state_store_lock().lock().await,
1889                )
1890                .await
1891                .expect("Failed to process sync");
1892
1893            let room = client.get_room(room_id).expect("No room found");
1894            assert_eq!(room.cached_display_name().unwrap().to_string(), "Hello World");
1895        }
1896
1897        {
1898            let client = {
1899                let mut store = StoreConfig::new(CrossProcessLockConfig::SingleProcess);
1900                store.state_store = state_store;
1901                let client =
1902                    BaseClient::new(store, ThreadingSupport::Disabled, DmRoomDefinition::default());
1903                client
1904                    .activate(
1905                        session_meta,
1906                        RoomLoadSettings::default(),
1907                        #[cfg(feature = "e2e-encryption")]
1908                        None,
1909                    )
1910                    .await
1911                    .expect("`activate` failed!");
1912
1913                client
1914            };
1915
1916            let room = client.get_room(room_id).expect("No room found");
1917            assert_eq!(room.cached_display_name().unwrap().to_string(), "Hello World");
1918        }
1919    }
1920
1921    #[async_test]
1922    async fn test_compute_heroes_from_sliding_sync() {
1923        // Given a logged-in client
1924        let client = logged_in_base_client(None).await;
1925        let room_id = room_id!("!r:e.uk");
1926        let gordon = owned_user_id!("@gordon:e.uk");
1927        let alice = owned_user_id!("@alice:e.uk");
1928
1929        // When I send sliding sync response containing a room (with
1930        // identifiable data in `heroes`)
1931        let mut room = http::response::Room::new();
1932        room.heroes = Some(vec![
1933            assign!(http::response::Hero::new(gordon), {
1934                name: Some("Gordon".to_owned()),
1935            }),
1936            assign!(http::response::Hero::new(alice), {
1937                name: Some("Alice".to_owned()),
1938                avatar: Some(owned_mxc_uri!("mxc://e.uk/med1"))
1939            }),
1940        ]);
1941        let response = response_with_room(room_id, room);
1942        let _sync_resp = client
1943            .process_sliding_sync(
1944                &response,
1945                &RequestedRequiredStates::default(),
1946                &client.state_store_lock().lock().await,
1947            )
1948            .await
1949            .expect("Failed to process sync");
1950
1951        // Then the room appears in the client.
1952        let client_room = client.get_room(room_id).expect("No room found");
1953        assert_eq!(client_room.room_id(), room_id);
1954        assert_eq!(client_room.state(), RoomState::Joined);
1955
1956        // And heroes are part of the summary.
1957        assert_eq!(
1958            client_room.clone_info().summary.heroes(),
1959            &[
1960                RoomHero {
1961                    user_id: owned_user_id!("@gordon:e.uk"),
1962                    display_name: Some("Gordon".to_owned()),
1963                    avatar_url: None
1964                },
1965                RoomHero {
1966                    user_id: owned_user_id!("@alice:e.uk"),
1967                    display_name: Some("Alice".to_owned()),
1968                    avatar_url: Some(owned_mxc_uri!("mxc://e.uk/med1"))
1969                },
1970            ]
1971        );
1972    }
1973
1974    #[cfg(feature = "unstable-msc4426")]
1975    #[async_test]
1976    async fn test_hero_global_profile_update_triggers_notable_update() {
1977        let client = logged_in_base_client(None).await;
1978        let room_id = room_id!("!r:e.uk");
1979        let alice = owned_user_id!("@alice:e.uk");
1980
1981        // Given a room where Alice is a hero.
1982        let mut room = http::response::Room::new();
1983        room.heroes = Some(vec![assign!(http::response::Hero::new(alice.clone()), {
1984            name: Some("Alice".to_owned()),
1985        })]);
1986        let response = response_with_room(room_id, room);
1987        client
1988            .process_sliding_sync(
1989                &response,
1990                &RequestedRequiredStates::default(),
1991                &client.state_store_lock().lock().await,
1992            )
1993            .await
1994            .expect("Failed to process sync");
1995
1996        let room = client.get_room(room_id).expect("The room should be known");
1997        let mut room_info_subscriber = room.subscribe_info();
1998        let mut room_info_notable_update = client.room_info_notable_update_receiver();
1999
2000        assert_pending!(room_info_subscriber);
2001
2002        // When a subsequent sync carries only a profiles-extension update for
2003        // Alice.
2004        let mut response = http::Response::new("1".to_owned());
2005        response.extensions.profiles.users.insert(
2006            alice.clone(),
2007            make_profile_update(ProfileFieldName::Status, json!({ "text": "Away", "emoji": "🌴" })),
2008        );
2009        client
2010            .process_sliding_sync(
2011                &response,
2012                &RequestedRequiredStates::default(),
2013                &client.state_store_lock().lock().await,
2014            )
2015            .await
2016            .expect("Failed to process sync");
2017
2018        // Then a `HEROES` notable update is emitted for the room, so consumers
2019        // re-read the hero profiles.
2020        assert_matches!(
2021            room_info_notable_update.recv().await,
2022            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons }) => {
2023                assert_eq!(received_room_id, room_id);
2024                assert!(reasons.contains(RoomInfoNotableUpdateReasons::HEROES));
2025            }
2026        );
2027        assert!(room_info_notable_update.is_empty());
2028
2029        // And the `RoomInfo` observable is notified too, so that subscribers of
2030        // `Room::subscribe_info` re-read the hero profiles as well.
2031        assert_ready!(room_info_subscriber);
2032        assert_pending!(room_info_subscriber);
2033
2034        // Sanity check: Alice's hero carries the new status.
2035        let heroes = room.heroes().await;
2036        assert_eq!(heroes.len(), 1);
2037        assert_eq!(heroes[0].user_id, alice);
2038        let status = heroes[0].status.as_ref().expect("Alice's status should be set");
2039        assert_eq!(status.text, "Away");
2040        assert_eq!(status.emoji, "🌴");
2041    }
2042
2043    #[async_test]
2044    async fn test_recency_stamp_is_found_when_processing_sliding_sync_response() {
2045        // Given a logged-in client
2046        let client = logged_in_base_client(None).await;
2047        let room_id = room_id!("!r:e.uk");
2048
2049        // When I send sliding sync response containing a room with a recency
2050        // stamp
2051        let room = assign!(http::response::Room::new(), {
2052            bump_stamp: Some(42u32.into()),
2053        });
2054        let response = response_with_room(room_id, room);
2055        client
2056            .process_sliding_sync(
2057                &response,
2058                &RequestedRequiredStates::default(),
2059                &client.state_store_lock().lock().await,
2060            )
2061            .await
2062            .expect("Failed to process sync");
2063
2064        // Then the room in the client has the recency stamp
2065        let client_room = client.get_room(room_id).expect("No room found");
2066        assert_eq!(client_room.recency_stamp().expect("No recency stamp"), 42.into());
2067    }
2068
2069    #[async_test]
2070    async fn test_recency_stamp_can_be_overwritten_when_present_in_a_sliding_sync_response() {
2071        // Given a logged-in client
2072        let client = logged_in_base_client(None).await;
2073        let room_id = room_id!("!r:e.uk");
2074
2075        {
2076            // When I send sliding sync response containing a room with a
2077            // recency stamp
2078            let room = assign!(http::response::Room::new(), {
2079                bump_stamp: Some(42u32.into()),
2080            });
2081            let response = response_with_room(room_id, room);
2082            client
2083                .process_sliding_sync(
2084                    &response,
2085                    &RequestedRequiredStates::default(),
2086                    &client.state_store_lock().lock().await,
2087                )
2088                .await
2089                .expect("Failed to process sync");
2090
2091            // Then the room in the client has the recency stamp
2092            let client_room = client.get_room(room_id).expect("No room found");
2093            assert_eq!(client_room.recency_stamp().expect("No recency stamp"), 42.into());
2094        }
2095
2096        {
2097            // When I send sliding sync response containing a room with NO
2098            // recency stamp
2099            let room = assign!(http::response::Room::new(), {
2100                bump_stamp: None,
2101            });
2102            let response = response_with_room(room_id, room);
2103            client
2104                .process_sliding_sync(
2105                    &response,
2106                    &RequestedRequiredStates::default(),
2107                    &client.state_store_lock().lock().await,
2108                )
2109                .await
2110                .expect("Failed to process sync");
2111
2112            // Then the room in the client has the previous recency stamp
2113            let client_room = client.get_room(room_id).expect("No room found");
2114            assert_eq!(client_room.recency_stamp().expect("No recency stamp"), 42.into());
2115        }
2116
2117        {
2118            // When I send sliding sync response containing a room with a NEW
2119            // recency timestamp
2120            let room = assign!(http::response::Room::new(), {
2121                bump_stamp: Some(153u32.into()),
2122            });
2123            let response = response_with_room(room_id, room);
2124            client
2125                .process_sliding_sync(
2126                    &response,
2127                    &RequestedRequiredStates::default(),
2128                    &client.state_store_lock().lock().await,
2129                )
2130                .await
2131                .expect("Failed to process sync");
2132
2133            // Then the room in the client has the recency stamp
2134            let client_room = client.get_room(room_id).expect("No room found");
2135            assert_eq!(client_room.recency_stamp().expect("No recency stamp"), 153.into());
2136        }
2137    }
2138
2139    #[async_test]
2140    async fn test_recency_stamp_can_trigger_a_notable_update_reason() {
2141        // Given a logged-in client
2142        let client = logged_in_base_client(None).await;
2143        let mut room_info_notable_update_stream = client.room_info_notable_update_receiver();
2144        let room_id = room_id!("!r:e.uk");
2145
2146        // When I send sliding sync response containing a room with a recency
2147        // stamp.
2148        let room = assign!(http::response::Room::new(), {
2149            bump_stamp: Some(42u32.into()),
2150        });
2151        let response = response_with_room(room_id, room);
2152        client
2153            .process_sliding_sync(
2154                &response,
2155                &RequestedRequiredStates::default(),
2156                &client.state_store_lock().lock().await,
2157            )
2158            .await
2159            .expect("Failed to process sync");
2160
2161        // Then a room info notable update is NOT received, because it's the
2162        // first time the room is seen.
2163        assert_matches!(
2164            room_info_notable_update_stream.recv().await,
2165            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
2166                assert_eq!(received_room_id, room_id);
2167                assert!(!received_reasons.contains(RoomInfoNotableUpdateReasons::RECENCY_STAMP));
2168            }
2169        );
2170        assert_matches!(
2171            room_info_notable_update_stream.recv().await,
2172            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
2173                assert_eq!(received_room_id, room_id);
2174                assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::DISPLAY_NAME));
2175            }
2176        );
2177        assert!(room_info_notable_update_stream.is_empty());
2178
2179        // When I send sliding sync response containing a room with a recency
2180        // stamp.
2181        let room = assign!(http::response::Room::new(), {
2182            bump_stamp: Some(43u32.into()),
2183        });
2184        let response = response_with_room(room_id, room);
2185        client
2186            .process_sliding_sync(
2187                &response,
2188                &RequestedRequiredStates::default(),
2189                &client.state_store_lock().lock().await,
2190            )
2191            .await
2192            .expect("Failed to process sync");
2193
2194        // Then a room info notable update is received.
2195        assert_matches!(
2196            room_info_notable_update_stream.recv().await,
2197            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
2198                assert_eq!(received_room_id, room_id);
2199                assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::RECENCY_STAMP));
2200            }
2201        );
2202        assert!(room_info_notable_update_stream.is_empty());
2203    }
2204
2205    #[async_test]
2206    async fn test_leaving_room_can_trigger_a_notable_update_reason() {
2207        // Given a logged-in client
2208        let client = logged_in_base_client(None).await;
2209        let mut room_info_notable_update_stream = client.room_info_notable_update_receiver();
2210
2211        // When I send sliding sync response containing a new room.
2212        let room_id = room_id!("!r:e.uk");
2213        let room = http::response::Room::new();
2214        let response = response_with_room(room_id, room);
2215        client
2216            .process_sliding_sync(
2217                &response,
2218                &RequestedRequiredStates::default(),
2219                &client.state_store_lock().lock().await,
2220            )
2221            .await
2222            .expect("Failed to process sync");
2223
2224        // Other notable update reason. We don't really care about them here.
2225        assert_matches!(
2226            room_info_notable_update_stream.recv().await,
2227            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
2228                assert_eq!(received_room_id, room_id);
2229                assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::NONE));
2230            }
2231        );
2232        assert_matches!(
2233            room_info_notable_update_stream.recv().await,
2234            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
2235                assert_eq!(received_room_id, room_id);
2236                assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::DISPLAY_NAME));
2237            }
2238        );
2239
2240        // Send sliding sync response containing a membership event with 'join'
2241        // value.
2242        let room_id = room_id!("!r:e.uk");
2243        let events = vec![
2244            Raw::from_json_string(
2245                json!({
2246                    "type": "m.room.member",
2247                    "event_id": "$3",
2248                    "content": { "membership": "join" },
2249                    "sender": "@u:h.uk",
2250                    "origin_server_ts": 12344445,
2251                    "state_key": "@u:e.uk",
2252                })
2253                .to_string(),
2254            )
2255            .unwrap(),
2256        ];
2257        let room = assign!(http::response::Room::new(), {
2258            required_state: events,
2259        });
2260        let response = response_with_room(room_id, room);
2261        client
2262            .process_sliding_sync(
2263                &response,
2264                &RequestedRequiredStates::default(),
2265                &client.state_store_lock().lock().await,
2266            )
2267            .await
2268            .expect("Failed to process sync");
2269
2270        // Room was already joined, no `MEMBERSHIP` update should be triggered
2271        // here
2272        assert_matches!(
2273            room_info_notable_update_stream.recv().await,
2274            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
2275                assert_eq!(received_room_id, room_id);
2276                assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::NONE));
2277            }
2278        );
2279        assert!(room_info_notable_update_stream.is_empty());
2280
2281        let events = vec![
2282            Raw::from_json_string(
2283                json!({
2284                    "type": "m.room.member",
2285                    "event_id": "$3",
2286                    "content": { "membership": "leave" },
2287                    "sender": "@u:h.uk",
2288                    "origin_server_ts": 12344445,
2289                    "state_key": "@u:e.uk",
2290                })
2291                .to_string(),
2292            )
2293            .unwrap(),
2294        ];
2295        let room = assign!(http::response::Room::new(), {
2296            required_state: events,
2297        });
2298        let response = response_with_room(room_id, room);
2299        client
2300            .process_sliding_sync(
2301                &response,
2302                &RequestedRequiredStates::default(),
2303                &client.state_store_lock().lock().await,
2304            )
2305            .await
2306            .expect("Failed to process sync");
2307
2308        // Then a room info notable update is received.
2309        assert_matches!(
2310            room_info_notable_update_stream.recv().await,
2311            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
2312                assert_eq!(received_room_id, room_id);
2313                assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::MEMBERSHIP));
2314            }
2315        );
2316        assert!(room_info_notable_update_stream.is_empty());
2317    }
2318
2319    #[async_test]
2320    async fn test_unread_marker_can_trigger_a_notable_update_reason() {
2321        // Given a logged-in client,
2322        let client = logged_in_base_client(None).await;
2323        let mut room_info_notable_update_stream = client.room_info_notable_update_receiver();
2324
2325        // When I receive a sliding sync response containing a new room,
2326        let room_id = room_id!("!r:e.uk");
2327        let room = http::response::Room::new();
2328        let response = response_with_room(room_id, room);
2329        client
2330            .process_sliding_sync(
2331                &response,
2332                &RequestedRequiredStates::default(),
2333                &client.state_store_lock().lock().await,
2334            )
2335            .await
2336            .expect("Failed to process sync");
2337
2338        // Other notable updates are received, but not the ones we are
2339        // interested by.
2340        assert_matches!(
2341            room_info_notable_update_stream.recv().await,
2342            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
2343                assert_eq!(received_room_id, room_id);
2344                assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::NONE), "{received_reasons:?}");
2345            }
2346        );
2347        assert_matches!(
2348            room_info_notable_update_stream.recv().await,
2349            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
2350                assert_eq!(received_room_id, room_id);
2351                assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::DISPLAY_NAME), "{received_reasons:?}");
2352            }
2353        );
2354        assert!(room_info_notable_update_stream.is_empty());
2355
2356        // When I receive a sliding sync response containing one update about an
2357        // unread marker,
2358        let room_id = room_id!("!r:e.uk");
2359        let room_account_data_events = vec![
2360            Raw::from_json_string(
2361                json!({
2362                    "type": "m.marked_unread",
2363                    "event_id": "$1",
2364                    "content": { "unread": true },
2365                    "sender": client.session_meta().unwrap().user_id,
2366                    "origin_server_ts": 12344445,
2367                })
2368                .to_string(),
2369            )
2370            .unwrap(),
2371        ];
2372        let mut response = response_with_room(room_id, http::response::Room::new());
2373        response.extensions.account_data.rooms.insert(room_id.to_owned(), room_account_data_events);
2374
2375        client
2376            .process_sliding_sync(
2377                &response,
2378                &RequestedRequiredStates::default(),
2379                &client.state_store_lock().lock().await,
2380            )
2381            .await
2382            .expect("Failed to process sync");
2383
2384        // Then a room info notable update is received.
2385        assert_matches!(
2386            room_info_notable_update_stream.recv().await,
2387            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
2388                assert_eq!(received_room_id, room_id);
2389                assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::UNREAD_MARKER), "{received_reasons:?}");
2390            }
2391        );
2392
2393        // But getting it again won't trigger a new notable update…
2394        client
2395            .process_sliding_sync(
2396                &response,
2397                &RequestedRequiredStates::default(),
2398                &client.state_store_lock().lock().await,
2399            )
2400            .await
2401            .expect("Failed to process sync");
2402
2403        assert_matches!(
2404            room_info_notable_update_stream.recv().await,
2405            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
2406                assert_eq!(received_room_id, room_id);
2407                assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::NONE), "{received_reasons:?}");
2408            }
2409        );
2410        assert!(room_info_notable_update_stream.is_empty());
2411
2412        // …Unless its value changes!
2413        let room_account_data_events = vec![
2414            Raw::from_json_string(
2415                json!({
2416                    "type": "m.marked_unread",
2417                    "event_id": "$1",
2418                    "content": { "unread": false },
2419                    "sender": client.session_meta().unwrap().user_id,
2420                    "origin_server_ts": 12344445,
2421                })
2422                .to_string(),
2423            )
2424            .unwrap(),
2425        ];
2426        response.extensions.account_data.rooms.insert(room_id.to_owned(), room_account_data_events);
2427        client
2428            .process_sliding_sync(
2429                &response,
2430                &RequestedRequiredStates::default(),
2431                &client.state_store_lock().lock().await,
2432            )
2433            .await
2434            .expect("Failed to process sync");
2435
2436        assert_matches!(
2437            room_info_notable_update_stream.recv().await,
2438            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
2439                assert_eq!(received_room_id, room_id);
2440                assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::UNREAD_MARKER));
2441            }
2442        );
2443        assert!(room_info_notable_update_stream.is_empty());
2444    }
2445
2446    #[async_test]
2447    async fn test_empty_room_account_data_does_not_create_a_room_update() {
2448        let client = logged_in_base_client(None).await;
2449
2450        let room_id_a = room_id!("!a:e.uk");
2451        let room_id_b = room_id!("!b:e.uk");
2452        let user_id = client.session_meta().unwrap().user_id.clone();
2453
2454        let mut response = http::Response::new("0".to_owned());
2455        for room_id in [room_id_a, room_id_b] {
2456            let mut room = http::response::Room::new();
2457            set_room_joined(&mut room, &user_id);
2458            response.rooms.insert(room_id.to_owned(), room);
2459        }
2460        client
2461            .process_sliding_sync(
2462                &response,
2463                &RequestedRequiredStates::default(),
2464                &client.state_store_lock().lock().await,
2465            )
2466            .await
2467            .expect("Failed to process sync");
2468
2469        let mut response = response_with_room(room_id_a, http::response::Room::new());
2470        response.extensions.account_data.rooms.insert(room_id_b.to_owned(), vec![]);
2471
2472        let sync_response = client
2473            .process_sliding_sync(
2474                &response,
2475                &RequestedRequiredStates::default(),
2476                &client.state_store_lock().lock().await,
2477            )
2478            .await
2479            .expect("Failed to process sync");
2480
2481        assert!(sync_response.rooms.joined.contains_key(room_id_a));
2482        assert!(!sync_response.rooms.joined.contains_key(room_id_b));
2483        assert!(sync_response.rooms.left.is_empty());
2484    }
2485
2486    #[async_test]
2487    async fn test_fully_read_marker_can_trigger_a_notable_update_reason() {
2488        // Given a logged-in client,
2489        let client = logged_in_base_client(None).await;
2490        let mut room_info_notable_update_stream = client.room_info_notable_update_receiver();
2491
2492        // When I receive a sliding sync response containing a new room,
2493        let room_id = room_id!("!r:e.uk");
2494        let room = http::response::Room::new();
2495        let response = response_with_room(room_id, room);
2496        client
2497            .process_sliding_sync(
2498                &response,
2499                &RequestedRequiredStates::default(),
2500                &client.state_store_lock().lock().await,
2501            )
2502            .await
2503            .expect("Failed to process sync");
2504
2505        // Other notable updates are received, but not the ones we are
2506        // interested by.
2507        assert_matches!(
2508            room_info_notable_update_stream.recv().await,
2509            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
2510                assert_eq!(received_room_id, room_id);
2511                assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::NONE), "{received_reasons:?}");
2512            }
2513        );
2514        assert_matches!(
2515            room_info_notable_update_stream.recv().await,
2516            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
2517                assert_eq!(received_room_id, room_id);
2518                assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::DISPLAY_NAME), "{received_reasons:?}");
2519            }
2520        );
2521        assert!(room_info_notable_update_stream.is_empty());
2522
2523        // When I receive a sliding sync response containing an `m.fully_read`
2524        // account-data event,
2525        let room_account_data_events = vec![
2526            Raw::from_json_string(
2527                json!({
2528                    "type": "m.fully_read",
2529                    "content": { "event_id": "$first" },
2530                })
2531                .to_string(),
2532            )
2533            .unwrap(),
2534        ];
2535        let mut response = response_with_room(room_id, http::response::Room::new());
2536        response.extensions.account_data.rooms.insert(room_id.to_owned(), room_account_data_events);
2537
2538        client
2539            .process_sliding_sync(
2540                &response,
2541                &RequestedRequiredStates::default(),
2542                &client.state_store_lock().lock().await,
2543            )
2544            .await
2545            .expect("Failed to process sync");
2546
2547        // Then a `FULLY_READ` notable update is received,
2548        assert_matches!(
2549            room_info_notable_update_stream.recv().await,
2550            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
2551                assert_eq!(received_room_id, room_id);
2552                assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::FULLY_READ), "{received_reasons:?}");
2553            }
2554        );
2555
2556        let room = client.get_room(room_id).expect("room should exist");
2557        assert_eq!(room.fully_read_event_id().as_deref().map(|id| id.as_str()), Some("$first"),);
2558
2559        // But getting the same value again won't trigger a new notable update…
2560        client
2561            .process_sliding_sync(
2562                &response,
2563                &RequestedRequiredStates::default(),
2564                &client.state_store_lock().lock().await,
2565            )
2566            .await
2567            .expect("Failed to process sync");
2568
2569        assert_matches!(
2570            room_info_notable_update_stream.recv().await,
2571            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
2572                assert_eq!(received_room_id, room_id);
2573                assert!(!received_reasons.contains(RoomInfoNotableUpdateReasons::FULLY_READ), "{received_reasons:?}");
2574            }
2575        );
2576        assert!(room_info_notable_update_stream.is_empty());
2577
2578        // … Unless the event ID changes!
2579        let room_account_data_events = vec![
2580            Raw::from_json_string(
2581                json!({
2582                    "type": "m.fully_read",
2583                    "content": { "event_id": "$second" },
2584                })
2585                .to_string(),
2586            )
2587            .unwrap(),
2588        ];
2589        response.extensions.account_data.rooms.insert(room_id.to_owned(), room_account_data_events);
2590        client
2591            .process_sliding_sync(
2592                &response,
2593                &RequestedRequiredStates::default(),
2594                &client.state_store_lock().lock().await,
2595            )
2596            .await
2597            .expect("Failed to process sync");
2598
2599        assert_matches!(
2600            room_info_notable_update_stream.recv().await,
2601            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
2602                assert_eq!(received_room_id, room_id);
2603                assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::FULLY_READ), "{received_reasons:?}");
2604            }
2605        );
2606        assert_eq!(room.fully_read_event_id().as_deref().map(|id| id.as_str()), Some("$second"),);
2607        assert!(room_info_notable_update_stream.is_empty());
2608    }
2609
2610    #[async_test]
2611    async fn test_unstable_unread_marker_is_ignored_after_stable() {
2612        // Given a logged-in client,
2613        let client = logged_in_base_client(None).await;
2614        let mut room_info_notable_update_stream = client.room_info_notable_update_receiver();
2615
2616        // When I receive a sliding sync response containing a new room,
2617        let room_id = room_id!("!r:e.uk");
2618        let room = http::response::Room::new();
2619        let response = response_with_room(room_id, room);
2620        client
2621            .process_sliding_sync(
2622                &response,
2623                &RequestedRequiredStates::default(),
2624                &client.state_store_lock().lock().await,
2625            )
2626            .await
2627            .expect("Failed to process sync");
2628
2629        // Other notable updates are received, but not the ones we are
2630        // interested by.
2631        assert_matches!(
2632            room_info_notable_update_stream.recv().await,
2633            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
2634                assert_eq!(received_room_id, room_id);
2635                assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::NONE), "{received_reasons:?}");
2636            }
2637        );
2638        assert_matches!(
2639            room_info_notable_update_stream.recv().await,
2640            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
2641                assert_eq!(received_room_id, room_id);
2642                assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::DISPLAY_NAME), "{received_reasons:?}");
2643            }
2644        );
2645        assert!(room_info_notable_update_stream.is_empty());
2646
2647        // When I receive a sliding sync response containing one update about an
2648        // unstable unread marker,
2649        let room_id = room_id!("!r:e.uk");
2650        let unstable_room_account_data_events = vec![
2651            Raw::from_json_string(
2652                json!({
2653                    "type": "com.famedly.marked_unread",
2654                    "event_id": "$1",
2655                    "content": { "unread": true },
2656                    "sender": client.session_meta().unwrap().user_id,
2657                    "origin_server_ts": 12344445,
2658                })
2659                .to_string(),
2660            )
2661            .unwrap(),
2662        ];
2663        let mut response = response_with_room(room_id, http::response::Room::new());
2664        response
2665            .extensions
2666            .account_data
2667            .rooms
2668            .insert(room_id.to_owned(), unstable_room_account_data_events.clone());
2669
2670        client
2671            .process_sliding_sync(
2672                &response,
2673                &RequestedRequiredStates::default(),
2674                &client.state_store_lock().lock().await,
2675            )
2676            .await
2677            .expect("Failed to process sync");
2678
2679        // Then a room info notable update is received.
2680        assert_matches!(
2681            room_info_notable_update_stream.recv().await,
2682            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
2683                assert_eq!(received_room_id, room_id);
2684                assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::UNREAD_MARKER), "{received_reasons:?}");
2685            }
2686        );
2687        assert!(room_info_notable_update_stream.is_empty());
2688
2689        // When I receive a sliding sync response with a stable unread marker
2690        // update,
2691        let stable_room_account_data_events = vec![
2692            Raw::from_json_string(
2693                json!({
2694                    "type": "m.marked_unread",
2695                    "event_id": "$1",
2696                    "content": { "unread": false },
2697                    "sender": client.session_meta().unwrap().user_id,
2698                    "origin_server_ts": 12344445,
2699                })
2700                .to_string(),
2701            )
2702            .unwrap(),
2703        ];
2704        response
2705            .extensions
2706            .account_data
2707            .rooms
2708            .insert(room_id.to_owned(), stable_room_account_data_events);
2709        client
2710            .process_sliding_sync(
2711                &response,
2712                &RequestedRequiredStates::default(),
2713                &client.state_store_lock().lock().await,
2714            )
2715            .await
2716            .expect("Failed to process sync");
2717
2718        // Then a room info notable update is received.
2719        assert_matches!(
2720            room_info_notable_update_stream.recv().await,
2721            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
2722                assert_eq!(received_room_id, room_id);
2723                assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::UNREAD_MARKER));
2724            }
2725        );
2726        assert!(room_info_notable_update_stream.is_empty());
2727
2728        // When I receive a sliding sync response with an unstable unread marker
2729        // update again,
2730        response
2731            .extensions
2732            .account_data
2733            .rooms
2734            .insert(room_id.to_owned(), unstable_room_account_data_events);
2735        client
2736            .process_sliding_sync(
2737                &response,
2738                &RequestedRequiredStates::default(),
2739                &client.state_store_lock().lock().await,
2740            )
2741            .await
2742            .expect("Failed to process sync");
2743
2744        // There is no notable update.
2745        assert_matches!(
2746            room_info_notable_update_stream.recv().await,
2747            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
2748                assert_eq!(received_room_id, room_id);
2749                assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::NONE), "{received_reasons:?}");
2750            }
2751        );
2752        assert!(room_info_notable_update_stream.is_empty());
2753
2754        // Finally, when I receive a sliding sync response with a stable unread
2755        // marker update again,
2756        let stable_room_account_data_events = vec![
2757            Raw::from_json_string(
2758                json!({
2759                    "type": "m.marked_unread",
2760                    "event_id": "$3",
2761                    "content": { "unread": true },
2762                    "sender": client.session_meta().unwrap().user_id,
2763                    "origin_server_ts": 12344445,
2764                })
2765                .to_string(),
2766            )
2767            .unwrap(),
2768        ];
2769        response
2770            .extensions
2771            .account_data
2772            .rooms
2773            .insert(room_id.to_owned(), stable_room_account_data_events);
2774        client
2775            .process_sliding_sync(
2776                &response,
2777                &RequestedRequiredStates::default(),
2778                &client.state_store_lock().lock().await,
2779            )
2780            .await
2781            .expect("Failed to process sync");
2782
2783        // Then a room info notable update is received.
2784        assert_matches!(
2785            room_info_notable_update_stream.recv().await,
2786            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
2787                assert_eq!(received_room_id, room_id);
2788                assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::UNREAD_MARKER));
2789            }
2790        );
2791        assert!(room_info_notable_update_stream.is_empty());
2792    }
2793
2794    #[async_test]
2795    async fn test_pinned_events_are_updated_on_sync() {
2796        let user_a_id = user_id!("@a:e.uk");
2797        let client = logged_in_base_client(Some(user_a_id)).await;
2798        let room_id = room_id!("!r:e.uk");
2799        let pinned_event_id = owned_event_id!("$an-id:e.uk");
2800
2801        // Create room
2802        let mut room_response = http::response::Room::new();
2803        set_room_joined(&mut room_response, user_a_id);
2804        let response = response_with_room(room_id, room_response);
2805        client
2806            .process_sliding_sync(
2807                &response,
2808                &RequestedRequiredStates::default(),
2809                &client.state_store_lock().lock().await,
2810            )
2811            .await
2812            .expect("Failed to process sync");
2813
2814        // The newly created room has no pinned event ids
2815        let room = client.get_room(room_id).unwrap();
2816        let pinned_event_ids = room.pinned_event_ids();
2817        assert_matches!(pinned_event_ids, None);
2818
2819        // Load new pinned event id
2820        let mut room_response = http::response::Room::new();
2821        room_response.required_state.push(make_state_event(
2822            user_a_id,
2823            "",
2824            RoomPinnedEventsEventContent::new(vec![pinned_event_id.clone()]),
2825            None,
2826        ));
2827        let response = response_with_room(room_id, room_response);
2828        client
2829            .process_sliding_sync(
2830                &response,
2831                &RequestedRequiredStates::default(),
2832                &client.state_store_lock().lock().await,
2833            )
2834            .await
2835            .expect("Failed to process sync");
2836
2837        let pinned_event_ids = room.pinned_event_ids().unwrap_or_default();
2838        assert_eq!(pinned_event_ids.len(), 1);
2839        assert_eq!(pinned_event_ids[0], pinned_event_id);
2840
2841        // Pinned event ids are now empty
2842        let mut room_response = http::response::Room::new();
2843        room_response.required_state.push(make_state_event(
2844            user_a_id,
2845            "",
2846            RoomPinnedEventsEventContent::new(Vec::new()),
2847            None,
2848        ));
2849        let response = response_with_room(room_id, room_response);
2850        client
2851            .process_sliding_sync(
2852                &response,
2853                &RequestedRequiredStates::default(),
2854                &client.state_store_lock().lock().await,
2855            )
2856            .await
2857            .expect("Failed to process sync");
2858        let pinned_event_ids = room.pinned_event_ids().unwrap();
2859        assert!(pinned_event_ids.is_empty());
2860    }
2861
2862    #[async_test]
2863    async fn test_dms_are_processed_in_any_sync_response() {
2864        let current_user_id = user_id!("@current:e.uk");
2865        let client = logged_in_base_client(Some(current_user_id)).await;
2866        let user_a_id = user_id!("@a:e.uk");
2867        let user_b_id = user_id!("@b:e.uk");
2868        let room_id_1 = room_id!("!r:e.uk");
2869        let room_id_2 = room_id!("!s:e.uk");
2870
2871        let mut room_response = http::response::Room::new();
2872        set_room_joined(&mut room_response, user_a_id);
2873        let mut response = response_with_room(room_id_1, room_response);
2874        let mut direct_content: BTreeMap<OwnedDirectUserIdentifier, Vec<OwnedRoomId>> =
2875            BTreeMap::new();
2876        direct_content.insert(user_a_id.into(), vec![room_id_1.to_owned()]);
2877        direct_content.insert(user_b_id.into(), vec![room_id_2.to_owned()]);
2878        response
2879            .extensions
2880            .account_data
2881            .global
2882            .push(make_global_account_data_event(DirectEventContent(direct_content)));
2883        client
2884            .process_sliding_sync(
2885                &response,
2886                &RequestedRequiredStates::default(),
2887                &client.state_store_lock().lock().await,
2888            )
2889            .await
2890            .expect("Failed to process sync");
2891
2892        let room_1 = client.get_room(room_id_1).unwrap();
2893        assert!(room_1.is_direct().await.unwrap());
2894
2895        // Now perform a sync without new account data
2896        let mut room_response = http::response::Room::new();
2897        set_room_joined(&mut room_response, user_b_id);
2898        let response = response_with_room(room_id_2, room_response);
2899        client
2900            .process_sliding_sync(
2901                &response,
2902                &RequestedRequiredStates::default(),
2903                &client.state_store_lock().lock().await,
2904            )
2905            .await
2906            .expect("Failed to process sync");
2907
2908        let room_2 = client.get_room(room_id_2).unwrap();
2909        assert!(room_2.is_direct().await.unwrap());
2910    }
2911
2912    #[async_test]
2913    async fn test_room_encryption_state_is_and_is_not_encrypted() {
2914        let user_id = user_id!("@raclette:patate");
2915        let client = logged_in_base_client(Some(user_id)).await;
2916        let room_id_0 = room_id!("!r0");
2917        let room_id_1 = room_id!("!r1");
2918        let room_id_2 = room_id!("!r2");
2919
2920        // A room is considered encrypted when it receives a `m.room.encryption`
2921        // event, period.
2922        //
2923        // A room is considered **not** encrypted when it receives no
2924        // `m.room.encryption` event but it was requested, period.
2925        //
2926        // We are going to test three rooms:
2927        //
2928        // - two of them receive a `m.room.encryption` event
2929        // - the last one does not receive a `m.room.encryption`.
2930        // - the first one is configured with a `required_state` for this event, the
2931        //   others have nothing.
2932        //
2933        // The trick is that, since sliding sync makes an union of all the
2934        // `required_state`s, then all rooms are technically requesting a
2935        // `m.room.encryption`.
2936        let requested_required_states = RequestedRequiredStates::from(&{
2937            let mut request = http::Request::new();
2938
2939            request.room_subscriptions.insert(room_id_0.to_owned(), {
2940                let mut room_subscription = http::request::RoomSubscription::default();
2941
2942                room_subscription
2943                    .required_state
2944                    .push((StateEventType::RoomEncryption, "".to_owned()));
2945
2946                room_subscription
2947            });
2948
2949            request
2950        });
2951
2952        let mut response = http::Response::new("0".to_owned());
2953
2954        // Create two rooms that are encrypted, i.e. they have a
2955        // `m.room.encryption` state event in their `required_state`. Create a
2956        // third room that is not encrypted, i.e. it doesn't have a
2957        // `m.room.encryption` state event.
2958        {
2959            let not_encrypted_room = http::response::Room::new();
2960            let mut encrypted_room = http::response::Room::new();
2961            set_room_is_encrypted(&mut encrypted_room, user_id);
2962
2963            response.rooms.insert(room_id_0.to_owned(), encrypted_room.clone());
2964            response.rooms.insert(room_id_1.to_owned(), encrypted_room);
2965            response.rooms.insert(room_id_2.to_owned(), not_encrypted_room);
2966        }
2967
2968        client
2969            .process_sliding_sync(
2970                &response,
2971                &requested_required_states,
2972                &client.state_store_lock().lock().await,
2973            )
2974            .await
2975            .expect("Failed to process sync");
2976
2977        // They are both encrypted, yepee.
2978        assert_matches!(
2979            client.get_room(room_id_0).unwrap().encryption_state(),
2980            EncryptionState::Encrypted
2981        );
2982        assert_matches!(
2983            client.get_room(room_id_1).unwrap().encryption_state(),
2984            EncryptionState::Encrypted
2985        );
2986        // This one is not encrypted because it has received nothing.
2987        assert_matches!(
2988            client.get_room(room_id_2).unwrap().encryption_state(),
2989            EncryptionState::NotEncrypted
2990        )
2991    }
2992
2993    #[async_test]
2994    async fn test_room_encryption_state_is_unknown() {
2995        let user_id = user_id!("@raclette:patate");
2996        let client = logged_in_base_client(Some(user_id)).await;
2997        let room_id_0 = room_id!("!r0");
2998        let room_id_1 = room_id!("!r1");
2999
3000        // A room is considered encrypted when it receives a `m.room.encryption`
3001        // event, period.
3002        //
3003        // A room is considered **not** encrypted when it receives no
3004        // `m.room.encryption` event but it was requested, period.
3005        //
3006        // We are going to test two rooms:
3007        //
3008        // - one that receives a `m.room.encryption` event,
3009        // - one that receives nothing,
3010        // - none of them have requested the state event.
3011
3012        let requested_required_states = RequestedRequiredStates::from(&http::Request::new());
3013
3014        let mut response = http::Response::new("0".to_owned());
3015
3016        // Create two rooms with and without a `m.room.encryption` event.
3017        {
3018            let not_encrypted_room = http::response::Room::new();
3019            let mut encrypted_room = http::response::Room::new();
3020            set_room_is_encrypted(&mut encrypted_room, user_id);
3021
3022            response.rooms.insert(room_id_0.to_owned(), encrypted_room);
3023            response.rooms.insert(room_id_1.to_owned(), not_encrypted_room);
3024        }
3025
3026        client
3027            .process_sliding_sync(
3028                &response,
3029                &requested_required_states,
3030                &client.state_store_lock().lock().await,
3031            )
3032            .await
3033            .expect("Failed to process sync");
3034
3035        // Encrypted, because the presence of a `m.room.encryption` always mean
3036        // the room is encrypted.
3037        assert_matches!(
3038            client.get_room(room_id_0).unwrap().encryption_state(),
3039            EncryptionState::Encrypted
3040        );
3041        // Unknown, because the absence of `m.room.encryption` when not
3042        // requested means we don't know what the state is.
3043        assert_matches!(
3044            client.get_room(room_id_1).unwrap().encryption_state(),
3045            EncryptionState::Unknown
3046        );
3047    }
3048
3049    async fn membership(
3050        client: &BaseClient,
3051        room_id: &RoomId,
3052        user_id: &UserId,
3053    ) -> MembershipState {
3054        let room = client.get_room(room_id).expect("Room not found!");
3055        let member = room.get_member(user_id).await.unwrap().expect("B not in room");
3056        member.membership().clone()
3057    }
3058
3059    fn direct_targets(client: &BaseClient, room_id: &RoomId) -> HashSet<OwnedDirectUserIdentifier> {
3060        let room = client.get_room(room_id).expect("Room not found!");
3061        room.direct_targets()
3062    }
3063
3064    /// Create a DM with the other user, setting our membership to Join and
3065    /// theirs to other_state
3066    async fn create_dm(
3067        client: &BaseClient,
3068        room_id: &RoomId,
3069        my_id: &UserId,
3070        their_id: &UserId,
3071        other_state: MembershipState,
3072    ) {
3073        let mut room = http::response::Room::new();
3074        set_room_joined(&mut room, my_id);
3075
3076        match other_state {
3077            MembershipState::Join => {
3078                room.joined_count = Some(uint!(2));
3079                room.invited_count = None;
3080            }
3081
3082            MembershipState::Invite => {
3083                room.joined_count = Some(uint!(1));
3084                room.invited_count = Some(uint!(1));
3085            }
3086
3087            _ => {
3088                room.joined_count = Some(uint!(1));
3089                room.invited_count = None;
3090            }
3091        }
3092
3093        room.required_state.push(make_membership_event(their_id, other_state));
3094
3095        let mut response = response_with_room(room_id, room);
3096        set_direct_with(&mut response, their_id.to_owned(), vec![room_id.to_owned()]);
3097        client
3098            .process_sliding_sync(
3099                &response,
3100                &RequestedRequiredStates::default(),
3101                &client.state_store_lock().lock().await,
3102            )
3103            .await
3104            .expect("Failed to process sync");
3105    }
3106
3107    /// Set this user's membership within this room to new_state
3108    async fn update_room_membership(
3109        client: &BaseClient,
3110        room_id: &RoomId,
3111        user_id: &UserId,
3112        new_state: MembershipState,
3113    ) {
3114        let mut room = http::response::Room::new();
3115        room.required_state.push(make_membership_event(user_id, new_state));
3116        let response = response_with_room(room_id, room);
3117        client
3118            .process_sliding_sync(
3119                &response,
3120                &RequestedRequiredStates::default(),
3121                &client.state_store_lock().lock().await,
3122            )
3123            .await
3124            .expect("Failed to process sync");
3125    }
3126
3127    fn set_direct_with(
3128        response: &mut http::Response,
3129        user_id: OwnedUserId,
3130        room_ids: Vec<OwnedRoomId>,
3131    ) {
3132        let mut direct_content: BTreeMap<OwnedDirectUserIdentifier, Vec<OwnedRoomId>> =
3133            BTreeMap::new();
3134        direct_content.insert(user_id.into(), room_ids);
3135        response
3136            .extensions
3137            .account_data
3138            .global
3139            .push(make_global_account_data_event(DirectEventContent(direct_content)));
3140    }
3141
3142    fn response_with_room(room_id: &RoomId, room: http::response::Room) -> http::Response {
3143        let mut response = http::Response::new("5".to_owned());
3144        response.rooms.insert(room_id.to_owned(), room);
3145        response
3146    }
3147
3148    fn room_with_avatar(avatar_uri: &MxcUri, user_id: &UserId) -> http::response::Room {
3149        let mut room = http::response::Room::new();
3150
3151        let mut avatar_event_content = RoomAvatarEventContent::new();
3152        avatar_event_content.url = Some(avatar_uri.to_owned());
3153
3154        room.required_state.push(make_state_event(user_id, "", avatar_event_content, None));
3155
3156        room
3157    }
3158
3159    fn room_with_canonical_alias(
3160        room_alias_id: &RoomAliasId,
3161        user_id: &UserId,
3162    ) -> http::response::Room {
3163        let mut room = http::response::Room::new();
3164
3165        let mut canonical_alias_event_content = RoomCanonicalAliasEventContent::new();
3166        canonical_alias_event_content.alias = Some(room_alias_id.to_owned());
3167
3168        room.required_state.push(make_state_event(
3169            user_id,
3170            "",
3171            canonical_alias_event_content,
3172            None,
3173        ));
3174
3175        room
3176    }
3177
3178    fn room_with_name(name: &str, user_id: &UserId) -> http::response::Room {
3179        let mut room = http::response::Room::new();
3180
3181        let name_event_content = RoomNameEventContent::new(name.to_owned());
3182
3183        room.required_state.push(make_state_event(user_id, "", name_event_content, None));
3184
3185        room
3186    }
3187
3188    fn set_room_name(room: &mut http::response::Room, sender: &UserId, name: String) {
3189        room.required_state.push(make_state_event(
3190            sender,
3191            "",
3192            RoomNameEventContent::new(name),
3193            None,
3194        ));
3195    }
3196
3197    fn set_room_invited(room: &mut http::response::Room, inviter: &UserId, invitee: &UserId) {
3198        // Sliding Sync shows an almost-empty event to indicate that we are
3199        // invited to a room. Just the type is supplied.
3200
3201        let evt = Raw::new(&json!({
3202            "type": "m.room.member",
3203            "sender": inviter,
3204            "content": {
3205                "is_direct": true,
3206                "membership": "invite",
3207            },
3208            "state_key": invitee,
3209        }))
3210        .expect("Failed to make raw event")
3211        .cast_unchecked();
3212
3213        room.invite_state = Some(vec![evt]);
3214
3215        // We expect that there will also be an invite event in the
3216        // required_state, assuming you've asked for this type of event.
3217        room.required_state.push(make_state_event(
3218            inviter,
3219            invitee.as_str(),
3220            RoomMemberEventContent::new(MembershipState::Invite),
3221            None,
3222        ));
3223    }
3224
3225    fn set_room_knocked(room: &mut http::response::Room, knocker: &UserId) {
3226        // Sliding Sync shows an almost-empty event to indicate that we are
3227        // invited to a room. Just the type is supplied.
3228
3229        let evt = Raw::new(&json!({
3230            "type": "m.room.member",
3231            "sender": knocker,
3232            "content": {
3233                "is_direct": true,
3234                "membership": "knock",
3235            },
3236            "state_key": knocker,
3237        }))
3238        .expect("Failed to make raw event")
3239        .cast_unchecked();
3240
3241        room.invite_state = Some(vec![evt]);
3242    }
3243
3244    fn set_room_joined(room: &mut http::response::Room, user_id: &UserId) {
3245        room.required_state.push(make_membership_event(user_id, MembershipState::Join));
3246    }
3247
3248    fn set_room_left(room: &mut http::response::Room, user_id: &UserId) {
3249        room.required_state.push(make_membership_event(user_id, MembershipState::Leave));
3250    }
3251
3252    fn set_room_left_as_timeline_event(room: &mut http::response::Room, user_id: &UserId) {
3253        room.timeline.push(make_membership_event(user_id, MembershipState::Leave));
3254    }
3255
3256    fn set_room_is_encrypted(room: &mut http::response::Room, user_id: &UserId) {
3257        room.required_state.push(make_encryption_event(user_id));
3258    }
3259
3260    fn make_membership_event<K>(user_id: &UserId, state: MembershipState) -> Raw<K> {
3261        make_state_event(user_id, user_id.as_str(), RoomMemberEventContent::new(state), None)
3262    }
3263
3264    fn make_encryption_event<K>(user_id: &UserId) -> Raw<K> {
3265        make_state_event(user_id, "", RoomEncryptionEventContent::with_recommended_defaults(), None)
3266    }
3267
3268    fn make_global_account_data_event<C: GlobalAccountDataEventContent, E>(content: C) -> Raw<E> {
3269        Raw::new(&json!({
3270            "type": content.event_type(),
3271            "content": content,
3272        }))
3273        .expect("Failed to create account data event")
3274        .cast_unchecked()
3275    }
3276
3277    fn make_state_event<C: StateEventContent, E>(
3278        sender: &UserId,
3279        state_key: &str,
3280        content: C,
3281        prev_content: Option<C>,
3282    ) -> Raw<E> {
3283        let unsigned = if let Some(prev_content) = prev_content {
3284            json!({ "prev_content": prev_content })
3285        } else {
3286            json!({})
3287        };
3288
3289        Raw::new(&json!({
3290            "type": content.event_type(),
3291            "state_key": state_key,
3292            "content": content,
3293            "event_id": event_id!("$evt"),
3294            "sender": sender,
3295            "origin_server_ts": 10,
3296            "unsigned": unsigned,
3297        }))
3298        .expect("Failed to create state event")
3299        .cast_unchecked()
3300    }
3301
3302    fn make_profile_update(field: ProfileFieldName, value: serde_json::Value) -> UserProfileUpdate {
3303        let mut changes = UserProfileChanges::new();
3304        changes.updated.insert(field, value);
3305        UserProfileUpdate::Updated(changes)
3306    }
3307}