Skip to main content

matrix_sdk_ui/spaces/
mod.rs

1// Copyright 2025 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 that specific language governing permissions and
13// limitations under the License.
14
15//! High level interfaces for working with Spaces
16//!
17//! The `SpaceService` is an UI oriented, high-level interface for working with
18//! [Matrix Spaces](https://spec.matrix.org/latest/client-server-api/#spaces).
19//! It provides methods to retrieve joined spaces, subscribe
20//! to updates, and navigate space hierarchies.
21//!
22//! It consists of 3 main components:
23//! - `SpaceService`: The main service for managing spaces. It
24//! - `SpaceGraph`: An utility that maps the `m.space.parent` and
25//!   `m.space.child` fields into a graph structure, removing cycles and
26//!   providing access to top level parents.
27//! - `SpaceRoomList`: A component for retrieving a space's children rooms and
28//!   their details.
29
30use std::{
31    cmp::Ordering,
32    collections::{HashMap, HashSet, VecDeque},
33    pin::pin,
34    sync::Arc,
35};
36
37use eyeball_im::{ObservableVector, VectorSubscriberBatchedStream};
38use futures_util::future::join_all;
39use imbl::Vector;
40use itertools::Itertools;
41use matrix_sdk::{
42    Client, Error as SDKError, Room, deserialized_responses::SyncOrStrippedState,
43    task_monitor::BackgroundTaskHandle,
44};
45use ruma::{
46    OwnedRoomId, RoomId, SpaceChildOrder,
47    events::{
48        self, StateEventType, StaticEventContent, SyncStateEvent,
49        space::{child::SpaceChildEventContent, parent::SpaceParentEventContent},
50    },
51};
52use thiserror::Error;
53use tokio::sync::Mutex as AsyncMutex;
54use tracing::{error, trace, warn};
55
56use crate::spaces::{graph::SpaceGraph, leave::LeaveSpaceHandle, room::SpaceRoomChildState};
57pub use crate::spaces::{room::SpaceRoom, room_list::SpaceRoomList};
58
59pub mod graph;
60pub mod leave;
61pub mod room;
62pub mod room_list;
63
64/// Possible [`SpaceService`] errors.
65#[derive(Debug, Error)]
66pub enum Error {
67    /// The user ID was not available from the client.
68    #[error("User ID not available from client")]
69    UserIdNotFound,
70
71    /// The requested room was not found.
72    #[error("Room `{0}` not found")]
73    RoomNotFound(OwnedRoomId),
74
75    /// The space parent/child state was missing.
76    #[error("Missing `{0}` for `{1}`")]
77    MissingState(StateEventType, OwnedRoomId),
78
79    /// Failed to set either of the m.space.parent or m.space.child state
80    /// events.
81    #[error("Failed to set either of the m.space.parent or m.space.child state events")]
82    UpdateRelationship(SDKError),
83
84    /// Failed to set the expected m.space.parent state event (but any
85    /// m.space.child changes were successful).
86    #[error(
87        "Failed to set the expected m.space.parent state event (but any m.space.child changes were successful)"
88    )]
89    UpdateInverseRelationship(SDKError),
90
91    /// Failed to leave a space.
92    #[error("Failed to leave space")]
93    LeaveSpace(SDKError),
94
95    /// Failed to load members.
96    #[error("Failed to load members")]
97    LoadRoomMembers(SDKError),
98}
99
100struct SpaceState {
101    graph: SpaceGraph,
102    top_level_joined_spaces: ObservableVector<SpaceRoom>,
103    space_filters: ObservableVector<SpaceFilter>,
104}
105
106/// The main entry point into the Spaces facilities.
107///
108/// The spaces service is responsible for retrieving one's joined rooms,
109/// building a graph out of their `m.space.parent` and `m.space.child` state
110/// events, and providing access to the top-level spaces and their children.
111///
112/// # Examples
113///
114/// ```no_run
115/// use futures_util::StreamExt;
116/// use matrix_sdk::Client;
117/// use matrix_sdk_ui::spaces::SpaceService;
118/// use ruma::owned_room_id;
119///
120/// # async {
121/// # let client: Client = todo!();
122/// let space_service = SpaceService::new(client.clone()).await;
123///
124/// // Get a list of all the joined spaces
125/// let joined_spaces = space_service.top_level_joined_spaces().await;
126///
127/// // And subscribe to changes on them
128/// // `initial_values` is equal to `top_level_joined_spaces` if nothing changed meanwhile
129/// let (initial_values, stream) =
130///     space_service.subscribe_to_top_level_joined_spaces().await;
131///
132/// while let Some(diffs) = stream.next().await {
133///     println!("Received joined spaces updates: {diffs:?}");
134/// }
135///
136/// // Get a list of all the rooms in a particular space
137/// let room_list = space_service
138///     .space_room_list(owned_room_id!("!some_space:example.org"))
139///     .await;
140///
141/// // Which can be used to retrieve information about the children rooms
142/// let children = room_list.rooms().await;
143/// # anyhow::Ok(()) };
144/// ```
145pub struct SpaceService {
146    client: Client,
147
148    space_state: Arc<AsyncMutex<SpaceState>>,
149
150    _room_update_handle: AsyncMutex<BackgroundTaskHandle>,
151}
152
153impl SpaceService {
154    /// Creates a new `SpaceService` instance.
155    pub async fn new(client: Client) -> Self {
156        let space_state = Arc::new(AsyncMutex::new(SpaceState {
157            graph: SpaceGraph::new(),
158            top_level_joined_spaces: ObservableVector::new(),
159            space_filters: ObservableVector::new(),
160        }));
161
162        let room_update_handle = client
163            .task_monitor()
164            .spawn_infinite_task("space_service", {
165                let client = client.clone();
166                let space_state = Arc::clone(&space_state);
167                let all_room_updates_receiver = client.subscribe_to_all_room_updates();
168
169                async move {
170                    let mut all_room_updates_receiver = pin!(all_room_updates_receiver);
171
172                    loop {
173                        match all_room_updates_receiver.recv().await {
174                            Ok(updates) => {
175                                if updates.is_empty() {
176                                    continue;
177                                }
178
179                                let (spaces, filters, graph) =
180                                    Self::build_space_state(&client).await;
181                                Self::update_space_state_if_needed(
182                                    Vector::from(spaces),
183                                    Vector::from(filters),
184                                    graph,
185                                    &space_state,
186                                )
187                                .await;
188                            }
189                            Err(err) => {
190                                error!("error when listening to room updates: {err}");
191                            }
192                        }
193                    }
194                }
195            })
196            .abort_on_drop();
197
198        // Make sure to also update the currently joined spaces for the initial
199        // values.
200        let (spaces, filters, graph) = Self::build_space_state(&client).await;
201        Self::update_space_state_if_needed(
202            Vector::from(spaces),
203            Vector::from(filters),
204            graph,
205            &space_state,
206        )
207        .await;
208
209        Self { client, space_state, _room_update_handle: AsyncMutex::new(room_update_handle) }
210    }
211
212    /// Subscribes to updates on the joined spaces list. If space rooms are
213    /// joined or left, the stream will yield diffs that reflect the changes.
214    pub async fn subscribe_to_top_level_joined_spaces(
215        &self,
216    ) -> (Vector<SpaceRoom>, VectorSubscriberBatchedStream<SpaceRoom>) {
217        self.space_state
218            .lock()
219            .await
220            .top_level_joined_spaces
221            .subscribe()
222            .into_values_and_batched_stream()
223    }
224
225    /// Returns a list of all the top-level joined spaces. It will eagerly
226    /// compute the latest version and also notify subscribers if there were any
227    /// changes.
228    pub async fn top_level_joined_spaces(&self) -> Vec<SpaceRoom> {
229        let (top_level_joined_spaces, filters, graph) = Self::build_space_state(&self.client).await;
230
231        Self::update_space_state_if_needed(
232            Vector::from(top_level_joined_spaces.clone()),
233            Vector::from(filters),
234            graph,
235            &self.space_state,
236        )
237        .await;
238
239        top_level_joined_spaces
240    }
241
242    /// Space filters provide access to a custom subset of the space graph that
243    /// can be used in tandem with the [`crate::RoomListService`] to narrow down
244    /// the presented rooms. A [`crate::room_list_service::RoomList`]'s
245    /// [`crate::room_list_service::RoomListDynamicEntriesController`] can take
246    /// a filter, which in this case can be a
247    /// [`crate::room_list_service::filters::new_filter_identifiers`] pointing
248    /// to the space descendants retrieved from the filters.
249    ///
250    /// They are limited to the first 2 levels of the graph, with the first
251    /// level only containing direct descendants while the second holds the rest
252    /// of them recursively.
253    ///
254    /// # Examples
255    ///
256    /// ```no_run
257    /// use futures_util::StreamExt;
258    /// use matrix_sdk::Client;
259    /// use matrix_sdk_ui::{
260    ///     room_list_service::{RoomListService, filters},
261    ///     spaces::SpaceService,
262    /// };
263    /// use ruma::owned_room_id;
264    ///
265    /// # async {
266    /// # let client: Client = todo!();
267    /// let space_service = SpaceService::new(client.clone()).await;
268    /// let room_list_service = RoomListService::new(client.clone()).await?;
269    ///
270    /// // Get the list of filters derived from the space hierarchy.
271    /// let space_filters = space_service.space_filters().await;
272    /// // Pick a filter/space
273    /// let space_filter = space_filters.first().unwrap();
274    ///
275    /// // Create a room list stream and a controller that accepts filters.
276    /// let all_rooms = room_list_service.all_rooms().await?;
277    /// let (_, controller) = all_rooms.entries_with_dynamic_adapters(25);
278    ///
279    /// // Apply an identifiers filter built from the space filter descendants.
280    /// controller.set_filter(Box::new(filters::new_filter_identifiers(
281    ///     space_filter.descendants.clone(),
282    /// )));
283    ///
284    /// # anyhow::Ok(()) };
285    /// ```
286    pub async fn space_filters(&self) -> Vec<SpaceFilter> {
287        let (top_level_joined_spaces, filters, graph) = Self::build_space_state(&self.client).await;
288
289        Self::update_space_state_if_needed(
290            Vector::from(top_level_joined_spaces),
291            Vector::from(filters.clone()),
292            graph,
293            &self.space_state,
294        )
295        .await;
296
297        filters
298    }
299
300    /// Subscribe to changes or updates to the space filters.
301    pub async fn subscribe_to_space_filters(
302        &self,
303    ) -> (Vector<SpaceFilter>, VectorSubscriberBatchedStream<SpaceFilter>) {
304        self.space_state.lock().await.space_filters.subscribe().into_values_and_batched_stream()
305    }
306
307    /// Returns a flattened list containing all the spaces where the user has
308    /// permission to send `m.space.child` state events.
309    ///
310    /// Note: Unlike [`Self::top_level_joined_spaces()`], this method does not
311    /// recompute graph, nor does it notify subscribers about changes.
312    pub async fn editable_spaces(&self) -> Vec<SpaceRoom> {
313        let Some(user_id) = self.client.user_id() else {
314            return vec![];
315        };
316
317        let graph = &self.space_state.lock().await.graph;
318        let rooms = self.client.joined_space_rooms();
319
320        let mut editable_spaces = Vec::new();
321        for room in &rooms {
322            if let Ok(power_levels) = room.power_levels().await
323                && power_levels.user_can_send_state(user_id, StateEventType::SpaceChild)
324            {
325                let room_id = room.room_id();
326                editable_spaces.push(
327                    SpaceRoom::new_from_known(room, graph.children_of(room_id).len() as u64).await,
328                );
329            }
330        }
331
332        editable_spaces
333    }
334
335    /// Returns a `SpaceRoomList` for the given space ID.
336    pub async fn space_room_list(&self, space_id: OwnedRoomId) -> SpaceRoomList {
337        SpaceRoomList::new(self.client.clone(), space_id).await
338    }
339
340    /// Returns all known direct-parents of a given space room ID.
341    pub async fn joined_parents_of_child(&self, child_id: &RoomId) -> Vec<SpaceRoom> {
342        let graph = &self.space_state.lock().await.graph;
343
344        let rooms = graph
345            .parents_of(child_id)
346            .into_iter()
347            .filter_map(|parent_id| self.client.get_room(parent_id));
348
349        join_all(rooms.map(|room| async move {
350            SpaceRoom::new_from_known(&room, graph.children_of(room.room_id()).len() as u64).await
351        }))
352        .await
353    }
354
355    /// Returns the room IDs of all known direct parents of the given child
356    /// space or room.
357    ///
358    /// This is a much cheaper version of [`Self::joined_parents_of_child()`]
359    /// that doesn't build any [`SpaceRoom`] instances, it only reads the
360    /// existing space graph.
361    ///
362    /// The returned IDs are always joined spaces, as that's all the space graph
363    /// includes. Note that an empty result either means that the child is a
364    /// top-level space (which has no direct parents) or the child isn't part of
365    /// the space graph at all. See [`Self::top_level_ancestors_of()`] if you
366    /// need that particular level of detail.
367    ///
368    /// Note: Unlike [`Self::top_level_joined_spaces()`], this method does not
369    /// recompute the space graph nor notify subscribers about changes.
370    pub async fn joined_parent_ids_of_child(&self, child_id: &RoomId) -> Vec<OwnedRoomId> {
371        self.space_state
372            .lock()
373            .await
374            .graph
375            .parents_of(child_id)
376            .into_iter()
377            .map(ToOwned::to_owned)
378            .collect()
379    }
380
381    /// Returns the room IDs of the top-level joined space(s) that the given
382    /// child room/space descends from, by walking the space graph upwards.
383    ///
384    /// A room/space can be the child of multiple spaces, so this might return
385    /// multiple top-level spaces (in no order).
386    ///
387    /// A top-level space is its own only ancestor, so a returned set holding
388    /// just `child_id` is a cheap top-level space check.
389    ///
390    /// Returns an empty set if the room isn't part of the graph, which is
391    /// notably the case for a room that was joined too recently for the graph
392    /// to have been rebuilt.
393    ///
394    /// Note: Unlike [`Self::top_level_joined_spaces()`], this method does not
395    /// recompute the space graph nor notify subscribers about changes.
396    pub async fn top_level_ancestors_of(&self, child_id: &RoomId) -> HashSet<OwnedRoomId> {
397        let space_state = self.space_state.lock().await;
398        let graph = &space_state.graph;
399
400        if !graph.has_node(child_id) {
401            return HashSet::new();
402        }
403
404        let mut queue = VecDeque::from([child_id.to_owned()]);
405        let mut visited = HashSet::from([child_id.to_owned()]);
406        let mut roots = HashSet::new();
407        while let Some(current) = queue.pop_front() {
408            let parents = graph.parents_of(&current);
409            if parents.is_empty() {
410                // A node without parents must be a joined space.
411                roots.insert(current);
412                continue;
413            }
414            for parent in parents {
415                if visited.insert(parent.to_owned()) {
416                    queue.push_back(parent.to_owned());
417                }
418            }
419        }
420        roots
421    }
422
423    /// Returns the corresponding `SpaceRoom` for the given room ID, or `None`
424    /// if it isn't known.
425    pub async fn get_space_room(&self, room_id: &RoomId) -> Option<SpaceRoom> {
426        let graph = &self.space_state.lock().await.graph;
427
428        if graph.has_node(room_id)
429            && let Some(room) = self.client.get_room(room_id)
430        {
431            Some(
432                SpaceRoom::new_from_known(&room, graph.children_of(room.room_id()).len() as u64)
433                    .await,
434            )
435        } else {
436            None
437        }
438    }
439
440    pub async fn add_child_to_space(
441        &self,
442        child_id: OwnedRoomId,
443        space_id: OwnedRoomId,
444    ) -> Result<(), Error> {
445        let user_id = self.client.user_id().ok_or(Error::UserIdNotFound)?;
446        let space_room =
447            self.client.get_room(&space_id).ok_or(Error::RoomNotFound(space_id.to_owned()))?;
448        let child_room =
449            self.client.get_room(&child_id).ok_or(Error::RoomNotFound(child_id.to_owned()))?;
450        let child_power_levels = child_room
451            .power_levels()
452            .await
453            .map_err(|error| Error::UpdateRelationship(matrix_sdk::Error::from(error)))?;
454
455        // Add the child to the space.
456        let child_route = child_room.route().await.map_err(Error::UpdateRelationship)?;
457        space_room
458            .send_state_event_for_key(&child_id, SpaceChildEventContent::new(child_route))
459            .await
460            .map_err(Error::UpdateRelationship)?;
461
462        // Add the space as parent of the child if allowed.
463        if child_power_levels.user_can_send_state(user_id, StateEventType::SpaceParent) {
464            let parent_route =
465                space_room.route().await.map_err(Error::UpdateInverseRelationship)?;
466            child_room
467                .send_state_event_for_key(&space_id, SpaceParentEventContent::new(parent_route))
468                .await
469                .map_err(Error::UpdateInverseRelationship)?;
470        } else {
471            warn!("The current user doesn't have permission to set the child's parent.");
472        }
473
474        Ok(())
475    }
476
477    pub async fn remove_child_from_space(
478        &self,
479        child_id: OwnedRoomId,
480        space_id: OwnedRoomId,
481    ) -> Result<(), Error> {
482        let user_id = self.client.user_id().ok_or(Error::UserIdNotFound)?;
483        let space_room =
484            self.client.get_room(&space_id).ok_or(Error::RoomNotFound(space_id.to_owned()))?;
485
486        if let Ok(Some(_)) =
487            space_room.get_state_event_static_for_key::<SpaceChildEventContent, _>(&child_id).await
488        {
489            // Redacting state is a "weird" thing to do, so send {} instead.
490            // https://github.com/matrix-org/matrix-spec/issues/2252
491            //
492            // Specifically, "The redaction of the state doesn't participate in
493            // state resolution so behaves quite differently from e.g. sending
494            // an empty form of that state events".
495            space_room
496                .send_state_event_raw(
497                    SpaceChildEventContent::TYPE,
498                    child_id.as_str(),
499                    serde_json::json!({}),
500                )
501                .await
502                .map_err(Error::UpdateRelationship)?;
503        } else {
504            warn!("A space child event wasn't found on the parent, ignoring.");
505        }
506
507        if let Some(child_room) = self.client.get_room(&child_id) {
508            let power_levels = child_room.power_levels().await.map_err(|error| {
509                Error::UpdateInverseRelationship(matrix_sdk::Error::from(error))
510            })?;
511
512            if power_levels.user_can_send_state(user_id, StateEventType::SpaceParent)
513                && let Ok(Some(_)) = child_room
514                    .get_state_event_static_for_key::<SpaceParentEventContent, _>(&space_id)
515                    .await
516            {
517                // Same as the comment above.
518                child_room
519                    .send_state_event_raw(
520                        SpaceParentEventContent::TYPE,
521                        space_id.as_str(),
522                        serde_json::json!({}),
523                    )
524                    .await
525                    .map_err(Error::UpdateInverseRelationship)?;
526            } else {
527                warn!("A space parent event wasn't found on the child, ignoring.");
528            }
529        } else {
530            warn!("The child room is unknown, skipping m.space.parent removal.");
531        }
532
533        Ok(())
534    }
535
536    /// Start a space leave process returning a [`LeaveSpaceHandle`] from which
537    /// rooms can be retrieved in reversed BFS order starting from the requested
538    /// `space_id` graph node. If the room is unknown then an error will be
539    /// returned.
540    ///
541    /// Once the rooms to be left are chosen the handle can be used to leave
542    /// them.
543    pub async fn leave_space(&self, space_id: &RoomId) -> Result<LeaveSpaceHandle, Error> {
544        let space_state = self.space_state.lock().await;
545
546        if !space_state.graph.has_node(space_id) {
547            return Err(Error::RoomNotFound(space_id.to_owned()));
548        }
549
550        let room_ids = space_state.graph.flattened_bottom_up_subtree(space_id);
551
552        let handle = LeaveSpaceHandle::new(self.client.clone(), room_ids).await;
553
554        Ok(handle)
555    }
556
557    async fn update_space_state_if_needed(
558        new_spaces: Vector<SpaceRoom>,
559        new_filters: Vector<SpaceFilter>,
560        new_graph: SpaceGraph,
561        space_state: &Arc<AsyncMutex<SpaceState>>,
562    ) {
563        let mut space_state = space_state.lock().await;
564
565        if new_spaces != space_state.top_level_joined_spaces.clone() {
566            space_state.top_level_joined_spaces.clear();
567            space_state.top_level_joined_spaces.append(new_spaces);
568        }
569
570        if new_filters != space_state.space_filters.clone() {
571            space_state.space_filters.clear();
572            space_state.space_filters.append(new_filters);
573        }
574
575        space_state.graph = new_graph;
576    }
577
578    async fn build_space_state(client: &Client) -> (Vec<SpaceRoom>, Vec<SpaceFilter>, SpaceGraph) {
579        let joined_spaces = client.joined_space_rooms();
580        let joined_space_ids =
581            joined_spaces.iter().map(|space| space.room_id()).collect::<HashSet<_>>();
582
583        // Build a graph to hold the parent-child relations
584        let mut graph = SpaceGraph::new();
585
586        // And also store `m.space.child` ordering info for later use
587        let mut space_child_states = HashMap::<OwnedRoomId, SpaceRoomChildState>::new();
588
589        // Iterate over all joined spaces and populate the graph with edges
590        // based on `m.space.parent` and `m.space.child` state events.
591        for space in joined_spaces.iter() {
592            graph.add_node(space.room_id().to_owned());
593
594            if let Ok(parents) = space.get_state_events_static::<SpaceParentEventContent>().await {
595                parents.into_iter()
596                .flat_map(|parent_event| match parent_event.deserialize() {
597                    Ok(SyncOrStrippedState::Sync(SyncStateEvent::Original(e))) => {
598                        Some(e.state_key)
599                    }
600                    Ok(SyncOrStrippedState::Sync(SyncStateEvent::Redacted(_))) => None,
601                    Ok(SyncOrStrippedState::Stripped(e)) => Some(e.state_key),
602                    Err(e) => {
603                        trace!(room_id = ?space.room_id(), "Could not deserialize m.space.parent: {e}");
604                        None
605                    }
606                })
607                // Note: this filter, together with the fact that the loop only
608                // ever adds edges out of a joined space, is what guarantees
609                // that the parent end of every edge is a joined space. Both
610                // `joined_parent_ids_of_child` and `top_level_ancestors_of`
611                // rely on it to return joined spaces without re-checking.
612                .filter(|parent| joined_space_ids.contains(&**parent))
613                .for_each(|parent| graph.add_edge(parent, space.room_id().to_owned()));
614            } else {
615                error!(room_id = ?space.room_id(), "Could not get m.space.parent events");
616            }
617
618            if let Ok(children) = space.get_state_events_static::<SpaceChildEventContent>().await {
619                children.into_iter()
620                .filter_map(|child_event| match child_event.deserialize() {
621                    Ok(SyncOrStrippedState::Sync(SyncStateEvent::Original(e))) => {
622                        space_child_states.insert(
623                            e.state_key.to_owned(),
624                            SpaceRoomChildState {
625                                order: e.content.order.clone(),
626                                origin_server_ts: e.origin_server_ts,
627                            },
628                        );
629
630                        Some(e.state_key)
631                    }
632                    Ok(SyncOrStrippedState::Sync(SyncStateEvent::Redacted(_))) => None,
633                    Ok(SyncOrStrippedState::Stripped(e)) => Some(e.state_key),
634                    Err(e) => {
635                        trace!(room_id = ?space.room_id(), "Could not deserialize m.space.child: {e}");
636                        None
637                    }
638                }).for_each(|child| graph.add_edge(space.room_id().to_owned(), child));
639            } else {
640                error!(room_id = ?space.room_id(), "Could not get m.space.child events");
641            }
642        }
643
644        // Remove cycles from the graph. This is important because they are not
645        // enforced backend side.
646        graph.remove_cycles();
647
648        let root_nodes = graph.root_nodes();
649
650        // Proceed with filtering to the top level spaces, sorting them by their
651        // (optional) order field (as defined in MSC3230) and then mapping them
652        // to `SpaceRoom`s.
653        let top_level_space_rooms = joined_spaces
654            .iter()
655            .filter(|room| root_nodes.contains(&room.room_id()))
656            .collect::<Vec<_>>();
657
658        let mut top_level_space_order = HashMap::new();
659        for space in &top_level_space_rooms {
660            if let Ok(Some(raw_event)) =
661                space.account_data_static::<events::space_order::SpaceOrderEventContent>().await
662                && let Ok(event) = raw_event.deserialize()
663            {
664                top_level_space_order.insert(space.room_id().to_owned(), event.content.order);
665            }
666        }
667
668        let top_level_space_rooms = top_level_space_rooms
669            .into_iter()
670            .sorted_by(|a, b| {
671                let a = (a.room_id(), top_level_space_order.get(a.room_id()).map(AsRef::as_ref));
672                let b = (b.room_id(), top_level_space_order.get(b.room_id()).map(AsRef::as_ref));
673
674                compare_top_level_space_rooms(a, b)
675            })
676            .collect::<Vec<_>>();
677
678        let mut top_level_spaces = Vec::new();
679
680        for room in &top_level_space_rooms {
681            top_level_spaces.push(
682                SpaceRoom::new_from_known(room, graph.children_of(room.room_id()).len() as u64)
683                    .await,
684            );
685        }
686
687        let space_filters =
688            Self::build_space_filters(client, &graph, top_level_space_rooms, space_child_states)
689                .await;
690
691        (top_level_spaces, space_filters, graph)
692    }
693
694    /// Build the 2 levels required for space filters. As per product
695    /// requirements, the first level space filters only include direct
696    /// descendants while second level ones contain _all_ descendants.
697    ///
698    /// The sorting mechanism is different between first level spaces/filters
699    /// and second level ones so while the former are already sorted at this
700    /// point the latter need to be manually taken care of here though the use
701    /// of the collected `m.space.child` state event details.
702    async fn build_space_filters(
703        client: &Client,
704        graph: &SpaceGraph,
705        top_level_space_rooms: Vec<&Room>,
706        space_child_states: HashMap<OwnedRoomId, SpaceRoomChildState>,
707    ) -> Vec<SpaceFilter> {
708        let mut filters = Vec::new();
709        for top_level_space in top_level_space_rooms {
710            let children = graph
711                .children_of(top_level_space.room_id())
712                .into_iter()
713                .map(|id| id.to_owned())
714                .collect::<Vec<_>>();
715
716            filters.push(SpaceFilter {
717                space_room: SpaceRoom::new_from_known(top_level_space, children.len() as u64).await,
718                level: 0,
719                descendants: children.clone(),
720            });
721
722            let children_rooms = join_all(
723                children
724                    .iter()
725                    .filter_map(|child| client.get_room(child))
726                    .filter(|room| room.is_space())
727                    .map(|room| async move {
728                        SpaceRoom::new_from_known(
729                            &room,
730                            graph.children_of(room.room_id()).len() as u64,
731                        )
732                        .await
733                    }),
734            )
735            .await;
736            filters.append(
737                &mut children_rooms
738                    .into_iter()
739                    .sorted_by(|a, b| {
740                        let a_state = space_child_states.get(&a.room_id).cloned();
741                        let b_state = space_child_states.get(&b.room_id).cloned();
742
743                        SpaceRoom::compare_rooms(
744                            (&a.room_id, a_state.as_ref()),
745                            (&b.room_id, b_state.as_ref()),
746                        )
747                    })
748                    .map(|space_room| {
749                        let descendants = graph.flattened_bottom_up_subtree(&space_room.room_id);
750
751                        SpaceFilter { space_room, level: 1, descendants }
752                    })
753                    .collect::<Vec<_>>(),
754            );
755        }
756
757        filters
758    }
759}
760
761// MSC3230: lexicographically by `order` and then by room ID
762fn compare_top_level_space_rooms(
763    a: (&RoomId, Option<&SpaceChildOrder>),
764    b: (&RoomId, Option<&SpaceChildOrder>),
765) -> Ordering {
766    let (a_room_id, a_order) = a;
767    let (b_room_id, b_order) = b;
768
769    match (a_order, b_order) {
770        (Some(a_order), Some(b_order)) => a_order.cmp(b_order).then(a_room_id.cmp(b_room_id)),
771        (Some(_), None) => Ordering::Less,
772        (None, Some(_)) => Ordering::Greater,
773        (None, None) => a_room_id.cmp(b_room_id),
774    }
775}
776
777#[derive(Debug, Clone, PartialEq)]
778pub struct SpaceFilter {
779    /// The underlying [`SpaceRoom`]
780    pub space_room: SpaceRoom,
781
782    /// The level of the space filter in the tree/hierarchy. At this point in
783    /// time the filters are limited to the first 2 levels.
784    pub level: u8,
785
786    /// The room identifiers of the descendants of this space. For top level
787    /// spaces (level 0) these will be direct descendants while for first level
788    /// spaces they will be all other descendants, recursively.
789    pub descendants: Vec<OwnedRoomId>,
790}
791
792#[cfg(test)]
793mod tests {
794    use std::{collections::BTreeMap, pin::pin};
795
796    use eyeball_im::VectorDiff;
797    use futures_util::StreamExt;
798    use matrix_sdk::{room::ParentSpace, test_utils::mocks::MatrixMockServer};
799    use matrix_sdk_test::{
800        JoinedRoomBuilder, LeftRoomBuilder, async_test, event_factory::EventFactory,
801    };
802    use proptest::prelude::*;
803    use ruma::{
804        MilliSecondsSinceUnixEpoch, OwnedSpaceChildOrder, RoomVersionId, UserId, event_id,
805        owned_room_id, room_id, serde::Raw,
806    };
807    use serde_json::json;
808    use strass::assert_let;
809    use stream_assert::{assert_next_eq, assert_pending};
810
811    use super::*;
812
813    #[async_test]
814    async fn test_spaces_hierarchy() {
815        let server = MatrixMockServer::new().await;
816        let client = server.client_builder().build().await;
817        let user_id = client.user_id().unwrap();
818        let space_service = SpaceService::new(client.clone()).await;
819        let factory = EventFactory::new();
820
821        server.mock_room_state_encryption().plain().mount().await;
822
823        // Given one parent space with 2 children spaces
824
825        let parent_space_id = room_id!("!parent_space:example.org");
826        let child_space_id_1 = room_id!("!child_space_1:example.org");
827        let child_space_id_2 = room_id!("!child_space_2:example.org");
828
829        add_space_rooms(
830            vec![
831                MockSpaceRoomParameters {
832                    room_id: child_space_id_1,
833                    order: None,
834                    parents: vec![parent_space_id],
835                    children: vec![],
836                    power_level: None,
837                },
838                MockSpaceRoomParameters {
839                    room_id: child_space_id_2,
840                    order: None,
841                    parents: vec![parent_space_id],
842                    children: vec![],
843                    power_level: None,
844                },
845                MockSpaceRoomParameters {
846                    room_id: parent_space_id,
847                    order: None,
848                    parents: vec![],
849                    children: vec![child_space_id_1, child_space_id_2],
850                    power_level: None,
851                },
852            ],
853            &client,
854            &server,
855            &factory,
856            user_id,
857        )
858        .await;
859
860        // Only the parent space is returned
861        assert_eq!(
862            space_service
863                .top_level_joined_spaces()
864                .await
865                .iter()
866                .map(|s| s.room_id.to_owned())
867                .collect::<Vec<_>>(),
868            vec![parent_space_id]
869        );
870
871        // and it has 2 children
872        assert_eq!(
873            space_service
874                .top_level_joined_spaces()
875                .await
876                .iter()
877                .map(|s| s.children_count)
878                .collect::<Vec<_>>(),
879            vec![2]
880        );
881
882        let parent_space = client.get_room(parent_space_id).unwrap();
883        assert!(parent_space.is_space());
884
885        // And the parent space and the two child spaces are linked
886
887        let spaces: Vec<ParentSpace> = client
888            .get_room(child_space_id_1)
889            .unwrap()
890            .parent_spaces()
891            .await
892            .unwrap()
893            .map(Result::unwrap)
894            .collect()
895            .await;
896
897        assert_let!(ParentSpace::Reciprocal(parent) = spaces.first().unwrap());
898        assert_eq!(parent.room_id(), parent_space.room_id());
899
900        let spaces: Vec<ParentSpace> = client
901            .get_room(child_space_id_2)
902            .unwrap()
903            .parent_spaces()
904            .await
905            .unwrap()
906            .map(Result::unwrap)
907            .collect()
908            .await;
909
910        assert_let!(ParentSpace::Reciprocal(parent) = spaces.last().unwrap());
911        assert_eq!(parent.room_id(), parent_space.room_id());
912    }
913
914    #[async_test]
915    async fn test_joined_spaces_updates() {
916        let server = MatrixMockServer::new().await;
917        let client = server.client_builder().build().await;
918        let user_id = client.user_id().unwrap();
919        let factory = EventFactory::new();
920
921        server.mock_room_state_encryption().plain().mount().await;
922
923        let first_space_id = room_id!("!first_space:example.org");
924        let second_space_id = room_id!("!second_space:example.org");
925
926        // Join the first space
927        server
928            .sync_room(
929                &client,
930                JoinedRoomBuilder::new(first_space_id)
931                    .add_state_event(factory.create(user_id, RoomVersionId::V1).with_space_type()),
932            )
933            .await;
934
935        // Build the `SpaceService` and expect the room to show up with no
936        // updates pending
937
938        let space_service = SpaceService::new(client.clone()).await;
939
940        let (initial_values, joined_spaces_subscriber) =
941            space_service.subscribe_to_top_level_joined_spaces().await;
942        let mut joined_spaces_subscriber = pin!(joined_spaces_subscriber);
943        assert_pending!(joined_spaces_subscriber);
944
945        assert_eq!(
946            initial_values,
947            vec![SpaceRoom::new_from_known(&client.get_room(first_space_id).unwrap(), 0).await]
948                .into()
949        );
950
951        assert_eq!(
952            space_service.top_level_joined_spaces().await,
953            vec![SpaceRoom::new_from_known(&client.get_room(first_space_id).unwrap(), 0).await]
954        );
955
956        // And the stream is still pending as the initial values were already
957        // set.
958        assert_pending!(joined_spaces_subscriber);
959
960        // Join the second space
961
962        server
963            .sync_room(
964                &client,
965                JoinedRoomBuilder::new(second_space_id)
966                    .add_state_event(factory.create(user_id, RoomVersionId::V1).with_space_type())
967                    .add_state_event(
968                        factory
969                            .space_child(
970                                second_space_id.to_owned(),
971                                owned_room_id!("!child:example.org"),
972                            )
973                            .sender(user_id),
974                    ),
975            )
976            .await;
977
978        // And expect the list to update
979        assert_eq!(
980            space_service.top_level_joined_spaces().await,
981            vec![
982                SpaceRoom::new_from_known(&client.get_room(first_space_id).unwrap(), 0).await,
983                SpaceRoom::new_from_known(&client.get_room(second_space_id).unwrap(), 1).await
984            ]
985        );
986
987        assert_next_eq!(
988            joined_spaces_subscriber,
989            vec![
990                VectorDiff::Clear,
991                VectorDiff::Append {
992                    values: vec![
993                        SpaceRoom::new_from_known(&client.get_room(first_space_id).unwrap(), 0)
994                            .await,
995                        SpaceRoom::new_from_known(&client.get_room(second_space_id).unwrap(), 1)
996                            .await
997                    ]
998                    .into()
999                },
1000            ]
1001        );
1002
1003        server.sync_room(&client, LeftRoomBuilder::new(second_space_id)).await;
1004
1005        // and when one is left
1006        assert_next_eq!(
1007            joined_spaces_subscriber,
1008            vec![
1009                VectorDiff::Clear,
1010                VectorDiff::Append {
1011                    values: vec![
1012                        SpaceRoom::new_from_known(&client.get_room(first_space_id).unwrap(), 0)
1013                            .await
1014                    ]
1015                    .into()
1016                },
1017            ]
1018        );
1019
1020        // but it doesn't when a non-space room gets joined
1021        server
1022            .sync_room(
1023                &client,
1024                JoinedRoomBuilder::new(room_id!("!room:example.org"))
1025                    .add_state_event(factory.create(user_id, RoomVersionId::V1)),
1026            )
1027            .await;
1028
1029        // and the subscriber doesn't yield any updates
1030        assert_pending!(joined_spaces_subscriber);
1031        assert_eq!(
1032            space_service.top_level_joined_spaces().await,
1033            vec![SpaceRoom::new_from_known(&client.get_room(first_space_id).unwrap(), 0).await]
1034        );
1035    }
1036
1037    #[async_test]
1038    async fn test_joined_child_space_becomes_top_level_after_leaving_parent() {
1039        let server = MatrixMockServer::new().await;
1040        let client = server.client_builder().build().await;
1041
1042        server.mock_room_state_encryption().plain().mount().await;
1043
1044        let parent_space_id = room_id!("!parent_space:example.org");
1045        let child_space_id = room_id!("!child_space:example.org");
1046        let child_room_id = room_id!("!child_room:example.org");
1047
1048        add_space_rooms(
1049            vec![
1050                MockSpaceRoomParameters {
1051                    room_id: child_space_id,
1052                    order: None,
1053                    parents: vec![parent_space_id],
1054                    children: vec![child_room_id],
1055                    power_level: None,
1056                },
1057                MockSpaceRoomParameters {
1058                    room_id: parent_space_id,
1059                    order: None,
1060                    parents: vec![],
1061                    children: vec![child_space_id],
1062                    power_level: None,
1063                },
1064            ],
1065            &client,
1066            &server,
1067            &EventFactory::new(),
1068            client.user_id().unwrap(),
1069        )
1070        .await;
1071
1072        let space_service = SpaceService::new(client.clone()).await;
1073
1074        let top_level_spaces = space_service.top_level_joined_spaces().await;
1075        assert_eq!(top_level_spaces.len(), 1);
1076        assert_eq!(top_level_spaces[0].room_id, parent_space_id);
1077
1078        server.sync_room(&client, LeftRoomBuilder::new(parent_space_id)).await;
1079
1080        let top_level_spaces = space_service.top_level_joined_spaces().await;
1081        assert_eq!(top_level_spaces.len(), 1);
1082        assert_eq!(top_level_spaces[0].room_id, child_space_id);
1083        assert_eq!(top_level_spaces[0].children_count, 1);
1084
1085        let filters = space_service.space_filters().await;
1086        assert_eq!(filters.len(), 1);
1087        assert_eq!(filters[0].space_room.room_id, child_space_id);
1088        assert_eq!(filters[0].descendants, vec![child_room_id]);
1089    }
1090
1091    #[async_test]
1092    async fn test_space_filters() {
1093        let server = MatrixMockServer::new().await;
1094        let client = server.client_builder().build().await;
1095
1096        server.mock_room_state_encryption().plain().mount().await;
1097
1098        add_space_rooms(
1099            vec![
1100                MockSpaceRoomParameters {
1101                    room_id: room_id!("!1:a.b"),
1102                    order: None,
1103                    parents: vec![],
1104                    children: vec![],
1105                    power_level: None,
1106                },
1107                MockSpaceRoomParameters {
1108                    room_id: room_id!("!1.2:a.b"),
1109                    order: None,
1110                    parents: vec![room_id!("!1:a.b")],
1111                    children: vec![],
1112                    power_level: None,
1113                },
1114                MockSpaceRoomParameters {
1115                    room_id: room_id!("!1.2.3:a.b"),
1116                    order: None,
1117                    parents: vec![room_id!("!1.2:a.b")],
1118                    children: vec![],
1119                    power_level: None,
1120                },
1121                MockSpaceRoomParameters {
1122                    room_id: room_id!("!1.2.3.4:a.b"),
1123                    order: None,
1124                    parents: vec![room_id!("!1.2.3:a.b")],
1125                    children: vec![],
1126                    power_level: None,
1127                },
1128            ],
1129            &client,
1130            &server,
1131            &EventFactory::new(),
1132            client.user_id().unwrap(),
1133        )
1134        .await;
1135
1136        let space_service = SpaceService::new(client.clone()).await;
1137
1138        let filters = space_service.space_filters().await;
1139        assert_eq!(filters.len(), 2);
1140        assert_eq!(filters[0].space_room.room_id, "!1:a.b");
1141        assert_eq!(filters[0].level, 0);
1142        assert_eq!(filters[0].descendants.len(), 1); //
1143        assert_eq!(filters[1].space_room.room_id, "!1.2:a.b");
1144        assert_eq!(filters[1].level, 1);
1145        assert_eq!(filters[1].descendants.len(), 3);
1146
1147        let (initial_values, space_filters_subscriber) =
1148            space_service.subscribe_to_space_filters().await;
1149        let mut space_filters_subscriber = pin!(space_filters_subscriber);
1150        assert_pending!(space_filters_subscriber);
1151
1152        assert_eq!(initial_values, filters.into());
1153
1154        add_space_rooms(
1155            vec![MockSpaceRoomParameters {
1156                room_id: room_id!("!1.2.3.4.5:a.b"),
1157                order: None,
1158                parents: vec![room_id!("!1.2.3.4:a.b")],
1159                children: vec![],
1160                power_level: None,
1161            }],
1162            &client,
1163            &server,
1164            &EventFactory::new(),
1165            client.user_id().unwrap(),
1166        )
1167        .await;
1168
1169        space_filters_subscriber.next().await;
1170
1171        let filters = space_service.space_filters().await;
1172        assert_eq!(filters[0].descendants.len(), 1);
1173        assert_eq!(filters[1].descendants.len(), 4);
1174    }
1175
1176    #[async_test]
1177    async fn test_top_level_space_order() {
1178        let server = MatrixMockServer::new().await;
1179        let client = server.client_builder().build().await;
1180
1181        server.mock_room_state_encryption().plain().mount().await;
1182
1183        add_space_rooms(
1184            vec![
1185                MockSpaceRoomParameters {
1186                    room_id: room_id!("!2:a.b"),
1187                    order: Some("2"),
1188                    parents: vec![],
1189                    children: vec![],
1190                    power_level: None,
1191                },
1192                MockSpaceRoomParameters {
1193                    room_id: room_id!("!4:a.b"),
1194                    order: None,
1195                    parents: vec![],
1196                    children: vec![],
1197                    power_level: None,
1198                },
1199                MockSpaceRoomParameters {
1200                    room_id: room_id!("!3:a.b"),
1201                    order: None,
1202                    parents: vec![],
1203                    children: vec![],
1204                    power_level: None,
1205                },
1206                MockSpaceRoomParameters {
1207                    room_id: room_id!("!1:a.b"),
1208                    order: Some("1"),
1209                    parents: vec![],
1210                    children: vec![],
1211                    power_level: None,
1212                },
1213            ],
1214            &client,
1215            &server,
1216            &EventFactory::new(),
1217            client.user_id().unwrap(),
1218        )
1219        .await;
1220
1221        let space_service = SpaceService::new(client.clone()).await;
1222
1223        // Space with an `order` field set should come first in lexicographic
1224        // order and rest sorted by room ID.
1225        assert_eq!(
1226            space_service.top_level_joined_spaces().await,
1227            vec![
1228                SpaceRoom::new_from_known(&client.get_room(room_id!("!1:a.b")).unwrap(), 0).await,
1229                SpaceRoom::new_from_known(&client.get_room(room_id!("!2:a.b")).unwrap(), 0).await,
1230                SpaceRoom::new_from_known(&client.get_room(room_id!("!3:a.b")).unwrap(), 0).await,
1231                SpaceRoom::new_from_known(&client.get_room(room_id!("!4:a.b")).unwrap(), 0).await,
1232            ]
1233        );
1234    }
1235
1236    #[async_test]
1237    async fn test_editable_spaces() {
1238        // Given a space hierarchy where the user is admin of some spaces and
1239        // subspaces.
1240        let server = MatrixMockServer::new().await;
1241        let client = server.client_builder().build().await;
1242        let user_id = client.user_id().unwrap();
1243        let factory = EventFactory::new();
1244
1245        server.mock_room_state_encryption().plain().mount().await;
1246
1247        let admin_space_id = room_id!("!admin_space:example.org");
1248        let admin_subspace_id = room_id!("!admin_subspace:example.org");
1249        let regular_space_id = room_id!("!regular_space:example.org");
1250        let regular_subspace_id = room_id!("!regular_subspace:example.org");
1251
1252        add_space_rooms(
1253            vec![
1254                MockSpaceRoomParameters {
1255                    room_id: admin_space_id,
1256                    order: None,
1257                    parents: vec![],
1258                    children: vec![regular_subspace_id],
1259                    power_level: Some(100),
1260                },
1261                MockSpaceRoomParameters {
1262                    room_id: admin_subspace_id,
1263                    order: None,
1264                    parents: vec![regular_space_id],
1265                    children: vec![],
1266                    power_level: Some(100),
1267                },
1268                MockSpaceRoomParameters {
1269                    room_id: regular_space_id,
1270                    order: None,
1271                    parents: vec![],
1272                    children: vec![admin_subspace_id],
1273                    power_level: Some(0),
1274                },
1275                MockSpaceRoomParameters {
1276                    room_id: regular_subspace_id,
1277                    order: None,
1278                    parents: vec![admin_space_id],
1279                    children: vec![],
1280                    power_level: Some(0),
1281                },
1282            ],
1283            &client,
1284            &server,
1285            &factory,
1286            user_id,
1287        )
1288        .await;
1289
1290        let space_service = SpaceService::new(client.clone()).await;
1291
1292        // When retrieving all editable joined spaces.
1293        let editable_spaces = space_service.editable_spaces().await;
1294
1295        // Then only the spaces where the user is admin are returned.
1296        assert_eq!(
1297            editable_spaces.iter().map(|room| room.room_id.to_owned()).collect::<Vec<_>>(),
1298            vec![admin_space_id.to_owned(), admin_subspace_id.to_owned()]
1299        );
1300    }
1301
1302    #[async_test]
1303    async fn test_joined_parents_of_child() {
1304        // Given a space with three parent spaces, two of which are joined.
1305        let server = MatrixMockServer::new().await;
1306        let client = server.client_builder().build().await;
1307        let user_id = client.user_id().unwrap();
1308        let factory = EventFactory::new();
1309
1310        server.mock_room_state_encryption().plain().mount().await;
1311
1312        let parent_space_id_1 = room_id!("!parent_space_1:example.org");
1313        let parent_space_id_2 = room_id!("!parent_space_2:example.org");
1314        let unknown_parent_space_id = room_id!("!unknown_parent_space:example.org");
1315        let child_space_id = room_id!("!child_space:example.org");
1316
1317        add_space_rooms(
1318            vec![
1319                MockSpaceRoomParameters {
1320                    room_id: child_space_id,
1321                    order: None,
1322                    parents: vec![parent_space_id_1, parent_space_id_2, unknown_parent_space_id],
1323                    children: vec![],
1324                    power_level: None,
1325                },
1326                MockSpaceRoomParameters {
1327                    room_id: parent_space_id_1,
1328                    order: None,
1329                    parents: vec![],
1330                    children: vec![child_space_id],
1331                    power_level: None,
1332                },
1333                MockSpaceRoomParameters {
1334                    room_id: parent_space_id_2,
1335                    order: None,
1336                    parents: vec![],
1337                    children: vec![child_space_id],
1338                    power_level: None,
1339                },
1340            ],
1341            &client,
1342            &server,
1343            &factory,
1344            user_id,
1345        )
1346        .await;
1347
1348        let space_service = SpaceService::new(client.clone()).await;
1349
1350        // When retrieving the joined parents of the child space
1351        let parents = space_service.joined_parents_of_child(child_space_id).await;
1352
1353        // Then both parent spaces are returned
1354        assert_eq!(
1355            parents.iter().map(|space| space.room_id.to_owned()).collect::<Vec<_>>(),
1356            vec![parent_space_id_1, parent_space_id_2]
1357        );
1358    }
1359
1360    #[async_test]
1361    async fn test_joined_parent_ids_of_child() {
1362        // Given a space with three parent spaces, two of which are joined, and
1363        // a plain room.
1364        let server = MatrixMockServer::new().await;
1365        let client = server.client_builder().build().await;
1366        let user_id = client.user_id().unwrap();
1367        let factory = EventFactory::new();
1368
1369        server.mock_room_state_encryption().plain().mount().await;
1370
1371        let parent_space_id_1 = room_id!("!parent_space_1:example.org");
1372        let parent_space_id_2 = room_id!("!parent_space_2:example.org");
1373        let unknown_parent_space_id = room_id!("!unknown_parent_space:example.org");
1374        let child_space_id = room_id!("!child_space:example.org");
1375        let child_room_id = room_id!("!child_room:example.org");
1376
1377        add_space_rooms(
1378            vec![
1379                MockSpaceRoomParameters {
1380                    room_id: child_space_id,
1381                    order: None,
1382                    parents: vec![parent_space_id_1, parent_space_id_2, unknown_parent_space_id],
1383                    children: vec![child_room_id],
1384                    power_level: None,
1385                },
1386                MockSpaceRoomParameters {
1387                    room_id: parent_space_id_1,
1388                    order: None,
1389                    parents: vec![],
1390                    children: vec![child_space_id],
1391                    power_level: None,
1392                },
1393                MockSpaceRoomParameters {
1394                    room_id: parent_space_id_2,
1395                    order: None,
1396                    parents: vec![],
1397                    children: vec![child_space_id],
1398                    power_level: None,
1399                },
1400            ],
1401            &client,
1402            &server,
1403            &factory,
1404            user_id,
1405        )
1406        .await;
1407
1408        let space_service = SpaceService::new(client.clone()).await;
1409
1410        // When retrieving the parent IDs of the child space.
1411        let parent_ids = space_service.joined_parent_ids_of_child(child_space_id).await;
1412
1413        // Then only the two joined parent spaces are returned, ordered by room
1414        // ID. The unjoined one never made it into the graph in the first place,
1415        // since `m.space.parent` events pointing at a room that isn't a joined
1416        // space are dropped while building it.
1417        assert_eq!(parent_ids, vec![parent_space_id_1.to_owned(), parent_space_id_2.to_owned()]);
1418
1419        // And the result matches the one of the more expensive
1420        // `joined_parents_of_child`.
1421        assert_eq!(
1422            space_service
1423                .joined_parents_of_child(child_space_id)
1424                .await
1425                .into_iter()
1426                .map(|space| space.room_id)
1427                .collect::<Vec<_>>(),
1428            parent_ids
1429        );
1430
1431        // And a plain room, which is only known as the child of a joined space,
1432        // still reports its parent.
1433        assert_eq!(
1434            space_service.joined_parent_ids_of_child(child_room_id).await,
1435            vec![child_space_id.to_owned()]
1436        );
1437
1438        // And a top-level space has no parents at all.
1439        assert!(space_service.joined_parent_ids_of_child(parent_space_id_1).await.is_empty());
1440
1441        // And neither does a room the graph doesn't know about.
1442        assert!(
1443            space_service
1444                .joined_parent_ids_of_child(room_id!("!unknown_room:example.org"))
1445                .await
1446                .is_empty()
1447        );
1448    }
1449
1450    #[async_test]
1451    async fn test_top_level_ancestors_of() {
1452        // Given two top-level spaces sharing a subspace, which in turn contains
1453        // a plain room.
1454        let server = MatrixMockServer::new().await;
1455        let client = server.client_builder().build().await;
1456        let user_id = client.user_id().unwrap();
1457        let factory = EventFactory::new();
1458
1459        server.mock_room_state_encryption().plain().mount().await;
1460
1461        let top_level_space_id_1 = room_id!("!top_level_space_1:example.org");
1462        let top_level_space_id_2 = room_id!("!top_level_space_2:example.org");
1463        let middle_space_id = room_id!("!middle_space:example.org");
1464        let leaf_room_id = room_id!("!leaf_room:example.org");
1465
1466        add_space_rooms(
1467            vec![
1468                MockSpaceRoomParameters {
1469                    room_id: top_level_space_id_1,
1470                    order: None,
1471                    parents: vec![],
1472                    children: vec![middle_space_id],
1473                    power_level: None,
1474                },
1475                MockSpaceRoomParameters {
1476                    room_id: top_level_space_id_2,
1477                    order: None,
1478                    parents: vec![],
1479                    children: vec![middle_space_id],
1480                    power_level: None,
1481                },
1482                MockSpaceRoomParameters {
1483                    room_id: middle_space_id,
1484                    order: None,
1485                    parents: vec![top_level_space_id_1, top_level_space_id_2],
1486                    children: vec![leaf_room_id],
1487                    power_level: None,
1488                },
1489            ],
1490            &client,
1491            &server,
1492            &factory,
1493            user_id,
1494        )
1495        .await;
1496
1497        let space_service = SpaceService::new(client.clone()).await;
1498
1499        // Then a room several levels down resolves to both top-level spaces.
1500        assert_eq!(
1501            space_service.top_level_ancestors_of(leaf_room_id).await,
1502            HashSet::from([top_level_space_id_1.to_owned(), top_level_space_id_2.to_owned()])
1503        );
1504
1505        // And so does the subspace they share.
1506        assert_eq!(
1507            space_service.top_level_ancestors_of(middle_space_id).await,
1508            HashSet::from([top_level_space_id_1.to_owned(), top_level_space_id_2.to_owned()])
1509        );
1510
1511        // And a top-level space is its own only ancestor.
1512        assert_eq!(
1513            space_service.top_level_ancestors_of(top_level_space_id_1).await,
1514            HashSet::from([top_level_space_id_1.to_owned()])
1515        );
1516
1517        // And a room the graph doesn't know about has no ancestors, which is
1518        // how it can be told apart from a top-level space.
1519        assert!(
1520            space_service
1521                .top_level_ancestors_of(room_id!("!unknown_room:example.org"))
1522                .await
1523                .is_empty()
1524        );
1525    }
1526
1527    #[async_test]
1528    async fn test_top_level_ancestors_of_cyclic_spaces() {
1529        // Given two spaces that are each other's parent, which the homeserver
1530        // doesn't prevent.
1531        let server = MatrixMockServer::new().await;
1532        let client = server.client_builder().build().await;
1533        let user_id = client.user_id().unwrap();
1534        let factory = EventFactory::new();
1535
1536        server.mock_room_state_encryption().plain().mount().await;
1537
1538        let space_id_1 = room_id!("!cycle_space_1:example.org");
1539        let space_id_2 = room_id!("!cycle_space_2:example.org");
1540
1541        add_space_rooms(
1542            vec![
1543                MockSpaceRoomParameters {
1544                    room_id: space_id_1,
1545                    order: None,
1546                    parents: vec![],
1547                    children: vec![space_id_2],
1548                    power_level: None,
1549                },
1550                MockSpaceRoomParameters {
1551                    room_id: space_id_2,
1552                    order: None,
1553                    parents: vec![],
1554                    children: vec![space_id_1],
1555                    power_level: None,
1556                },
1557            ],
1558            &client,
1559            &server,
1560            &factory,
1561            user_id,
1562        )
1563        .await;
1564
1565        let space_service = SpaceService::new(client.clone()).await;
1566
1567        // Then the walk terminates, on the cycle-free graph the service builds:
1568        // one of the two back edges has been removed, leaving a single root
1569        // that both spaces resolve to. Which of the two it is depends on the
1570        // order the de-cycling happens to visit them in, which isn't part of
1571        // the contract, so it isn't asserted here.
1572        let ancestors_of_1 = space_service.top_level_ancestors_of(space_id_1).await;
1573        let ancestors_of_2 = space_service.top_level_ancestors_of(space_id_2).await;
1574
1575        assert_eq!(ancestors_of_1.len(), 1);
1576        assert_eq!(ancestors_of_1, ancestors_of_2);
1577        let root = ancestors_of_1.iter().next().unwrap();
1578        assert!([space_id_1, space_id_2].contains(&&**root));
1579    }
1580
1581    #[async_test]
1582    async fn test_get_space_room_for_id() {
1583        let server = MatrixMockServer::new().await;
1584        let client = server.client_builder().build().await;
1585        let user_id = client.user_id().unwrap();
1586        let factory = EventFactory::new();
1587
1588        server.mock_room_state_encryption().plain().mount().await;
1589
1590        let space_id = room_id!("!single_space:example.org");
1591
1592        add_space_rooms(
1593            vec![MockSpaceRoomParameters {
1594                room_id: space_id,
1595                order: None,
1596                parents: vec![],
1597                children: vec![],
1598                power_level: None,
1599            }],
1600            &client,
1601            &server,
1602            &factory,
1603            user_id,
1604        )
1605        .await;
1606
1607        let space_service = SpaceService::new(client.clone()).await;
1608
1609        let found = space_service.get_space_room(space_id).await;
1610        assert!(found.is_some());
1611
1612        let expected = SpaceRoom::new_from_known(&client.get_room(space_id).unwrap(), 0).await;
1613        assert_eq!(found.unwrap(), expected);
1614    }
1615
1616    #[async_test]
1617    async fn test_add_child_to_space() {
1618        // Given a space and child room where the user is admin of both.
1619        let server = MatrixMockServer::new().await;
1620        let client = server.client_builder().build().await;
1621        let user_id = client.user_id().unwrap();
1622        let factory = EventFactory::new();
1623
1624        server.mock_room_state_encryption().plain().mount().await;
1625
1626        let space_child_event_id = event_id!("$1");
1627        let space_parent_event_id = event_id!("$2");
1628        server.mock_set_space_child().ok(space_child_event_id.to_owned()).expect(1).mount().await;
1629        server.mock_set_space_parent().ok(space_parent_event_id.to_owned()).expect(1).mount().await;
1630
1631        let space_id = room_id!("!my_space:example.org");
1632        let child_id = room_id!("!my_child:example.org");
1633
1634        add_space_rooms(
1635            vec![
1636                MockSpaceRoomParameters {
1637                    room_id: space_id,
1638                    order: None,
1639                    parents: vec![],
1640                    children: vec![],
1641                    power_level: Some(100),
1642                },
1643                MockSpaceRoomParameters {
1644                    room_id: child_id,
1645                    order: None,
1646                    parents: vec![],
1647                    children: vec![],
1648                    power_level: Some(100),
1649                },
1650            ],
1651            &client,
1652            &server,
1653            &factory,
1654            user_id,
1655        )
1656        .await;
1657
1658        let space_service = SpaceService::new(client.clone()).await;
1659
1660        // When adding the child to the space.
1661        let result =
1662            space_service.add_child_to_space(child_id.to_owned(), space_id.to_owned()).await;
1663
1664        // Then both space child and parent events are set successfully.
1665        assert!(result.is_ok());
1666    }
1667
1668    #[async_test]
1669    async fn test_add_child_to_space_without_space_admin() {
1670        // Given a space and child room where the user is a regular member of
1671        // both.
1672        let server = MatrixMockServer::new().await;
1673        let client = server.client_builder().build().await;
1674        let user_id = client.user_id().unwrap();
1675        let factory = EventFactory::new();
1676
1677        server.mock_room_state_encryption().plain().mount().await;
1678
1679        server.mock_set_space_child().unauthorized().expect(1).mount().await;
1680        server.mock_set_space_parent().unauthorized().expect(0).mount().await;
1681
1682        let space_id = room_id!("!my_space:example.org");
1683        let child_id = room_id!("!my_child:example.org");
1684
1685        add_space_rooms(
1686            vec![
1687                MockSpaceRoomParameters {
1688                    room_id: space_id,
1689                    order: None,
1690                    parents: vec![],
1691                    children: vec![],
1692                    power_level: Some(0),
1693                },
1694                MockSpaceRoomParameters {
1695                    room_id: child_id,
1696                    order: None,
1697                    parents: vec![],
1698                    children: vec![],
1699                    power_level: Some(0),
1700                },
1701            ],
1702            &client,
1703            &server,
1704            &factory,
1705            user_id,
1706        )
1707        .await;
1708
1709        let space_service = SpaceService::new(client.clone()).await;
1710
1711        // When adding the child to the space.
1712        let result =
1713            space_service.add_child_to_space(child_id.to_owned(), space_id.to_owned()).await;
1714
1715        // Then the operation fails when trying to set the space child event and
1716        // the parent event is not attempted.
1717        assert!(result.is_err());
1718    }
1719
1720    #[async_test]
1721    async fn test_add_child_to_space_without_child_admin() {
1722        // Given a space and child room where the user is admin of the space but
1723        // not of the child.
1724        let server = MatrixMockServer::new().await;
1725        let client = server.client_builder().build().await;
1726        let user_id = client.user_id().unwrap();
1727        let factory = EventFactory::new();
1728
1729        server.mock_room_state_encryption().plain().mount().await;
1730
1731        let space_child_event_id = event_id!("$1");
1732        server.mock_set_space_child().ok(space_child_event_id.to_owned()).expect(1).mount().await;
1733        server.mock_set_space_parent().unauthorized().expect(0).mount().await;
1734
1735        let space_id = room_id!("!my_space:example.org");
1736        let child_id = room_id!("!my_child:example.org");
1737
1738        add_space_rooms(
1739            vec![
1740                MockSpaceRoomParameters {
1741                    room_id: space_id,
1742                    order: None,
1743                    parents: vec![],
1744                    children: vec![],
1745                    power_level: Some(100),
1746                },
1747                MockSpaceRoomParameters {
1748                    room_id: child_id,
1749                    order: None,
1750                    parents: vec![],
1751                    children: vec![],
1752                    power_level: Some(0),
1753                },
1754            ],
1755            &client,
1756            &server,
1757            &factory,
1758            user_id,
1759        )
1760        .await;
1761
1762        let space_service = SpaceService::new(client.clone()).await;
1763
1764        // When adding the child to the space.
1765        let result =
1766            space_service.add_child_to_space(child_id.to_owned(), space_id.to_owned()).await;
1767
1768        error!("result: {:?}", result);
1769        // Then the operation succeeds in setting the space child event and the
1770        // parent event is not attempted.
1771        assert!(result.is_ok());
1772    }
1773
1774    #[async_test]
1775    async fn test_remove_child_from_space() {
1776        // Given a space and child room where the user is admin of both.
1777        let server = MatrixMockServer::new().await;
1778        let client = server.client_builder().build().await;
1779        let user_id = client.user_id().unwrap();
1780        let factory = EventFactory::new();
1781
1782        server.mock_room_state_encryption().plain().mount().await;
1783
1784        let space_child_event_id = event_id!("$1");
1785        let space_parent_event_id = event_id!("$2");
1786        server.mock_set_space_child().ok(space_child_event_id.to_owned()).expect(1).mount().await;
1787        server.mock_set_space_parent().ok(space_parent_event_id.to_owned()).expect(1).mount().await;
1788
1789        let parent_id = room_id!("!parent_space:example.org");
1790        let child_id = room_id!("!child_space:example.org");
1791
1792        add_space_rooms(
1793            vec![
1794                MockSpaceRoomParameters {
1795                    room_id: parent_id,
1796                    order: None,
1797                    parents: vec![],
1798                    children: vec![child_id],
1799                    power_level: None,
1800                },
1801                MockSpaceRoomParameters {
1802                    room_id: child_id,
1803                    order: None,
1804                    parents: vec![parent_id],
1805                    children: vec![],
1806                    power_level: None,
1807                },
1808            ],
1809            &client,
1810            &server,
1811            &factory,
1812            user_id,
1813        )
1814        .await;
1815
1816        let space_service = SpaceService::new(client.clone()).await;
1817
1818        // When removing the child from the space.
1819        let result =
1820            space_service.remove_child_from_space(child_id.to_owned(), parent_id.to_owned()).await;
1821
1822        // Then both space child and parent events are removed successfully.
1823        assert!(result.is_ok());
1824    }
1825
1826    #[async_test]
1827    async fn test_remove_child_from_space_without_parent_event() {
1828        // Given a space with a child where the m.space.parent event wasn't set.
1829        let server = MatrixMockServer::new().await;
1830        let client = server.client_builder().build().await;
1831        let user_id = client.user_id().unwrap();
1832        let factory = EventFactory::new();
1833
1834        server.mock_room_state_encryption().plain().mount().await;
1835
1836        let space_child_event_id = event_id!("$1");
1837        server.mock_set_space_child().ok(space_child_event_id.to_owned()).expect(1).mount().await;
1838        server.mock_set_space_parent().unauthorized().expect(0).mount().await;
1839
1840        let parent_id = room_id!("!parent_space:example.org");
1841        let child_id = room_id!("!child_space:example.org");
1842
1843        add_space_rooms(
1844            vec![
1845                MockSpaceRoomParameters {
1846                    room_id: parent_id,
1847                    order: None,
1848                    parents: vec![],
1849                    children: vec![child_id],
1850                    power_level: None,
1851                },
1852                MockSpaceRoomParameters {
1853                    room_id: child_id,
1854                    order: None,
1855                    parents: vec![],
1856                    children: vec![],
1857                    power_level: None,
1858                },
1859            ],
1860            &client,
1861            &server,
1862            &factory,
1863            user_id,
1864        )
1865        .await;
1866
1867        let space_service = SpaceService::new(client.clone()).await;
1868
1869        // When removing the child from the space.
1870        let result =
1871            space_service.remove_child_from_space(child_id.to_owned(), parent_id.to_owned()).await;
1872
1873        // Then the child event is removed successfully and the parent event
1874        // removal is not attempted.
1875        assert!(result.is_ok());
1876    }
1877
1878    #[async_test]
1879    async fn test_remove_child_from_space_without_child_event() {
1880        // Given a space with a child where the space's m.space.child event
1881        // wasn't set.
1882        let server = MatrixMockServer::new().await;
1883        let client = server.client_builder().build().await;
1884        let user_id = client.user_id().unwrap();
1885        let factory = EventFactory::new();
1886
1887        server.mock_room_state_encryption().plain().mount().await;
1888
1889        let space_parent_event_id = event_id!("$2");
1890        server.mock_set_space_child().unauthorized().expect(0).mount().await;
1891        server.mock_set_space_parent().ok(space_parent_event_id.to_owned()).expect(1).mount().await;
1892
1893        let parent_id = room_id!("!parent_space:example.org");
1894        let child_id = room_id!("!child_space:example.org");
1895
1896        add_space_rooms(
1897            vec![
1898                MockSpaceRoomParameters {
1899                    room_id: parent_id,
1900                    order: None,
1901                    parents: vec![],
1902                    children: vec![],
1903                    power_level: None,
1904                },
1905                MockSpaceRoomParameters {
1906                    room_id: child_id,
1907                    order: None,
1908                    parents: vec![parent_id],
1909                    children: vec![],
1910                    power_level: None,
1911                },
1912            ],
1913            &client,
1914            &server,
1915            &factory,
1916            user_id,
1917        )
1918        .await;
1919
1920        let space_service = SpaceService::new(client.clone()).await;
1921
1922        // When removing the child from the space.
1923        let result =
1924            space_service.remove_child_from_space(child_id.to_owned(), parent_id.to_owned()).await;
1925
1926        // Then the parent event is removed successfully and the child event
1927        // removal is not attempted.
1928        assert!(result.is_ok());
1929    }
1930
1931    #[async_test]
1932    async fn test_remove_unknown_child_from_space() {
1933        // Given a space with a child room that is unknown (not in the client
1934        // store).
1935        let server = MatrixMockServer::new().await;
1936        let client = server.client_builder().build().await;
1937        let user_id = client.user_id().unwrap();
1938        let factory = EventFactory::new();
1939
1940        server.mock_room_state_encryption().plain().mount().await;
1941
1942        let space_child_event_id = event_id!("$1");
1943        server.mock_set_space_child().ok(space_child_event_id.to_owned()).expect(1).mount().await;
1944        // The parent event should not be attempted since the child room is
1945        // unknown.
1946        server.mock_set_space_parent().unauthorized().expect(0).mount().await;
1947
1948        let parent_id = room_id!("!parent_space:example.org");
1949        let unknown_child_id = room_id!("!unknown_child:example.org");
1950
1951        // Only add the parent space, not the child room.
1952        add_space_rooms(
1953            vec![MockSpaceRoomParameters {
1954                room_id: parent_id,
1955                order: None,
1956                parents: vec![],
1957                children: vec![unknown_child_id],
1958                power_level: None,
1959            }],
1960            &client,
1961            &server,
1962            &factory,
1963            user_id,
1964        )
1965        .await;
1966
1967        // Verify that the child room is indeed unknown.
1968        assert!(client.get_room(unknown_child_id).is_none());
1969
1970        let space_service = SpaceService::new(client.clone()).await;
1971
1972        // When removing the unknown child from the space.
1973        let result = space_service
1974            .remove_child_from_space(unknown_child_id.to_owned(), parent_id.to_owned())
1975            .await;
1976
1977        // Then the operation succeeds: the child event is removed from the
1978        // space, and the parent event removal is skipped since the child room
1979        // is unknown.
1980        assert!(result.is_ok());
1981    }
1982
1983    #[async_test]
1984    async fn test_space_child_updates() {
1985        // Test child updates received via sync.
1986        let server = MatrixMockServer::new().await;
1987        let client = server.client_builder().build().await;
1988        let user_id = client.user_id().unwrap();
1989        let factory = EventFactory::new();
1990
1991        server.mock_room_state_encryption().plain().mount().await;
1992
1993        let space_id = room_id!("!space:localhost");
1994        let first_child_id = room_id!("!first_child:localhost");
1995        let second_child_id = room_id!("!second_child:localhost");
1996
1997        // The space is joined.
1998        server
1999            .sync_room(
2000                &client,
2001                JoinedRoomBuilder::new(space_id)
2002                    .add_state_event(factory.create(user_id, RoomVersionId::V11).with_space_type()),
2003            )
2004            .await;
2005
2006        // Build the `SpaceService` and expect the room to show up with no
2007        // updates pending
2008        let space_service = SpaceService::new(client.clone()).await;
2009
2010        let (initial_values, joined_spaces_subscriber) =
2011            space_service.subscribe_to_top_level_joined_spaces().await;
2012        let mut joined_spaces_subscriber = pin!(joined_spaces_subscriber);
2013        assert_pending!(joined_spaces_subscriber);
2014
2015        assert_eq!(
2016            initial_values,
2017            vec![SpaceRoom::new_from_known(&client.get_room(space_id).unwrap(), 0).await].into()
2018        );
2019
2020        assert_eq!(
2021            space_service.top_level_joined_spaces().await,
2022            vec![SpaceRoom::new_from_known(&client.get_room(space_id).unwrap(), 0).await]
2023        );
2024
2025        // Two children are added.
2026        server
2027            .sync_room(
2028                &client,
2029                JoinedRoomBuilder::new(space_id)
2030                    .add_state_event(
2031                        factory
2032                            .space_child(space_id.to_owned(), first_child_id.to_owned())
2033                            .sender(user_id),
2034                    )
2035                    .add_state_event(
2036                        factory
2037                            .space_child(space_id.to_owned(), second_child_id.to_owned())
2038                            .sender(user_id),
2039                    ),
2040            )
2041            .await;
2042
2043        // And expect the list to update.
2044        assert_eq!(
2045            space_service.top_level_joined_spaces().await,
2046            vec![SpaceRoom::new_from_known(&client.get_room(space_id).unwrap(), 2).await]
2047        );
2048        assert_next_eq!(
2049            joined_spaces_subscriber,
2050            vec![
2051                VectorDiff::Clear,
2052                VectorDiff::Append {
2053                    values: vec![
2054                        SpaceRoom::new_from_known(&client.get_room(space_id).unwrap(), 2).await
2055                    ]
2056                    .into()
2057                },
2058            ]
2059        );
2060
2061        // Then remove a child by replacing the state event with an empty one.
2062        server
2063            .sync_room(
2064                &client,
2065                JoinedRoomBuilder::new(space_id).add_state_bulk([Raw::new(&json!({
2066                    "content": {},
2067                    "type": "m.space.child",
2068                    "event_id": "$cancelsecondchild",
2069                    "origin_server_ts": MilliSecondsSinceUnixEpoch::now(),
2070                    "sender": user_id,
2071                    "state_key": second_child_id,
2072                }))
2073                .unwrap()
2074                .cast_unchecked()]),
2075            )
2076            .await;
2077
2078        // And expect the list to update.
2079        assert_eq!(
2080            space_service.top_level_joined_spaces().await,
2081            vec![SpaceRoom::new_from_known(&client.get_room(space_id).unwrap(), 1).await]
2082        );
2083        assert_next_eq!(
2084            joined_spaces_subscriber,
2085            vec![
2086                VectorDiff::Clear,
2087                VectorDiff::Append {
2088                    values: vec![
2089                        SpaceRoom::new_from_known(&client.get_room(space_id).unwrap(), 1).await
2090                    ]
2091                    .into()
2092                },
2093            ]
2094        );
2095    }
2096
2097    async fn add_space_rooms(
2098        rooms: Vec<MockSpaceRoomParameters>,
2099        client: &Client,
2100        server: &MatrixMockServer,
2101        factory: &EventFactory,
2102        user_id: &UserId,
2103    ) {
2104        for parameters in rooms {
2105            let mut builder = JoinedRoomBuilder::new(parameters.room_id)
2106                .add_state_event(factory.create(user_id, RoomVersionId::V1).with_space_type());
2107
2108            if let Some(order) = parameters.order {
2109                builder = builder.add_account_data(factory.space_order(order));
2110            }
2111
2112            for parent_id in parameters.parents {
2113                builder = builder.add_state_event(
2114                    factory
2115                        .space_parent(parent_id.to_owned(), parameters.room_id.to_owned())
2116                        .sender(user_id),
2117                );
2118            }
2119
2120            for child_id in parameters.children {
2121                builder = builder.add_state_event(
2122                    factory
2123                        .space_child(parameters.room_id.to_owned(), child_id.to_owned())
2124                        .sender(user_id),
2125                );
2126            }
2127
2128            let mut power_levels = if let Some(power_level) = parameters.power_level {
2129                BTreeMap::from([(user_id.to_owned(), power_level.into())])
2130            } else {
2131                BTreeMap::from([(user_id.to_owned(), 100.into())])
2132            };
2133
2134            builder = builder.add_state_event(
2135                factory.power_levels(&mut power_levels).state_key("").sender(user_id),
2136            );
2137
2138            server.sync_room(client, builder).await;
2139        }
2140    }
2141
2142    struct MockSpaceRoomParameters {
2143        room_id: &'static RoomId,
2144        order: Option<&'static str>,
2145        parents: Vec<&'static RoomId>,
2146        children: Vec<&'static RoomId>,
2147        power_level: Option<i32>,
2148    }
2149
2150    fn any_room_id_and_space_room_order()
2151    -> impl Strategy<Value = (OwnedRoomId, Option<OwnedSpaceChildOrder>)> {
2152        let room_id = "[a-zA-Z]{1,5}".prop_map(|r| {
2153            RoomId::new_v2(&r).expect("Any string starting with ! should be a valid room ID")
2154        });
2155
2156        let order = prop::option::of("[a-zA-Z]{1,5}").prop_map(|order| {
2157            order.map(|o| SpaceChildOrder::parse(o).expect("Any string should be a valid order"))
2158        });
2159
2160        (room_id, order)
2161    }
2162
2163    proptest! {
2164        #[test]
2165        fn sort_top_level_space_room_never_panics(mut v in prop::collection::vec(any_room_id_and_space_room_order(), 0..100)) {
2166            v.sort_by(|a, b| {
2167                let (a_room_id, a_order) = a;
2168                let (b_room_id, b_order) = b;
2169
2170                let a = (a_room_id.as_ref(), a_order.as_deref());
2171                let b = (b_room_id.as_ref(), b_order.as_deref());
2172
2173                compare_top_level_space_rooms(a, b)
2174            })
2175        }
2176
2177        #[test]
2178        fn test_compare_top_level_rooms_reflexive(a in any_room_id_and_space_room_order()) {
2179            let (a_room_id, a_order) = a;
2180            let a = (a_room_id.as_ref(), a_order.as_deref());
2181
2182            prop_assert_eq!(compare_top_level_space_rooms(a, a), Ordering::Equal);
2183        }
2184
2185        #[test]
2186        fn test_compare_top_level_rooms_antisymmetric(a in any_room_id_and_space_room_order(), b in any_room_id_and_space_room_order()) {
2187            let (a_room_id, a_order) = a;
2188            let (b_room_id, b_order) = b;
2189
2190            let a = (a_room_id.as_ref(), a_order.as_deref());
2191            let b = (b_room_id.as_ref(), b_order.as_deref());
2192
2193            let ab = compare_top_level_space_rooms(a, b);
2194            let ba = compare_top_level_space_rooms(b, a);
2195
2196            prop_assert_eq!(ab, ba.reverse());
2197        }
2198
2199        #[test]
2200        fn test_compare_top_level_rooms_transitive(
2201            a in any_room_id_and_space_room_order(),
2202            b in any_room_id_and_space_room_order(),
2203            c in any_room_id_and_space_room_order()
2204        ) {
2205            let (a_room_id, a_order) = a;
2206            let (b_room_id, b_order) = b;
2207            let (c_room_id, c_order) = c;
2208
2209            let a = (a_room_id.as_ref(), a_order.as_deref());
2210            let b = (b_room_id.as_ref(), b_order.as_deref());
2211            let c = (c_room_id.as_ref(), c_order.as_deref());
2212
2213            let ab = compare_top_level_space_rooms(a, b);
2214            let bc = compare_top_level_space_rooms(b, c);
2215            let ac = compare_top_level_space_rooms(a, c);
2216
2217            if ab == Ordering::Less && bc == Ordering::Less {
2218                prop_assert_eq!(ac, Ordering::Less);
2219            }
2220
2221            if ab == Ordering::Equal && bc == Ordering::Equal {
2222                prop_assert_eq!(ac, Ordering::Equal);
2223            }
2224
2225            if ab == Ordering::Greater && bc == Ordering::Greater {
2226                prop_assert_eq!(ac, Ordering::Greater);
2227            }
2228        }
2229    }
2230}